[recovery mode] Возможна ли работа с bluetooth в Android без местоположения?

575007c6b946ea7da4bc9b8921228edc.png

Разработчики Вы там совсем обнаглели ? Зачем Вам мое местоположение?

Типовой отзыв для андроид приложения, работающего с блютуз устройством.

Статья построена в виде спора с воображаемым пользователем. Для андроид разработчиков рассмотрены как классические способы, так и рекомендумые альтернативы.

От вашей программы требуется только отправить данные на устройство !

Минимально необходимый код (далее МНК) для отправки данных на классическое блютуз устройство с реализаций 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 переработали набор пермишенов. Ознакомиться подробнее можно по ссылке  https://developer.android.com/guide/topics/connectivity/bluetooth/permissions

Главное отличие новых разрешений в том, что их нужно не только добавить в манифест, но и запросить у пользователя явно.

Вот. Сами пишите, что доступ к местоположению не нужен.

 Обратите внимание на следующую строку

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 {
   getBonded();
}
btActivityResultLauncher = registerForActivityResult(
       new ActivityResultContracts.StartActivityForResult(),
       result -> {
           if (result.getResultCode() == Activity.RESULT_OK) {
               getBonded();
           }
       });

Тут вопрос от знатока. Можно же mBtAdapter.enable () использовать, почему так сложно ?

Вариант выше не требует дополнительных разрешений. Метод enabled () становиться депрекайтед в 13-м андроиде. Пример выше основан на рекомендованной альтернативе. Для предыдущих версий в манифесте должен быть еще пермишен BLUETOOTH_ADMIN. Но главное из-за выделенного жирным в документации

https://developer.android.com/reference/android/bluetooth/BluetoothAdapter#enable ()

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 .

315ea71f8e9fad738f1c64e912962957.jpg

Единственное чего мы достигли, пользователи более ранних версий останутся в неведении.  Напомню, что пермишен нужен и для работы МНК (.getRemoteDevice (), .connect ()).

Почему же так?

У вас сопряжен с телефоном телевизор, колонки.  Программа это увидела и если у нее есть биг дата по пользователям, то как минимум она вычислит Ваш город. Если мало мобильное устройство в зоне досягаемости (удалось к нему подключиться), то можно местоположение сузить  до 100 метров. 

Именно о таком теоретическом возможном  риске Вас предупреждают. 

У меня Android 6–11. Почему же я вижу запрос к местоположению ?

Мы рассмотрели вариант, когда Вы предварительно сделали сопряжение, не все пользователи могут сделать этот шаг самостоятельно.  Часто для простоты даже не смотрят в список сопряженных, а начинают опрос эфира.

До 6-го андроида разрешения предоставлялись автоматически по факту упоминания в манифесте. Потом разрешения решили поделить, условно безопасные так и остались, а остальные стало требоваться запрашивать явно. В коде программ появился костыль вида

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
  if (checkSelfPermission(Manifest.permission. …) != PackageManager.PERMISSION_GRANTED ){
	…
  }
}

Пользователи  стали видеть запросы.

Процесс опроса эфира асинхронный.

1) Создаем слушателя

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);

3) Запускаем процесс

mBtAdapter.startDiscovery();

Прочитать документацию Вы можете самостоятельно

https://developer.android.com/reference/android/bluetooth/BluetoothAdapter#startDiscovery ()

Выскажу свое мнение почему там сбоку прикрутили геолокацию. Так у нас два пермишена BLUETOOTH слишком общий, BLUETOOTH_ADMIN нужен для изменения статуса и позволяет сканировать. Сделать его явно запрашиваемым, поломается много программ. У нас тут еще есть ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION, которые нужно явно запрашивать. Так может скрестим и по смыслу подходят.

И вот пошли шатания от версии к версии. Нужно ли именно FINE или хватит COARSE.

В 12 м нужна связка BLUETOOTH_SCAN & ACCESS_FINE_LOCATION.

В андроиде есть неудобная для программистов практика без ошибки игнорировать действия, которые потом решили запретить и/или возвращать пустой/фиктивный результат. Делается это для того, чтобы устаревшие программы не завершились аварийно с ошибкой. Да еще вендоры могут внести свое видение того как правильно. 

Кроме того в момент первоначальной настройки нового телефона/приставки можно запретить всем приложениям доступ к местоположению. После этого узнать, что всем или конкретно нам запрещено нет возможности. Все методы  отрабатывают без ошибок, а бродкаст не приходит ;(

Вопрос от знатока. А почему не написали про Companion Device Manager (CDM)  ?

Когда я про неё прочитал, тоже подумал, что вот оно. Именно это решит проблему с пользователями. А вот реальность подкачала.

К сожалению документация, а конкретно примеры для java немного устарели (использованы депрекейтед  StartIntentSenderForResult и onActivityResult), поэтому приведу уже поправленные коды.

https://github.com/Muraveiko/AndroidBtCompanionDemo/blob/main/app/src/main/java/ru/a402d/btcompaniondemo/MainActivity.java

Код поправлен на поиск всех устройств поблизости. Не важно есть у них имя или нет и какие типы интерфейсов поддерживают. В данном случае нас интересуют только само поведение «подружить» . Также для простоты minSDK поставлен от 8.0.

Если мы посмотрим в исходные коды операционной системы (Android SDK), то увидем

public final class CompanionDeviceManager {
public void associate(
       @NonNull AssociationRequest request,
       @NonNull Callback callback,
       @Nullable Handler handler) {
   if (!checkFeaturePresent()) {
       return;
   }

 так нелюбимое мною умирание молча. Что мешало сперва проверить callback, и если не работает вызвать failure? Даже если это исправят, в предыдущих версиях андроида проблема останется :(

А причина ? В функции проверяется, что внутренняя переменная mService не null.

Конструктор принимает параметр службы как @Nullable. Получаем мы этот объект уже готовым 

CompanionDeviceManager deviceManager 
         = context.getSystemService(CompanionDeviceManager.class);

Наш объект существует, но на практике часто приходит не работоспособным. И получается нажали на кнопку  «подружить с новым»  и никакой реакции.  Это первое мое разочарование. 

Failure вообще оказался неинформативным. Вызывается только при отказе выбора. Текст ошибки всегда один и тот же.

Запустите предложенный демо пример.

b0bb4b4d599d2e9c7bfab7150dd62e91.jpg

Работает правильно, только если определение местоположения включено .

Если что-то из показанных стрелками выключено, диалог просто не показывается. Никаких ошибок в callback не передается.

И пришли мы к тому, что должны пользователю показать диалог

ВКЛЮЧИ! ОПРЕДЕЛЕНИЕ МЕСТОПОЛОЖЕНИЯ

Это меня окончательно разочаровало

Выводы

Блютуз не может работать без геолокации. Ничего не поменялось с появлением альтернатив.

Внедрять их все же придется, чтобы приложение соответствовало правилам Google Play.

Как минимум учесть новые разрешения для работы с блуютуз.

© Habrahabr.ru