UPDATE: Added Main Activity code which contains Bluetooth permissions logic
I'm trying to utilize Android's CompanionDeviceManager API to find nearby bluetooth (non LE) devices on my Pixel 5 running Android 13, but it only ever seems to find nearby WiFi networks. I'm suspicious that the deviceFilter isn't working properly.
Initially, my code to configure the BluetoothDeviceFilter looked like this:
private val deviceFilter: BluetoothDeviceFilter = BluetoothDeviceFilter.Builder()
// Match only Bluetooth devices whose name matches the pattern
.setNamePattern(Pattern.compile("(?i)\\b(Certain Device Name)\\b"))
.build()
private val pairingRequest: AssociationRequest = AssociationRequest.Builder()
// Find only devices that match our request filter
.addDeviceFilter(deviceFilter)
// Don't stop scanning as soon as one device matching the filter is found.
.setSingleDevice(false)
.build()
With this code, however, no devices ever appear within the system generated Companion Device Pairing screen. The spinner spins until timeout
Thinking maybe my regex was unintentionally too restrictive, I changed the filter to use a regexp that allows everything, like so:
.setNamePattern(Pattern.compile(".*"))
But even this filter fails to allow any nearby bluetooth devices to appear in the Pairing screen.
When I intentionally don't add any filter all I see are WiFi networks, so the Companion Device Manager can work, it's just seemingly misconfigured for Bluetooth results.
private val pairingRequest: AssociationRequest = AssociationRequest.Builder()
// No filter, let's see it all!
.setSingleDevice(false)
.build()
Using the Android OS's system Bluetooth menu I clearly see there are Bluetooth devices within range of my device, and I can even connect to them, but the same devices never appear within my app.
What am I doing wrong that's causing no nearby Bluetooth devices to appear in my CompanionDeviceManager Pairing Screen?
Code below:
HomeFragment.kt
class HomeFragment : Fragment() {
//Filter visible Bluetooth devices so only Mozis within range are displayed
private val deviceFilter: BluetoothDeviceFilter = BluetoothDeviceFilter.Builder()
// Match only Bluetooth devices whose name matches the pattern.
.setNamePattern(Pattern.compile(BLUETOOTH_DEVICE_NAME_REGEX_TO_FILTER_FOR))
.build()
private val pairingRequest: AssociationRequest = AssociationRequest.Builder()
// Find only devices that match this request filter.
.addDeviceFilter(deviceFilter)
// Don't stop scanning as soon as one device matching the filter is found.
.setSingleDevice(false)
.build()
private val deviceManager: CompanionDeviceManager by lazy {
requireContext().getSystemService(Context.COMPANION_DEVICE_SERVICE) as CompanionDeviceManager
}
private val executor: Executor = Executor { it.run() }
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
setupPairingButton()
}
/**
* This callback listens for the result of connection attempts to our Mozi Bluetooth devices
*/
#Deprecated("Deprecated in Java")
#SuppressLint("MissingPermission")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) {
SELECT_DEVICE_REQUEST_CODE -> when (resultCode) {
Activity.RESULT_OK -> {
// The user chose to pair the app with a Bluetooth device.
val deviceToPair: BluetoothDevice? =
data?.getParcelableExtra(CompanionDeviceManager.EXTRA_DEVICE)
deviceToPair?.createBond()
}
}
else -> super.onActivityResult(requestCode, resultCode, data)
}
}
private fun setupPairingButton() {
binding.buttonPair.setOnClickListener {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
/**
* This is the approach to show a pairing dialog for Android 33+
*/
deviceManager.associate(pairingRequest, executor,
object : CompanionDeviceManager.Callback() {
// Called when a device is found. Launch the IntentSender so the user
// can select the device they want to pair with
override fun onAssociationPending(intentSender: IntentSender) {
intentSender.let { sender ->
activity?.let { fragmentActivity ->
startIntentSenderForResult(
fragmentActivity,
sender,
SELECT_DEVICE_REQUEST_CODE,
null,
0,
0,
0,
null
)
}
}
}
override fun onAssociationCreated(associationInfo: AssociationInfo) {
// Association created.
// AssociationInfo object is created and get association id and the
// macAddress.
var associationId = associationInfo.id
var macAddress: MacAddress? = associationInfo.deviceMacAddress
}
override fun onFailure(errorMessage: CharSequence?) {
// Handle the failure.
showBluetoothErrorMessage(errorMessage)
}
})
} else {
/**
* This is the approach to show a pairing dialog for Android 32 and below
*/
// When the app tries to pair with a Bluetooth device, show the
// corresponding dialog box to the user.
deviceManager.associate(
pairingRequest,
object : CompanionDeviceManager.Callback() {
override fun onDeviceFound(chooserLauncher: IntentSender) {
startIntentSenderForResult(
chooserLauncher,
SELECT_DEVICE_REQUEST_CODE,
null,
0,
0,
0,
null
)
}
override fun onFailure(error: CharSequence?) {
// Handle the failure.
showBluetoothErrorMessage(error)
}
}, null
)
}
}
}
companion object {
private const val SELECT_DEVICE_REQUEST_CODE = 0
private const val BLUETOOTH_DEVICE_NAME_REGEX_TO_FILTER_FOR = "(?i)\\bCertain Device Name\\b"
}}
MainActivity.kt
class MainActivity : AppCompatActivity() {
private val enableBluetoothIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
private var bluetoothEnableResultLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
binding.loadingSpinner.hide()
when (result.resultCode) {
Activity.RESULT_OK -> {
Snackbar.make(
binding.root,
resources.getString(R.string.bluetooth_enabled_lets_pair_with_your_mozi),
Snackbar.LENGTH_SHORT
).show()
}
Activity.RESULT_CANCELED -> {
Snackbar.make(
binding.root,
getString(R.string.without_bluetooth_you_cant_pair_with_your_mozi),
Snackbar.LENGTH_INDEFINITE
)
.setAction(resources.getString(R.string._retry)) {
ensureBluetoothIsEnabled()
}
.show()
}
}
}
private val requestBluetoothPermissionLauncher =
registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
if (isGranted) {
bluetoothEnableResultLauncher.launch(enableBluetoothIntent)
} else {
// Explain to the user that the feature is unavailable because the
// feature requires a permission that the user has denied. At the
// same time, respect the user's decision. Don't link to system
// settings in an effort to convince the user to change their
// decision.
Snackbar.make(
binding.root,
getString(R.string.without_bluetooth_you_cant_pair_with_your_mozi),
Snackbar.LENGTH_INDEFINITE
)
.setAction(resources.getString(R.string._retry)) {
ensureBluetoothIsEnabled()
}
.show()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setupViews()
ensureBluetoothIsEnabled()
}
private fun setupViews() {
//Here we setup the behavior of the button in our rationale dialog: basically we need to
// rerun the permissions check logic if it was already denied
binding.bluetoothPermissionsRationaleDialogButton.setOnClickListener {
binding.permissionsRationaleDialog.animateShow(false)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
requestBluetoothPermissionLauncher.launch(Manifest.permission.BLUETOOTH_CONNECT)
} else {
requestBluetoothPermissionLauncher.launch(Manifest.permission.BLUETOOTH)
}
}
}
private fun ensureBluetoothIsEnabled() {
binding.loadingSpinner.show()
val bluetoothManager: BluetoothManager = getSystemService(BluetoothManager::class.java)
val bluetoothAdapter: BluetoothAdapter? = bluetoothManager.adapter
if (bluetoothAdapter == null) {
// Device doesn't support Bluetooth
binding.loadingSpinner.hide()
Snackbar.make(
binding.root,
resources.getString(R.string.you_need_a_bluetooth_enabled_device),
Snackbar.LENGTH_INDEFINITE
).show()
}
if (bluetoothAdapter?.isEnabled == false) {
// Check if Bluetooth permissions have been granted before we try to enable the
// device
if (ActivityCompat.checkSelfPermission(
this,
Manifest.permission.BLUETOOTH_CONNECT //TODO: test if this needs variant for legacy devices
) != PackageManager.PERMISSION_GRANTED
) {
/**
* We DON'T have Bluetooth permissions. We have to get them before we can ask the
* user to enable Bluetooth
*/
binding.loadingSpinner.hide()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (shouldShowRequestPermissionRationale(Manifest.permission.BLUETOOTH_CONNECT)) {
binding.permissionsRationaleDialog.animateShow(true)
} else {
requestBluetoothPermissionLauncher.launch(Manifest.permission.BLUETOOTH_CONNECT)
}
} else {
if (shouldShowRequestPermissionRationale(Manifest.permission.BLUETOOTH)) {
binding.permissionsRationaleDialog.animateShow(true)
} else {
requestBluetoothPermissionLauncher.launch(Manifest.permission.BLUETOOTH)
}
}
return
} else {
/**
* We DO have Bluetooth permissions. Now let's prompt the user to enable their
* Bluetooth radio
*/
binding.loadingSpinner.hide()
bluetoothEnableResultLauncher.launch(enableBluetoothIntent)
}
} else {
/**
* Bluetooth is enabled, we're good to continue with normal app flow
*/
binding.loadingSpinner.hide()
}
}
}
Android Manifest
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- Bluetooth Permissions -->
<uses-feature android:name="android.software.companion_device_setup" android:required="true"/>
<uses-feature android:name="android.hardware.bluetooth" android:required="true"/>
<!-- Request legacy Bluetooth permissions on older devices. -->
<uses-permission android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<!-- Needed only if your app looks for Bluetooth devices.
If your app doesn't use Bluetooth scan results to derive physical
location information, you can strongly assert that your app
doesn't derive physical location. -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags= "neverForLocation"
tools:targetApi="s" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
...
</manifest>
You could try using an empty BluetoothDeviceFilter like this:
private val deviceFilter: BluetoothDeviceFilter = BluetoothDeviceFilter.Builder().build()
to signal to the API that you want Bluetooth devices, and see if at least the phone sees your device.
Then you could try again with the name filter, this time adding a service UUID filter with BluetoothDeviceFilter.Builder.addServiceUuid.
If you don't know the UUID of your device or don't want to use it as a filter, you can use an arbitrary one and set the mask to all zeros (the docs suggest that it might also work using null values).
This is a hackish solution, but it might help you move a step further
It might be a permission issue.
In the docs, I read:
The BLUETOOTH_ADVERTISE, BLUETOOTH_CONNECT, and BLUETOOTH_SCAN permissions are runtime permissions. Therefore, you must explicitly request user approval in your app before you can look for Bluetooth devices, make a device discoverable to other devices, or communicate with already-paired Bluetooth devices.
So you could to add the following code in your HomeFragment class:
private val requestMultiplePermissions = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
permissions.entries.forEach {
Log.d("Permission Request", "${it.key} = ${it.value}")
}
}
private val requestBluetooth = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
// granted
} else {
// denied
}
}
and in the onCreateView method:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
requestMultiplePermissions.launch(arrayOf(
Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.BLUETOOTH_CONNECT
))
} else {
val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
requestBluetooth.launch(enableBtIntent)
}
to request the permissions at runtime.
The documentation does not mention it, but it appears that even with the CompanionDeviceManager the location access must be enabled on the device.
The app does not need the location permission anymore, but it must be enabled.
Related
Im trying to implement permission handling for healthconnect. This is done in the funciton checkPermissionAndRun (strongly inspired by the documentation), however I'm not sure what the difference between the two else{...} code segments is.Both check if the permission is given already, but why do we need 2 of those? Furthermore, is it alright if I call the checkpermissionandrun function once in the oncreate, or should i do this also when interacting with HealthConnect?
class MainActivity : AppCompatActivity() {
// build a set of permissions for required data types
val PERMISSIONS =
setOf(
HealthPermission.createReadPermission(HeartRateRecord::class),
HealthPermission.createWritePermission(HeartRateRecord::class),
HealthPermission.createReadPermission(StepsRecord::class),
HealthPermission.createWritePermission(StepsRecord::class)
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val hcManager = HealthConnectManager(this)
checkPermissionsAndRun(hcManager.healthConnectClient, PERMISSIONS)
}
private fun checkPermissionsAndRun(client: HealthConnectClient, PERMISSIONS: Set<HealthPermission>) {
val requestPermissionActivityContract = PermissionController.createRequestPermissionResultContract()
val requestPermissions =
registerForActivityResult(requestPermissionActivityContract) { granted ->
if (granted.containsAll(PERMISSIONS)) {
//Permission granted text 1
Toast.makeText(applicationContext, "permissions granted 1", Toast.LENGTH_SHORT).show()
} else {
// Lack of required permissions, But what is different to the coroutine else below?
}
}
lifecycleScope.launch {
val granted = client.permissionController.getGrantedPermissions(PERMISSIONS)
if (granted.containsAll(PERMISSIONS)) {
//Permission Granted text 2
Toast.makeText(applicationContext, "permissions granted 2", Toast.LENGTH_SHORT).show()
} else {
//lack of required permissions
requestPermissions.launch(PERMISSIONS)
}
}
}
}
Thank you in advance
I'm trying to apply a tutorial about request permission when the app start, but when I tested on my real device android 12, it's not show the permission dialog it show a snack bar that request from user to go to setting and grant it manually
Manifiest file
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- TODO: Step 1 add in permissions for fine location and background-->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<application
android:allowBackup="true"
android:icon="#mipmap/ic_launcher"
android:label="#string/app_name"
android:roundIcon="#mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="#style/AppTheme">
<activity android:name=".HuntMainActivity"
android:label="#string/title_activity_hunt"
android:launchMode="singleInstance"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<receiver android:name=".GeofenceBroadcastReceiver"/>
</application>
</manifest>
this my code
class HuntMainActivity : AppCompatActivity() {
private lateinit var binding: ActivityHuntMainBinding
private lateinit var geofencingClient: GeofencingClient
private lateinit var viewModel: GeofenceViewModel
private val runningQOrLater = android.os.Build.VERSION.SDK_INT >=
android.os.Build.VERSION_CODES.Q
// A PendingIntent for the Broadcast Receiver that handles geofence transitions.
// TODO: Step 8 add in a pending intent
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_hunt_main)
viewModel = ViewModelProvider(this, SavedStateViewModelFactory(this.application,
this)).get(GeofenceViewModel::class.java)
binding.viewmodel = viewModel
binding.lifecycleOwner = this
// TODO: Step 9 instantiate the geofencing client
// Create channel for notifications
createChannel(this )
}
override fun onStart() {
super.onStart()
checkPermissionsAndStartGeofencing()
}
/*
* When we get the result from asking the user to turn on device location, we call
* checkDeviceLocationSettingsAndStartGeofence again to make sure it's actually on, but
* we don't resolve the check to keep the user from seeing an endless loop.
*/
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
// TODO: Step 7 add code to check that the user turned on their device location and ask
// again if they did not
}
/*
* When the user clicks on the notification, this method will be called, letting us know that
* the geofence has been triggered, and it's time to move to the next one in the treasure
* hunt.
*/
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
val extras = intent?.extras
if(extras != null){
if(extras.containsKey(GeofencingConstants.EXTRA_GEOFENCE_INDEX)){
viewModel.updateHint(extras.getInt(GeofencingConstants.EXTRA_GEOFENCE_INDEX))
checkPermissionsAndStartGeofencing()
}
}
}
/*
* In all cases, we need to have the location permission. On Android 10+ (Q) we need to have
* the background permission as well.
*/
#SuppressLint("MissingSuperCall")
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
Log.d(TAG, "onRequestPermissionResult")
if (
grantResults.isEmpty() ||
grantResults[LOCATION_PERMISSION_INDEX] == PackageManager.PERMISSION_DENIED ||
(requestCode == REQUEST_FOREGROUND_AND_BACKGROUND_PERMISSION_RESULT_CODE &&
grantResults[BACKGROUND_LOCATION_PERMISSION_INDEX] ==
PackageManager.PERMISSION_DENIED))
{
Snackbar.make(
binding.activityMapsMain,
R.string.permission_denied_explanation,
Snackbar.LENGTH_INDEFINITE
)
.setAction(R.string.settings) {
startActivity(Intent().apply {
action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
data = Uri.fromParts("package", BuildConfig.APPLICATION_ID, null)
flags = Intent.FLAG_ACTIVITY_NEW_TASK
})
}.show()
} else {
checkDeviceLocationSettingsAndStartGeofence()
}
}
/**
* This will also destroy any saved state in the associated ViewModel, so we remove the
* geofences here.
*/
override fun onDestroy() {
super.onDestroy()
removeGeofences()
}
/**
* Starts the permission check and Geofence process only if the Geofence associated with the
* current hint isn't yet active.
*/
private fun checkPermissionsAndStartGeofencing() {
if (viewModel.geofenceIsActive()) return
if (foregroundAndBackgroundLocationPermissionApproved()) {
checkDeviceLocationSettingsAndStartGeofence()
} else {
requestForegroundAndBackgroundLocationPermissions()
}
}
/*
* Uses the Location Client to check the current state of location settings, and gives the user
* the opportunity to turn on location services within our app.
*/
private fun checkDeviceLocationSettingsAndStartGeofence(resolve:Boolean = true) {
val locationRequest = LocationRequest.create().apply {
priority = LocationRequest.PRIORITY_LOW_POWER
}
val builder = LocationSettingsRequest.Builder().addLocationRequest(locationRequest)
val settingsClient = LocationServices.getSettingsClient(this)
val locationSettingsResponseTask =
settingsClient.checkLocationSettings(builder.build())
locationSettingsResponseTask.addOnFailureListener { exception ->
if (exception is ResolvableApiException && resolve){
try {
exception.startResolutionForResult(this#HuntMainActivity,
REQUEST_TURN_DEVICE_LOCATION_ON)
} catch (sendEx: IntentSender.SendIntentException) {
Log.d(TAG, "Error getting location settings resolution: " + sendEx.message)
}
} else {
Snackbar.make(
binding.activityMapsMain,
R.string.location_required_error, Snackbar.LENGTH_INDEFINITE
).setAction(android.R.string.ok) {
checkDeviceLocationSettingsAndStartGeofence()
}.show()
}
}
locationSettingsResponseTask.addOnCompleteListener {
if ( it.isSuccessful ) {
addGeofenceForClue()
}
}
}
/*
* Determines whether the app has the appropriate permissions across Android 10+ and all other
* Android versions.
*/
#TargetApi(29)
private fun foregroundAndBackgroundLocationPermissionApproved(): Boolean {
val foregroundLocationApproved = (
PackageManager.PERMISSION_GRANTED ==
ActivityCompat.checkSelfPermission(this,
Manifest.permission.ACCESS_FINE_LOCATION))
val backgroundPermissionApproved =
if (runningQOrLater) {
PackageManager.PERMISSION_GRANTED ==
ActivityCompat.checkSelfPermission(
this, Manifest.permission.ACCESS_BACKGROUND_LOCATION
)
} else {
true
}
return foregroundLocationApproved && backgroundPermissionApproved
}
/*
* Requests ACCESS_FINE_LOCATION and (on Android 10+ (Q) ACCESS_BACKGROUND_LOCATION.
*/
#TargetApi(29 )
private fun requestForegroundAndBackgroundLocationPermissions() {
if (foregroundAndBackgroundLocationPermissionApproved())
return
var permissionsArray = arrayOf(Manifest.permission.ACCESS_FINE_LOCATION)
val resultCode = when {
runningQOrLater -> {
permissionsArray += Manifest.permission.ACCESS_BACKGROUND_LOCATION
REQUEST_FOREGROUND_AND_BACKGROUND_PERMISSION_RESULT_CODE
}
else -> REQUEST_FOREGROUND_ONLY_PERMISSIONS_REQUEST_CODE
}
Log.d(TAG, "Request foreground only location permission")
ActivityCompat.requestPermissions(
this#HuntMainActivity,
permissionsArray,
resultCode
)
}
/*
* Adds a Geofence for the current clue if needed, and removes any existing Geofence. This
* method should be called after the user has granted the location permission. If there are
* no more geofences, we remove the geofence and let the viewmodel know that the ending hint
* is now "active."
*/
private fun addGeofenceForClue() {
// TODO: Step 10 add in code to add the geofence
}
/**
* Removes geofences. This method should be called after the user has granted the location
* permission.
*/
private fun removeGeofences() {
// TODO: Step 12 add in code to remove the geofences
}
companion object {
internal const val ACTION_GEOFENCE_EVENT =
"HuntMainActivity.treasureHunt.action.ACTION_GEOFENCE_EVENT"
}
}
private const val REQUEST_FOREGROUND_AND_BACKGROUND_PERMISSION_RESULT_CODE = 33
private const val REQUEST_FOREGROUND_ONLY_PERMISSIONS_REQUEST_CODE = 34
private const val REQUEST_TURN_DEVICE_LOCATION_ON = 29
private const val TAG = "HuntMainActivity"
private const val LOCATION_PERMISSION_INDEX = 0
private const val BACKGROUND_LOCATION_PERMISSION_INDEX = 1
I tested it on my real device, what i excopecting is showing the permission dialog when the app starts
In my driving-companion app, I have a need to detect the state of Android Auto. For several years now, I've been using UiModeManager to get the current state at startup and a BroadcastReceiver to detect state changes while the app is running. This has always worked perfectly, until Android 12. With Android 12, UiModeManager always reports UI_MODE_TYPE_NORMAL, even when Android Auto is connected and active, and my BroadcastReceiver is never called after connecting or disconnecting.
This is my code for detecting state at startup:
inCarMode = uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_CAR;
and this is my BroadcastReceiver setup:
IntentFilter carModeFilter = new IntentFilter();
carModeFilter.addAction(UiModeManager.ACTION_ENTER_CAR_MODE);
carModeFilter.addAction(UiModeManager.ACTION_EXIT_CAR_MODE);
registerReceiver(carModeReceiver, carModeFilter);
Again, this has always worked perfectly with Android 5 through Android 11. Is this a bug in Android 12, or is there some new way to detect Android Auto state in Android 12?
You need to use the CarConnection API documented here
Configuration.UI_MODE_TYPE_CAR is not working on Anroid 12. As #Pierre-Olivier Dybman said, you can use CarConnection API in the androidx.car.app:app library. But that is too heavy to import entire library only for car connections if you don't need other features.
So I write a piece of code base on the CarConnection to detect Android Auto connection, as below:
class AutoConnectionDetector(val context: Context) {
companion object {
const val TAG = "AutoConnectionDetector"
// columnName for provider to query on connection status
const val CAR_CONNECTION_STATE = "CarConnectionState"
// auto app on your phone will send broadcast with this action when connection state changes
const val ACTION_CAR_CONNECTION_UPDATED = "androidx.car.app.connection.action.CAR_CONNECTION_UPDATED"
// phone is not connected to car
const val CONNECTION_TYPE_NOT_CONNECTED = 0
// phone is connected to Automotive OS
const val CONNECTION_TYPE_NATIVE = 1
// phone is connected to Android Auto
const val CONNECTION_TYPE_PROJECTION = 2
private const val QUERY_TOKEN = 42
private const val CAR_CONNECTION_AUTHORITY = "androidx.car.app.connection"
private val PROJECTION_HOST_URI = Uri.Builder().scheme("content").authority(CAR_CONNECTION_AUTHORITY).build()
}
private val carConnectionReceiver = CarConnectionBroadcastReceiver()
private val carConnectionQueryHandler = CarConnectionQueryHandler(context.contentResolver)
fun registerCarConnectionReceiver() {
context.registerReceiver(carConnectionReceiver, IntentFilter(ACTION_CAR_CONNECTION_UPDATED))
queryForState()
}
fun unRegisterCarConnectionReceiver() {
context.unregisterReceiver(carConnectionReceiver)
}
private fun queryForState() {
carConnectionQueryHandler.startQuery(
QUERY_TOKEN,
null,
PROJECTION_HOST_URI,
arrayOf(CAR_CONNECTION_STATE),
null,
null,
null
)
}
inner class CarConnectionBroadcastReceiver : BroadcastReceiver() {
// query for connection state every time the receiver receives the broadcast
override fun onReceive(context: Context?, intent: Intent?) {
queryForState()
}
}
internal class CarConnectionQueryHandler(resolver: ContentResolver?) : AsyncQueryHandler(resolver) {
// notify new queryed connection status when query complete
override fun onQueryComplete(token: Int, cookie: Any?, response: Cursor?) {
if (response == null) {
Log.w(TAG, "Null response from content provider when checking connection to the car, treating as disconnected")
notifyCarDisconnected()
return
}
val carConnectionTypeColumn = response.getColumnIndex(CAR_CONNECTION_STATE)
if (carConnectionTypeColumn < 0) {
Log.w(TAG, "Connection to car response is missing the connection type, treating as disconnected")
notifyCarDisconnected()
return
}
if (!response.moveToNext()) {
Log.w(TAG, "Connection to car response is empty, treating as disconnected")
notifyCarDisconnected()
return
}
val connectionState = response.getInt(carConnectionTypeColumn)
if (connectionState == CONNECTION_TYPE_NOT_CONNECTED) {
Log.i(TAG, "Android Auto disconnected")
notifyCarDisconnected()
} else {
Log.i(TAG, "Android Auto connected")
notifyCarConnected()
}
}
}
}
This solution works on android 6~12. If you need to detect car connection status on android 5, use the Configuration.UI_MODE_TYPE_CAR solution.
I am trying to write a program to communicate with ESP32 modules via bluetooth. For the program to work, Bt must be turned on and the FINE_LOCATION permission granted. I am using API 29.
The code below works, but it can be done much better.
I am a beginner, this is the only way I can do it.
I have a few questions :
Can I use shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION) together with ActivityResultContracts.RequestPermission(), if yes how?
To achieve my goal if the user refuses the first time
to grant permissions, I run an almost identical contract with a different dialog.How can this code be reduced?
How to simplify this constant checking:
if (conditions.isReady()) {
buildInterfaceOk()
} else buildInterfaceError()
Half my code seems redundant, I don't know how to get rid of it.
All these problems actually concern the first run, then everything is fine.
Code:
const val TAG = "DEBUG"
data class Conditions (var isBtEnabled : Boolean , var permissionsOk :Boolean){
fun isReady():Boolean{
if (isBtEnabled && permissionsOk) return true
else return false
}
fun log(){
Log.d("DEBUG","Conditions-> $isBtEnabled , $permissionsOk")
}}
class MainActivity : AppCompatActivity() {
private lateinit var bind: ActivityMainBinding
private lateinit var broadcastReceiver: BroadcastReceiver
private lateinit var bluetoothAdapter: BluetoothAdapter
private var conditions = Conditions(false, false)
private var requestBluetoothEnable =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
Log.d(TAG, "IT RESULT CODE: ${it.resultCode.toString()}")
//kiedy bt jest wlaczone , result -1 , kiedy wylaczone i wlaczamy i akceptujemy tez -1
//a jak odrzucamy to 0
if (it.resultCode == -1) {
conditions.log()
conditions.isBtEnabled = true
}
if (conditions.isReady()) {
buildInterfaceOk()
} else buildInterfaceError()
}
//use it when user denied first time
private val requestPermissionLocationSecond =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
if (granted) {
Log.d(TAG, "Permission granted by contract 2")
conditions.permissionsOk = checkPermissions()
if (conditions.isReady()) {
buildInterfaceOk()
} else buildInterfaceError()
} else {
val builder = AlertDialog.Builder(this#MainActivity)
builder.setTitle("V2 - Hi!")
builder.setMessage(
" Please go to the app settings and manually turn on " +
"\"location permission\". Without this permission, I do not work. "
)
builder.setPositiveButton("Ok") { dialog, which -> }
val dialog: AlertDialog = builder.create()
dialog.show()
Log.d(TAG, " V2-> Permission denied, - contract 2")
}
}
// first try to get permission
private var requestPermissionLocation =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
if (granted) {
Log.d(TAG, "Permission granted by contract 1")
conditions.permissionsOk = checkPermissions()
if (conditions.isReady()) {
buildInterfaceOk()
} else buildInterfaceError()
//shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)
} else {
Log.d(TAG, "Permission denied by contract 1")
val builder = AlertDialog.Builder(this#MainActivity)
builder.setTitle("V2 - Uprawnienie do lokalizacji")
builder.setMessage("I need these permissions to work with Bt devices ")
builder.setPositiveButton("YES") { dialog, which ->
requestPermissionLocationSecond.launch(android.Manifest.permission.ACCESS_FINE_LOCATION)
}
builder.setNegativeButton("No") { dialog, which -> }
val dialog: AlertDialog = builder.create()
dialog.show()
conditions.permissionsOk = checkPermissions()
if (conditions.isReady()) {
buildInterfaceOk()
} else buildInterfaceError()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
bind = ActivityMainBinding.inflate(layoutInflater)
super.onCreate(savedInstanceState)
setContentView(bind.root)
Log.d(TAG, "BUild version : ${Build.VERSION.SDK_INT} -> ${Build.VERSION.CODENAME}")
val currentDebug = getString(R.string.app_name)
Log.d(TAG, "CURRENT DEBUG : $currentDebug")
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
requestBluetoothEnable.launch(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE))
requestPermissionLocation.launch(Manifest.permission.ACCESS_FINE_LOCATION)
conditions.isBtEnabled = bluetoothAdapter.isEnabled
conditions.permissionsOk = checkPermissions()
Log.d(TAG, "FIRST conditions check :")
if (conditions.isReady()) {
conditions.log()
buildInterfaceOk()
}
}
private fun buildInterfaceOk() {
Log.d(TAG, "BUILDING INTERFACE : all is fine")
bind.tvInfo.text = "All is fine i can build interface"
}
private fun buildInterfaceError() {
Log.d(TAG, "BUILDING INTERFACE : errors")
bind.tvInfo.text = "Some errors..."
}
private fun checkPermissions(): Boolean {
val permissionsRequired =
arrayOf(
Manifest.permission.BLUETOOTH,
Manifest.permission.BLUETOOTH_ADMIN,
//Manifest.permission.BLUETOOTH_CONNECT, //to znow wymagane od S(API 31) ??
//Manifest.permission.BLUETOOTH_SCAN, //to znow wymagane od S(API 31) ??
Manifest.permission.ACCESS_FINE_LOCATION
//Manifest.permission.ACCESS_BACKGROUND_LOCATION
)
var permissionsOk = true
permissionsRequired.forEach { requiredPermission ->
if (ContextCompat.checkSelfPermission(
this.applicationContext,
requiredPermission
) == PackageManager.PERMISSION_GRANTED
) {
Log.d(TAG, "PERMISSION : $requiredPermission -> GRANTED")
} else {
Log.d(TAG, "PERMISSION : $requiredPermission -> NOT GRANTED")
permissionsOk = false
}
}
return permissionsOk
}
}
What I would do is display an AlertDialog first saying, you MUST ACCEPT all permissions in order to precede then Request Permissions until the user agrees to them all.
Check Permission -> Pass -> Start App
Check Permission -> Fail -> Alert Dialog "You must accept all permissions for the app to start."
Request Permission -> Check Permission -> Pass -> Start App
Request Permission -> Check Permission -> Fail -> Request Permission
Request Permission -> Check Permission -> Fail & Never ask again ->
Alert Dialog "Go to setting to turn on permissions" -> onPositive "OK" -> Request Permission
The only problem with this is the user can choose "Never ask again", meaning you can no longer Request Permissions.
Luckily you can tell if the user has chosen "Never ask again", this should get you started
Android M - check runtime permission - how to determine if the user checked "Never ask again"?
This way will encourage code reuse because you are essentially doing the same things in a loop until the user accepts permissions. The only way out of the loop is permission granted across the board.
In my app, I want to get certain details about the connected bluetooth headset.
First, I thought of getting the connected devices whose profile is headset.
val result = BluetoothAdapter.getDefaultAdapter()
.getProfileProxy(context, mProfileListener, BluetoothProfile.HEADSET)
The listener snippet is as follows :
private var mBluetoothHeadset: BluetoothHeadset? = null
private val mProfileListener = object : BluetoothProfile.ServiceListener {
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
if (profile == BluetoothProfile.HEADSET) {
mBluetoothHeadset = proxy as BluetoothHeadset
val devices = mBluetoothHeadset?.connectedDevices
devices?.forEach {
println(it.name)
}
}
}
override fun onServiceDisconnected(profile: Int) {
if (profile == BluetoothProfile.HEADSET) {
mBluetoothHeadset = null
}
}
}
I have declared the necessary permissions in the manifest
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
But mBluetoothHeadset?.connectedDevices is always returning an empty list. But in my tablet, the device is already connected to a bluetooth headset. Am I missing anything here?
It looks like we can get the list of connected devices by filtering it based on the various connection states. The following snippet worked for me
private val states = intArrayOf(
BluetoothProfile.STATE_DISCONNECTING,
BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_CONNECTING
)
private val mProfileListener = object : BluetoothProfile.ServiceListener {
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
if (profile == BluetoothProfile.HEADSET) {
mBluetoothHeadset = proxy as BluetoothHeadset
val devices = mBluetoothHeadset?.getDevicesMatchingConnectionStates(states)
devices?.forEach {
println("${it.name} ${it.bondState}")
}
}
}