Android 12 bluetooth permissions confusion
Targeting Android 12 my working solution is to declare the permissions in this way:
Like you said, BLUETOOTH_SCAN is not sufficient and you need BLUETOOTH_CONNECT (also if you decide, like me, to ask to the user to enable Bluetooth starting a new startActivityForResult with action BluetoothAdapter.ACTION_REQUEST_ENABLE)
If the BLUETOOTH_CONNECT permission needs to be requested at runtime what is the correct full way to do it? Meaning checking if it’s already granted then requesting it if it’s not. I have no Android 12 device so no way to test this code.
To improve @AndreasGobs answer, below the code to test if the connection with a device is viable or not based on current available permissions. In the manifest I’ve set that the COARSE and FINE location permissions must be limited to max API 30. Tested on Android 6, 8.1, 11 and 12 devices. I hope this will be useful.
/** * - API < S * - Check ACCESS_COARSE_LOCATION and ACCESS_FINE_LOCATION permissions * - API < O * - Check has GPS * - Check GPS enabled * - API >= S * - Check BLUETOOTH_SCAN permission * - Check BLUETOOTH_CONNECT permission * - Check Bluetooth enabled */ private boolean canConnect() < Timber.d("canConnect called"); ListdeniedPermissions = new ArrayList<>(); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) < if (!checkPermission(Manifest.permission.ACCESS_COARSE_LOCATION)) deniedPermissions.add(Manifest.permission.ACCESS_COARSE_LOCATION); if (!checkPermission(Manifest.permission.ACCESS_FINE_LOCATION)) deniedPermissions.add(Manifest.permission.ACCESS_FINE_LOCATION); if(deniedPermissions.isEmpty())< if (!MmcDeviceCapabilities.hasLocationGps() //check if the device has GPS || Build.VERSION.SDK_INT < Build.VERSION_CODES.O || MmcDeviceCapabilities.isGpsEnabled())< //check if the GPS is enabled if(MmcDeviceCapabilities.bluetoothEnabled()) //check if bluetooth is enabled return true; else < requestEnableBluetooth(); //method to request enable bluetooth return false; >> else < Timber.d("Request enable GPS"); requestEnableGps(); //method to request enable GPS (improving devices scan) return false; >> else < Timber.d("Request GPS permissions"); requestRuntimePermissions( "Bluetooth GPS request", "GPS permissions request rationale", GPS_PERMISSIONS_CODE, deniedPermissions.toArray(new String[0])); return false; >> else < // Build.VERSION_CODES.S or later if(!checkPermission(Manifest.permission.BLUETOOTH_SCAN)) deniedPermissions.add(Manifest.permission.BLUETOOTH_SCAN); if(!checkPermission(Manifest.permission.BLUETOOTH_CONNECT)) deniedPermissions.add(Manifest.permission.BLUETOOTH_CONNECT); if(deniedPermissions.isEmpty()) if(MmcDeviceCapabilities.bluetoothEnabled()) //check if bluetooth is enabled return true; else < requestEnableBluetooth(); //method to request enable bluetooth return false; >else < Timber.d("Request bluetooth permissions"); requestRuntimePermissions( "Bluetooth permissions request", "Bluetooth permissions request rationale", CONNECT_PERMISSIONS_CODE, deniedPermissions.toArray(new String[0])); return false; >> > /** * This method checks if a runtime permission has been granted. * @param permission The permission to check. * @return TRUE
if the permission has been granted, FALSE
otherwise. */ @SuppressWarnings("BooleanMethodIsAlwaysInverted") private boolean checkPermission(@NonNull String permission) < return ActivityCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED; >private void requestRuntimePermissions(@NonNull String title, @NonNull String description, int requestCode, @NonNull String. permissions) < if (ActivityCompat.shouldShowRequestPermissionRationale(this, permissions[0])) < AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this); builder .setTitle(title) .setMessage(description) .setCancelable(false) .setNegativeButton(android.R.string.no, (dialog, id) ->< //do nothing >) .setPositiveButton(android.R.string.ok, (dialog, id) -> ActivityCompat.requestPermissions(this, permissions, requestCode)); showDialog(builder); //method to show a dialog > else ActivityCompat.requestPermissions(this, permissions, requestCode); >
Возможна ли работа с bluetooth в Android без местоположения?
Типовой отзыв для андроид приложения, работающего с блютуз устройством.
Статья построена в виде спора с воображаемым пользователем. Для андроид разработчиков рассмотрены как классические способы, так и рекомендумые альтернативы.
От вашей программы требуется только отправить данные на устройство!
Минимально необходимый код (далее МНК) для отправки данных на классическое блютуз устройство с реализаций SPP (Serial Port Protocol)
UUID myUUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"); BluetoothAdapter defaultAdapter = BluetoothAdapter.getDefaultAdapter(); BluetoothDevice remoteDevice = defaultAdapter.getRemoteDevice("DC:0D:30:8A:AD:C8"); BluetoothSocket socketToServiceRecord = remoteDevice.createRfcommSocketToServiceRecord(myUUID); socketToServiceRecord.connect(); DataOutputStream dataOutputStream = new DataOutputStream(socketToServiceRecord.getOutputStream()); dataOutputStream.write("Hello mir!\n\n".getBytes(StandardCharsets.UTF_8)); dataOutputStream.flush(); Thread.sleep(1000); dataOutputStream.close(); socketToServiceRecord.close();
Если мы запустим МНК, то получим:
java.lang.SecurityException: Need BLUETOOTH permission: Neither user 10632 nor current process has android.permission.BLUETOOTH.
Необходимо добавить для работоспособности этого кода в манифесте приложения:
До апи 31 существовали только 2 разрешения:
android.permission.BLUETOOTH и android.permission.BLUETOOTH_ADMIN
В андроид 12 переработали набор пермишенов. Ознакомиться подробнее можно по ссылке.
Главное отличие новых разрешений в том, что их нужно не только добавить в манифест, но и запросить у пользователя явно.
Вот. Сами пишете, что доступ к местоположению не нужен.
Обратите внимание на следующую строку
defaultAdapter.getRemoteDevice("DC:0D:30:8A:AD:C8");
Видите строку с двоеточиями ? Это mac адрес устройства. У каждого устройства он должен быть своим и уникальным. Вы знаете адрес например своего принтера ? Устроит ли вас просто поле ввода для этого значения ?
На моем принтере есть наклейка с QR. Считайте ее.
Это исключения, кроме того нет единого формата для кодирования информации о параметрах подключения.
Я буду сам сопрягать устройства через системные настройки. Дайте мне выбрать нужное из них.
Я могу помочь Вам попасть сразу в нужное место системных настроек:
Intent intentOpenBluetoothSettings = new Intent(); intentOpenBluetoothSettings.setAction(android.provider.Settings.ACTION_BLUETOOTH_SETTINGS); startActivity(intentOpenBluetoothSettings);
и вам не придется долго добираться до него.
Получить список сопряженных:
Set btDevices = mBtAdapter.getBondedDevices();
Казалось бы одна строчка кода, что тут может не работать?
Во первых, всё таки встречаются устройства без BT. Подстраховаться можно строкой в манифесте.
В этом случае приложение нельзя будет поставить на андроид устройство без блютуз адаптера. Если с перефирийным устройством можно общаться еще через USB или сеть, то меняем на required=»false».
Если адаптера нет, то BluetoothAdapter.getDefaultAdapter() вернет null.
Попутно пожалуюсь. Ну зачем ее сделали депрекайтед? Альтернатива ужасно не удобная. Теперь еще контекст в фоновые потоки протаскивать для получения адаптера или сам адаптер. А за столько лет существования андроида так и не сделали, чтобы два и более адаптера поддерживалось одновременно. А еще проблем добавляют .
Во вторых, опять головная боль в 12м андроиде. Нужно учесть, что пермишен BLUETOOTH_CONNECT предоставлен.
Ворчание. Раньше было проще. Автоматом давался по факту наличия в манифесте. Теперь придется аналогично критичными. А еще нельзя попросить один раз и запомнить, что получил. Механизм автоматического отзыва у неиспользуемых приложений появился. Так что здравствуй куча проверок начиная с того, на какой версии андроида запущено.
Совет вместо проверок, лучше обернуть SecurityException в кастомное исключение и обработать его там, где есть возможность позвать запрос на предоставление разрещения иначе там получается большая лапша проверок начиная с того, что версия андроида 12 и выше и далее а дано ли разрешение.
В третьих, getBondedDevices() вернет null при выключенном адаптере.
Действия с получением списка сопряженных и их обработкой вынесем в функцию getBonded() . Вместо тривиального уведомления «Включите» реализуем включение.
if (!mBtAdapter.isEnabled()) < Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); btActivityResultLauncher.launch(enableIntent); >else
btActivityResultLauncher = registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), result -> < if (result.getResultCode() == Activity.RESULT_OK) < getBonded(); >>);
Тут вопрос от знатока. Можно же mBtAdapter.enable() использовать, почему так сложно ?
Вариант выше не требует дополнительных разрешений. Метод enabled() становиться депрекайтед в 13-м андроиде. Пример выше основан на рекомендованной альтернативе. Для предыдущих версий в манифесте должен быть еще пермишен BLUETOOTH_ADMIN. Но главное из-за выделенного жирным в документации
Bluetooth should never be enabled without direct user consent.
легко попасть под reject (отклонение обновления или нового приложения) или словить снятие с публикации.
Вернемся к выбору из списка сопряженных.
Мы получили список объектов типа BluetoothDevice, а нам нужно показать имя и узнать mac:
BluetoothDevice d = getItem(position); String name = d.getName(); // требует BLUETOOH_CONNECT String mac = d.getAddress(); // а это удивительно нет
Получается ли , что мы обошлись без необходимости в геолокации?
Так выглядит запрос BLUETOOH_CONNECT в Android 12.
Единственное чего мы достигли, пользователи более ранних версий останутся в неведении. Напомню, что пермишен нужен и для работы МНК ( .getRemoteDevice(), .connect() ).
Почему же так?
У вас сопряжен с телефоном телевизор, колонки . Программа это увидела и если у нее есть биг дата по пользователям, то как минимум она вычислит Ваш город. Если мало мобильное устройство в зоне досягаемости (удалось к нему подключиться), то можно местоположение сузить до 100 метров.
Именно о таком теоретическом возможном риске Вас предупреждают.
У меня Android 6-11. Почему же я вижу запрос к местоположению?
Мы рассмотрели вариант, когда Вы предварительно сделали сопряжение, не все пользователи могут сделать этот шаг самостоятельно. Часто для простоты даже не смотрят в список сопряженных, а начинают опрос эфира.
До 6-го андроида разрешения предоставлялись автоматически по факту упоминания в манифесте. Потом разрешения решили поделить, условно безопасные так и остались, а остальные стало требоваться запрашивать явно. В коде программ появился костыль вида:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) < if (checkSelfPermission(Manifest.permission. …) != PackageManager.PERMISSION_GRANTED )< … >>
Пользователи стали видеть запросы.
Процесс опроса эфира асинхронный.
private final BroadcastReceiver mReceiver = new BroadcastReceiver() < public void onReceive(Context context, Intent intent) < String action = intent.getAction(); if (BluetoothDevice.ACTION_FOUND.equals(action)) < // Найдено BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); …… делаем с ним что нужно >else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) < // процесс поиска завершен >> >;
2) Регистрируем слушателя сообщений.
IntentFilter filter = new IntentFilter(); filter.addAction(BluetoothDevice.ACTION_FOUND); filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED); registerReceiver(mReceiver, filter);
Прочитать документацию Вы можете самостоятельно.
Выскажу свое мнение почему там сбоку прикрутили геолокацию. Так у нас два пермишена BLUETOOTH слишком общий, BLUETOOTH_ADMIN нужен для изменения статуса и позволяет сканировать. Сделать его явно запрашиваемым, поломается много программ. У нас тут еще есть ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION, которые нужно явно запрашивать. Так может скрестим и по смыслу подходят.
И вот пошли шатания от версии к версии. Нужно ли именно FINE или хватит COARSE.
В 12м нужна связка BLUETOOTH_SCAN & ACCESS_FINE_LOCATION.
В андроиде есть неудобная для программистов практика без ошибки игнорировать действия, которые потом решили запретить и/или возвращать пустой/фиктивный результат. Делается это для того, чтобы устаревшие программы не завершились аварийно с ошибкой. Да еще вендоры могут внести свое видение того как правильно.
Кроме того в момент первоначальной настройки нового телефона/приставки можно запретить всем приложениям доступ к местоположению. После этого узнать, что всем или конкретно нам запрещено нет возможности. Все методы отрабатывают без ошибок, а бродкаст не приходит ;(
Вопрос от знатока. А почему не написали про Companion Device Manager (CDM)?
Когда я про неё прочитал, тоже подумал, что вот оно. Именно это решит проблему с пользователями. А вот реальность подкачала.
К сожалению документация, а конкретно примеры для java немного устарели (использованы депрекейтед StartIntentSenderForResult и onActivityResult), поэтому приведу уже поправленные коды.
Код поправлен на поиск всех устройств поблизости. Не важно есть у них имя или нет и какие типы интерфейсов поддерживают. В данном случае нас интересуют только само поведение «подружить» . Также для простоты minSDK поставлен от 8.0.
Если мы посмотрим в исходные коды операционной системы (Android SDK), то увидем:
public final class CompanionDeviceManager
так нелюбимое мною умирание молча . Что мешало сперва проверить callback, и если не работает вызвать failure ? Даже если это исправят, в предыдущих версиях андроида проблема останется 🙁
А причина ? В функции проверяется, что внутренняя переменная mService не null.
Конструктор принимает параметр службы как @Nullable. Получаем мы этот объект уже готовым:
CompanionDeviceManager deviceManager = context.getSystemService(CompanionDeviceManager.class);
Наш объект существует, но на практике часто приходит не работоспособным. И получается нажали на кнопку «подружить с новым» и никакой реакции. Это первое мое разочарование.
Failure вообще оказался неинформативным. Вызывается только при отказе выбора. Текст ошибки всегда один и тот же.
Запустите предложенный демо пример.
Работает правильно, только если определение местоположения включено .
Если что-то из показанных стрелками выключено, диалог просто не показывается. Никаких ошибок в callback не передается.
И пришли мы к тому, что должны пользователю показать диалог
Включи! Определение местоположения.
Это меня окончательно разочаровало.
Выводы
Блютуз не может работать без геолокации. Ничего не поменялось с появлением альтернатив.
Внедрять их все же придется, чтобы приложение соответствовало правилам Google Play.
Как минимум учесть новые разрешения для работы с блуютуз.