Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Разработчики Вы там совсем обнаглели ? Зачем Вам мое местоположение ?
Типовой отзыв для андроид приложения, работающего с блютуз устройством.
Статья построена в виде спора с воображаемым пользователем. Для андроид разработчиков рассмотрены как классические способы, так и рекомендумые альтернативы.
От вашей программы требуется только отправить данные на устройство !
Минимально необходимый код (далее МНК) для отправки данных на классическое блютуз устройство с реализаций 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 переработали набор пермишенов . Ознакомиться подробнее можно по ссылке 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<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. Но главное из-за выделенного жирным в документации
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 .
Единственное чего мы достигли, пользователи более ранних версий останутся в неведении. Напомню, что пермишен нужен и для работы МНК ( .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 вообще оказался неинформативным. Вызывается только при отказе выбора. Текст ошибки всегда один и тот же.
Запустите предложенный демо пример.
Работает правильно, только если определение местоположения включено .
Если что-то из показанных стрелками выключено, диалог просто не показывается. Никаких ошибок в callback не передается.
И пришли мы к тому, что должны пользователю показать диалог
ВКЛЮЧИ! ОПРЕДЕЛЕНИЕ МЕСТОПОЛОЖЕНИЯ
Это меня окончательно разочаровало
Выводы
Блютуз не может работать без геолокации. Ничего не поменялось с появлением альтернатив.
Внедрять их все же придется, чтобы приложение соответствовало правилам Google Play.
Как минимум учесть новые разрешения для работы с блуютуз.