From f538e91c1f76fdd51d52e8fb390599e96c7b31aa Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Mon, 4 Jul 2022 13:53:10 +0300 Subject: [PATCH 01/43] Add element call widget type. --- .../android/sdk/api/session/widgets/model/WidgetType.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetType.kt index ee098f9bf2e..24f3a155edf 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetType.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetType.kt @@ -28,7 +28,8 @@ private val DEFINED_TYPES by lazy { WidgetType.StickerPicker, WidgetType.Grafana, WidgetType.Custom, - WidgetType.IntegrationManager + WidgetType.IntegrationManager, + WidgetType.ElementCall ) } @@ -47,6 +48,7 @@ sealed class WidgetType(open val preferred: String, open val legacy: String = pr object Grafana : WidgetType("m.grafana") object Custom : WidgetType("m.custom") object IntegrationManager : WidgetType("m.integration_manager") + object ElementCall : WidgetType("io.element.call") data class Fallback(override val preferred: String) : WidgetType(preferred) fun matches(type: String): Boolean { From 022ae91002d56f610847e709411d7cde1d9da503 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Mon, 4 Jul 2022 13:54:57 +0300 Subject: [PATCH 02/43] Create BLE service. --- vector/src/main/AndroidManifest.xml | 4 ++ .../widgets/ptt/BluetoothLowEnergyService.kt | 59 +++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 032e256bfac..bd4ca14e176 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -385,6 +385,10 @@ android:foregroundServiceType="mediaProjection" tools:targetApi="Q" /> + + () + private var bluetoothAdapter: BluetoothAdapter? = null + + private val gattCallback = object : BluetoothGattCallback() { + override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) { + when (newState) { + BluetoothProfile.STATE_CONNECTING -> Timber.d("### BluetoothLowEnergyService.newState: STATE_CONNECTING") + BluetoothProfile.STATE_CONNECTED -> Timber.d("### BluetoothLowEnergyService.newState: STATE_CONNECTED") + BluetoothProfile.STATE_DISCONNECTING -> Timber.d("### BluetoothLowEnergyService.newState: STATE_DISCONNECTING") + BluetoothProfile.STATE_DISCONNECTED -> Timber.d("### BluetoothLowEnergyService.newState: STATE_DISCONNECTED") + } + } + } + + override fun onCreate() { + super.onCreate() + + initializeBluetoothAdapter() + } + + private fun initializeBluetoothAdapter() { + bluetoothAdapter = bluetoothManager?.adapter + } + + fun connect(address: String) { + bluetoothAdapter + ?.getRemoteDevice(address) + ?.connectGatt(applicationContext, true, gattCallback) + } +} From 09c435ae59122355f88146a0f96ec90691faac54 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Mon, 4 Jul 2022 17:34:22 +0300 Subject: [PATCH 03/43] Add required bluetooth permissions. --- vector/src/main/AndroidManifest.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index bd4ca14e176..a7b2c6b410e 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -8,6 +8,8 @@ android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" /> + + From cf8056e0d8f1a05cd01024cfd7f072b73c91e0bb Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Mon, 4 Jul 2022 17:35:07 +0300 Subject: [PATCH 04/43] Create custom widget args for element call. --- .../im/vector/app/features/navigation/DefaultNavigator.kt | 3 +++ .../im/vector/app/features/widgets/WidgetArgsBuilder.kt | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt index 2de01a55313..6b29ce62801 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt @@ -516,6 +516,9 @@ class DefaultNavigator @Inject constructor( val enableVideo = options?.get(JitsiCallViewModel.ENABLE_VIDEO_OPTION) == true context.startActivity(VectorJitsiActivity.newIntent(context, roomId = roomId, widgetId = widget.widgetId, enableVideo = enableVideo)) } + } else if (widget.type is WidgetType.ElementCall) { + val widgetArgs = widgetArgsBuilder.buildElementCallWidgetArgs(roomId, widget) + context.startActivity(WidgetActivity.newIntent(context, widgetArgs)) } else { val widgetArgs = widgetArgsBuilder.buildRoomWidgetArgs(roomId, widget) context.startActivity(WidgetActivity.newIntent(context, widgetArgs)) diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetArgsBuilder.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetArgsBuilder.kt index 777bd9cc7e7..83ea100cb62 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetArgsBuilder.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetArgsBuilder.kt @@ -78,6 +78,13 @@ class WidgetArgsBuilder @Inject constructor( ) } + fun buildElementCallWidgetArgs(roomId: String, widget: Widget): WidgetArgs { + return buildRoomWidgetArgs(roomId, widget) + .copy( + kind = WidgetKind.ELEMENT_CALL + ) + } + @Suppress("UNCHECKED_CAST") private fun Map.filterNotNull(): Map { return filterValues { it != null } as Map From 35dad02bd1ec8615130a550bd02481099adabe98 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Mon, 4 Jul 2022 17:36:24 +0300 Subject: [PATCH 05/43] Scan available BLE devices and show in a dialog. --- .../app/features/widgets/WidgetFragment.kt | 57 ++++++++++++++++--- .../app/features/widgets/WidgetViewState.kt | 3 +- .../ptt/BluetoothLowEnergyDeviceScanner.kt | 50 ++++++++++++++++ .../widgets/ptt/BluetoothLowEnergyService.kt | 10 +++- 4 files changed, 107 insertions(+), 13 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyDeviceScanner.kt diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt index cbd4b8e1ee4..d1f267cd69a 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt @@ -17,6 +17,9 @@ package im.vector.app.features.widgets import android.app.Activity +import android.bluetooth.BluetoothDevice +import android.bluetooth.le.ScanCallback +import android.bluetooth.le.ScanResult import android.content.Intent import android.content.pm.PackageManager import android.os.Bundle @@ -28,6 +31,7 @@ import android.view.View import android.view.ViewGroup import android.webkit.PermissionRequest import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AlertDialog import androidx.core.view.isInvisible import androidx.core.view.isVisible import com.airbnb.mvrx.Fail @@ -45,10 +49,12 @@ import im.vector.app.core.platform.VectorBaseFragment 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.webview.WebviewPermissionUtils import im.vector.app.features.widgets.webview.clearAfterWidget import im.vector.app.features.widgets.webview.setupForWidget import kotlinx.parcelize.Parcelize +import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.terms.TermsService import timber.log.Timber import java.net.URISyntaxException @@ -64,7 +70,8 @@ data class WidgetArgs( ) : Parcelable class WidgetFragment @Inject constructor( - private val permissionUtils: WebviewPermissionUtils + private val permissionUtils: WebviewPermissionUtils, + private val bluetoothLowEnergyDeviceScanner: BluetoothLowEnergyDeviceScanner, ) : VectorBaseFragment(), WebEventListener, @@ -84,6 +91,11 @@ class WidgetFragment @Inject constructor( if (fragmentArgs.kind.isAdmin()) { viewModel.getPostAPIMediator().setWebView(views.widgetWebView) } + + if (fragmentArgs.kind == WidgetKind.ELEMENT_CALL) { + startBluetoothScanning() + } + viewModel.observeViewEvents { Timber.v("Observed view events: $it") when (it) { @@ -128,14 +140,6 @@ class WidgetFragment @Inject constructor( } } - override fun onPause() { - super.onPause() - views.widgetWebView.let { - it.pauseTimers() - it.onPause() - } - } - override fun onPrepareOptionsMenu(menu: Menu) = withState(viewModel) { state -> val widget = state.asyncWidget() menu.findItem(R.id.action_edit)?.isVisible = state.widgetKind != WidgetKind.INTEGRATION_MANAGER @@ -340,4 +344,39 @@ class WidgetFragment @Inject constructor( private fun revokeWidget() { viewModel.handle(WidgetAction.RevokeWidget) } + + private var deviceListDialog: AlertDialog? = null + + private fun startBluetoothScanning() { + val deviceListDialogBuilder = MaterialAlertDialogBuilder(requireContext()) + val bluetoothDevices = mutableListOf() + + bluetoothLowEnergyDeviceScanner.callback = object : ScanCallback() { + override fun onScanResult(callbackType: Int, result: ScanResult) { + super.onScanResult(callbackType, result) + Timber.d("### WidgetFragment. New BLE device found: " + result.device.name + " - " + result.device.address) + if (result.device.name == null) { + return + } + bluetoothDevices.add(result.device) + + deviceListDialogBuilder.setItems( + bluetoothDevices.map { it.name + " " + it.address }.toTypedArray() + ) { _, which -> + Timber.d("### WidgetFragment. $which selected") + onBluetoothDeviceSelected(bluetoothDevices[which]) + } + + if (deviceListDialog?.isShowing.orFalse()) { + deviceListDialog?.dismiss() + } + deviceListDialog = deviceListDialogBuilder.show() + } + } + bluetoothLowEnergyDeviceScanner.startScanning() + } + + private fun onBluetoothDeviceSelected(device: BluetoothDevice) { + + } } diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetViewState.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetViewState.kt index 2d98f734dda..7619fea7660 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetViewState.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetViewState.kt @@ -33,7 +33,8 @@ enum class WidgetStatus { enum class WidgetKind(@StringRes val nameRes: Int, val screenId: String?) { ROOM(R.string.room_widget_activity_title, null), STICKER_PICKER(R.string.title_activity_choose_sticker, WidgetType.StickerPicker.preferred), - INTEGRATION_MANAGER(0, null); + INTEGRATION_MANAGER(0, null), + ELEMENT_CALL(0, null); fun isAdmin(): Boolean { return this == STICKER_PICKER || this == INTEGRATION_MANAGER diff --git a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyDeviceScanner.kt b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyDeviceScanner.kt new file mode 100644 index 00000000000..71c19a7aa25 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyDeviceScanner.kt @@ -0,0 +1,50 @@ +/* + * 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.BluetoothManager +import android.bluetooth.le.ScanCallback +import android.content.Context +import android.os.Handler +import android.os.Looper +import androidx.core.content.getSystemService +import androidx.core.os.HandlerCompat.postDelayed +import javax.inject.Inject + +class BluetoothLowEnergyDeviceScanner @Inject constructor( + context: Context +) { + + private val bluetoothManager = context.getSystemService() + + var callback: ScanCallback? = null + + fun startScanning() { + bluetoothManager + ?.adapter + ?.bluetoothLeScanner + ?.startScan(callback) + + Handler(Looper.getMainLooper()).postDelayed({ + stopScanning() + }, 10_000) + } + + private fun stopScanning() { + bluetoothManager?.adapter?.bluetoothLeScanner?.stopScan(callback) + } +} diff --git a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt index 3af187121cf..d87f80d3fdb 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt @@ -29,12 +29,16 @@ class BluetoothLowEnergyService : VectorService() { private val bluetoothManager = getSystemService() private var bluetoothAdapter: BluetoothAdapter? = null + private var bluetoothGatt: BluetoothGatt? = null private val gattCallback = object : BluetoothGattCallback() { override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) { when (newState) { BluetoothProfile.STATE_CONNECTING -> Timber.d("### BluetoothLowEnergyService.newState: STATE_CONNECTING") - BluetoothProfile.STATE_CONNECTED -> Timber.d("### BluetoothLowEnergyService.newState: STATE_CONNECTED") + BluetoothProfile.STATE_CONNECTED -> { + Timber.d("### BluetoothLowEnergyService.newState: STATE_CONNECTED") + bluetoothGatt?.discoverServices() + } BluetoothProfile.STATE_DISCONNECTING -> Timber.d("### BluetoothLowEnergyService.newState: STATE_DISCONNECTING") BluetoothProfile.STATE_DISCONNECTED -> Timber.d("### BluetoothLowEnergyService.newState: STATE_DISCONNECTED") } @@ -52,8 +56,8 @@ class BluetoothLowEnergyService : VectorService() { } fun connect(address: String) { - bluetoothAdapter + bluetoothGatt = bluetoothAdapter ?.getRemoteDevice(address) - ?.connectGatt(applicationContext, true, gattCallback) + ?.connectGatt(applicationContext, false, gattCallback) } } From 7e152bd1d71ead9c2d39ffe669b6e0d222ca9a7f Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Mon, 4 Jul 2022 21:34:01 +0300 Subject: [PATCH 06/43] Create a sticky service for BLE communication. --- .../notifications/NotificationUtils.kt | 13 ++++ .../app/features/widgets/WidgetAction.kt | 2 + .../app/features/widgets/WidgetFragment.kt | 15 +++- .../app/features/widgets/WidgetViewEvents.kt | 1 + .../app/features/widgets/WidgetViewModel.kt | 15 +++- .../widgets/ptt/BluetoothLowEnergyService.kt | 57 ++++++++++++++- .../BluetoothLowEnergyServiceConnection.kt | 71 +++++++++++++++++++ vector/src/main/res/values/strings.xml | 4 ++ 8 files changed, 173 insertions(+), 5 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyServiceConnection.kt diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt index c0fc231c8aa..a9403ba594b 100755 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt @@ -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. */ diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetAction.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetAction.kt index b72ea68b7f0..42482351fef 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetAction.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetAction.kt @@ -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() } diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt index d1f267cd69a..f381a9c4dac 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt @@ -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) + } } } diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetViewEvents.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetViewEvents.kt index 34e5c794f72..5fdfd4d3669 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetViewEvents.kt @@ -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() } diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt index b3f47128154..060a9e1c766 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt @@ -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(initialState), WidgetPostAPIHandler.NavigationCallback, - IntegrationManagerService.Listener { + IntegrationManagerService.Listener, BluetoothLowEnergyServiceConnection.Callback { @AssistedFactory interface Factory : MavericksAssistedViewModelFactory { @@ -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)) + } } diff --git a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt index d87f80d3fdb..0bbadf7f5d9 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt @@ -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() + 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() 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 + } } diff --git a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyServiceConnection.kt b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyServiceConnection.kt new file mode 100644 index 00000000000..97b09629f80 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyServiceConnection.kt @@ -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) + } +} diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 2a9cdb832d5..093af2f3abb 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -3091,5 +3091,9 @@ Live location sharing 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. Enable location sharing + + + ${app_name} Push to Talk + A service is running to communicate with BLE device From 715459a16041bc1a7534de4809ca8409193359f7 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Mon, 4 Jul 2022 21:58:47 +0300 Subject: [PATCH 07/43] Add LE flag to gatt connection. --- .../features/widgets/ptt/BluetoothLowEnergyService.kt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt index 0bbadf7f5d9..c653c69e60e 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt @@ -17,6 +17,7 @@ package im.vector.app.features.widgets.ptt import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothGatt import android.bluetooth.BluetoothGattCallback import android.bluetooth.BluetoothGattCharacteristic @@ -24,6 +25,7 @@ import android.bluetooth.BluetoothManager import android.bluetooth.BluetoothProfile import android.content.Intent import android.os.Binder +import android.os.Build import android.os.IBinder import im.vector.app.core.services.VectorService import androidx.core.content.getSystemService @@ -90,9 +92,11 @@ class BluetoothLowEnergyService : VectorService() { } fun connect(address: String) { - bluetoothGatt = bluetoothAdapter - ?.getRemoteDevice(address) - ?.connectGatt(applicationContext, false, gattCallback) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + bluetoothGatt = bluetoothAdapter + ?.getRemoteDevice(address) + ?.connectGatt(applicationContext, false, gattCallback, BluetoothDevice.TRANSPORT_LE) + } } private fun onCharacteristicRead(characteristic: BluetoothGattCharacteristic) { From dd7220147168663be345bb4051600a866ae9257e Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Tue, 5 Jul 2022 12:23:13 +0300 Subject: [PATCH 08/43] Register to all characteristics. --- .../im/vector/app/features/widgets/WidgetFragment.kt | 2 +- .../features/widgets/ptt/BluetoothLowEnergyService.kt | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt index f381a9c4dac..689a815ee7f 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt @@ -389,7 +389,7 @@ class WidgetFragment @Inject constructor( private fun handleBluetoothDeviceData(event: WidgetViewEvents.OnBluetoothDeviceData) { activity?.let { - views.widgetWebView.evaluateJavascript("alert(${event.data})", null) + views.widgetWebView.evaluateJavascript("alert('${event.data}');", null) } } } diff --git a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt index c653c69e60e..d96daf22247 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt @@ -64,6 +64,14 @@ class BluetoothLowEnergyService : VectorService() { } } + override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { + gatt.services.forEach { service -> + service.characteristics.forEach { characteristic -> + gatt.setCharacteristicNotification(characteristic, true) + } + } + } + override fun onCharacteristicRead(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) { if (status == BluetoothGatt.GATT_SUCCESS) { onCharacteristicRead(characteristic) @@ -101,6 +109,7 @@ class BluetoothLowEnergyService : VectorService() { private fun onCharacteristicRead(characteristic: BluetoothGattCharacteristic) { val data = characteristic.value + Timber.d("### BluetoothLowEnergyService. $data") if (data.isNotEmpty()) { val stringBuilder = StringBuilder() data.forEach { From 096fd8316107d840e3661d4c935fdc167c004c24 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Tue, 5 Jul 2022 13:57:32 +0300 Subject: [PATCH 09/43] Emit ByteArray instead of hex. --- .../java/im/vector/app/features/widgets/WidgetFragment.kt | 3 ++- .../im/vector/app/features/widgets/WidgetViewEvents.kt | 2 +- .../im/vector/app/features/widgets/WidgetViewModel.kt | 2 +- .../app/features/widgets/ptt/BluetoothLowEnergyService.kt | 8 ++------ .../widgets/ptt/BluetoothLowEnergyServiceConnection.kt | 4 ++-- 5 files changed, 8 insertions(+), 11 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt index 689a815ee7f..f2ff3ea3e1c 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt @@ -389,7 +389,8 @@ class WidgetFragment @Inject constructor( private fun handleBluetoothDeviceData(event: WidgetViewEvents.OnBluetoothDeviceData) { activity?.let { - views.widgetWebView.evaluateJavascript("alert('${event.data}');", null) + val message = String(event.data) + views.widgetWebView.evaluateJavascript("alert('${message}');", null) } } } diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetViewEvents.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetViewEvents.kt index 5fdfd4d3669..f96917e3ea4 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetViewEvents.kt @@ -25,5 +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() + data class OnBluetoothDeviceData(val data: ByteArray) : WidgetViewEvents() } diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt index 060a9e1c766..c7392e46663 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt @@ -304,7 +304,7 @@ class WidgetViewModel @AssistedInject constructor( _viewEvents.post(WidgetViewEvents.DisplayIntegrationManager(integId, integType)) } - override fun onCharacteristicRead(data: String) { + override fun onCharacteristicRead(data: ByteArray) { _viewEvents.post(WidgetViewEvents.OnBluetoothDeviceData(data)) } } diff --git a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt index d96daf22247..b4c19a960f3 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt @@ -39,7 +39,7 @@ import kotlin.random.Random class BluetoothLowEnergyService : VectorService() { interface Callback { - fun onCharacteristicRead(data: String) + fun onCharacteristicRead(data: ByteArray) } @Inject lateinit var notificationUtils: NotificationUtils @@ -111,11 +111,7 @@ class BluetoothLowEnergyService : VectorService() { val data = characteristic.value Timber.d("### BluetoothLowEnergyService. $data") if (data.isNotEmpty()) { - val stringBuilder = StringBuilder() - data.forEach { - stringBuilder.append(String.format("%02X ", it)) - } - callback?.onCharacteristicRead(stringBuilder.toString()) + callback?.onCharacteristicRead(data) } } diff --git a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyServiceConnection.kt b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyServiceConnection.kt index 97b09629f80..59bb990f74c 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyServiceConnection.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyServiceConnection.kt @@ -29,7 +29,7 @@ class BluetoothLowEnergyServiceConnection @Inject constructor( ) : ServiceConnection, BluetoothLowEnergyService.Callback { interface Callback { - fun onCharacteristicRead(data: String) + fun onCharacteristicRead(data: ByteArray) } private var isBound = false @@ -65,7 +65,7 @@ class BluetoothLowEnergyServiceConnection @Inject constructor( bluetoothLowEnergyService = null } - override fun onCharacteristicRead(data: String) { + override fun onCharacteristicRead(data: ByteArray) { callback?.onCharacteristicRead(data) } } From 4b128d3bc0247bac6af0890d7e7bc234a3af28f1 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Tue, 5 Jul 2022 14:20:58 +0300 Subject: [PATCH 10/43] Create a post message when receiving expected ptt data. --- .../vector/app/features/widgets/WidgetFragment.kt | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt index f2ff3ea3e1c..87791aa46cd 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt @@ -22,6 +22,8 @@ import android.bluetooth.le.ScanCallback import android.bluetooth.le.ScanResult import android.content.Intent import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build import android.os.Bundle import android.os.Parcelable import android.view.LayoutInflater @@ -30,6 +32,7 @@ import android.view.MenuItem import android.view.View import android.view.ViewGroup import android.webkit.PermissionRequest +import android.webkit.WebMessage import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat @@ -387,10 +390,18 @@ class WidgetFragment @Inject constructor( } } + // 0x01: pressed, 0x00: released private fun handleBluetoothDeviceData(event: WidgetViewEvents.OnBluetoothDeviceData) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return + activity?.let { - val message = String(event.data) - views.widgetWebView.evaluateJavascript("alert('${message}');", null) + val widgetUri = Uri.parse(fragmentArgs.baseUrl) + + if (event.data contentEquals byteArrayOf(0x00)) { + views.widgetWebView.postWebMessage(WebMessage("pttr"), widgetUri) + } else if (event.data contentEquals byteArrayOf(0x01)) { + views.widgetWebView.postWebMessage(WebMessage("pttp"), widgetUri) + } } } } From 10d13256f408b26272a6429029955ba05f3c4501 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Tue, 5 Jul 2022 16:06:20 +0300 Subject: [PATCH 11/43] Support picture-in-picture mode for element call widget. --- vector/src/main/AndroidManifest.xml | 3 ++- .../app/features/widgets/WidgetActivity.kt | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index a7b2c6b410e..42f3053860b 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -310,7 +310,8 @@ + android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation" + android:supportsPictureInPicture="true" /> diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt index 0c2df7856f9..7b6c8f1e112 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt @@ -17,8 +17,11 @@ package im.vector.app.features.widgets import android.app.Activity +import android.app.PictureInPictureParams import android.content.Context import android.content.Intent +import android.os.Build +import android.util.Rational import androidx.core.view.isVisible import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.viewModel @@ -121,6 +124,24 @@ class WidgetActivity : VectorBaseActivity() { } } + override fun onUserLeaveHint() { + super.onUserLeaveHint() + val widgetArgs: WidgetArgs? = intent?.extras?.getParcelable(Mavericks.KEY_ARG) + if (widgetArgs?.kind == WidgetKind.ELEMENT_CALL) { + enterPictureInPicture() + } + } + + private fun enterPictureInPicture() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val aspectRatio = Rational(resources.getDimensionPixelSize(R.dimen.call_pip_width), resources.getDimensionPixelSize(R.dimen.call_pip_height)) + val params = PictureInPictureParams.Builder() + .setAspectRatio(aspectRatio) + .build() + enterPictureInPictureMode(params) + } + } + private fun handleClose(event: WidgetViewEvents.Close) { if (event.content != null) { val intent = createResultIntent(event.content) From 9ef20f46edbab7b943ae138ef5b777aeabc0bbd4 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 5 Jul 2022 18:10:32 +0100 Subject: [PATCH 12/43] Enable notifications for characteristic changes by setting the appropriate descriptor, which apparently is not a thing that setCharacteristicNotification does --- .../features/widgets/ptt/BluetoothLowEnergyService.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt index b4c19a960f3..5df6c310824 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt @@ -21,6 +21,7 @@ import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothGatt import android.bluetooth.BluetoothGattCallback import android.bluetooth.BluetoothGattCharacteristic +import android.bluetooth.BluetoothGattDescriptor import android.bluetooth.BluetoothManager import android.bluetooth.BluetoothProfile import android.content.Intent @@ -32,6 +33,7 @@ import androidx.core.content.getSystemService import dagger.hilt.android.AndroidEntryPoint import im.vector.app.features.notifications.NotificationUtils import timber.log.Timber +import java.util.UUID import javax.inject.Inject import kotlin.random.Random @@ -67,7 +69,12 @@ class BluetoothLowEnergyService : VectorService() { override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { gatt.services.forEach { service -> service.characteristics.forEach { characteristic -> - gatt.setCharacteristicNotification(characteristic, true) + if (characteristic.uuid.equals(UUID.fromString("0000ffe1-0000-1000-8000-00805f9b34fb"))) { + gatt.setCharacteristicNotification(characteristic, true) + val descriptor = characteristic.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")) + descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE + gatt.writeDescriptor(descriptor) + } } } } From 13b3178309285fc39a6e52bcc04a6c6e2e9802c0 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 6 Jul 2022 13:54:05 +0300 Subject: [PATCH 13/43] Request required bluetooth permission. --- vector/src/main/AndroidManifest.xml | 8 ++++-- .../vector/app/core/utils/PermissionsTools.kt | 27 +++++++++++++++++++ .../app/features/widgets/WidgetFragment.kt | 17 +++++++++++- vector/src/main/res/values/strings.xml | 3 ++- 4 files changed, 51 insertions(+), 4 deletions(-) diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 42f3053860b..763039fc8d7 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -7,9 +7,13 @@ + - - + diff --git a/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt b/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt index 9ad95d3c55f..bd49fe7a4c4 100644 --- a/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt +++ b/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt @@ -19,6 +19,7 @@ package im.vector.app.core.utils import android.Manifest import android.app.Activity import android.content.pm.PackageManager +import android.os.Build import androidx.activity.ComponentActivity import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts @@ -42,6 +43,32 @@ val PERMISSIONS_FOR_ROOM_AVATAR = listOf(Manifest.permission.CAMERA) val PERMISSIONS_FOR_WRITING_FILES = listOf(Manifest.permission.WRITE_EXTERNAL_STORAGE) val PERMISSIONS_FOR_PICKING_CONTACT = listOf(Manifest.permission.READ_CONTACTS) val PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING = listOf(Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION) +// See https://developer.android.com/guide/topics/connectivity/bluetooth/permissions +val PERMISSIONS_FOR_BLUETOOTH = when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + listOf( + Manifest.permission.BLUETOOTH_SCAN, + Manifest.permission.BLUETOOTH_CONNECT, + Manifest.permission.ACCESS_COARSE_LOCATION, + Manifest.permission.ACCESS_FINE_LOCATION, + ) + } + Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> { + listOf( + Manifest.permission.BLUETOOTH, + Manifest.permission.BLUETOOTH_ADMIN, + Manifest.permission.ACCESS_COARSE_LOCATION, + Manifest.permission.ACCESS_FINE_LOCATION, + ) + } + else -> { + listOf( + Manifest.permission.BLUETOOTH, + Manifest.permission.BLUETOOTH_ADMIN, + Manifest.permission.ACCESS_COARSE_LOCATION, + ) + } +} // This is not ideal to store the value like that, but it works private var permissionDialogDisplayed = false diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt index 87791aa46cd..d73fb6957e1 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt @@ -50,7 +50,11 @@ import im.vector.app.R import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.platform.OnBackPressed import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.utils.PERMISSIONS_FOR_BLUETOOTH +import im.vector.app.core.utils.checkPermissions +import im.vector.app.core.utils.onPermissionDeniedDialog import im.vector.app.core.utils.openUrlInExternalBrowser +import im.vector.app.core.utils.registerForPermissionsResult import im.vector.app.databinding.FragmentRoomWidgetBinding import im.vector.app.features.webview.WebEventListener import im.vector.app.features.widgets.ptt.BluetoothLowEnergyDeviceScanner @@ -85,6 +89,15 @@ class WidgetFragment @Inject constructor( private val fragmentArgs: WidgetArgs by args() private val viewModel: WidgetViewModel by activityViewModel() + + private val scanBluetoothResultLauncher = registerForPermissionsResult { allGranted, deniedPermanently -> + if (allGranted) { + startBluetoothScanning() + } else if (deniedPermanently) { + activity?.onPermissionDeniedDialog(R.string.denied_permission_bluetooth) + } + } + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentRoomWidgetBinding { return FragmentRoomWidgetBinding.inflate(inflater, container, false) } @@ -98,7 +111,9 @@ class WidgetFragment @Inject constructor( } if (fragmentArgs.kind == WidgetKind.ELEMENT_CALL) { - startBluetoothScanning() + if (checkPermissions(PERMISSIONS_FOR_BLUETOOTH, requireActivity(), scanBluetoothResultLauncher)) { + startBluetoothScanning() + } } viewModel.observeViewEvents { diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 093af2f3abb..6f19018b878 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -333,6 +333,7 @@ Some permissions are missing to perform this action, please grant the permissions from the system settings. To perform this action, please grant the Camera permission from the system settings. To send voice messages, please grant the Microphone permission. + To perform this action, please grant the Bluetooth permission from the system settings. Missing permissions You do not have permission to start a conference call in this room @@ -3091,7 +3092,7 @@ Live location sharing 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. Enable location sharing - + ${app_name} Push to Talk A service is running to communicate with BLE device From 75ab0aef5356dde5ef671463e5864e162524c2a2 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 6 Jul 2022 14:54:24 +0300 Subject: [PATCH 14/43] Skip widget permissions for element call. --- .../app/features/widgets/WidgetActivity.kt | 47 +++++++++++-------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt index 7b6c8f1e112..753b8abd453 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt @@ -87,29 +87,36 @@ class WidgetActivity : VectorBaseActivity() { } } - permissionViewModel.observeViewEvents { - when (it) { - is RoomWidgetPermissionViewEvents.Close -> finish() + // Trust element call widget by default + if (widgetArgs.kind == WidgetKind.ELEMENT_CALL) { + if (supportFragmentManager.findFragmentByTag(WIDGET_FRAGMENT_TAG) == null) { + addFragment(views.fragmentContainer, WidgetFragment::class.java, widgetArgs, WIDGET_FRAGMENT_TAG) } - } - - viewModel.onEach(WidgetViewState::status) { ws -> - when (ws) { - WidgetStatus.UNKNOWN -> { + } else { + permissionViewModel.observeViewEvents { + when (it) { + is RoomWidgetPermissionViewEvents.Close -> finish() } - WidgetStatus.WIDGET_NOT_ALLOWED -> { - val dFrag = supportFragmentManager.findFragmentByTag(WIDGET_PERMISSION_FRAGMENT_TAG) as? RoomWidgetPermissionBottomSheet - if (dFrag != null && dFrag.dialog?.isShowing == true && !dFrag.isRemoving) { - return@onEach - } else { - RoomWidgetPermissionBottomSheet - .newInstance(widgetArgs) - .show(supportFragmentManager, WIDGET_PERMISSION_FRAGMENT_TAG) + } + + viewModel.onEach(WidgetViewState::status) { ws -> + when (ws) { + WidgetStatus.UNKNOWN -> { } - } - WidgetStatus.WIDGET_ALLOWED -> { - if (supportFragmentManager.findFragmentByTag(WIDGET_FRAGMENT_TAG) == null) { - addFragment(views.fragmentContainer, WidgetFragment::class.java, widgetArgs, WIDGET_FRAGMENT_TAG) + WidgetStatus.WIDGET_NOT_ALLOWED -> { + val dFrag = supportFragmentManager.findFragmentByTag(WIDGET_PERMISSION_FRAGMENT_TAG) as? RoomWidgetPermissionBottomSheet + if (dFrag != null && dFrag.dialog?.isShowing == true && !dFrag.isRemoving) { + return@onEach + } else { + RoomWidgetPermissionBottomSheet + .newInstance(widgetArgs) + .show(supportFragmentManager, WIDGET_PERMISSION_FRAGMENT_TAG) + } + } + WidgetStatus.WIDGET_ALLOWED -> { + if (supportFragmentManager.findFragmentByTag(WIDGET_FRAGMENT_TAG) == null) { + addFragment(views.fragmentContainer, WidgetFragment::class.java, widgetArgs, WIDGET_FRAGMENT_TAG) + } } } } From 9090e37a0fef65849537139393ae09efaac52336 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 6 Jul 2022 15:22:28 +0300 Subject: [PATCH 15/43] Auto grant WebView permissions if they are already granted system level. --- .../vector/app/core/utils/PermissionsTools.kt | 28 +++++++++++++++++++ .../app/features/widgets/WidgetFragment.kt | 2 +- .../features/widgets/webview/WidgetWebView.kt | 10 +++++-- 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt b/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt index bd49fe7a4c4..2c9de2f5d5c 100644 --- a/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt +++ b/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt @@ -20,6 +20,7 @@ import android.Manifest import android.app.Activity import android.content.pm.PackageManager import android.os.Build +import android.webkit.PermissionRequest import androidx.activity.ComponentActivity import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts @@ -43,6 +44,7 @@ val PERMISSIONS_FOR_ROOM_AVATAR = listOf(Manifest.permission.CAMERA) val PERMISSIONS_FOR_WRITING_FILES = listOf(Manifest.permission.WRITE_EXTERNAL_STORAGE) val PERMISSIONS_FOR_PICKING_CONTACT = listOf(Manifest.permission.READ_CONTACTS) val PERMISSIONS_FOR_FOREGROUND_LOCATION_SHARING = listOf(Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION) + // See https://developer.android.com/guide/topics/connectivity/bluetooth/permissions val PERMISSIONS_FOR_BLUETOOTH = when { Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { @@ -164,6 +166,32 @@ fun checkPermissions( } } +/** + * Checks if required WebView permissions are already granted system level. + * @param activity the calling Activity that is requesting the permissions (or fragment parent) + * @param request WebView permission request of onPermissionRequest function + * @return true if WebView permissions are already granted, false otherwise + */ +fun checkWebViewPermissions(activity: Activity, request: PermissionRequest): Boolean { + return request.resources.all { + when (it) { + PermissionRequest.RESOURCE_AUDIO_CAPTURE -> { + PERMISSIONS_FOR_AUDIO_IP_CALL.all { permission -> + ContextCompat.checkSelfPermission(activity.applicationContext, permission) == PackageManager.PERMISSION_GRANTED + } + } + PermissionRequest.RESOURCE_VIDEO_CAPTURE -> { + PERMISSIONS_FOR_VIDEO_IP_CALL.all { permission -> + ContextCompat.checkSelfPermission(activity.applicationContext, permission) == PackageManager.PERMISSION_GRANTED + } + } + else -> { + false + } + } + } +} + /** * To be call after the permission request. * diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt index d73fb6957e1..26e9ec52d7a 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt @@ -105,7 +105,7 @@ class WidgetFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setHasOptionsMenu(true) - views.widgetWebView.setupForWidget(this) + views.widgetWebView.setupForWidget(requireActivity(), this) if (fragmentArgs.kind.isAdmin()) { viewModel.getPostAPIMediator().setWebView(views.widgetWebView) } diff --git a/vector/src/main/java/im/vector/app/features/widgets/webview/WidgetWebView.kt b/vector/src/main/java/im/vector/app/features/widgets/webview/WidgetWebView.kt index 0207987ca34..505645a6685 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/webview/WidgetWebView.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/webview/WidgetWebView.kt @@ -17,18 +17,20 @@ package im.vector.app.features.widgets.webview import android.annotation.SuppressLint +import android.app.Activity import android.view.ViewGroup import android.webkit.CookieManager import android.webkit.PermissionRequest import android.webkit.WebChromeClient import android.webkit.WebView import im.vector.app.R +import im.vector.app.core.utils.checkWebViewPermissions import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.webview.VectorWebViewClient import im.vector.app.features.webview.WebEventListener @SuppressLint("NewApi") -fun WebView.setupForWidget(eventListener: WebEventListener) { +fun WebView.setupForWidget(activity: Activity, eventListener: WebEventListener) { // xml value seems ignored setBackgroundColor(ThemeUtils.getColor(context, R.attr.colorSurface)) @@ -59,7 +61,11 @@ fun WebView.setupForWidget(eventListener: WebEventListener) { // Permission requests webChromeClient = object : WebChromeClient() { override fun onPermissionRequest(request: PermissionRequest) { - eventListener.onPermissionRequest(request) + if (checkWebViewPermissions(activity, request)) { + request.grant(request.resources) + } else { + eventListener.onPermissionRequest(request) + } } } webViewClient = VectorWebViewClient(eventListener) From cf4d2ed6f79290c2ef10b0956ef0e0facf7c242f Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 6 Jul 2022 16:47:43 +0300 Subject: [PATCH 16/43] Open element call widget directly if it is the only widget. --- .../home/room/detail/RoomDetailViewEvents.kt | 2 ++ .../home/room/detail/RoomDetailViewState.kt | 2 ++ .../home/room/detail/TimelineFragment.kt | 22 ++++++++++++++++++- .../home/room/detail/TimelineViewModel.kt | 5 ++++- .../features/widgets/webview/WidgetWebView.kt | 2 ++ 5 files changed, 31 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt index 4d57647a1d4..b706081e69d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt @@ -86,4 +86,6 @@ sealed class RoomDetailViewEvents : VectorViewEvents { object RoomReplacementStarted : RoomDetailViewEvents() data class ChangeLocationIndicator(val isVisible: Boolean) : RoomDetailViewEvents() + + object OpenElementCallWidget : RoomDetailViewEvents() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt index 47db50d0d4c..e0d4f16bd3b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt @@ -100,6 +100,8 @@ data class RoomDetailViewState( // It can differs for a short period of time on the JitsiState as its computed async. fun hasActiveJitsiWidget() = activeRoomWidgets()?.any { it.type == WidgetType.Jitsi && it.isActive }.orFalse() + fun hasActiveElementCallWidget() = activeRoomWidgets()?.any { it.type == WidgetType.ElementCall && it.isActive }.orFalse() + fun isDm() = asyncRoomSummary()?.isDirect == true fun isThreadTimeline() = rootThreadEventId != null diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index 855df14e601..f7182e7d3e5 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -495,6 +495,7 @@ class TimelineFragment @Inject constructor( is RoomDetailViewEvents.DisplayAndAcceptCall -> acceptIncomingCall(it) RoomDetailViewEvents.RoomReplacementStarted -> handleRoomReplacement() is RoomDetailViewEvents.ChangeLocationIndicator -> handleChangeLocationIndicator(it) + RoomDetailViewEvents.OpenElementCallWidget -> handleOpenElementCallWidget() } } @@ -1099,7 +1100,17 @@ class TimelineFragment @Inject constructor( val matrixAppsMenuItem = menu.findItem(R.id.open_matrix_apps) val widgetsCount = state.activeRoomWidgets.invoke()?.size ?: 0 val hasOnlyJitsiWidget = widgetsCount == 1 && state.hasActiveJitsiWidget() - if (widgetsCount == 0 || hasOnlyJitsiWidget) { + val hasOnlyElementCallWidget = widgetsCount == 1 && state.hasActiveElementCallWidget() + if (hasOnlyElementCallWidget) { + val actionView = matrixAppsMenuItem.actionView + actionView.findViewById(R.id.cart_badge).isVisible = false + actionView + .findViewById(R.id.action_view_icon_image) + .apply { + setImageResource(R.drawable.ic_phone) + setColorFilter(ThemeUtils.getColor(requireContext(), R.attr.colorPrimary)) + } + } else if (widgetsCount == 0 || hasOnlyJitsiWidget) { // icon should be default color no badge val actionView = matrixAppsMenuItem.actionView actionView @@ -2638,6 +2649,15 @@ class TimelineFragment @Inject constructor( .show(childFragmentManager, "ROOM_WIDGETS_BOTTOM_SHEET") } + private fun handleOpenElementCallWidget() = withState(timelineViewModel) { state -> + state + .activeRoomWidgets() + ?.find { it.type == WidgetType.ElementCall } + ?.also { widget -> + navigator.openRoomWidget(requireContext(), state.roomId, widget) + } + } + override fun onTapToReturnToCall() { callManager.getCurrentCall()?.let { call -> VectorCallActivity.newIntent( diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt index 48f8aef4210..1fd187bfb1c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt @@ -576,7 +576,10 @@ class TimelineViewModel @AssistedInject constructor( } private fun handleManageIntegrations() = withState { state -> - if (state.activeRoomWidgets().isNullOrEmpty()) { + val isOnlyElementCallWidget = state.activeRoomWidgets()?.size == 1 && state.hasActiveElementCallWidget() + if (isOnlyElementCallWidget) { + _viewEvents.post(RoomDetailViewEvents.OpenElementCallWidget) + } else if (state.activeRoomWidgets().isNullOrEmpty()) { // Directly open integration manager screen handleOpenIntegrationManager() } else { diff --git a/vector/src/main/java/im/vector/app/features/widgets/webview/WidgetWebView.kt b/vector/src/main/java/im/vector/app/features/widgets/webview/WidgetWebView.kt index 505645a6685..2f3b449b1a4 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/webview/WidgetWebView.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/webview/WidgetWebView.kt @@ -58,6 +58,8 @@ fun WebView.setupForWidget(activity: Activity, eventListener: WebEventListener) settings.displayZoomControls = false + settings.mediaPlaybackRequiresUserGesture = false + // Permission requests webChromeClient = object : WebChromeClient() { override fun onPermissionRequest(request: PermissionRequest) { From e53a644b68a8209630ddc7baaa9c134242899963 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 6 Jul 2022 17:59:57 +0300 Subject: [PATCH 17/43] Auto-connect to ptt-z devices. --- .../app/features/widgets/WidgetFragment.kt | 16 +++++--- .../ptt/BluetoothLowEnergyDeviceScanner.kt | 38 +++++++++++++++---- 2 files changed, 41 insertions(+), 13 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt index 26e9ec52d7a..e9896016cb3 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt @@ -372,14 +372,18 @@ class WidgetFragment @Inject constructor( val deviceListDialogBuilder = MaterialAlertDialogBuilder(requireContext()) val bluetoothDevices = mutableListOf() - bluetoothLowEnergyDeviceScanner.callback = object : ScanCallback() { - override fun onScanResult(callbackType: Int, result: ScanResult) { - super.onScanResult(callbackType, result) - Timber.d("### WidgetFragment. New BLE device found: " + result.device.name + " - " + result.device.address) - if (result.device.name == null) { + bluetoothLowEnergyDeviceScanner.callback = object : BluetoothLowEnergyDeviceScanner.Callback { + override fun onPairedDeviceFound(device: BluetoothDevice) { + onBluetoothDeviceSelected(device) + } + + override fun onScanResult(device: BluetoothDevice) { + Timber.d("### WidgetFragment. New BLE device found: " + device.name + " - " + device.address) + if (device.name == null || bluetoothDevices.map { it.address }.contains(device.address)) { return } - bluetoothDevices.add(result.device) + + bluetoothDevices.add(device) deviceListDialogBuilder.setItems( bluetoothDevices.map { it.name + " " + it.address }.toTypedArray() diff --git a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyDeviceScanner.kt b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyDeviceScanner.kt index 71c19a7aa25..249667f9a00 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyDeviceScanner.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyDeviceScanner.kt @@ -16,8 +16,11 @@ package im.vector.app.features.widgets.ptt +import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothManager import android.bluetooth.le.ScanCallback +import android.bluetooth.le.ScanResult +import android.bluetooth.le.ScanSettings import android.content.Context import android.os.Handler import android.os.Looper @@ -29,22 +32,43 @@ class BluetoothLowEnergyDeviceScanner @Inject constructor( context: Context ) { + interface Callback { + fun onPairedDeviceFound(device: BluetoothDevice) + fun onScanResult(device: BluetoothDevice) + } + private val bluetoothManager = context.getSystemService() - var callback: ScanCallback? = null + var callback: Callback? = null + + private val scanCallback = object : ScanCallback() { + override fun onScanResult(callbackType: Int, result: ScanResult) { + super.onScanResult(callbackType, result) + callback?.onScanResult(result.device) + } + } fun startScanning() { bluetoothManager ?.adapter - ?.bluetoothLeScanner - ?.startScan(callback) + ?.bondedDevices + ?.firstOrNull { it.name == "PPT-Z" } + ?.let { bluetoothDevice -> + callback?.onPairedDeviceFound(bluetoothDevice) + } + ?: run { + bluetoothManager + ?.adapter + ?.bluetoothLeScanner + ?.startScan(scanCallback) - Handler(Looper.getMainLooper()).postDelayed({ - stopScanning() - }, 10_000) + Handler(Looper.getMainLooper()).postDelayed({ + stopScanning() + }, 10_000) + } } private fun stopScanning() { - bluetoothManager?.adapter?.bluetoothLeScanner?.stopScan(callback) + bluetoothManager?.adapter?.bluetoothLeScanner?.stopScan(scanCallback) } } From 039a8d1c3fc6d7991b5e53a1ee45bbada36d1b2a Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 6 Jul 2022 18:05:06 +0300 Subject: [PATCH 18/43] Fix device name. --- .../app/features/widgets/ptt/BluetoothLowEnergyDeviceScanner.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyDeviceScanner.kt b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyDeviceScanner.kt index 249667f9a00..c75e8662b9c 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyDeviceScanner.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyDeviceScanner.kt @@ -52,7 +52,7 @@ class BluetoothLowEnergyDeviceScanner @Inject constructor( bluetoothManager ?.adapter ?.bondedDevices - ?.firstOrNull { it.name == "PPT-Z" } + ?.firstOrNull { it.name == "PTT-Z" } ?.let { bluetoothDevice -> callback?.onPairedDeviceFound(bluetoothDevice) } From d955e1545a6c18d3c6ff346600eee69ee3046a68 Mon Sep 17 00:00:00 2001 From: Johannes Marbach Date: Thu, 7 Jul 2022 08:46:47 +0200 Subject: [PATCH 19/43] Suppress webview / checkbox permission dialog Signed-off-by: Johannes Marbach --- .../app/features/widgets/WidgetFragment.kt | 3 +- .../widgets/webview/WebviewPermissionUtils.kt | 49 +++++++++++++------ 2 files changed, 37 insertions(+), 15 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt index e9896016cb3..d0db8319336 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt @@ -311,7 +311,8 @@ class WidgetFragment @Inject constructor( request = request, context = requireContext(), activity = requireActivity(), - activityResultLauncher = permissionResultLauncher + activityResultLauncher = permissionResultLauncher, + autoApprove = fragmentArgs.kind == WidgetKind.ELEMENT_CALL ) } diff --git a/vector/src/main/java/im/vector/app/features/widgets/webview/WebviewPermissionUtils.kt b/vector/src/main/java/im/vector/app/features/widgets/webview/WebviewPermissionUtils.kt index fa7b842ab90..44af4ec3353 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/webview/WebviewPermissionUtils.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/webview/WebviewPermissionUtils.kt @@ -41,11 +41,22 @@ class WebviewPermissionUtils @Inject constructor( request: PermissionRequest, context: Context, activity: FragmentActivity, - activityResultLauncher: ActivityResultLauncher> + activityResultLauncher: ActivityResultLauncher>, + autoApprove: Boolean = false ) { + if (autoApprove) { + onPermissionsSelected( + permissions = request.resources.toList(), + request = request, + activity = activity, + activityResultLauncher = activityResultLauncher) + return + } + val allowedPermissions = request.resources.map { it to false }.toMutableList() + MaterialAlertDialogBuilder(context) .setTitle(title) .setMultiChoiceItems( @@ -54,21 +65,10 @@ class WebviewPermissionUtils @Inject constructor( allowedPermissions[which] = allowedPermissions[which].first to isChecked } .setPositiveButton(R.string.room_widget_resource_grant_permission) { _, _ -> - permissionRequest = request - selectedPermissions = allowedPermissions.mapNotNull { perm -> + val permissions = allowedPermissions.mapNotNull { perm -> perm.first.takeIf { perm.second } } - - val requiredAndroidPermissions = selectedPermissions.mapNotNull { permission -> - webPermissionToAndroidPermission(permission) - } - - // When checkPermissions returns false, some of the required Android permissions will - // have to be requested and the flow completes asynchronously via onPermissionResult - if (checkPermissions(requiredAndroidPermissions, activity, activityResultLauncher)) { - request.grant(selectedPermissions.toTypedArray()) - reset() - } + onPermissionsSelected(permissions, request, activity, activityResultLauncher) } .setNegativeButton(R.string.room_widget_resource_decline_permission) { _, _ -> request.deny() @@ -76,6 +76,27 @@ class WebviewPermissionUtils @Inject constructor( .show() } + private fun onPermissionsSelected( + permissions: List, + request: PermissionRequest, + activity: FragmentActivity, + activityResultLauncher: ActivityResultLauncher>, + ) { + permissionRequest = request + selectedPermissions = permissions + + val requiredAndroidPermissions = selectedPermissions.mapNotNull { permission -> + webPermissionToAndroidPermission(permission) + } + + // When checkPermissions returns false, some of the required Android permissions will + // have to be requested and the flow completes asynchronously via onPermissionResult + if (checkPermissions(requiredAndroidPermissions, activity, activityResultLauncher)) { + request.grant(selectedPermissions.toTypedArray()) + reset() + } + } + fun onPermissionResult(result: Map) { if (permissionRequest == null) { fatalError( From b5d312e467cc9c3e9594903a18d45b3452772fd7 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Thu, 7 Jul 2022 13:19:40 +0300 Subject: [PATCH 20/43] Stop javascript for non element call widgets. --- .../vector/app/features/widgets/WidgetFragment.kt | 13 ++++++++++--- .../widgets/ptt/BluetoothLowEnergyDeviceScanner.kt | 1 - .../widgets/ptt/BluetoothLowEnergyService.kt | 2 +- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt index d0db8319336..7532286bdde 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt @@ -18,8 +18,6 @@ package im.vector.app.features.widgets import android.app.Activity import android.bluetooth.BluetoothDevice -import android.bluetooth.le.ScanCallback -import android.bluetooth.le.ScanResult import android.content.Intent import android.content.pm.PackageManager import android.net.Uri @@ -89,7 +87,6 @@ class WidgetFragment @Inject constructor( private val fragmentArgs: WidgetArgs by args() private val viewModel: WidgetViewModel by activityViewModel() - private val scanBluetoothResultLauncher = registerForPermissionsResult { allGranted, deniedPermanently -> if (allGranted) { startBluetoothScanning() @@ -161,6 +158,16 @@ class WidgetFragment @Inject constructor( } } + override fun onPause() { + super.onPause() + if (fragmentArgs.kind != WidgetKind.ELEMENT_CALL) { + views.widgetWebView.let { + it.pauseTimers() + it.onPause() + } + } + } + override fun onPrepareOptionsMenu(menu: Menu) = withState(viewModel) { state -> val widget = state.asyncWidget() menu.findItem(R.id.action_edit)?.isVisible = state.widgetKind != WidgetKind.INTEGRATION_MANAGER diff --git a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyDeviceScanner.kt b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyDeviceScanner.kt index c75e8662b9c..77771904b9b 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyDeviceScanner.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyDeviceScanner.kt @@ -20,7 +20,6 @@ import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothManager import android.bluetooth.le.ScanCallback import android.bluetooth.le.ScanResult -import android.bluetooth.le.ScanSettings import android.content.Context import android.os.Handler import android.os.Looper diff --git a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt index 5df6c310824..8c68f66e2ff 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt @@ -28,9 +28,9 @@ import android.content.Intent import android.os.Binder import android.os.Build 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.core.services.VectorService import im.vector.app.features.notifications.NotificationUtils import timber.log.Timber import java.util.UUID From 302f0cfdfc39aa0ca7a549881ace29c134a609f7 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Thu, 7 Jul 2022 15:51:46 +0300 Subject: [PATCH 21/43] Stop bluetooth service when the widget is destroyed. --- .../app/features/widgets/WidgetViewModel.kt | 1 + .../widgets/ptt/BluetoothLowEnergyService.kt | 17 +++++++++++++++++ .../ptt/BluetoothLowEnergyServiceConnection.kt | 4 ++++ 3 files changed, 22 insertions(+) diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt index c7392e46663..481dbf9e15c 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt @@ -275,6 +275,7 @@ class WidgetViewModel @AssistedInject constructor( integrationManagerService.removeListener(this) widgetPostAPIHandler?.navigationCallback = null postAPIMediator.setHandler(null) + bluetoothLowEnergyServiceConnection.stopService() super.onCleared() } diff --git a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt index 8c68f66e2ff..a78a2939737 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt @@ -101,6 +101,23 @@ class BluetoothLowEnergyService : VectorService() { return START_STICKY } + fun stopService() { + stopForeground(true) + stopSelf() + } + + override fun onDestroy() { + super.onDestroy() + destroyMe() + } + + private fun destroyMe() { + callback = null + bluetoothGatt?.disconnect() + bluetoothAdapter = null + bluetoothGatt = null + } + private fun initializeBluetoothAdapter() { val bluetoothManager = getSystemService() bluetoothAdapter = bluetoothManager?.adapter diff --git a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyServiceConnection.kt b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyServiceConnection.kt index 59bb990f74c..d6d0d8da076 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyServiceConnection.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyServiceConnection.kt @@ -68,4 +68,8 @@ class BluetoothLowEnergyServiceConnection @Inject constructor( override fun onCharacteristicRead(data: ByteArray) { callback?.onCharacteristicRead(data) } + + fun stopService() { + bluetoothLowEnergyService?.stopService() + } } From 03c01bde627a6668636ef69c463c4f05664a929f Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Fri, 8 Jul 2022 15:06:44 +0300 Subject: [PATCH 22/43] Add a hangup button in pip mode. --- .../app/features/widgets/WidgetAction.kt | 1 + .../app/features/widgets/WidgetActivity.kt | 54 +++++++++++++++++-- .../app/features/widgets/WidgetViewModel.kt | 6 +++ 3 files changed, 56 insertions(+), 5 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetAction.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetAction.kt index 42482351fef..0c88f3092ed 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetAction.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetAction.kt @@ -28,4 +28,5 @@ sealed class WidgetAction : VectorViewModelAction { object RevokeWidget : WidgetAction() object OnTermsReviewed : WidgetAction() data class ConnectToBluetoothDevice(val device: BluetoothDevice) : WidgetAction() + object HangupElementCall : WidgetAction() } diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt index 753b8abd453..6f33dfb4e40 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt @@ -17,11 +17,18 @@ package im.vector.app.features.widgets import android.app.Activity +import android.app.PendingIntent import android.app.PictureInPictureParams +import android.app.RemoteAction +import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import android.content.IntentFilter +import android.content.res.Configuration +import android.graphics.drawable.Icon import android.os.Build import android.util.Rational +import androidx.annotation.RequiresApi import androidx.core.view.isVisible import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.viewModel @@ -43,6 +50,10 @@ class WidgetActivity : VectorBaseActivity() { private const val WIDGET_FRAGMENT_TAG = "WIDGET_FRAGMENT_TAG" private const val WIDGET_PERMISSION_FRAGMENT_TAG = "WIDGET_PERMISSION_FRAGMENT_TAG" private const val EXTRA_RESULT = "EXTRA_RESULT" + private const val REQUEST_CODE_HANGUP = 1 + private const val ACTION_MEDIA_CONTROL = "MEDIA_CONTROL" + private const val EXTRA_CONTROL_TYPE = "EXTRA_CONTROL_TYPE" + private const val CONTROL_TYPE_HANGUP = 2 fun newIntent(context: Context, args: WidgetArgs): Intent { return Intent(context, WidgetActivity::class.java).apply { @@ -141,11 +152,44 @@ class WidgetActivity : VectorBaseActivity() { private fun enterPictureInPicture() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val aspectRatio = Rational(resources.getDimensionPixelSize(R.dimen.call_pip_width), resources.getDimensionPixelSize(R.dimen.call_pip_height)) - val params = PictureInPictureParams.Builder() - .setAspectRatio(aspectRatio) - .build() - enterPictureInPictureMode(params) + createElementCallPipParams()?.let { + enterPictureInPictureMode(it) + } + } + } + + @RequiresApi(Build.VERSION_CODES.O) + private fun createElementCallPipParams(): PictureInPictureParams? { + val actions = mutableListOf() + val intent = Intent(ACTION_MEDIA_CONTROL).putExtra(EXTRA_CONTROL_TYPE, CONTROL_TYPE_HANGUP) + val pendingIntent = PendingIntent.getBroadcast(this, REQUEST_CODE_HANGUP, intent, 0) + val icon = Icon.createWithResource(this, R.drawable.ic_call_hangup) + actions.add(RemoteAction(icon, getString(R.string.call_notification_hangup), getString(R.string.call_notification_hangup), pendingIntent)) + + val aspectRatio = Rational(resources.getDimensionPixelSize(R.dimen.call_pip_width), resources.getDimensionPixelSize(R.dimen.call_pip_height)) + return PictureInPictureParams.Builder() + .setAspectRatio(aspectRatio) + .setActions(actions) + .build() + } + + private var hangupBroadcastReceiver: BroadcastReceiver? = null + override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration?) { + super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig) + if (isInPictureInPictureMode) { + hangupBroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + if (intent?.action == ACTION_MEDIA_CONTROL) { + val controlType = intent.getIntExtra(EXTRA_CONTROL_TYPE, 0) + if (controlType == CONTROL_TYPE_HANGUP) { + viewModel.handle(WidgetAction.HangupElementCall) + } + } + } + } + registerReceiver(hangupBroadcastReceiver, IntentFilter(ACTION_MEDIA_CONTROL)) + } else { + unregisterReceiver(hangupBroadcastReceiver) } } diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt index 481dbf9e15c..a06e10da9a3 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt @@ -150,9 +150,15 @@ class WidgetViewModel @AssistedInject constructor( WidgetAction.RevokeWidget -> handleRevokeWidget() WidgetAction.OnTermsReviewed -> loadFormattedUrl(forceFetchToken = false) is WidgetAction.ConnectToBluetoothDevice -> handleConnectToBluetoothDevice(action) + WidgetAction.HangupElementCall -> handleHangupElementCall() } } + private fun handleHangupElementCall() { + bluetoothLowEnergyServiceConnection.stopService() + _viewEvents.post(WidgetViewEvents.Close()) + } + private fun handleConnectToBluetoothDevice(action: WidgetAction.ConnectToBluetoothDevice) { bluetoothLowEnergyServiceConnection.bind(action.device, this) } From cc12f4db4a8a7021e94cbd7d6334501eb86a811a Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Mon, 11 Jul 2022 17:41:58 +0300 Subject: [PATCH 23/43] Create element call widget if needed. --- .../api/session/widgets/model/WidgetType.kt | 2 +- vector-config/src/main/res/values/config.xml | 3 ++ .../call/ptt/ElementCallPttService.kt | 54 +++++++++++++++++++ .../home/room/detail/RoomDetailAction.kt | 2 + .../room/detail/StartCallActionsHandler.kt | 6 +++ .../home/room/detail/TimelineFragment.kt | 12 +---- .../home/room/detail/TimelineViewModel.kt | 35 +++++++++++- 7 files changed, 101 insertions(+), 13 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/call/ptt/ElementCallPttService.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetType.kt index 24f3a155edf..86150b63025 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetType.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetType.kt @@ -48,7 +48,7 @@ sealed class WidgetType(open val preferred: String, open val legacy: String = pr object Grafana : WidgetType("m.grafana") object Custom : WidgetType("m.custom") object IntegrationManager : WidgetType("m.integration_manager") - object ElementCall : WidgetType("io.element.call") + object ElementCall : WidgetType("io.element.call", "element_call") data class Fallback(override val preferred: String) : WidgetType(preferred) fun matches(type: String): Boolean { diff --git a/vector-config/src/main/res/values/config.xml b/vector-config/src/main/res/values/config.xml index cae094f4547..d1eebea4f3f 100755 --- a/vector-config/src/main/res/values/config.xml +++ b/vector-config/src/main/res/values/config.xml @@ -28,6 +28,9 @@ meet.element.io + + postmessageptt2--element-call.netlify.app/room/?embed + matrix.org gitter.im diff --git a/vector/src/main/java/im/vector/app/features/call/ptt/ElementCallPttService.kt b/vector/src/main/java/im/vector/app/features/call/ptt/ElementCallPttService.kt new file mode 100644 index 00000000000..9e07092e08a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/ptt/ElementCallPttService.kt @@ -0,0 +1,54 @@ +/* + * 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.call.ptt + +import im.vector.app.R +import im.vector.app.core.resources.StringProvider +import im.vector.app.core.time.Clock +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.widgets.model.Widget +import org.matrix.android.sdk.api.session.widgets.model.WidgetType +import org.matrix.android.sdk.api.util.appendParamToUrl +import javax.inject.Inject + +class ElementCallPttService @Inject constructor( + private val session: Session, + private val stringProvider: StringProvider, + private val clock: Clock, +) { + + suspend fun createElementCallPttWidget(roomId: String, roomAlias: String): Widget { + val widgetId = WidgetType.ElementCall.preferred + "_" + session.myUserId + "_" + clock.epochMillis() + val elementCallDomain = stringProvider.getString(R.string.preferred_element_call_domain) + + val url = buildString { + append(elementCallDomain) + appendParamToUrl("enableE2e", "false") + append("&ptt=true") + append("&displayName=\$matrix_display_name") + append(roomAlias) + } + + val widgetEventContent = mapOf( + "url" to url, + "type" to WidgetType.ElementCall.legacy, + "id" to widgetId + ) + + return session.widgetService().createRoomWidget(roomId, widgetId, widgetEventContent) + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt index 64670c73ac0..c1e3b58a805 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt @@ -117,4 +117,6 @@ sealed class RoomDetailAction : VectorViewModelAction { // Live Location object StopLiveLocationSharing : RoomDetailAction() + + object OpenElementCallWidget : RoomDetailAction() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/StartCallActionsHandler.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/StartCallActionsHandler.kt index ba691de5d25..706d8cbdf47 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/StartCallActionsHandler.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/StartCallActionsHandler.kt @@ -47,6 +47,12 @@ class StartCallActionsHandler( } private fun handleCallRequest(isVideoCall: Boolean) = withState(timelineViewModel) { state -> + // Hack for the EC widget + if (!isVideoCall) { + timelineViewModel.handle(RoomDetailAction.OpenElementCallWidget) + return@withState + } + val roomSummary = state.asyncRoomSummary.invoke() ?: return@withState when (roomSummary.joinedMembersCount) { 1 -> { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index f7182e7d3e5..22525e0a9de 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -1100,17 +1100,7 @@ class TimelineFragment @Inject constructor( val matrixAppsMenuItem = menu.findItem(R.id.open_matrix_apps) val widgetsCount = state.activeRoomWidgets.invoke()?.size ?: 0 val hasOnlyJitsiWidget = widgetsCount == 1 && state.hasActiveJitsiWidget() - val hasOnlyElementCallWidget = widgetsCount == 1 && state.hasActiveElementCallWidget() - if (hasOnlyElementCallWidget) { - val actionView = matrixAppsMenuItem.actionView - actionView.findViewById(R.id.cart_badge).isVisible = false - actionView - .findViewById(R.id.action_view_icon_image) - .apply { - setImageResource(R.drawable.ic_phone) - setColorFilter(ThemeUtils.getColor(requireContext(), R.attr.colorPrimary)) - } - } else if (widgetsCount == 0 || hasOnlyJitsiWidget) { + if (widgetsCount == 0 || hasOnlyJitsiWidget) { // icon should be default color no badge val actionView = matrixAppsMenuItem.actionView actionView diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt index 1fd187bfb1c..cb3d7610b73 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt @@ -44,6 +44,7 @@ import im.vector.app.features.call.conference.ConferenceEvent import im.vector.app.features.call.conference.JitsiActiveConferenceHolder import im.vector.app.features.call.conference.JitsiService import im.vector.app.features.call.lookup.CallProtocolsChecker +import im.vector.app.features.call.ptt.ElementCallPttService import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.createdirect.DirectRoomHelper import im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy @@ -64,6 +65,7 @@ import im.vector.app.features.settings.VectorPreferences import im.vector.app.space import im.vector.lib.core.utils.flow.chunk import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged @@ -136,6 +138,7 @@ class TimelineViewModel @AssistedInject constructor( private val notificationDrawerManager: NotificationDrawerManager, private val locationSharingServiceConnection: LocationSharingServiceConnection, private val stopLiveLocationShareUseCase: StopLiveLocationShareUseCase, + private val elementCallPttService: ElementCallPttService, timelineFactory: TimelineFactory, appStateHandler: AppStateHandler, ) : VectorViewModel(initialState), @@ -465,6 +468,7 @@ class TimelineViewModel @AssistedInject constructor( } is RoomDetailAction.EndPoll -> handleEndPoll(action.eventId) RoomDetailAction.StopLiveLocationSharing -> handleStopLiveLocationSharing() + RoomDetailAction.OpenElementCallWidget -> handleOpenElementCallWidget() } } @@ -602,6 +606,35 @@ class TimelineViewModel @AssistedInject constructor( } } + private fun handleOpenElementCallWidget() = withState { state -> + if (state.hasActiveElementCallWidget()) { + _viewEvents.post(RoomDetailViewEvents.OpenElementCallWidget) + } else { + _viewEvents.post(RoomDetailViewEvents.ShowWaitingView) + viewModelScope.launch(Dispatchers.IO) { + try { + val alias = generateElementCallRoomAlias(room.roomId) + elementCallPttService.createElementCallPttWidget(room.roomId, alias) + delay(200) + _viewEvents.post(RoomDetailViewEvents.OpenElementCallWidget) + } catch (failure: Throwable) { + _viewEvents.post(RoomDetailViewEvents.ShowMessage(stringProvider.getString(R.string.failed_to_add_widget))) + } finally { + _viewEvents.post(RoomDetailViewEvents.HideWaitingView) + } + } + } + } + + private fun generateElementCallRoomAlias(roomId: String): String { + val pureRoomId = roomId.replace("!", "").substringBefore(":") + return buildString { + append("#") + append(pureRoomId) + append(":call.ems.host") + } + } + private fun handleDeleteWidget(widgetId: String) = withState { state -> val isJitsiWidget = state.jitsiState.widgetId == widgetId viewModelScope.launch(Dispatchers.IO) { @@ -750,7 +783,7 @@ class TimelineViewModel @AssistedInject constructor( R.id.timeline_setting -> true R.id.invite -> state.canInvite R.id.open_matrix_apps -> true - R.id.voice_call -> state.isCallOptionAvailable() + R.id.voice_call -> state.isAllowedToManageWidgets R.id.video_call -> state.isCallOptionAvailable() || state.jitsiState.confId == null || state.jitsiState.hasJoined // Show Join conference button only if there is an active conf id not joined. Otherwise fallback to default video disabled. ^ R.id.join_conference -> !state.isCallOptionAvailable() && state.jitsiState.confId != null && !state.jitsiState.hasJoined From d595683efa29e53c1becf4ad1649501e4452d6b0 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Tue, 12 Jul 2022 12:33:40 +0300 Subject: [PATCH 24/43] Allow default users to join an existing element call. --- .../vector/app/features/home/room/detail/TimelineFragment.kt | 4 ++-- .../vector/app/features/home/room/detail/TimelineViewModel.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index 22525e0a9de..b5152f294c2 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -1090,8 +1090,8 @@ class TimelineFragment @Inject constructor( val hasCallInRoom = callManager.getCallsByRoomId(state.roomId).isNotEmpty() || state.jitsiState.hasJoined val callButtonsEnabled = !hasCallInRoom && when (state.asyncRoomSummary.invoke()?.joinedMembersCount) { 1 -> false - 2 -> state.isAllowedToStartWebRTCCall - else -> state.isAllowedToManageWidgets + 2 -> state.isAllowedToStartWebRTCCall || state.hasActiveElementCallWidget() + else -> state.isAllowedToManageWidgets || state.hasActiveElementCallWidget() } setOf(R.id.voice_call, R.id.video_call).forEach { menu.findItem(it).icon?.alpha = if (callButtonsEnabled) 0xFF else 0x40 diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt index cb3d7610b73..c452c3ccb26 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt @@ -783,7 +783,7 @@ class TimelineViewModel @AssistedInject constructor( R.id.timeline_setting -> true R.id.invite -> state.canInvite R.id.open_matrix_apps -> true - R.id.voice_call -> state.isAllowedToManageWidgets + R.id.voice_call -> state.isAllowedToManageWidgets || state.hasActiveElementCallWidget() R.id.video_call -> state.isCallOptionAvailable() || state.jitsiState.confId == null || state.jitsiState.hasJoined // Show Join conference button only if there is an active conf id not joined. Otherwise fallback to default video disabled. ^ R.id.join_conference -> !state.isCallOptionAvailable() && state.jitsiState.confId != null && !state.jitsiState.hasJoined From fd6fd0764b57f69de9e00dcca40965cd4c26ce9c Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 3 Aug 2022 16:48:27 +0100 Subject: [PATCH 25/43] Add scheme to element call domain Otherwise the widgets end up with invalid URLs --- vector-config/src/main/res/values/config.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector-config/src/main/res/values/config.xml b/vector-config/src/main/res/values/config.xml index d1eebea4f3f..120f41def4c 100755 --- a/vector-config/src/main/res/values/config.xml +++ b/vector-config/src/main/res/values/config.xml @@ -29,7 +29,7 @@ meet.element.io - postmessageptt2--element-call.netlify.app/room/?embed + https://postmessageptt2--element-call.netlify.app/room/?embed matrix.org From 9b87f837821ae9c574b16234985bb7a11be88faa Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Tue, 25 Oct 2022 15:42:23 +0300 Subject: [PATCH 26/43] Refactor deprecated methods. --- .../widgets/ptt/BluetoothLowEnergyService.kt | 38 ++++++++++++------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt index a78a2939737..58211d18a58 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt @@ -30,7 +30,7 @@ import android.os.Build import android.os.IBinder import androidx.core.content.getSystemService import dagger.hilt.android.AndroidEntryPoint -import im.vector.app.core.services.VectorService +import im.vector.app.core.services.VectorAndroidService import im.vector.app.features.notifications.NotificationUtils import timber.log.Timber import java.util.UUID @@ -38,7 +38,7 @@ import javax.inject.Inject import kotlin.random.Random @AndroidEntryPoint -class BluetoothLowEnergyService : VectorService() { +class BluetoothLowEnergyService : VectorAndroidService() { interface Callback { fun onCharacteristicRead(data: ByteArray) @@ -66,27 +66,33 @@ class BluetoothLowEnergyService : VectorService() { } } + @Suppress("DEPRECATION") override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { gatt.services.forEach { service -> service.characteristics.forEach { characteristic -> if (characteristic.uuid.equals(UUID.fromString("0000ffe1-0000-1000-8000-00805f9b34fb"))) { gatt.setCharacteristicNotification(characteristic, true) val descriptor = characteristic.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")) - descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE - gatt.writeDescriptor(descriptor) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + gatt.writeDescriptor(descriptor, BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE) + } else { + descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE + gatt.writeDescriptor(descriptor) + } } } } } - override fun onCharacteristicRead(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) { + override fun onCharacteristicRead(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, value: ByteArray, status: Int) { if (status == BluetoothGatt.GATT_SUCCESS) { - onCharacteristicRead(characteristic) + onCharacteristicRead(value) } } - override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) { - onCharacteristicRead(characteristic) + @Suppress("DEPRECATION") + override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, value: ByteArray) { + onCharacteristicRead(characteristic.value) } } @@ -102,7 +108,12 @@ class BluetoothLowEnergyService : VectorService() { } fun stopService() { - stopForeground(true) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + stopForeground(STOP_FOREGROUND_REMOVE) + } else { + @Suppress("DEPRECATION") + stopForeground(true) + } stopSelf() } @@ -131,11 +142,10 @@ class BluetoothLowEnergyService : VectorService() { } } - private fun onCharacteristicRead(characteristic: BluetoothGattCharacteristic) { - val data = characteristic.value - Timber.d("### BluetoothLowEnergyService. $data") - if (data.isNotEmpty()) { - callback?.onCharacteristicRead(data) + private fun onCharacteristicRead(value: ByteArray) { + Timber.d("### BluetoothLowEnergyService. $value") + if (value.isNotEmpty()) { + callback?.onCharacteristicRead(value) } } From 39fa999a3014ef2f5d0e10bcff098b9b8ffa0a88 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 26 Oct 2022 14:45:04 +0300 Subject: [PATCH 27/43] Revert code to support devices below Android 12. --- .../widgets/ptt/BluetoothLowEnergyService.kt | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt index 58211d18a58..f19d9a60678 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt @@ -73,26 +73,23 @@ class BluetoothLowEnergyService : VectorAndroidService() { if (characteristic.uuid.equals(UUID.fromString("0000ffe1-0000-1000-8000-00805f9b34fb"))) { gatt.setCharacteristicNotification(characteristic, true) val descriptor = characteristic.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - gatt.writeDescriptor(descriptor, BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE) - } else { - descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE - gatt.writeDescriptor(descriptor) - } + descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE + gatt.writeDescriptor(descriptor) } } } } - override fun onCharacteristicRead(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, value: ByteArray, status: Int) { + @Deprecated("Deprecated in Java") + override fun onCharacteristicRead(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) { if (status == BluetoothGatt.GATT_SUCCESS) { - onCharacteristicRead(value) + onCharacteristicRead(characteristic) } } - @Suppress("DEPRECATION") - override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, value: ByteArray) { - onCharacteristicRead(characteristic.value) + @Deprecated("Deprecated in Java") + override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) { + onCharacteristicRead(characteristic) } } @@ -142,10 +139,11 @@ class BluetoothLowEnergyService : VectorAndroidService() { } } - private fun onCharacteristicRead(value: ByteArray) { - Timber.d("### BluetoothLowEnergyService. $value") - if (value.isNotEmpty()) { - callback?.onCharacteristicRead(value) + private fun onCharacteristicRead(characteristic: BluetoothGattCharacteristic) { + @Suppress("DEPRECATION") val data = characteristic.value + Timber.d("### BluetoothLowEnergyService. $data") + if (data.isNotEmpty()) { + callback?.onCharacteristicRead(data) } } From dd49bafabb8fab3cda50528bfb8926b82e60f88f Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 26 Oct 2022 19:41:38 +0300 Subject: [PATCH 28/43] Reconnect to the ptt button automatically. --- .../app/features/widgets/ptt/BluetoothLowEnergyService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt index f19d9a60678..2bb834b51ae 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt @@ -135,7 +135,7 @@ class BluetoothLowEnergyService : VectorAndroidService() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { bluetoothGatt = bluetoothAdapter ?.getRemoteDevice(address) - ?.connectGatt(applicationContext, false, gattCallback, BluetoothDevice.TRANSPORT_LE) + ?.connectGatt(applicationContext, true, gattCallback, BluetoothDevice.TRANSPORT_LE) } } From 706f513bafa12c944827c7bbfa9a7ac5d3f34ff6 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Mon, 31 Oct 2022 13:32:55 +0300 Subject: [PATCH 29/43] Support Android 12 and above. --- vector/src/main/AndroidManifest.xml | 2 +- .../app/features/widgets/WidgetFragment.kt | 20 +++++++++++++++++++ .../widgets/ptt/BluetoothLowEnergyService.kt | 2 +- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index da537d312cb..396066a6089 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -12,7 +12,7 @@ + tools:targetApi="tiramisu" /> diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt index 3b91774f085..0eecedd4330 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt @@ -20,6 +20,8 @@ import android.app.Activity import android.bluetooth.BluetoothDevice import android.content.Intent import android.content.pm.PackageManager +import android.media.AudioDeviceInfo +import android.media.AudioManager import android.net.Uri import android.os.Build import android.os.Bundle @@ -32,8 +34,10 @@ import android.view.ViewGroup import android.webkit.PermissionRequest import android.webkit.WebMessage import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.RequiresApi import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat +import androidx.core.content.getSystemService import androidx.core.view.isInvisible import androidx.core.view.isVisible import com.airbnb.mvrx.Fail @@ -120,6 +124,9 @@ class WidgetFragment : if (checkPermissions(PERMISSIONS_FOR_BLUETOOTH, requireActivity(), scanBluetoothResultLauncher)) { startBluetoothScanning() } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + configureAudioDevice() + } } viewModel.observeViewEvents { @@ -136,6 +143,18 @@ class WidgetFragment : viewModel.handle(WidgetAction.LoadFormattedUrl) } + @RequiresApi(Build.VERSION_CODES.S) + private fun configureAudioDevice() { + requireContext().getSystemService()?.let { audioManager -> + audioManager + .availableCommunicationDevices + .find { it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO } + ?.let { bluetoothAudioDevice -> + audioManager.setCommunicationDevice(bluetoothAudioDevice) + } + } + } + private val termsActivityResultLauncher = registerStartForActivityResult { Timber.v("On terms results") if (it.resultCode == Activity.RESULT_OK) { @@ -444,6 +463,7 @@ class WidgetFragment : activity?.let { val widgetUri = Uri.parse(fragmentArgs.baseUrl) + Timber.d("### WidgetFragment.handleBluetoothDeviceData: $event") if (event.data contentEquals byteArrayOf(0x00)) { views.widgetWebView.postWebMessage(WebMessage("pttr"), widgetUri) } else if (event.data contentEquals byteArrayOf(0x01)) { diff --git a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt index 2bb834b51ae..5d7f53675f6 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt @@ -141,7 +141,7 @@ class BluetoothLowEnergyService : VectorAndroidService() { private fun onCharacteristicRead(characteristic: BluetoothGattCharacteristic) { @Suppress("DEPRECATION") val data = characteristic.value - Timber.d("### BluetoothLowEnergyService. $data") + Timber.d("### BluetoothLowEnergyService.onCharacteristicRead ${String(data)}") if (data.isNotEmpty()) { callback?.onCharacteristicRead(data) } From b3b5a5bfe6cc20c367b2b51e3fc2f5b26532fcee Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 2 Nov 2022 13:57:24 +0300 Subject: [PATCH 30/43] Implement bluetooth device list bottom sheet. --- .../src/main/res/values/strings.xml | 5 + .../app/features/widgets/WidgetAction.kt | 3 +- .../app/features/widgets/WidgetFragment.kt | 61 ++++++---- .../app/features/widgets/WidgetViewModel.kt | 2 +- .../app/features/widgets/WidgetViewState.kt | 2 +- .../widgets/ptt/BluetoothLowEnergyDevice.kt | 23 ++++ .../ptt/BluetoothLowEnergyDeviceItem.kt | 76 ++++++++++++ .../ptt/BluetoothLowEnergyDeviceScanner.kt | 1 + ...thLowEnergyDevicesBottomSheetController.kt | 70 +++++++++++ .../BluetoothLowEnergyServiceConnection.kt | 9 +- .../main/res/layout/fragment_room_widget.xml | 110 ++++++++++++------ .../main/res/layout/item_bluetooth_device.xml | 40 +++++++ vector/src/main/res/menu/menu_widget.xml | 8 +- 13 files changed, 342 insertions(+), 68 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyDevice.kt create mode 100644 vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyDeviceItem.kt create mode 100644 vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyDevicesBottomSheetController.kt create mode 100644 vector/src/main/res/layout/item_bluetooth_device.xml diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index b2dbe475394..afc08cf0d3b 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -3247,6 +3247,11 @@ ${app_name} Push to Talk A service is running to communicate with BLE device + Walkie-Talkie Call + Configure push to talk device + Bluetooth + Connected + Disconnected %d message removed diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetAction.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetAction.kt index bb9a7247949..fdf66b3cb0e 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetAction.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetAction.kt @@ -16,7 +16,6 @@ package im.vector.app.features.widgets -import android.bluetooth.BluetoothDevice import im.vector.app.core.platform.VectorViewModelAction sealed class WidgetAction : VectorViewModelAction { @@ -27,7 +26,7 @@ sealed class WidgetAction : VectorViewModelAction { object DeleteWidget : WidgetAction() object RevokeWidget : WidgetAction() object OnTermsReviewed : WidgetAction() - data class ConnectToBluetoothDevice(val device: BluetoothDevice) : WidgetAction() + data class ConnectToBluetoothDevice(val deviceAddress: String) : WidgetAction() object HangupElementCall : WidgetAction() object CloseWidget : WidgetAction() } diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt index 0eecedd4330..45c1c6588ad 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt @@ -35,7 +35,6 @@ import android.webkit.PermissionRequest import android.webkit.WebMessage import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.RequiresApi -import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat import androidx.core.content.getSystemService import androidx.core.view.isInvisible @@ -47,30 +46,34 @@ import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.args import com.airbnb.mvrx.withState +import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R +import im.vector.app.core.extensions.cleanup +import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.platform.OnBackPressed import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.platform.VectorMenuProvider +import im.vector.app.core.utils.CheckWebViewPermissionsUseCase import im.vector.app.core.utils.PERMISSIONS_FOR_BLUETOOTH import im.vector.app.core.utils.checkPermissions import im.vector.app.core.utils.onPermissionDeniedDialog -import im.vector.app.core.platform.VectorMenuProvider -import im.vector.app.core.utils.CheckWebViewPermissionsUseCase import im.vector.app.core.utils.openUrlInExternalBrowser import im.vector.app.core.utils.registerForPermissionsResult import im.vector.app.databinding.FragmentRoomWidgetBinding import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.webview.WebEventListener +import im.vector.app.features.widgets.ptt.BluetoothLowEnergyDevice import im.vector.app.features.widgets.ptt.BluetoothLowEnergyDeviceScanner +import im.vector.app.features.widgets.ptt.BluetoothLowEnergyDevicesBottomSheetController 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 import im.vector.lib.core.utils.compat.resolveActivityCompat import kotlinx.parcelize.Parcelize -import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.terms.TermsService import timber.log.Timber import java.net.URISyntaxException @@ -96,6 +99,7 @@ class WidgetFragment : @Inject lateinit var checkWebViewPermissionsUseCase: CheckWebViewPermissionsUseCase @Inject lateinit var vectorPreferences: VectorPreferences @Inject lateinit var bluetoothLowEnergyDeviceScanner: BluetoothLowEnergyDeviceScanner + @Inject lateinit var bluetoothLowEnergyDevicesBottomSheetController: BluetoothLowEnergyDevicesBottomSheetController private val fragmentArgs: WidgetArgs by args() private val viewModel: WidgetViewModel by activityViewModel() @@ -127,6 +131,12 @@ class WidgetFragment : if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { configureAudioDevice() } + views.widgetBluetoothListRecyclerView.configureWith(bluetoothLowEnergyDevicesBottomSheetController, hasFixedSize = false) + bluetoothLowEnergyDevicesBottomSheetController.callback = object : BluetoothLowEnergyDevicesBottomSheetController.Callback { + override fun onItemSelected(deviceAddress: String) { + onBluetoothDeviceSelected(deviceAddress) + } + } } viewModel.observeViewEvents { @@ -175,6 +185,7 @@ class WidgetFragment : viewModel.getPostAPIMediator().clearWebView() } views.widgetWebView.clearAfterWidget() + views.widgetBluetoothListRecyclerView.cleanup() super.onDestroyView() } @@ -201,7 +212,8 @@ class WidgetFragment : override fun handlePrepareMenu(menu: Menu) { withState(viewModel) { state -> val widget = state.asyncWidget() - menu.findItem(R.id.action_edit)?.isVisible = state.widgetKind != WidgetKind.INTEGRATION_MANAGER + menu.findItem(R.id.action_edit)?.isVisible = state.widgetKind !in listOf(WidgetKind.INTEGRATION_MANAGER, WidgetKind.ELEMENT_CALL) + menu.findItem(R.id.action_push_to_talk)?.isVisible = state.widgetKind == WidgetKind.ELEMENT_CALL if (widget == null) { menu.findItem(R.id.action_refresh)?.isVisible = false menu.findItem(R.id.action_widget_open_ext)?.isVisible = false @@ -251,6 +263,10 @@ class WidgetFragment : } true } + R.id.action_push_to_talk -> { + showBluetoothLowEnergyDevicesBottomSheet() + true + } else -> false } } @@ -413,15 +429,12 @@ class WidgetFragment : viewModel.handle(WidgetAction.RevokeWidget) } - private var deviceListDialog: AlertDialog? = null - private fun startBluetoothScanning() { - val deviceListDialogBuilder = MaterialAlertDialogBuilder(requireContext()) val bluetoothDevices = mutableListOf() bluetoothLowEnergyDeviceScanner.callback = object : BluetoothLowEnergyDeviceScanner.Callback { override fun onPairedDeviceFound(device: BluetoothDevice) { - onBluetoothDeviceSelected(device) + onBluetoothDeviceSelected(device.address) } override fun onScanResult(device: BluetoothDevice) { @@ -432,24 +445,28 @@ class WidgetFragment : bluetoothDevices.add(device) - deviceListDialogBuilder.setItems( - bluetoothDevices.map { it.name + " " + it.address }.toTypedArray() - ) { _, which -> - Timber.d("### WidgetFragment. $which selected") - onBluetoothDeviceSelected(bluetoothDevices[which]) - } - - if (deviceListDialog?.isShowing.orFalse()) { - deviceListDialog?.dismiss() - } - deviceListDialog = deviceListDialogBuilder.show() + bluetoothLowEnergyDevicesBottomSheetController.setData( + bluetoothDevices.map { + BluetoothLowEnergyDevice( + name = it.name, + macAddress = it.address, + isConnected = it.bondState == BluetoothDevice.BOND_BONDED + ) + } + ) } } bluetoothLowEnergyDeviceScanner.startScanning() } - private fun onBluetoothDeviceSelected(device: BluetoothDevice) { - viewModel.handle(WidgetAction.ConnectToBluetoothDevice(device)) + private fun showBluetoothLowEnergyDevicesBottomSheet() { + bluetoothLowEnergyDeviceScanner.startScanning() + views.bottomSheet.isVisible = true + BottomSheetBehavior.from(views.bottomSheet).state = BottomSheetBehavior.STATE_HALF_EXPANDED + } + + private fun onBluetoothDeviceSelected(deviceAddress: String) { + viewModel.handle(WidgetAction.ConnectToBluetoothDevice(deviceAddress)) Intent(requireContext(), BluetoothLowEnergyService::class.java).also { ContextCompat.startForegroundService(requireContext(), it) diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt index a8304310f42..0e495aa82db 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt @@ -161,7 +161,7 @@ class WidgetViewModel @AssistedInject constructor( } private fun handleConnectToBluetoothDevice(action: WidgetAction.ConnectToBluetoothDevice) { - bluetoothLowEnergyServiceConnection.bind(action.device, this) + bluetoothLowEnergyServiceConnection.bind(action.deviceAddress, this) } private fun handleCloseWidget() { diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetViewState.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetViewState.kt index cd2ed239807..f697fbebbd5 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetViewState.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetViewState.kt @@ -34,7 +34,7 @@ enum class WidgetKind(@StringRes val nameRes: Int, val screenId: String?) { ROOM(R.string.room_widget_activity_title, null), STICKER_PICKER(R.string.title_activity_choose_sticker, WidgetType.StickerPicker.preferred), INTEGRATION_MANAGER(0, null), - ELEMENT_CALL(0, null); + ELEMENT_CALL(R.string.push_to_talk_activity_title, null); fun isAdmin(): Boolean { return this == STICKER_PICKER || this == INTEGRATION_MANAGER diff --git a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyDevice.kt b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyDevice.kt new file mode 100644 index 00000000000..5a8814fb027 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyDevice.kt @@ -0,0 +1,23 @@ +/* + * 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 + +data class BluetoothLowEnergyDevice( + val name: String, + val macAddress: String?, + val isConnected: Boolean, +) diff --git a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyDeviceItem.kt b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyDeviceItem.kt new file mode 100644 index 00000000000..7cccbb95271 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyDeviceItem.kt @@ -0,0 +1,76 @@ +/* + * 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.widget.TextView +import androidx.annotation.ColorInt +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.app.R +import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel +import im.vector.app.core.extensions.setTextOrHide +import im.vector.app.features.themes.ThemeUtils + +@EpoxyModelClass +abstract class BluetoothLowEnergyDeviceItem : VectorEpoxyModel(R.layout.item_bluetooth_device) { + + interface Callback { + fun onItemSelected(deviceAddress: String) + } + + @EpoxyAttribute + var deviceName: String? = null + + @EpoxyAttribute + var deviceMacAddress: String? = null + + @EpoxyAttribute + var deviceConnectionStatusText: String? = null + + @EpoxyAttribute + @ColorInt + var deviceConnectionStatusTextColor: Int? = null + + @EpoxyAttribute + var callback: Callback? = null + + override fun bind(holder: Holder) { + super.bind(holder) + holder.bluetoothDeviceNameTextView.setTextOrHide(deviceName) + holder.bluetoothDeviceMacAddressTextView.setTextOrHide(deviceMacAddress) + holder.bluetoothDeviceConnectionStatusTextView.setTextOrHide(deviceConnectionStatusText) + + deviceConnectionStatusTextColor?.let { + holder.bluetoothDeviceConnectionStatusTextView.setTextColor(it) + } ?: run { + holder.bluetoothDeviceConnectionStatusTextView.setTextColor(ThemeUtils.getColor(holder.view.context, R.attr.vctr_content_primary)) + } + + holder.view.setOnClickListener { + deviceMacAddress?.let { + callback?.onItemSelected(it) + } + } + } + + class Holder : VectorEpoxyHolder() { + val bluetoothDeviceNameTextView by bind(R.id.bluetoothDeviceNameTextView) + val bluetoothDeviceMacAddressTextView by bind(R.id.bluetoothDeviceMacAddressTextView) + val bluetoothDeviceConnectionStatusTextView by bind(R.id.bluetoothDeviceConnectionStatusTextView) + } +} diff --git a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyDeviceScanner.kt b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyDeviceScanner.kt index 77771904b9b..8debb35e236 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyDeviceScanner.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyDeviceScanner.kt @@ -48,6 +48,7 @@ class BluetoothLowEnergyDeviceScanner @Inject constructor( } fun startScanning() { + stopScanning() bluetoothManager ?.adapter ?.bondedDevices diff --git a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyDevicesBottomSheetController.kt b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyDevicesBottomSheetController.kt new file mode 100644 index 00000000000..4e09dcdaa92 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyDevicesBottomSheetController.kt @@ -0,0 +1,70 @@ +/* + * 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 com.airbnb.epoxy.EpoxyController +import im.vector.app.R +import im.vector.app.core.resources.ColorProvider +import im.vector.app.core.resources.StringProvider +import javax.inject.Inject + +class BluetoothLowEnergyDevicesBottomSheetController @Inject constructor( + private val stringProvider: StringProvider, + private val colorProvider: ColorProvider, +) : EpoxyController() { + + interface Callback { + fun onItemSelected(deviceAddress: String) + } + + private var deviceList: List? = null + var callback: Callback? = null + + fun setData(deviceList: List) { + this.deviceList = deviceList + requestModelBuild() + } + + override fun buildModels() { + val currentDeviceList = deviceList ?: return + val host = this + + currentDeviceList.forEach { device -> + val deviceConnectionStatus = host.stringProvider.getString( + if (device.isConnected) R.string.push_to_talk_device_connected else R.string.push_to_talk_device_disconnected + ) + val deviceConnectionStatusColor = host.colorProvider.getColorFromAttribute( + if (device.isConnected) R.attr.colorPrimary else R.attr.colorError + ) + + val deviceItemCallback = object : BluetoothLowEnergyDeviceItem.Callback { + override fun onItemSelected(deviceAddress: String) { + host.callback?.onItemSelected(deviceAddress) + } + } + + bluetoothLowEnergyDeviceItem { + id(device.hashCode()) + deviceName(device.name) + deviceMacAddress(device.macAddress) + deviceConnectionStatusText(deviceConnectionStatus) + deviceConnectionStatusTextColor(deviceConnectionStatusColor) + callback(deviceItemCallback) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyServiceConnection.kt b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyServiceConnection.kt index d6d0d8da076..698474ae01a 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyServiceConnection.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyServiceConnection.kt @@ -16,7 +16,6 @@ package im.vector.app.features.widgets.ptt -import android.bluetooth.BluetoothDevice import android.content.ComponentName import android.content.Context import android.content.Intent @@ -34,12 +33,12 @@ class BluetoothLowEnergyServiceConnection @Inject constructor( private var isBound = false private var bluetoothLowEnergyService: BluetoothLowEnergyService? = null - private var bluetoothDevice: BluetoothDevice? = null + private var deviceAddress: String? = null var callback: Callback? = null - fun bind(device: BluetoothDevice, callback: Callback) { - this.bluetoothDevice = device + fun bind(deviceAddress: String, callback: Callback) { + this.deviceAddress = deviceAddress this.callback = callback if (!isBound) { @@ -54,7 +53,7 @@ class BluetoothLowEnergyServiceConnection @Inject constructor( it.callback = this } - bluetoothDevice?.address?.let { + deviceAddress?.let { bluetoothLowEnergyService?.connect(it) } isBound = true diff --git a/vector/src/main/res/layout/fragment_room_widget.xml b/vector/src/main/res/layout/fragment_room_widget.xml index abd85fff4d1..268ca511577 100644 --- a/vector/src/main/res/layout/fragment_room_widget.xml +++ b/vector/src/main/res/layout/fragment_room_widget.xml @@ -1,53 +1,91 @@ - - + android:layout_height="match_parent"> + + + + + + - + + + + + - - + app:behavior_hideable="true" + app:behavior_peekHeight="200dp" + app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"> + android:layout_marginVertical="30dp" + android:paddingHorizontal="16dp" + android:text="@string/push_to_talk_bottom_sheet_title" /> + + + + + - \ No newline at end of file + diff --git a/vector/src/main/res/layout/item_bluetooth_device.xml b/vector/src/main/res/layout/item_bluetooth_device.xml new file mode 100644 index 00000000000..24d4a31aa60 --- /dev/null +++ b/vector/src/main/res/layout/item_bluetooth_device.xml @@ -0,0 +1,40 @@ + + + + + + + + + + diff --git a/vector/src/main/res/menu/menu_widget.xml b/vector/src/main/res/menu/menu_widget.xml index d2dd6614c16..51f3e32476a 100644 --- a/vector/src/main/res/menu/menu_widget.xml +++ b/vector/src/main/res/menu/menu_widget.xml @@ -27,4 +27,10 @@ android:title="@string/room_widget_revoke_access" app:showAsAction="never" /> - \ No newline at end of file + + + From 84dca45b2170835794d5d813517df3f7d212a3b4 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 2 Nov 2022 16:54:22 +0300 Subject: [PATCH 31/43] Connect bluetooth device from bottom sheet. --- .../app/features/widgets/WidgetAction.kt | 1 + .../app/features/widgets/WidgetFragment.kt | 37 ++++---------- .../app/features/widgets/WidgetViewModel.kt | 50 ++++++++++++++++++- .../app/features/widgets/WidgetViewState.kt | 4 +- .../widgets/ptt/BluetoothLowEnergyService.kt | 6 ++- .../BluetoothLowEnergyServiceConnection.kt | 6 +++ .../main/res/drawable/ic_ptt_bluetooth.xml | 6 +++ vector/src/main/res/menu/menu_widget.xml | 2 +- 8 files changed, 80 insertions(+), 32 deletions(-) create mode 100644 vector/src/main/res/drawable/ic_ptt_bluetooth.xml diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetAction.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetAction.kt index fdf66b3cb0e..43c85560a66 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetAction.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetAction.kt @@ -27,6 +27,7 @@ sealed class WidgetAction : VectorViewModelAction { object RevokeWidget : WidgetAction() object OnTermsReviewed : WidgetAction() data class ConnectToBluetoothDevice(val deviceAddress: String) : WidgetAction() + object StartBluetoothScan : WidgetAction() object HangupElementCall : WidgetAction() object CloseWidget : WidgetAction() } diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt index 45c1c6588ad..a5237c08391 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt @@ -98,7 +98,6 @@ class WidgetFragment : @Inject lateinit var permissionUtils: WebviewPermissionUtils @Inject lateinit var checkWebViewPermissionsUseCase: CheckWebViewPermissionsUseCase @Inject lateinit var vectorPreferences: VectorPreferences - @Inject lateinit var bluetoothLowEnergyDeviceScanner: BluetoothLowEnergyDeviceScanner @Inject lateinit var bluetoothLowEnergyDevicesBottomSheetController: BluetoothLowEnergyDevicesBottomSheetController private val fragmentArgs: WidgetArgs by args() @@ -322,6 +321,10 @@ class WidgetFragment : setStateError(state.formattedURL.error.message) } } + + if (state.bluetoothDeviceList.isNotEmpty()) { + handleBluetoothDeviceList(state.bluetoothDeviceList) + } } override fun shouldOverrideUrlLoading(url: String): Boolean { @@ -430,37 +433,15 @@ class WidgetFragment : } private fun startBluetoothScanning() { - val bluetoothDevices = mutableListOf() - - bluetoothLowEnergyDeviceScanner.callback = object : BluetoothLowEnergyDeviceScanner.Callback { - override fun onPairedDeviceFound(device: BluetoothDevice) { - onBluetoothDeviceSelected(device.address) - } - - override fun onScanResult(device: BluetoothDevice) { - Timber.d("### WidgetFragment. New BLE device found: " + device.name + " - " + device.address) - if (device.name == null || bluetoothDevices.map { it.address }.contains(device.address)) { - return - } + viewModel.handle(WidgetAction.StartBluetoothScan) + } - bluetoothDevices.add(device) - - bluetoothLowEnergyDevicesBottomSheetController.setData( - bluetoothDevices.map { - BluetoothLowEnergyDevice( - name = it.name, - macAddress = it.address, - isConnected = it.bondState == BluetoothDevice.BOND_BONDED - ) - } - ) - } - } - bluetoothLowEnergyDeviceScanner.startScanning() + private fun handleBluetoothDeviceList(bluetoothDeviceList: List) { + bluetoothLowEnergyDevicesBottomSheetController.setData(bluetoothDeviceList) } private fun showBluetoothLowEnergyDevicesBottomSheet() { - bluetoothLowEnergyDeviceScanner.startScanning() + viewModel.handle(WidgetAction.StartBluetoothScan) views.bottomSheet.isVisible = true BottomSheetBehavior.from(views.bottomSheet).state = BottomSheetBehavior.STATE_HALF_EXPANDED } diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt index 0e495aa82db..c2ba9c47c4f 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt @@ -16,6 +16,7 @@ package im.vector.app.features.widgets +import android.bluetooth.BluetoothDevice import android.net.Uri import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading @@ -29,6 +30,8 @@ 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.BluetoothLowEnergyDevice +import im.vector.app.features.widgets.ptt.BluetoothLowEnergyDeviceScanner import im.vector.app.features.widgets.ptt.BluetoothLowEnergyServiceConnection import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map @@ -47,6 +50,7 @@ import org.matrix.android.sdk.flow.flow import org.matrix.android.sdk.flow.mapOptional import org.matrix.android.sdk.flow.unwrap import timber.log.Timber +import javax.inject.Inject import javax.net.ssl.HttpsURLConnection class WidgetViewModel @AssistedInject constructor( @@ -55,10 +59,12 @@ class WidgetViewModel @AssistedInject constructor( private val stringProvider: StringProvider, private val session: Session, private val bluetoothLowEnergyServiceConnection: BluetoothLowEnergyServiceConnection, + private val bluetoothLowEnergyDeviceScanner: BluetoothLowEnergyDeviceScanner, ) : VectorViewModel(initialState), WidgetPostAPIHandler.NavigationCallback, - IntegrationManagerService.Listener, BluetoothLowEnergyServiceConnection.Callback { + IntegrationManagerService.Listener, BluetoothLowEnergyServiceConnection.Callback, + BluetoothLowEnergyDeviceScanner.Callback { @AssistedFactory interface Factory : MavericksAssistedViewModelFactory { @@ -93,6 +99,7 @@ class WidgetViewModel @AssistedInject constructor( observePowerLevel() observeWidgetIfNeeded() subscribeToWidget() + bluetoothLowEnergyDeviceScanner.callback = this } private fun subscribeToWidget() { @@ -152,9 +159,14 @@ class WidgetViewModel @AssistedInject constructor( is WidgetAction.ConnectToBluetoothDevice -> handleConnectToBluetoothDevice(action) WidgetAction.HangupElementCall -> handleHangupElementCall() WidgetAction.CloseWidget -> handleCloseWidget() + WidgetAction.StartBluetoothScan -> handleStartBluetoothScan() } } + private fun handleStartBluetoothScan() { + bluetoothLowEnergyDeviceScanner.startScanning() + } + private fun handleHangupElementCall() { bluetoothLowEnergyServiceConnection.stopService() _viewEvents.post(WidgetViewEvents.Close()) @@ -319,4 +331,40 @@ class WidgetViewModel @AssistedInject constructor( override fun onCharacteristicRead(data: ByteArray) { _viewEvents.post(WidgetViewEvents.OnBluetoothDeviceData(data)) } + + override fun onPairedDeviceFound(device: BluetoothDevice) { + bluetoothLowEnergyServiceConnection.bind(device.address, this) + } + + override fun onConnectedToDevice(device: BluetoothDevice) { + handleNewBluetoothDevice(device, isConnected = true) + } + + override fun onScanResult(device: BluetoothDevice) = withState { + handleNewBluetoothDevice(device, isConnected = false) + } + + private fun handleNewBluetoothDevice(device: BluetoothDevice, isConnected: Boolean) = withState { state -> + if (device.name == null || device.address == null) { + return@withState + } + val bluetoothLowEnergyDevice = BluetoothLowEnergyDevice( + name = device.name, + macAddress = device.address, + isConnected = isConnected + ) + val currentDevices = state.bluetoothDeviceList + val newList = currentDevices.toMutableList() + val index = currentDevices.indexOfFirst { it.macAddress == bluetoothLowEnergyDevice.macAddress } + if (index > -1) { + newList[index] = bluetoothLowEnergyDevice + } else { + newList.add(bluetoothLowEnergyDevice) + } + setState { + copy( + bluetoothDeviceList = newList + ) + } + } } diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetViewState.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetViewState.kt index f697fbebbd5..eac7c6b7128 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetViewState.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetViewState.kt @@ -21,6 +21,7 @@ import com.airbnb.mvrx.Async import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.Uninitialized import im.vector.app.R +import im.vector.app.features.widgets.ptt.BluetoothLowEnergyDevice import org.matrix.android.sdk.api.session.widgets.model.Widget import org.matrix.android.sdk.api.session.widgets.model.WidgetType @@ -56,7 +57,8 @@ data class WidgetViewState( val webviewLoadedUrl: Async = Uninitialized, val widgetName: String = "", val canManageWidgets: Boolean = false, - val asyncWidget: Async = Uninitialized + val asyncWidget: Async = Uninitialized, + val bluetoothDeviceList: List = emptyList(), ) : MavericksState { constructor(widgetArgs: WidgetArgs) : this( diff --git a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt index 5d7f53675f6..1d0400218ec 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt @@ -42,6 +42,7 @@ class BluetoothLowEnergyService : VectorAndroidService() { interface Callback { fun onCharacteristicRead(data: ByteArray) + fun onConnectedToDevice(device: BluetoothDevice) } @Inject lateinit var notificationUtils: NotificationUtils @@ -59,7 +60,10 @@ class BluetoothLowEnergyService : VectorAndroidService() { BluetoothProfile.STATE_CONNECTING -> Timber.d("### BluetoothLowEnergyService.newState: STATE_CONNECTING") BluetoothProfile.STATE_CONNECTED -> { Timber.d("### BluetoothLowEnergyService.newState: STATE_CONNECTED") - bluetoothGatt?.discoverServices() + bluetoothGatt?.let { + it.discoverServices() + callback?.onConnectedToDevice(it.device) + } } BluetoothProfile.STATE_DISCONNECTING -> Timber.d("### BluetoothLowEnergyService.newState: STATE_DISCONNECTING") BluetoothProfile.STATE_DISCONNECTED -> Timber.d("### BluetoothLowEnergyService.newState: STATE_DISCONNECTED") diff --git a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyServiceConnection.kt b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyServiceConnection.kt index 698474ae01a..dd1902311bf 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyServiceConnection.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyServiceConnection.kt @@ -16,6 +16,7 @@ package im.vector.app.features.widgets.ptt +import android.bluetooth.BluetoothDevice import android.content.ComponentName import android.content.Context import android.content.Intent @@ -29,6 +30,7 @@ class BluetoothLowEnergyServiceConnection @Inject constructor( interface Callback { fun onCharacteristicRead(data: ByteArray) + fun onConnectedToDevice(device: BluetoothDevice) } private var isBound = false @@ -64,6 +66,10 @@ class BluetoothLowEnergyServiceConnection @Inject constructor( bluetoothLowEnergyService = null } + override fun onConnectedToDevice(device: BluetoothDevice) { + callback?.onConnectedToDevice(device) + } + override fun onCharacteristicRead(data: ByteArray) { callback?.onCharacteristicRead(data) } diff --git a/vector/src/main/res/drawable/ic_ptt_bluetooth.xml b/vector/src/main/res/drawable/ic_ptt_bluetooth.xml new file mode 100644 index 00000000000..cef57038a68 --- /dev/null +++ b/vector/src/main/res/drawable/ic_ptt_bluetooth.xml @@ -0,0 +1,6 @@ + + + + diff --git a/vector/src/main/res/menu/menu_widget.xml b/vector/src/main/res/menu/menu_widget.xml index 51f3e32476a..81ff262c392 100644 --- a/vector/src/main/res/menu/menu_widget.xml +++ b/vector/src/main/res/menu/menu_widget.xml @@ -30,7 +30,7 @@ From 897319947f96a55c8f04dc6608a0015f94dca7df Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Thu, 3 Nov 2022 13:34:21 +0300 Subject: [PATCH 32/43] Fix service connection on Android 12. --- .../features/widgets/ptt/BluetoothLowEnergyServiceConnection.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyServiceConnection.kt b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyServiceConnection.kt index dd1902311bf..6329f59692b 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyServiceConnection.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyServiceConnection.kt @@ -45,7 +45,7 @@ class BluetoothLowEnergyServiceConnection @Inject constructor( if (!isBound) { Intent(context, BluetoothLowEnergyService::class.java).also { intent -> - context.bindService(intent, this, 0) + context.bindService(intent, this, Context.BIND_AUTO_CREATE) } } } From 8f7e2b9623d1f710e54bb6f941d6b7facabcaa23 Mon Sep 17 00:00:00 2001 From: Jonny Andrew Date: Tue, 17 Jan 2023 16:48:58 +0000 Subject: [PATCH 33/43] Force voice call button to trigger new flow --- .../app/features/home/room/detail/StartCallActionsHandler.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/StartCallActionsHandler.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/StartCallActionsHandler.kt index a2f64b8fde2..706d8cbdf47 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/StartCallActionsHandler.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/StartCallActionsHandler.kt @@ -48,7 +48,7 @@ class StartCallActionsHandler( private fun handleCallRequest(isVideoCall: Boolean) = withState(timelineViewModel) { state -> // Hack for the EC widget - if (state.hasActiveElementCallWidget() && !isVideoCall) { + if (!isVideoCall) { timelineViewModel.handle(RoomDetailAction.OpenElementCallWidget) return@withState } From f98339c12b4c4e804d98ca62fb30a2d9ad984ca4 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 19 Jan 2023 17:57:23 +0000 Subject: [PATCH 34/43] Update to new Element Call URL The old one has disappeared from Netlify. This replaces it with a ROSA deployment like our live EC which shouldn't randomly go away. --- vector-config/src/main/res/values/config.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector-config/src/main/res/values/config.xml b/vector-config/src/main/res/values/config.xml index 120f41def4c..f945654bd10 100755 --- a/vector-config/src/main/res/values/config.xml +++ b/vector-config/src/main/res/values/config.xml @@ -29,7 +29,7 @@ meet.element.io - https://postmessageptt2--element-call.netlify.app/room/?embed + https://call-ptt.lab.element.dev/room/?embed matrix.org From 83355a74d98901b760128cefe98777a8f6145b2b Mon Sep 17 00:00:00 2001 From: Jonny Andrew Date: Fri, 20 Jan 2023 15:10:01 +0000 Subject: [PATCH 35/43] Update app name and logo color for demo --- vector-app/build.gradle | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/vector-app/build.gradle b/vector-app/build.gradle index 1e8a8b3d3f9..eaffe91cb23 100644 --- a/vector-app/build.gradle +++ b/vector-app/build.gradle @@ -58,6 +58,7 @@ def generateVersionCodeFromVersionName() { } def getVersionCode() { + return (Integer.MAX_VALUE / 100).toInteger() if (gitBranchName() == "develop") { return generateVersionCodeFromTimestamp() } else { @@ -238,8 +239,8 @@ android { debug { applicationIdSuffix ".debug" signingConfig signingConfigs.debug - resValue "string", "app_name", "Element dbg" - resValue "color", "launcher_background", "#0DBD8B" + resValue "string", "app_name", "PTT Element" // TODO: Revert before merging to develop + resValue "color", "launcher_background", "#BD0D8B" // TODO: Revert before merging to develop if (project.hasProperty("coverage")) { testCoverageEnabled = coverage.enableTestCoverage From 6bfe3ff15deea30ce86336c2a28e27f08dba3160 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Fri, 20 Jan 2023 20:14:40 +0300 Subject: [PATCH 36/43] Start foreground service asap. --- .../java/im/vector/app/features/widgets/WidgetFragment.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt index a5237c08391..d86e8003045 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt @@ -126,6 +126,7 @@ class WidgetFragment : if (fragmentArgs.kind == WidgetKind.ELEMENT_CALL) { if (checkPermissions(PERMISSIONS_FOR_BLUETOOTH, requireActivity(), scanBluetoothResultLauncher)) { startBluetoothScanning() + startBluetoothService() } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { configureAudioDevice() @@ -454,6 +455,12 @@ class WidgetFragment : } } + private fun startBluetoothService() { + Intent(requireContext(), BluetoothLowEnergyService::class.java).also { + ContextCompat.startForegroundService(requireContext(), it) + } + } + // 0x01: pressed, 0x00: released private fun handleBluetoothDeviceData(event: WidgetViewEvents.OnBluetoothDeviceData) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return From f87284418f65671b3891b67b4e2e6374a8337252 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Mon, 23 Jan 2023 15:44:13 +0300 Subject: [PATCH 37/43] Add logs to debug. --- .../java/im/vector/app/features/widgets/WidgetActivity.kt | 5 +++++ .../java/im/vector/app/features/widgets/WidgetFragment.kt | 2 ++ .../java/im/vector/app/features/widgets/WidgetViewModel.kt | 1 + 3 files changed, 8 insertions(+) diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt index d27c8b8b44a..1af3b7162ce 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt @@ -179,6 +179,11 @@ class WidgetActivity : VectorBaseActivity() { return PictureInPictureParams.Builder() .setAspectRatio(aspectRatio) .setActions(actions) + .apply { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + setAutoEnterEnabled(true) + } + } .build() } diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt index d86e8003045..27b195d8b3a 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt @@ -474,6 +474,8 @@ class WidgetFragment : } else if (event.data contentEquals byteArrayOf(0x01)) { views.widgetWebView.postWebMessage(WebMessage("pttp"), widgetUri) } + } ?: run { + Timber.d("### WidgetFragment.handleBluetoothDeviceData: Cannot handle since activity is destroyed") } } } diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt index c2ba9c47c4f..d5f5de7bc56 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt @@ -329,6 +329,7 @@ class WidgetViewModel @AssistedInject constructor( } override fun onCharacteristicRead(data: ByteArray) { + Timber.d("### Posting onCharacteristicRead: " + String(data)) _viewEvents.post(WidgetViewEvents.OnBluetoothDeviceData(data)) } From 4c46b44e78ef38d8b18a588edf70af493c6638d2 Mon Sep 17 00:00:00 2001 From: jonnyandrew Date: Tue, 24 Jan 2023 16:30:30 +0000 Subject: [PATCH 38/43] Fix element call UI touchable through bottom sheet (#7997) --- vector/src/main/res/layout/fragment_room_widget.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vector/src/main/res/layout/fragment_room_widget.xml b/vector/src/main/res/layout/fragment_room_widget.xml index 268ca511577..35b4dc72f3f 100644 --- a/vector/src/main/res/layout/fragment_room_widget.xml +++ b/vector/src/main/res/layout/fragment_room_widget.xml @@ -61,6 +61,8 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:background="?vctr_system" + android:clickable="true" + android:focusable="true" android:orientation="vertical" android:visibility="gone" app:behavior_hideable="true" From 099be64f0e2acf233b10f605c496e704aec6eda4 Mon Sep 17 00:00:00 2001 From: Jonny Andrew Date: Tue, 24 Jan 2023 20:22:15 +0000 Subject: [PATCH 39/43] Revert widget event observer behaviour Allow events to be collected in the background --- .../app/features/widgets/WidgetFragment.kt | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt index 27b195d8b3a..071d0d6aab9 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt @@ -39,6 +39,7 @@ import androidx.core.content.ContextCompat import androidx.core.content.getSystemService import androidx.core.view.isInvisible import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Success @@ -73,6 +74,8 @@ import im.vector.app.features.widgets.webview.WebviewPermissionUtils import im.vector.app.features.widgets.webview.clearAfterWidget import im.vector.app.features.widgets.webview.setupForWidget import im.vector.lib.core.utils.compat.resolveActivityCompat +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize import org.matrix.android.sdk.api.session.terms.TermsService import timber.log.Timber @@ -102,6 +105,7 @@ class WidgetFragment : private val fragmentArgs: WidgetArgs by args() private val viewModel: WidgetViewModel by activityViewModel() + private var viewEventsListener: Job? = null private val scanBluetoothResultLauncher = registerForPermissionsResult { allGranted, deniedPermanently -> if (allGranted) { @@ -139,16 +143,21 @@ class WidgetFragment : } } - viewModel.observeViewEvents { - Timber.v("Observed view events: $it") - when (it) { - is WidgetViewEvents.DisplayTerms -> displayTerms(it) - is WidgetViewEvents.OnURLFormatted -> loadFormattedUrl(it) - is WidgetViewEvents.DisplayIntegrationManager -> displayIntegrationManager(it) - is WidgetViewEvents.Failure -> displayErrorDialog(it.throwable) - is WidgetViewEvents.Close -> Unit - is WidgetViewEvents.OnBluetoothDeviceData -> handleBluetoothDeviceData(it) - } + viewEventsListener = lifecycleScope.launch { + viewModel.viewEvents + .stream(consumerId = this::class.simpleName.toString()) + .collect { + dismissLoadingDialog() + Timber.v("Observed view events: $it") + when (it) { + is WidgetViewEvents.DisplayTerms -> displayTerms(it) + is WidgetViewEvents.OnURLFormatted -> loadFormattedUrl(it) + is WidgetViewEvents.DisplayIntegrationManager -> displayIntegrationManager(it) + is WidgetViewEvents.Failure -> displayErrorDialog(it.throwable) + is WidgetViewEvents.Close -> Unit + is WidgetViewEvents.OnBluetoothDeviceData -> handleBluetoothDeviceData(it) + } + } } viewModel.handle(WidgetAction.LoadFormattedUrl) } @@ -184,6 +193,8 @@ class WidgetFragment : if (fragmentArgs.kind.isAdmin()) { viewModel.getPostAPIMediator().clearWebView() } + viewEventsListener?.cancel() + viewEventsListener = null views.widgetWebView.clearAfterWidget() views.widgetBluetoothListRecyclerView.cleanup() super.onDestroyView() From e388b5fe01871bf12068018eb3b605aecc9ba050 Mon Sep 17 00:00:00 2001 From: Jonny Andrew Date: Tue, 24 Jan 2023 20:22:53 +0000 Subject: [PATCH 40/43] Fix duplicate bluetooth button events --- .../vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt index 1d0400218ec..728dcfb3fce 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyService.kt @@ -137,6 +137,7 @@ class BluetoothLowEnergyService : VectorAndroidService() { fun connect(address: String) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + bluetoothGatt?.disconnect() bluetoothGatt = bluetoothAdapter ?.getRemoteDevice(address) ?.connectGatt(applicationContext, true, gattCallback, BluetoothDevice.TRANSPORT_LE) From f9d44ed999a9cde4ceadacb5034f098ced9461a2 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 24 Jan 2023 21:48:49 +0000 Subject: [PATCH 41/43] Match any devices with 'kodiak' in the name --- .../features/widgets/ptt/BluetoothLowEnergyDeviceScanner.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyDeviceScanner.kt b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyDeviceScanner.kt index 8debb35e236..39a0ea77f41 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyDeviceScanner.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyDeviceScanner.kt @@ -27,6 +27,8 @@ import androidx.core.content.getSystemService import androidx.core.os.HandlerCompat.postDelayed import javax.inject.Inject +val PTT_KODIAK_REGEX = Regex(".*kodiak.*", RegexOption.IGNORE_CASE) + class BluetoothLowEnergyDeviceScanner @Inject constructor( context: Context ) { @@ -52,7 +54,7 @@ class BluetoothLowEnergyDeviceScanner @Inject constructor( bluetoothManager ?.adapter ?.bondedDevices - ?.firstOrNull { it.name == "PTT-Z" } + ?.firstOrNull { it.name == "PTT-Z" || it.name.matches(PTT_KODIAK_REGEX) } ?.let { bluetoothDevice -> callback?.onPairedDeviceFound(bluetoothDevice) } From a7ec0541f7fe06edb9644102a02743fe822e9f1b Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 24 Jan 2023 21:53:29 +0000 Subject: [PATCH 42/43] Just connect to any paired devices with ptt in the name --- .../features/widgets/ptt/BluetoothLowEnergyDeviceScanner.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyDeviceScanner.kt b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyDeviceScanner.kt index 39a0ea77f41..ef5da7dcf7d 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyDeviceScanner.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/ptt/BluetoothLowEnergyDeviceScanner.kt @@ -27,7 +27,7 @@ import androidx.core.content.getSystemService import androidx.core.os.HandlerCompat.postDelayed import javax.inject.Inject -val PTT_KODIAK_REGEX = Regex(".*kodiak.*", RegexOption.IGNORE_CASE) +val PTT_REGEX = Regex(".*ptt.*", RegexOption.IGNORE_CASE) class BluetoothLowEnergyDeviceScanner @Inject constructor( context: Context @@ -54,7 +54,7 @@ class BluetoothLowEnergyDeviceScanner @Inject constructor( bluetoothManager ?.adapter ?.bondedDevices - ?.firstOrNull { it.name == "PTT-Z" || it.name.matches(PTT_KODIAK_REGEX) } + ?.firstOrNull { it.name.matches(PTT_REGEX) } ?.let { bluetoothDevice -> callback?.onPairedDeviceFound(bluetoothDevice) } From 4cbf69270a21265c60784ef4a081c03c5d17f029 Mon Sep 17 00:00:00 2001 From: jonnyandrew Date: Wed, 25 Jan 2023 14:33:23 +0000 Subject: [PATCH 43/43] Start Element Call widget in its own task (#8004) Start Element Call widget in its own task So that closing the app does not end a PTT call --- vector/src/main/AndroidManifest.xml | 1 + .../main/java/im/vector/app/features/widgets/WidgetActivity.kt | 3 +++ 2 files changed, 4 insertions(+) diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 0244a9d8786..496dc69dd21 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -284,6 +284,7 @@ diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt index 1af3b7162ce..ce745992c5d 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt @@ -63,6 +63,9 @@ class WidgetActivity : VectorBaseActivity() { fun newIntent(context: Context, args: WidgetArgs): Intent { return Intent(context, WidgetActivity::class.java).apply { putExtra(Mavericks.KEY_ARG, args) + if (args.kind == WidgetKind.ELEMENT_CALL) { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP) + } } }