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

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

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

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

Минимально необходимый код (далее МНК) для отправки данных на классическое блютуз устройство с реализаций 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.

Необходимо добавить для работоспособности этого кода в манифесте приложения:

<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

До апи 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<BluetoothDevice> btDevices = mBtAdapter.getBondedDevices();

Казалось бы одна строчка кода, что тут может не работать?

Во первых, всё таки встречаются устройства без BT. Подстраховаться можно строкой в манифесте.

<uses-feature android:name="android.hardware.bluetooth" android:required="true" />

В этом случае приложение нельзя будет поставить на андроид устройство без блютуз адаптера. Если с перефирийным устройством можно общаться еще через 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. Но главное из-за выделенного жирным в документации

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 ){
	…
  }
}

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

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

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

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

Выскажу свое мнение почему там сбоку прикрутили геолокацию. Так у нас два пермишена 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 {
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 вообще оказался неинформативным. Вызывается только при отказе выбора. Текст ошибки всегда один и тот же.

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

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

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

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

Включи! Определение местоположения.

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

Выводы

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

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

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

Комментарии (15)


  1. Lebets_VI
    07.07.2022 16:10

    Позвольте мне:), я :),я :) ,я :),но сегодня без матов (и так замисули :)

    ТС, я искренне понимаю Ваше негодование, но:

    -- "Блютуз не может работать без геолокации " - может, но "гугл решил что нет" (С) (квест: почему?)

    -- "Включи! Определение местоположения." - хочешь юзать - исполняй, иначе ну просто не будет работать, почему? - (С) - читай первый пункт ;).

    Остальное - всего лишь следствие и разрыв шаблона, на самом деле всё гораздо... (каждый эксплуататор БТ вставит подходящий набор слов вместо троеточия).

    Реально, искренне желаю принять приколы БТ и стать философом используя эту технологию.


    1. Ztare
      07.07.2022 18:19
      +2

      Объяснили же. Знание онлайна даже одного блютуз мак адреса уже раскрывает ваше местоположение. Т.к. остальные девайсы могут сопоставить местоположение этого же мак адреса с гео и отправить на тотже сервер


      1. Lebets_VI
        07.07.2022 18:33

        Во-первых, спасибо за минусы.

        Во-вторых: Мне не нужно ничего объяснять, т.к. я наизусть знаю спецификацию.

        Во-третьих:  "Знание онлайна даже одного блютуз мак адреса уже раскрывает ваше местоположение" - откуда такие выводы? Почитайте спецификацию, а именно, что такое в блютусе мак-адрес. Он, если что, никакого отношения не имеет к общепринятому определению мак-адресов. А если это проблема из-за гугла, то scuzi, опять же, это проблема гугла, а не технологии вообще.

        Еще раз повторяю, если гугл так решил, это не значит что это распространяется на сам блютус.

        Кстати ТСу и за статью инкремент сделал и карму плюсанул.


        1. Ztare
          07.07.2022 19:34
          +2

          (Минусы не мои, прав нет) Очень просто - у вас дома условная кофеварка с блютусом. Ее адрес вы вбили в приложении (спарили, нашли etc). Этот же адрес видит условная машина яндекса сканирующая сети и картографирующая город. Вот теперь когда адрес виден в приложении как онлайн то сервер этого приложения может узнать у условного яндекса где вы с точностью до десятка метров. Понятно в целом нюансов много, но грубо так может работать.

          Аналогично сбором сведений о положении адресов могут заниматься другие смартфоны с включенным гео и блютусом


          1. Lebets_VI
            07.07.2022 20:20
            +2

            Даже приняв невероятное, а именно то, что кофеварка будет видна в эфире будучи спаренной и в это время Яндекс-машина подъедет к кофеварке ближе чем на 10 метров, это не противоречит тому, что именно Гугл (ни яблоко, ни Микрософт ни кто-то другой) привязывает разрешение работы блютуса к местоположению. Наверное все таки у гугла какие-то хитрые планы:)

            А я всего лишь сказал, что Гугл «редиска» :) и тут ничего не сделаешь. (Хотя этот прикол не на всех Андроидах соблюдается, но это уже другая история)


            1. Ztare
              09.07.2022 20:08

              Блютуз до 60 метров и в мобильниках умеет, думаю яндекс сможет и 100 через дома. Редмондовое блютус барахло именно так работает - всегда ищется. Гугл делает все возможное кроме реально важного )


              1. 402d Автор
                09.07.2022 23:45

                Не туда смотрите. Есть такой и подобные им облачные сервисы https://coolkit-technologies.github.io/eWeLink-API/#/en/APIReferenceV2 . И есть куча оборудования для умного дома. https://aliexpress.ru/popular/ewelink-smart-switch.html В таких устройствах есть bluetooth, wifi и оно еще в облаке. Детские смарт часы , фитнес браслеты, электросамокаты. А где и главное кому сервера принадлежат ? Китай. А гугл чей ?


  1. johnfound
    08.07.2022 00:37

    А почему у меня bluetooth работает без местоположения. И вообще-то я именно так и использую его, а местоположение включаю очень редко. Телефон Redmi какой-то...


    1. 402d Автор
      08.07.2022 08:35

      с 8го до 11го андроид сопряжение через компаин и общение с бле/классик не сопровождается внешними эфектами. Пермишены на локейшен не нужны. Но тут или пользователи будут ругать программу. Найти. Жму - оно ничего не делает.

      поэтому перед вызовом associate(), проверять приходиться из кода самостоятельно. Статус блютуз и геолокацию. И тут уже самостоятельно писать пользователю. Откройте шторку . Включите геолокацию. Круг замкнулся. Параноик ее выключил - А приложение ее просить включить.


    1. DaemonGloom
      08.07.2022 10:12
      +1

      У вас работает bluetooth (т.е. наушники, передача файлов, мыши/клавиатуры/джойстики) или приложения, использующие bluetooth для своих целей (связь с конкретным устройством типа принтеров или иных вещей, функционал которых отсутствует в системе изначально)?
      С первым в android проблем нет, только со вторым.


      1. johnfound
        08.07.2022 16:03

        Ага, кажется что понял. Google гадит – ничего странного. Я куплю телефон с Линукс. Как только завезут...


  1. Paulus
    08.07.2022 04:12

    Спасибо за статью! Несколько лет назад после смены одного Андроида на другой тоже удивился, что программа управления отоплением через BT внезапно стала требовать координат. Думал, что баг в новой версии, а оказывается это фича


  1. larasage
    08.07.2022 07:40
    +1

    Выдается разрешение приложению на определение местоположения, спаривается с устройством, разрешение отбирается. У жены браслет так подключал.

    Ну а про видимость MAC-адресов... Кто сказал, что они уникальные? :)


  1. NickH12
    08.07.2022 21:54

    android:usesPermissionFlags="neverForLocation"

    Не работает?


    1. 402d Автор
      09.07.2022 08:33

      BLUETOOTH_SCAN . Когда я стал свой код отлаживать под 12-й, то это разрешение у меня потребовалось только для одной функции

      if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
          if (defaultAdapter.isDiscovering()) {
                      defaultAdapter.cancelDiscovery();
          }
      }

      просто обернул. Пожертвовав надежностью работы. У меня служба печати. Небольшая теоретическая возможность была, что другая программа запустила сканирование.

      У меня в манифесте 4 пермишена. bluetooth, bluetooth_admin ограничены 30, bluetooth_connect для новых. И пермишен просящий приблизительное положение в версиях ниже андроида 8.0. Выше работаю через компанион.