Implement bluetooth device list bottom sheet.

This commit is contained in:
Onuray Sahin 2022-11-02 13:57:24 +03:00
parent 706f513baf
commit b3b5a5bfe6
13 changed files with 347 additions and 73 deletions

View File

@ -3247,6 +3247,11 @@
<!-- Element Call Widget - Push to Talk --> <!-- Element Call Widget - Push to Talk -->
<string name="push_to_talk_notification_title">${app_name} Push to Talk</string> <string name="push_to_talk_notification_title">${app_name} Push to Talk</string>
<string name="push_to_talk_notification_description">A service is running to communicate with BLE device</string> <string name="push_to_talk_notification_description">A service is running to communicate with BLE device</string>
<string name="push_to_talk_activity_title">Walkie-Talkie Call</string>
<string name="action_push_to_talk_configure_device">Configure push to talk device</string>
<string name="push_to_talk_bottom_sheet_title">Bluetooth</string>
<string name="push_to_talk_device_connected">Connected</string>
<string name="push_to_talk_device_disconnected">Disconnected</string>
<plurals name="room_removed_messages"> <plurals name="room_removed_messages">
<item quantity="one">%d message removed</item> <item quantity="one">%d message removed</item>

View File

@ -16,7 +16,6 @@
package im.vector.app.features.widgets package im.vector.app.features.widgets
import android.bluetooth.BluetoothDevice
import im.vector.app.core.platform.VectorViewModelAction import im.vector.app.core.platform.VectorViewModelAction
sealed class WidgetAction : VectorViewModelAction { sealed class WidgetAction : VectorViewModelAction {
@ -27,7 +26,7 @@ sealed class WidgetAction : VectorViewModelAction {
object DeleteWidget : WidgetAction() object DeleteWidget : WidgetAction()
object RevokeWidget : WidgetAction() object RevokeWidget : WidgetAction()
object OnTermsReviewed : WidgetAction() object OnTermsReviewed : WidgetAction()
data class ConnectToBluetoothDevice(val device: BluetoothDevice) : WidgetAction() data class ConnectToBluetoothDevice(val deviceAddress: String) : WidgetAction()
object HangupElementCall : WidgetAction() object HangupElementCall : WidgetAction()
object CloseWidget : WidgetAction() object CloseWidget : WidgetAction()
} }

View File

@ -35,7 +35,6 @@ import android.webkit.PermissionRequest
import android.webkit.WebMessage import android.webkit.WebMessage
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.core.view.isInvisible import androidx.core.view.isInvisible
@ -47,30 +46,34 @@ import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.args import com.airbnb.mvrx.args
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R 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.extensions.registerStartForActivityResult
import im.vector.app.core.platform.OnBackPressed import im.vector.app.core.platform.OnBackPressed
import im.vector.app.core.platform.VectorBaseFragment 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.PERMISSIONS_FOR_BLUETOOTH
import im.vector.app.core.utils.checkPermissions import im.vector.app.core.utils.checkPermissions
import im.vector.app.core.utils.onPermissionDeniedDialog 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.openUrlInExternalBrowser
import im.vector.app.core.utils.registerForPermissionsResult import im.vector.app.core.utils.registerForPermissionsResult
import im.vector.app.databinding.FragmentRoomWidgetBinding import im.vector.app.databinding.FragmentRoomWidgetBinding
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.webview.WebEventListener 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.BluetoothLowEnergyDeviceScanner
import im.vector.app.features.widgets.ptt.BluetoothLowEnergyDevicesBottomSheetController
import im.vector.app.features.widgets.ptt.BluetoothLowEnergyService import im.vector.app.features.widgets.ptt.BluetoothLowEnergyService
import im.vector.app.features.widgets.webview.WebviewPermissionUtils import im.vector.app.features.widgets.webview.WebviewPermissionUtils
import im.vector.app.features.widgets.webview.clearAfterWidget import im.vector.app.features.widgets.webview.clearAfterWidget
import im.vector.app.features.widgets.webview.setupForWidget import im.vector.app.features.widgets.webview.setupForWidget
import im.vector.lib.core.utils.compat.resolveActivityCompat import im.vector.lib.core.utils.compat.resolveActivityCompat
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.terms.TermsService import org.matrix.android.sdk.api.session.terms.TermsService
import timber.log.Timber import timber.log.Timber
import java.net.URISyntaxException import java.net.URISyntaxException
@ -96,6 +99,7 @@ class WidgetFragment :
@Inject lateinit var checkWebViewPermissionsUseCase: CheckWebViewPermissionsUseCase @Inject lateinit var checkWebViewPermissionsUseCase: CheckWebViewPermissionsUseCase
@Inject lateinit var vectorPreferences: VectorPreferences @Inject lateinit var vectorPreferences: VectorPreferences
@Inject lateinit var bluetoothLowEnergyDeviceScanner: BluetoothLowEnergyDeviceScanner @Inject lateinit var bluetoothLowEnergyDeviceScanner: BluetoothLowEnergyDeviceScanner
@Inject lateinit var bluetoothLowEnergyDevicesBottomSheetController: BluetoothLowEnergyDevicesBottomSheetController
private val fragmentArgs: WidgetArgs by args() private val fragmentArgs: WidgetArgs by args()
private val viewModel: WidgetViewModel by activityViewModel() private val viewModel: WidgetViewModel by activityViewModel()
@ -127,6 +131,12 @@ class WidgetFragment :
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
configureAudioDevice() configureAudioDevice()
} }
views.widgetBluetoothListRecyclerView.configureWith(bluetoothLowEnergyDevicesBottomSheetController, hasFixedSize = false)
bluetoothLowEnergyDevicesBottomSheetController.callback = object : BluetoothLowEnergyDevicesBottomSheetController.Callback {
override fun onItemSelected(deviceAddress: String) {
onBluetoothDeviceSelected(deviceAddress)
}
}
} }
viewModel.observeViewEvents { viewModel.observeViewEvents {
@ -175,6 +185,7 @@ class WidgetFragment :
viewModel.getPostAPIMediator().clearWebView() viewModel.getPostAPIMediator().clearWebView()
} }
views.widgetWebView.clearAfterWidget() views.widgetWebView.clearAfterWidget()
views.widgetBluetoothListRecyclerView.cleanup()
super.onDestroyView() super.onDestroyView()
} }
@ -201,7 +212,8 @@ class WidgetFragment :
override fun handlePrepareMenu(menu: Menu) { override fun handlePrepareMenu(menu: Menu) {
withState(viewModel) { state -> withState(viewModel) { state ->
val widget = state.asyncWidget() 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) { if (widget == null) {
menu.findItem(R.id.action_refresh)?.isVisible = false menu.findItem(R.id.action_refresh)?.isVisible = false
menu.findItem(R.id.action_widget_open_ext)?.isVisible = false menu.findItem(R.id.action_widget_open_ext)?.isVisible = false
@ -251,6 +263,10 @@ class WidgetFragment :
} }
true true
} }
R.id.action_push_to_talk -> {
showBluetoothLowEnergyDevicesBottomSheet()
true
}
else -> false else -> false
} }
} }
@ -413,15 +429,12 @@ class WidgetFragment :
viewModel.handle(WidgetAction.RevokeWidget) viewModel.handle(WidgetAction.RevokeWidget)
} }
private var deviceListDialog: AlertDialog? = null
private fun startBluetoothScanning() { private fun startBluetoothScanning() {
val deviceListDialogBuilder = MaterialAlertDialogBuilder(requireContext())
val bluetoothDevices = mutableListOf<BluetoothDevice>() val bluetoothDevices = mutableListOf<BluetoothDevice>()
bluetoothLowEnergyDeviceScanner.callback = object : BluetoothLowEnergyDeviceScanner.Callback { bluetoothLowEnergyDeviceScanner.callback = object : BluetoothLowEnergyDeviceScanner.Callback {
override fun onPairedDeviceFound(device: BluetoothDevice) { override fun onPairedDeviceFound(device: BluetoothDevice) {
onBluetoothDeviceSelected(device) onBluetoothDeviceSelected(device.address)
} }
override fun onScanResult(device: BluetoothDevice) { override fun onScanResult(device: BluetoothDevice) {
@ -432,24 +445,28 @@ class WidgetFragment :
bluetoothDevices.add(device) bluetoothDevices.add(device)
deviceListDialogBuilder.setItems( bluetoothLowEnergyDevicesBottomSheetController.setData(
bluetoothDevices.map { it.name + " " + it.address }.toTypedArray() bluetoothDevices.map {
) { _, which -> BluetoothLowEnergyDevice(
Timber.d("### WidgetFragment. $which selected") name = it.name,
onBluetoothDeviceSelected(bluetoothDevices[which]) macAddress = it.address,
} isConnected = it.bondState == BluetoothDevice.BOND_BONDED
)
if (deviceListDialog?.isShowing.orFalse()) { }
deviceListDialog?.dismiss() )
}
deviceListDialog = deviceListDialogBuilder.show()
} }
} }
bluetoothLowEnergyDeviceScanner.startScanning() bluetoothLowEnergyDeviceScanner.startScanning()
} }
private fun onBluetoothDeviceSelected(device: BluetoothDevice) { private fun showBluetoothLowEnergyDevicesBottomSheet() {
viewModel.handle(WidgetAction.ConnectToBluetoothDevice(device)) 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 { Intent(requireContext(), BluetoothLowEnergyService::class.java).also {
ContextCompat.startForegroundService(requireContext(), it) ContextCompat.startForegroundService(requireContext(), it)

View File

@ -161,7 +161,7 @@ class WidgetViewModel @AssistedInject constructor(
} }
private fun handleConnectToBluetoothDevice(action: WidgetAction.ConnectToBluetoothDevice) { private fun handleConnectToBluetoothDevice(action: WidgetAction.ConnectToBluetoothDevice) {
bluetoothLowEnergyServiceConnection.bind(action.device, this) bluetoothLowEnergyServiceConnection.bind(action.deviceAddress, this)
} }
private fun handleCloseWidget() { private fun handleCloseWidget() {

View File

@ -34,7 +34,7 @@ enum class WidgetKind(@StringRes val nameRes: Int, val screenId: String?) {
ROOM(R.string.room_widget_activity_title, null), ROOM(R.string.room_widget_activity_title, null),
STICKER_PICKER(R.string.title_activity_choose_sticker, WidgetType.StickerPicker.preferred), STICKER_PICKER(R.string.title_activity_choose_sticker, WidgetType.StickerPicker.preferred),
INTEGRATION_MANAGER(0, null), INTEGRATION_MANAGER(0, null),
ELEMENT_CALL(0, null); ELEMENT_CALL(R.string.push_to_talk_activity_title, null);
fun isAdmin(): Boolean { fun isAdmin(): Boolean {
return this == STICKER_PICKER || this == INTEGRATION_MANAGER return this == STICKER_PICKER || this == INTEGRATION_MANAGER

View File

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

View File

@ -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<BluetoothLowEnergyDeviceItem.Holder>(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<TextView>(R.id.bluetoothDeviceNameTextView)
val bluetoothDeviceMacAddressTextView by bind<TextView>(R.id.bluetoothDeviceMacAddressTextView)
val bluetoothDeviceConnectionStatusTextView by bind<TextView>(R.id.bluetoothDeviceConnectionStatusTextView)
}
}

View File

@ -48,6 +48,7 @@ class BluetoothLowEnergyDeviceScanner @Inject constructor(
} }
fun startScanning() { fun startScanning() {
stopScanning()
bluetoothManager bluetoothManager
?.adapter ?.adapter
?.bondedDevices ?.bondedDevices

View File

@ -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<BluetoothLowEnergyDevice>? = null
var callback: Callback? = null
fun setData(deviceList: List<BluetoothLowEnergyDevice>) {
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)
}
}
}
}

View File

@ -16,7 +16,6 @@
package im.vector.app.features.widgets.ptt package im.vector.app.features.widgets.ptt
import android.bluetooth.BluetoothDevice
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@ -34,12 +33,12 @@ class BluetoothLowEnergyServiceConnection @Inject constructor(
private var isBound = false private var isBound = false
private var bluetoothLowEnergyService: BluetoothLowEnergyService? = null private var bluetoothLowEnergyService: BluetoothLowEnergyService? = null
private var bluetoothDevice: BluetoothDevice? = null private var deviceAddress: String? = null
var callback: Callback? = null var callback: Callback? = null
fun bind(device: BluetoothDevice, callback: Callback) { fun bind(deviceAddress: String, callback: Callback) {
this.bluetoothDevice = device this.deviceAddress = deviceAddress
this.callback = callback this.callback = callback
if (!isBound) { if (!isBound) {
@ -54,7 +53,7 @@ class BluetoothLowEnergyServiceConnection @Inject constructor(
it.callback = this it.callback = this
} }
bluetoothDevice?.address?.let { deviceAddress?.let {
bluetoothLowEnergyService?.connect(it) bluetoothLowEnergyService?.connect(it)
} }
isBound = true isBound = true

View File

@ -1,53 +1,91 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<WebView <RelativeLayout
android:id="@+id/widgetWebView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
android:layout_alignParentBottom="true"
android:layout_marginBottom="0dp"
android:background="@android:color/transparent" />
<ProgressBar <WebView
android:id="@+id/widgetProgressBar" android:id="@+id/widgetWebView"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:layout_centerInParent="true" android:layout_alignParentBottom="true"
android:indeterminate="true" /> android:layout_marginBottom="0dp"
android:background="@android:color/transparent" />
<LinearLayout <ProgressBar
android:id="@+id/widgetErrorLayout" android:id="@+id/widgetProgressBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:background="?colorSurface"
android:orientation="horizontal"
android:padding="16dp"
android:visibility="gone"
tools:visibility="visible">
<ImageView
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center"
android:importantForAccessibility="no"
android:src="@drawable/error" />
<TextView
android:id="@+id/widgetErrorText"
style="@style/Widget.Vector.TextView.Subtitle"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_centerInParent="true"
android:layout_marginStart="@dimen/layout_horizontal_margin" android:indeterminate="true" />
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:textColor="?vctr_content_primary" <LinearLayout
android:textStyle="bold" android:id="@+id/widgetErrorLayout"
tools:text="Fail to load widget " /> android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:background="?colorSurface"
android:orientation="horizontal"
android:padding="16dp"
android:visibility="gone"
tools:visibility="visible">
<ImageView
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center"
android:importantForAccessibility="no"
android:src="@drawable/error" />
<TextView
android:id="@+id/widgetErrorText"
style="@style/Widget.Vector.TextView.Subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="@dimen/layout_horizontal_margin"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:textColor="?vctr_content_primary"
android:textStyle="bold"
tools:text="Fail to load widget " />
</LinearLayout>
</RelativeLayout>
<LinearLayout
android:id="@+id/bottomSheet"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?vctr_system"
android:orientation="vertical"
android:visibility="gone"
app:behavior_hideable="true"
app:behavior_peekHeight="200dp"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
<TextView
style="@style/TextAppearance.Vector.Headline.Medium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginVertical="30dp"
android:paddingHorizontal="16dp"
android:text="@string/push_to_talk_bottom_sheet_title" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?vctr_list_separator" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/widgetBluetoothListRecyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:listitem="@layout/item_bluetooth_device" />
</LinearLayout> </LinearLayout>
</RelativeLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/bluetoothDeviceNameTextView"
style="@style/TextAppearance.Vector.Body"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="30dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Device 1" />
<TextView
android:id="@+id/bluetoothDeviceMacAddressTextView"
style="@style/TextAppearance.Vector.Caption"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="@id/bluetoothDeviceNameTextView"
app:layout_constraintTop_toBottomOf="@id/bluetoothDeviceNameTextView"
tools:text="00:1B:44:11:3A:B7" />
<TextView
android:id="@+id/bluetoothDeviceConnectionStatusTextView"
style="@style/TextAppearance.Vector.Body"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
app:layout_constraintBottom_toBottomOf="@id/bluetoothDeviceMacAddressTextView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/bluetoothDeviceNameTextView"
tools:text="Disconnected"
tools:textColor="?colorError" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -27,4 +27,10 @@
android:title="@string/room_widget_revoke_access" android:title="@string/room_widget_revoke_access"
app:showAsAction="never" /> app:showAsAction="never" />
</menu> <item
android:id="@+id/action_push_to_talk"
android:title="@string/action_push_to_talk_configure_device"
android:icon="@drawable/quantum_ic_bluetooth_audio_grey600_24"
app:showAsAction="ifRoom" />
</menu>