Create a sticky service for BLE communication.
This commit is contained in:
parent
35dad02bd1
commit
7e152bd1d7
@ -552,6 +552,19 @@ class NotificationUtils @Inject constructor(
|
||||
.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a notification that indicates the application is communicating with a BLE device mainly for push-to-talk in Element Call Widget.
|
||||
*/
|
||||
fun buildBluetoothLowEnergyNotification(): Notification {
|
||||
return NotificationCompat.Builder(context, SILENT_NOTIFICATION_CHANNEL_ID)
|
||||
.setContentTitle(stringProvider.getString(R.string.push_to_talk_notification_title))
|
||||
.setContentText(stringProvider.getString(R.string.push_to_talk_notification_description))
|
||||
.setSmallIcon(R.drawable.quantum_ic_bluetooth_audio_white_36)
|
||||
.setColor(ThemeUtils.getColor(context, android.R.attr.colorPrimary))
|
||||
.setContentIntent(buildOpenHomePendingIntentForSummary())
|
||||
.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a notification that indicates the application is capturing the screen.
|
||||
*/
|
||||
|
||||
@ -16,6 +16,7 @@
|
||||
|
||||
package im.vector.app.features.widgets
|
||||
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import im.vector.app.core.platform.VectorViewModelAction
|
||||
|
||||
sealed class WidgetAction : VectorViewModelAction {
|
||||
@ -26,4 +27,5 @@ sealed class WidgetAction : VectorViewModelAction {
|
||||
object DeleteWidget : WidgetAction()
|
||||
object RevokeWidget : WidgetAction()
|
||||
object OnTermsReviewed : WidgetAction()
|
||||
data class ConnectToBluetoothDevice(val device: BluetoothDevice) : WidgetAction()
|
||||
}
|
||||
|
||||
@ -32,6 +32,7 @@ import android.view.ViewGroup
|
||||
import android.webkit.PermissionRequest
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.isVisible
|
||||
import com.airbnb.mvrx.Fail
|
||||
@ -50,6 +51,7 @@ import im.vector.app.core.utils.openUrlInExternalBrowser
|
||||
import im.vector.app.databinding.FragmentRoomWidgetBinding
|
||||
import im.vector.app.features.webview.WebEventListener
|
||||
import im.vector.app.features.widgets.ptt.BluetoothLowEnergyDeviceScanner
|
||||
import im.vector.app.features.widgets.ptt.BluetoothLowEnergyService
|
||||
import im.vector.app.features.widgets.webview.WebviewPermissionUtils
|
||||
import im.vector.app.features.widgets.webview.clearAfterWidget
|
||||
import im.vector.app.features.widgets.webview.setupForWidget
|
||||
@ -104,6 +106,7 @@ class WidgetFragment @Inject constructor(
|
||||
is WidgetViewEvents.DisplayIntegrationManager -> displayIntegrationManager(it)
|
||||
is WidgetViewEvents.Failure -> displayErrorDialog(it.throwable)
|
||||
is WidgetViewEvents.Close -> Unit
|
||||
is WidgetViewEvents.OnBluetoothDeviceData -> handleBluetoothDeviceData(it)
|
||||
}
|
||||
}
|
||||
viewModel.handle(WidgetAction.LoadFormattedUrl)
|
||||
@ -377,6 +380,16 @@ class WidgetFragment @Inject constructor(
|
||||
}
|
||||
|
||||
private fun onBluetoothDeviceSelected(device: BluetoothDevice) {
|
||||
|
||||
viewModel.handle(WidgetAction.ConnectToBluetoothDevice(device))
|
||||
|
||||
Intent(requireContext(), BluetoothLowEnergyService::class.java).also {
|
||||
ContextCompat.startForegroundService(requireContext(), it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleBluetoothDeviceData(event: WidgetViewEvents.OnBluetoothDeviceData) {
|
||||
activity?.let {
|
||||
views.widgetWebView.evaluateJavascript("alert(${event.data})", null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,4 +25,5 @@ sealed class WidgetViewEvents : VectorViewEvents {
|
||||
data class DisplayIntegrationManager(val integId: String?, val integType: String?) : WidgetViewEvents()
|
||||
data class OnURLFormatted(val formattedURL: String) : WidgetViewEvents()
|
||||
data class DisplayTerms(val url: String, val token: String) : WidgetViewEvents()
|
||||
data class OnBluetoothDeviceData(val data: String) : WidgetViewEvents()
|
||||
}
|
||||
|
||||
@ -29,6 +29,7 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.features.widgets.permissions.WidgetPermissionsHelper
|
||||
import im.vector.app.features.widgets.ptt.BluetoothLowEnergyServiceConnection
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
@ -52,11 +53,12 @@ class WidgetViewModel @AssistedInject constructor(
|
||||
@Assisted val initialState: WidgetViewState,
|
||||
widgetPostAPIHandlerFactory: WidgetPostAPIHandler.Factory,
|
||||
private val stringProvider: StringProvider,
|
||||
private val session: Session
|
||||
private val session: Session,
|
||||
private val bluetoothLowEnergyServiceConnection: BluetoothLowEnergyServiceConnection,
|
||||
) :
|
||||
VectorViewModel<WidgetViewState, WidgetAction, WidgetViewEvents>(initialState),
|
||||
WidgetPostAPIHandler.NavigationCallback,
|
||||
IntegrationManagerService.Listener {
|
||||
IntegrationManagerService.Listener, BluetoothLowEnergyServiceConnection.Callback {
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : MavericksAssistedViewModelFactory<WidgetViewModel, WidgetViewState> {
|
||||
@ -147,9 +149,14 @@ class WidgetViewModel @AssistedInject constructor(
|
||||
WidgetAction.DeleteWidget -> handleDeleteWidget()
|
||||
WidgetAction.RevokeWidget -> handleRevokeWidget()
|
||||
WidgetAction.OnTermsReviewed -> loadFormattedUrl(forceFetchToken = false)
|
||||
is WidgetAction.ConnectToBluetoothDevice -> handleConnectToBluetoothDevice(action)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleConnectToBluetoothDevice(action: WidgetAction.ConnectToBluetoothDevice) {
|
||||
bluetoothLowEnergyServiceConnection.bind(action.device, this)
|
||||
}
|
||||
|
||||
private fun handleRevokeWidget() {
|
||||
viewModelScope.launch {
|
||||
val widgetId = initialState.widgetId ?: return@launch
|
||||
@ -296,4 +303,8 @@ class WidgetViewModel @AssistedInject constructor(
|
||||
override fun openIntegrationManager(integId: String?, integType: String?) {
|
||||
_viewEvents.post(WidgetViewEvents.DisplayIntegrationManager(integId, integType))
|
||||
}
|
||||
|
||||
override fun onCharacteristicRead(data: String) {
|
||||
_viewEvents.post(WidgetViewEvents.OnBluetoothDeviceData(data))
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,18 +19,36 @@ package im.vector.app.features.widgets.ptt
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.bluetooth.BluetoothGatt
|
||||
import android.bluetooth.BluetoothGattCallback
|
||||
import android.bluetooth.BluetoothGattCharacteristic
|
||||
import android.bluetooth.BluetoothManager
|
||||
import android.bluetooth.BluetoothProfile
|
||||
import android.content.Intent
|
||||
import android.os.Binder
|
||||
import android.os.IBinder
|
||||
import im.vector.app.core.services.VectorService
|
||||
import androidx.core.content.getSystemService
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import im.vector.app.features.notifications.NotificationUtils
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import kotlin.random.Random
|
||||
|
||||
@AndroidEntryPoint
|
||||
class BluetoothLowEnergyService : VectorService() {
|
||||
|
||||
private val bluetoothManager = getSystemService<BluetoothManager>()
|
||||
interface Callback {
|
||||
fun onCharacteristicRead(data: String)
|
||||
}
|
||||
|
||||
@Inject lateinit var notificationUtils: NotificationUtils
|
||||
|
||||
private var bluetoothAdapter: BluetoothAdapter? = null
|
||||
private var bluetoothGatt: BluetoothGatt? = null
|
||||
|
||||
private val binder = LocalBinder()
|
||||
|
||||
var callback: Callback? = null
|
||||
|
||||
private val gattCallback = object : BluetoothGattCallback() {
|
||||
override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) {
|
||||
when (newState) {
|
||||
@ -43,15 +61,31 @@ class BluetoothLowEnergyService : VectorService() {
|
||||
BluetoothProfile.STATE_DISCONNECTED -> Timber.d("### BluetoothLowEnergyService.newState: STATE_DISCONNECTED")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCharacteristicRead(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) {
|
||||
if (status == BluetoothGatt.GATT_SUCCESS) {
|
||||
onCharacteristicRead(characteristic)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
|
||||
onCharacteristicRead(characteristic)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
initializeBluetoothAdapter()
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
val notification = notificationUtils.buildBluetoothLowEnergyNotification()
|
||||
startForeground(Random.nextInt(), notification)
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
private fun initializeBluetoothAdapter() {
|
||||
val bluetoothManager = getSystemService<BluetoothManager>()
|
||||
bluetoothAdapter = bluetoothManager?.adapter
|
||||
}
|
||||
|
||||
@ -60,4 +94,23 @@ class BluetoothLowEnergyService : VectorService() {
|
||||
?.getRemoteDevice(address)
|
||||
?.connectGatt(applicationContext, false, gattCallback)
|
||||
}
|
||||
|
||||
private fun onCharacteristicRead(characteristic: BluetoothGattCharacteristic) {
|
||||
val data = characteristic.value
|
||||
if (data.isNotEmpty()) {
|
||||
val stringBuilder = StringBuilder()
|
||||
data.forEach {
|
||||
stringBuilder.append(String.format("%02X ", it))
|
||||
}
|
||||
callback?.onCharacteristicRead(stringBuilder.toString())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder {
|
||||
return binder
|
||||
}
|
||||
|
||||
inner class LocalBinder : Binder() {
|
||||
fun getService(): BluetoothLowEnergyService = this@BluetoothLowEnergyService
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.widgets.ptt
|
||||
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.IBinder
|
||||
import javax.inject.Inject
|
||||
|
||||
class BluetoothLowEnergyServiceConnection @Inject constructor(
|
||||
private val context: Context,
|
||||
) : ServiceConnection, BluetoothLowEnergyService.Callback {
|
||||
|
||||
interface Callback {
|
||||
fun onCharacteristicRead(data: String)
|
||||
}
|
||||
|
||||
private var isBound = false
|
||||
private var bluetoothLowEnergyService: BluetoothLowEnergyService? = null
|
||||
private var bluetoothDevice: BluetoothDevice? = null
|
||||
|
||||
var callback: Callback? = null
|
||||
|
||||
fun bind(device: BluetoothDevice, callback: Callback) {
|
||||
this.bluetoothDevice = device
|
||||
this.callback = callback
|
||||
|
||||
if (!isBound) {
|
||||
Intent(context, BluetoothLowEnergyService::class.java).also { intent ->
|
||||
context.bindService(intent, this, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceConnected(name: ComponentName, binder: IBinder) {
|
||||
bluetoothLowEnergyService = (binder as BluetoothLowEnergyService.LocalBinder).getService().also {
|
||||
it.callback = this
|
||||
}
|
||||
|
||||
bluetoothDevice?.address?.let {
|
||||
bluetoothLowEnergyService?.connect(it)
|
||||
}
|
||||
isBound = true
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
isBound = false
|
||||
bluetoothLowEnergyService = null
|
||||
}
|
||||
|
||||
override fun onCharacteristicRead(data: String) {
|
||||
callback?.onCharacteristicRead(data)
|
||||
}
|
||||
}
|
||||
@ -3091,5 +3091,9 @@
|
||||
<string name="live_location_labs_promotion_title">Live location sharing</string>
|
||||
<string name="live_location_labs_promotion_description">Please note: this is a labs feature using a temporary implementation. This means you will not be able to delete your location history, and advanced users will be able to see your location history even after you stop sharing your live location with this room.</string>
|
||||
<string name="live_location_labs_promotion_switch_title">Enable location sharing</string>
|
||||
|
||||
<!-- Element Call Widget - Push to Talk -->
|
||||
<string name="push_to_talk_notification_title">${app_name} Push to Talk</string>
|
||||
<string name="push_to_talk_notification_description">A service is running to communicate with BLE device</string>
|
||||
|
||||
</resources>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user