Compare commits

...

6 Commits

Author SHA1 Message Date
David Baker
60c2c8e222 HACK: Grant all webview permissions 2022-06-14 18:33:15 +01:00
David Baker
9c81cf463b Allow media playback in webview without user interaction
also catch exception connecting to bluetooth socket
2022-06-14 18:05:40 +01:00
Johannes Marbach
d42fd395b0 Stop alerting socket read failures 2022-06-14 14:13:37 +02:00
Johannes Marbach
afcdf0b56f Use life-cycle scope / isActive / yield to properly cancel background job 2022-06-14 14:09:48 +02:00
David Baker
16ac259f4a Tweaks to make the rfcomm code work & send messages over postmessage 2022-06-06 18:49:51 +01:00
Johannes Marbach
8667831c8d Attempt to connect PTT device via RFCOMM 2022-06-03 10:48:20 +02:00
2 changed files with 173 additions and 2 deletions

View File

@ -16,9 +16,14 @@
package im.vector.app.features.widgets package im.vector.app.features.widgets
import android.Manifest
import android.app.Activity import android.app.Activity
import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothSocket
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.view.LayoutInflater import android.view.LayoutInflater
@ -27,9 +32,14 @@ import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.webkit.PermissionRequest import android.webkit.PermissionRequest
import android.webkit.WebMessage
import android.webkit.WebView
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.core.content.getSystemService
import androidx.core.view.isInvisible import androidx.core.view.isInvisible
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
@ -42,16 +52,27 @@ import im.vector.app.R
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.utils.checkPermissions
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.databinding.FragmentRoomWidgetBinding import im.vector.app.databinding.FragmentRoomWidgetBinding
import im.vector.app.features.webview.WebEventListener import im.vector.app.features.webview.WebEventListener
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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
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.io.IOException
import java.net.URISyntaxException import java.net.URISyntaxException
import java.nio.ByteBuffer
import java.nio.charset.StandardCharsets
import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
@Parcelize @Parcelize
@ -80,6 +101,7 @@ class WidgetFragment @Inject constructor(
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
setHasOptionsMenu(true) setHasOptionsMenu(true)
WebView.setWebContentsDebuggingEnabled(true);
views.widgetWebView.setupForWidget(this) views.widgetWebView.setupForWidget(this)
if (fragmentArgs.kind.isAdmin()) { if (fragmentArgs.kind.isAdmin()) {
viewModel.getPostAPIMediator().setWebView(views.widgetWebView) viewModel.getPostAPIMediator().setWebView(views.widgetWebView)
@ -266,6 +288,7 @@ class WidgetFragment @Inject constructor(
override fun onPageFinished(url: String) { override fun onPageFinished(url: String) {
viewModel.handle(WidgetAction.OnWebViewLoadingSuccess(url)) viewModel.handle(WidgetAction.OnWebViewLoadingSuccess(url))
connectBluetoothDevice()
} }
override fun onPageError(url: String, errorCode: Int, description: String) { override fun onPageError(url: String, errorCode: Int, description: String) {
@ -281,13 +304,14 @@ class WidgetFragment @Inject constructor(
} }
override fun onPermissionRequest(request: PermissionRequest) { override fun onPermissionRequest(request: PermissionRequest) {
permissionUtils.promptForPermissions( /*permissionUtils.promptForPermissions(
title = R.string.room_widget_resource_permission_title, title = R.string.room_widget_resource_permission_title,
request = request, request = request,
context = requireContext(), context = requireContext(),
activity = requireActivity(), activity = requireActivity(),
activityResultLauncher = permissionResultLauncher activityResultLauncher = permissionResultLauncher
) )*/
request.grant(request.resources);
} }
private fun displayTerms(displayTerms: WidgetViewEvents.DisplayTerms) { private fun displayTerms(displayTerms: WidgetViewEvents.DisplayTerms) {
@ -340,4 +364,149 @@ class WidgetFragment @Inject constructor(
private fun revokeWidget() { private fun revokeWidget() {
viewModel.handle(WidgetAction.RevokeWidget) viewModel.handle(WidgetAction.RevokeWidget)
} }
// Bluetooth hacks
private fun getRequiredBluetoothPermissions(): List<String> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
return listOf(Manifest.permission.BLUETOOTH_CONNECT)
}
return listOf(Manifest.permission.BLUETOOTH)
}
private val bluetoothPermissionLauncher = registerForPermissionsResult { allGranted, _ ->
if (allGranted) {
onBluetoothPermissionGranted()
} else {
informInWebView("Could not acquire Bluetooth permissions")
}
}
private var startedBluetoothConnection = false
private fun connectBluetoothDevice() {
if (startedBluetoothConnection) {
return
}
startedBluetoothConnection = true
if (checkPermissions(getRequiredBluetoothPermissions(), requireActivity(), bluetoothPermissionLauncher)) {
onBluetoothPermissionGranted()
}
}
private var bluetoothSocket: BluetoothSocket? = null
@RequiresApi(Build.VERSION_CODES.M)
private fun onBluetoothPermissionGranted() {
val manager = requireContext().getSystemService<BluetoothManager>()
val device = manager?.adapter?.bondedDevices?.firstOrNull {
//it.bluetoothClass.hasService(0x6666)
it.name.contains("PTT") || it.name.contains("B01")
}
if (device == null) {
val devices = manager?.adapter?.bondedDevices?.joinToString { "${it.name} (${it.address})" }
informInWebView("Could not locate PTT device among bonded devices $devices")
return
}
//informInWebView("Connected to PTT device ${device.name} (${device.address})")
Timber.i("Connected to PTT device ${device.name} (${device.address})")
bluetoothSocket = device.createRfcommSocketToServiceRecord(UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"));
// Alternatively: device.createInsecureRfcommSocketToServiceRecord(...)
try {
bluetoothSocket?.connect()
} catch (e: IOException) {
informInWebView("Failed to open RFCOMM socket: $e")
return;
}
//informInWebView("Opened RFCOMM socket")
//informInWebView("Created socket")
lifecycleScope.launch {
withContext(Dispatchers.IO) {
var inputStream = bluetoothSocket?.inputStream
val inputBuffer = ByteArray(1024)
/*try {
bluetoothSocket?.connect()
inputStream = bluetoothSocket?.inputStream
} catch (e: IOException){
informInWebView("Failed to open RFCOMM socket: $e")
bluetoothSocket = null
return@async;
}*/
//informInWebView("Opened RFCOMM socket")
while (isActive) {
/*if (bluetoothSocket?.isConnected != true) {
continue
}*/
try {
val numbytes = inputStream?.read(inputBuffer)
val strData = StandardCharsets.UTF_8.decode(numbytes?.let { ByteBuffer.wrap(inputBuffer, 0, it) });
//informInWebView("read $numbytes bytes: $strData, strdata is " + strData.length + " bytes, byte 6 is " + strData[6].code)
val widgetUri = Uri.parse(fragmentArgs.baseUrl)
//val widgetUri = Uri.EMPTY
if (strData.startsWith("+PTT=P")) {
//informInWebView("ptt down")
//val msg = JSONObject()
//msg.put("pttbutton", true)
yield()
requireActivity().runOnUiThread {
//views.widgetWebView.postWebMessage(WebMessage(msg.toString()), widgetUri)
views.widgetWebView.postWebMessage(WebMessage("pttp"), widgetUri)
}
} else if (strData.startsWith("+PTT=R")) {
//informInWebView("ptt up")
//val msg = JSONObject()
//msg.put("pttbutton", false)
yield()
requireActivity().runOnUiThread {
//views.widgetWebView.postWebMessage(WebMessage(msg.toString()), widgetUri)
views.widgetWebView.postWebMessage(WebMessage("pttr"), widgetUri)
}
}
} catch (e: IOException) {
yield()
// informInWebView("Failed to read from socket: $e")
break
}
//informInWebView("data: " + inputBuffer.to)
//val strData = StandardCharsets.UTF_8.decode(ByteBuffer.wrap(inputBuffer, 0, num));
//informInWebView("<$strData>")
//informInWebView("got data " + StandardCharsets.UTF_8.decode(ByteBuffer.wrap(inputBuffer)))
/*if (inputBuffer[5].toInt() == 80) {
informInWebView("Start talking inferred from ${inputBuffer.slice(0..32)}...")
} else {
informInWebView("Stop talking inferred from ${inputBuffer.slice(0..32)}...")
}*/
}
}
}
}
fun informInWebView(message: String) {
requireActivity().runOnUiThread {
views.widgetWebView.evaluateJavascript("alert('${message}');", null)
}
}
override fun onDestroy() {
super.onDestroy()
bluetoothSocket?.close()
}
} }

View File

@ -66,6 +66,8 @@ fun WebView.setupForWidget(eventListener: WebEventListener) {
val cookieManager = CookieManager.getInstance() val cookieManager = CookieManager.getInstance()
cookieManager.setAcceptThirdPartyCookies(this, false) cookieManager.setAcceptThirdPartyCookies(this, false)
settings.mediaPlaybackRequiresUserGesture = false
} }
fun WebView.clearAfterWidget() { fun WebView.clearAfterWidget() {