-
Notifications
You must be signed in to change notification settings - Fork 759
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Bluetooth-assisted Push to Talk (PTT) via Element Call #6464
base: develop
Are you sure you want to change the base?
Changes from 11 commits
f538e91
022ae91
09c435a
cf8056e
35dad02
7e152bd
715459a
dd72201
096fd83
4b128d3
10d1325
9ef20f4
13b3178
cea7193
75ab0ae
9090e37
cf4d2ed
e53a644
039a8d1
d955e15
ed1b861
b5d312e
302f0cf
03c01bd
cc12f4d
d595683
fd6fd07
48afcdd
07c0f79
9b87f83
39fa999
dd49baf
706f513
b3b5a5b
84dca45
8973199
8f7e2b9
f98339c
520eb2c
83355a7
6bfe3ff
67e391a
f872844
4c46b44
b552690
099be64
e388b5f
f9d44ed
a7ec054
4cbf692
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,8 +17,13 @@ | |
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 | ||
import android.os.Build | ||
import android.os.Bundle | ||
import android.os.Parcelable | ||
import android.view.LayoutInflater | ||
|
@@ -27,7 +32,10 @@ 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 | ||
import androidx.core.view.isInvisible | ||
import androidx.core.view.isVisible | ||
import com.airbnb.mvrx.Fail | ||
|
@@ -45,10 +53,13 @@ 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
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 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 +75,8 @@ data class WidgetArgs( | |
) : Parcelable | ||
|
||
class WidgetFragment @Inject constructor( | ||
private val permissionUtils: WebviewPermissionUtils | ||
private val permissionUtils: WebviewPermissionUtils, | ||
private val bluetoothLowEnergyDeviceScanner: BluetoothLowEnergyDeviceScanner, | ||
) : | ||
VectorBaseFragment<FragmentRoomWidgetBinding>(), | ||
WebEventListener, | ||
|
@@ -84,6 +96,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) { | ||
|
@@ -92,6 +109,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) | ||
|
@@ -128,14 +146,6 @@ class WidgetFragment @Inject constructor( | |
} | ||
} | ||
|
||
override fun onPause() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we possibly want to keep this for non-Element-Call widgets? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Exactly, it is in my to-do list. Nice catch though, thanks. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
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 +350,58 @@ 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<BluetoothDevice>() | ||
|
||
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) { | ||
viewModel.handle(WidgetAction.ConnectToBluetoothDevice(device)) | ||
|
||
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 | ||
|
||
activity?.let { | ||
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) | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<BluetoothManager>() | ||
|
||
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) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.