diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml
index b2dbe47539..afc08cf0d3 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 bb9a724794..fdf66b3cb0 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 0eecedd433..45c1c6588a 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 a8304310f4..0e495aa82d 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 cd2ed23980..f697fbebbd 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 0000000000..5a8814fb02
--- /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 0000000000..7cccbb9527
--- /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 77771904b9..8debb35e23 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 0000000000..4e09dcdaa9
--- /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 d6d0d8da07..698474ae01 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 abd85fff4d..268ca51157 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">
-
+
-
-
-
-
-
+ android:layout_centerInParent="true"
+ android:indeterminate="true" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
\ 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 0000000000..24d4a31aa6
--- /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 d2dd6614c1..51f3e32476 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
+
+
+