In my project, for security reasons, I need to be aware if USB debugging is enabled on device in order to disable some sensitive functionality.
I am able to figure out if it is currently enabled or not by doing the following check:
Settings.Global.getInt(context.contentResolver, Settings.Global.ADB_ENABLED, 0) == 1
But I also need to know when it is being enabled/disabled reactively. So I need to have a broadcast receiver which listens for this setting change events.
I searched the web but did not even found any question about this and even not found if Android even has such a broadcast receiver or not.
So any information related to this topic would be useful.
Well I found the answer to this question myself.
Actually there is no BroadcastReceiver for tracking changes of USB debugging setting. However, because Android settings are mostly exposed through ContentProvider Api we can register a ContentObserver on any data exposed through ContentProvider.
Below is the code snipped that did the work for me for USB debugging, but with slightly changes this can work to listen changes for almost any other Android setting change:
class UsbDebuggingStateObserver private constructor(val context: Context, handler: Handler, val changeListener: (Boolean) -> Unit) : ContentObserver(handler) {
private var lastEnabled: Boolean = isUsbDebuggingEnabled()
override fun onChange(selfChange: Boolean) {
val nowEnabled = isUsbDebuggingEnabled()
if(lastEnabled != nowEnabled){
lastEnabled = nowEnabled
changeListener.invoke(nowEnabled)
}
}
fun isUsbDebuggingEnabled(): Boolean {
return Settings.Global.getInt(context.contentResolver, Settings.Global.ADB_ENABLED, 0) == 1
}
companion object {
fun startObserving(context: Context, changeListener: (Boolean) -> Unit){
val observer = UsbDebuggingStateObserver(context, Handler(Looper.getMainLooper()), changeListener)
context.contentResolver
.registerContentObserver(Settings.Global.getUriFor(Settings.Global.ADB_ENABLED), true, observer)
}
}
}
And the usage is as follows:
UsbDebuggingStateObserver.startObserving(app) { enabled ->
// your code here
}
Note, I needed to observe this setting through whole lifetime of my application, so I start observing in onCreate of my application class and never stop. If you have different situation you might need also to unregister the observer.
Related
I have an external camera that's connected via a USB C dongle to my Android tablet. My goal is to have a constant stream of data from the camera into my phone, showing it to the user and allowing him to record it and save it to the local storage.
I am following the official docs from the following link -
https://developer.android.com/guide/topics/connectivity/usb/host#working-d
And I have spent the last couple of hours trying to figure out how things work, mapping the interfaces and endpoints, eventually finding an interface that has an endpoint that when I call bulkTransfer() on, does not return a failed value (-1).
I currently am facing 2 issues:
I have indeed got a valid response from the bulkTransfer() function, but my ByteArray does not fill with relevant information - when trying to print out the values they are all 0's. I though it may be a wrong endpoint as suggested in the official docs, but I have tried all combinations of interfaces and endpoints until I get an indexOutOfBoundException. That combination of interface + endpoint that I used is the only one that produced a valid bulk response. What am I missing?
I am looking for a stream of data that doesn't stop, but it seems like when calling bulkTransfer() it's one a one time oppression, unlike CameraX library for example that I get a constant callback each time a new chunck of data is available.
Here is the code on my main screen -
LaunchedEffect(key1 = true) {
val usbManager = context.getSystemService(Context.USB_SERVICE) as UsbManager
val filter = IntentFilter(ACTION_USB_PERMISSION)
registerReceiver(context, UsbBroadcastReceiver(), filter, RECEIVER_NOT_EXPORTED)
val hdCamera = usbManager.deviceList.values.find { device ->
val name = device.productName ?: return#LaunchedEffect
name.contains("HD camera")
} ?: return#LaunchedEffect
val permissionIntent = PendingIntent.getBroadcast(
context,
0, Intent(ACTION_USB_PERMISSION),
0
)
usbManager.requestPermission(hdCamera, permissionIntent)
}
And here is my BroadcastReceiver -
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action != ACTION_USB_PERMISSION) return
synchronized(this) {
val usbManager = context?.getSystemService(Context.USB_SERVICE) as UsbManager
val device: UsbDevice? = intent.getParcelable(UsbManager.EXTRA_DEVICE)
val usbInterface = device?.getInterface(0)
val endpoint = usbInterface?.getEndpoint(1) ?: return#synchronized
usbManager.openDevice(device)?.apply {
val array = ByteArray(endpoint.maxPacketSize)
claimInterface(usbInterface, true)
val bulkTransfer = bulkTransfer(endpoint, array, array.size, 0)
Log.d("defaultAppDebuger", "bulk array: $bulkTransfer") //prints a valid number - 512
array.forEach {
Log.d("defaultAppDebuger", "bulk array: $it") //the array values are empty
}
}
}
}
edit:
I have tried to move the BroadcastReceiver code to an async coroutine thinking that the loading of the information is related to the fact that I am in the wrong thread. Still didn't work, I get a valid result from the bulkTransfer and the byteArray is not filled -
fun BroadcastReceiver.goAsync(
context: CoroutineContext = Dispatchers.IO,
block: suspend CoroutineScope.() -> Unit
) {
val pendingResult = goAsync()
CoroutineScope(SupervisorJob()).launch(context) {
try {
block()
} finally {
pendingResult.finish()
}
}
}
override fun onReceive(context: Context?, intent: Intent?) = goAsync { .... }
Thanks!
After carefully researching I was not able to get an answer and ditched that mini project that I worked on. I followed this comment on the following thread -
https://stackoverflow.com/a/68120774/8943516
That, combined with a 2.5 days of deep researched of both USB Host protocol which was not able to connect to my camera and Camera2API which couldn't recognize my external camera brought me to a dead end.
I am using BluetoothLeScanner to scan for BLE devices and get a list of objects representing the devices to show inside my app (not connecting to any of them).
I am interested in doing the same, but using the CompanionDeviceManager now. Its callback CompanionDeviceManager.Callback.onDeviceFound(chooserLauncher: IntentSender?) unfortunately does not return any human readable form of found devices... the closest it gets is the IntentSender.writeToParcel method, but I am not sure how to use it in this situation.
I am not constrained to use the CompanionDeviceManager but I wanted to follow the OS version specific guidelines, we are supposed to use CompanionDeviceManager for Bluetooth devices scanning starting from API 26, but it seems useless in my case... so is there any way to get devices data from that callback, or should I just ditch it and stay with BluetoothLeScanner for all OS versions?
Late answer but it might help someone else. You can create a bluetooth device picker in combination with ActivityResultContracts.StartIntentSenderForResult() in order to get the BluetoothDevice. From there you will have access to all the device info that you need. Recent changes added some Android 12 permissions like android.permission.BLUETOOTH_CONNECT. Your mileage may vary.
val context = LocalContext.current
// Get the device manager instance
val deviceManager: CompanionDeviceManager by lazy {
ContextCompat.getSystemService(
context,
CompanionDeviceManager::class.java
) as CompanionDeviceManager
}
// Create a filter of your choice. Here I just look for specific device names
val deviceFilter: BluetoothDeviceFilter by lazy {
BluetoothDeviceFilter.Builder()
.setNamePattern(Pattern.compile(supportedDevices))
.build()
}
// Create a pairing request with your filter from the last step
val pairingRequest: AssociationRequest = AssociationRequest.Builder()
.addDeviceFilter(deviceFilter)
.build()
// Create a picker for discovered bluetooth devices
val bluetoothDevicePicker = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartIntentSenderForResult(),
onResult = {
val device: BluetoothDevice? =
it.data?.getParcelableExtra(CompanionDeviceManager.EXTRA_DEVICE)
try {
// Now that you have the desired device, do what you need to with it
device?.apply {
when {
name?.matches(Regex(firstDevicePattern)) == true -> {
Log.i(TAG, "${this.name} connected")
onFirstDeviceDiscovered(device)
}
name?.matches(Regex(secondDevicePattern)) == true -> {
Log.i(TAG, "${this.name} connected")
onSecondDeviceDiscovered(device)
}
}
}
} catch (e: SecurityException) {
e.printStackTrace()
//TODO: handle the security exception (this is possibly a bug)
// https://issuetracker.google.com/issues/198986283
}
}
)
// A utility function to centralize calling associate (optional)
val associateDevice: (AssociationRequest) -> Unit = { request ->
// Attempt to associate device(s)
deviceManager.associate(
request,
object : CompanionDeviceManager.Callback() {
override fun onDeviceFound(chooserLauncher: IntentSender) {
val sender = IntentSenderRequest.Builder(chooserLauncher)
.build()
bluetoothDevicePicker.launch(sender)
}
override fun onFailure(error: CharSequence?) {
//TODO: handle association failure
}
}, null
)
}
I have a class which monitors the network state.
I'm using Flow to be able to collect the network state updates.
However, I also need to leave an option to use manual listeners that programmers can "hook" onto to be able to receive the network changes.
My code is simple :
private val networkTypeState = MutableStateFlow<NetworkState>(NetworkState.Unknown)
val networkTypeAsFlow: StateFlow<NetworkState> by notifyDelegate(networkTypeState)
private fun <T> notifyDelegate(init: T) =
Delegates.observable(init) { prop, _, new ->
Lg.i("notify subscribers of network update: ${prop.name} = $new")
notifySubscribers()
}
sealed class NetworkState {
object Unknown: NetworkState()
object Disconnected: NetworkState()
data class Connected(val isCellularOn: Boolean, val isWifiOn: Boolean): NetworkState()
}
Then when I update the state,
for example :
networkTypeState.value = NetworkState.Disconnected ,
the delegates.observable does not get called.
Worth noting, when I use networkTypeAsFlow.collect { .. } , this works well, meaning - the networkTypeAsFlow does get updated, it just doesn't call the delegates.observable
The Observable delegate monitors changes to the property itself. It is futile to use Observable for a val property, because a val property is never set to a new value. Mutating the object pointed at by the property is completely invisible to the property delegate.
If you want to observe changes, you can launch and collect:
private val networkTypeState = MutableStateFlow<NetworkState>(NetworkState.Unknown)
val networkTypeAsFlow: StateFlow<NetworkState> = networkTypeState
init {
viewModelScope.launch {
networkTypeAsFlow.collect {
Lg.i("notify subscribers of network update: $it")
notifySubscribers()
}
}
}
An additional benefit here is that notifySubscribers will always be called from the same dispatcher, regardless of which thread the network state was changed from.
I'm trying to create an app that can broadcast Android views on the Chromecast, and I thought I found something promising in CastRemoteDisplayLocalService. I created a simple test app but found the callback onCreatePresentation was never called when I casted my device. After some searching I discovered it was because my application was not published as a Remote Display Application but a Custom Application Receiver from the Google Cast Developer Console.
Unfortunately when I try to create a new application from the console, Remote Display Application is not an option. After some searching, I came across this Stack Overflow question that said Remote Display API is now deprecated. There is an interface called CastRemoteDisplayApi which is marked as deprecated, but the classes I have been trying to use are not marked as such.
This leads me to wondering if CastRemoteDisplayLocalService and all other Remote Display classes not marked as deprecated are in fact deprecated and unusable, or if perhaps the functionality was shifted to work in a Custom Receiver by configuring it to accept remote displays.
This is what the relevant code looks like right now:
MainActivity.kt
private fun startCastService() {
val intent = Intent(this, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(this, 0, intent, 0)
val notificationSettings = CastRemoteDisplayLocalService.NotificationSettings.Builder().setNotificationPendingIntent(pendingIntent).build()
CastRemoteDisplayLocalService.startService(this, CastRemoteDisplayLocalServiceImpl::class.java, "2839EC8D", castDevice, notificationSettings, object : CastRemoteDisplayLocalService.Callbacks {
override fun onRemoteDisplaySessionEnded(p0: CastRemoteDisplayLocalService?) {
Log.d(TAG, "onRemoteDisplaySessionEnded")
}
override fun onRemoteDisplaySessionError(p0: Status?) {
Log.d(TAG, "onRemoteDisplaySessionError")
}
override fun onRemoteDisplaySessionStarted(p0: CastRemoteDisplayLocalService?) {
Log.d(TAG, "onRemoteDisplaySessionStarted")
}
override fun onServiceCreated(p0: CastRemoteDisplayLocalService?) {
Log.d(TAG, "onServiceCreated")
}
})
}
CastRemoteDisplayLocalServiceImpl.kt
class CastRemoteDisplayLocalServiceImpl : CastRemoteDisplayLocalService() {
val TAG = "CastRemoteDisplayLoc..."
// This function gets called
override fun onCreate() {
super.onCreate()
Log.d(TAG, "onCreate")
}
// This function does not get called
override fun onCreatePresentation(p0: Display?) {
Log.d(TAG, "onCreatePresentation")
}
override fun onDismissPresentation() {
Log.d(TAG, "onDismissPresentation")
}
}
If there's a way with the Custom Application Receiver to get the onCreatePresentation callback that would solve this, I'm having difficulty finding it. If CastRemoteDisplayLocalService is in fact deprecated, is there another way to easily cast Android views to a Chromecast? Thanks!
I've got a bit of my app that is dedicated to sharing files between devices over bluetooth using a quick, ad-hoc protocol that I put together. Currently, in the containing Activity I begin discovery, and add any device that I find into a RecyclerView. Here is the code for the BroadcastReceiver that is handling that:
private val scanReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == BluetoothDevice.ACTION_FOUND) {
val dev = intent.getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE)
Log.d(TAG, "Got device ${dev.name} with address ${dev.address}")
if (dev.name != null) {
Log.d(TAG, "Found nonnull device name, adding")
if (!viewAdapter.dataset.any { it.name == dev.name }) {
viewAdapter.dataset.add(dev)
viewAdapter.notifyDataSetChanged()
}
}
}
}
}
I wanted to modify this in such a way that it would only add devices who were broadcasting with the service UUID that I set up in the server portion of the app. After doing some research I came to this method that I could use to get the UUIDs of the services on the device. I integrated that into my BroadcastReceiver as such
private val scanReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
BluetoothDevice.ACTION_FOUND -> {
val dev = intent.getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE)
Log.d(TAG, "Got device ${dev.name} with address ${dev.address}")
if (dev.name != null) {
dev.fetchUuidsWithSdp()
}
}
//TODO: Untested code
BluetoothDevice.ACTION_UUID -> {
val id = intent.getParcelableExtra<ParcelUuid>(BluetoothDevice.EXTRA_UUID)
if (id.uuid == ShareServerSocket.SERVICE_UUID) {
val dev = intent.getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE)
if (!viewAdapter.dataset.any { it.name == dev.name }) {
viewAdapter.dataset.add(dev)
viewAdapter.notifyDataSetChanged()
}
}
}
}
}
}
(With requisite modifications to the IntentFilter I'm registering it with).
The code in the new branch gets called, I validated that with some debugging output. However, the ParcelUuid[] that I am given never contains the UUID of my service, and the device therefore never gets added. If I keep the entire setup the same on the device acting as a server, and bypass the new check on the client, I am able to connect and interact just fine. I'm unsure as to why my service wouldn't be being shown at this point.
P.S. I did also check the SDP cache, my service UUID is not there, either.
It turns out I was running into the same issue as described in Strange UUID reversal from fetchUuidsWithSdp. Stealing that workaround made it work.