Create a sticky service for BLE communication.

This commit is contained in:
Onuray Sahin 2022-07-04 21:34:01 +03:00
parent 35dad02bd1
commit 7e152bd1d7
8 changed files with 173 additions and 5 deletions

View File

@ -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.
*/

View File

@ -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()
}

View File

@ -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)
}
}
}

View File

@ -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()
}

View File

@ -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))
}
}

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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>