Хочу вам рассказать нашу историю из серии «ожидание и реальность» или о том, как слова товарища М.Ф. Квинтилиана: «Вредить легко, помогать трудно» приобрели для нас новый смысл.

Covid-19. Наверное, каждый слышал об этом вирусе. Я сейчас не берусь описывать свое личное отношение или обсуждать теории заговоров вокруг этого. Лично для меня это реальный кейс с близкими, которые заболели.

Столкнувшись с ковид лицом к лицу, наша команда решила внести свой вклад в борьбу с этим злом.


Мы разработали мобильное приложение, которое позволяет отслеживать местоположение опасных районов с ковид, с использованием данных о локации и по Bluetooth.

В отличие от аналогов, мы хотели сделать всё анонимно и добровольно. Без использования регистрации, привязки номера телефона и email.

Без отправки данных во все возможные инстанции.

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

Технических сложностей было много, начиная от работы локации в фоновом режиме и заканчивая обменом данных по Bluetooth без подключения.

Так как Bluetooth может вызвать много вопросов и, на наш взгляд, наиболее интересен, поделюсь хитрым решением с вами. Может кому-то пригодится в проектах.

Мы использовали Bluetooth одновременно как Beacon-маячок и как сканер маячков.

Наша задача находить контакты с заболевшими людьми рядом. Так как у нас анонимное приложение и у каждого пользователя есть только обезличенный ID, его мы и решили вещать по Bluetooth.

В Android для этого мы создали foreground службу BluetoothMonitorService. Эта служба должна вещать наш ID по Bluetooth и сканировать местность рядом на наличие других ID. В обычной службе операционная система не даст работать вашему приложению и, к сожалению, должно быть постоянное уведомление в трее.

Вторым важным моментом на этом этапе требуется наличие разрешения:

Manifest.permission.ACCESS_COARSE_LOCATION.

Так что локацию запрашивать придется. Без этого разрешения вам будет приходить пустой список.

Ну и конечно:

android.permission.BLUETOOTH
android.permission.BLUETOOTH_ADMIN

И так, начнем.

Для начала запустим вещание:

private void StartAdvertisingLegacy(long timeoutInMillis, int id) {

        byte[] bytesPID = ByteBuffer.allocate(charLength).putInt(id).array();

        data = new AdvertiseData.Builder()
                .setIncludeDeviceName(false)
                .setIncludeTxPowerLevel(false)
                .addServiceUuid(pUuid)
                .addManufacturerData(9, bytesPID)
                .build();

        try {
            Log.d(TAG, "Start advertising");
            advertiser = (advertiser != null) ? advertiser : BluetoothAdapter.getDefaultAdapter().getBluetoothLeAdvertiser();
            advertiser.startAdvertising(settings, data, advertisingCallback);
        } catch (Exception e) {
            Log.d(TAG, "Failed to start advertising legacy: ${e.message}");
        }
    }

Как можно заметить ID пользователя мы разместили в addManufacturerData.

Будьте внимательны, размер пакета ограничен 31 байтом. Отключите отображение имени и информацию об уровень мощности, чтобы хватило на ваш Payload.

addServiceUuid – это 128-битный уникальный идентификатор атрибута. Уникальный UUID для нашего приложения, по которому можно будет отфильтровать девайс при сканировании.

После этого слушаем ответ:

 AdvertiseCallback advertisingCallback = new AdvertiseCallback() {
        @Override
        public void onStartSuccess(AdvertiseSettings settingsInEffect) {
            super.onStartSuccess(settingsInEffect);
            Log.i(TAG, "Advertising onStartSuccess");
            Log.i(TAG, settingsInEffect.toString());
            isAdvertising = true;
        }

        @Override
        public void onStartFailure(int errorCode) {
            super.onStartFailure(errorCode);

            String reason;

            switch (errorCode) {
                case ADVERTISE_FAILED_ALREADY_STARTED:
                    reason = "ADVERTISE_FAILED_ALREADY_STARTED";
                    isAdvertising = true;
                    break;

                case ADVERTISE_FAILED_FEATURE_UNSUPPORTED:
                    reason = "ADVERTISE_FAILED_FEATURE_UNSUPPORTED";
                    isAdvertising = false;
                    break;

                case ADVERTISE_FAILED_INTERNAL_ERROR:
                    reason = "ADVERTISE_FAILED_INTERNAL_ERROR";
                    isAdvertising = false;
                    break;
                case ADVERTISE_FAILED_TOO_MANY_ADVERTISERS:
                    reason = "ADVERTISE_FAILED_TOO_MANY_ADVERTISERS";
                    isAdvertising = false;
                    break;
                case ADVERTISE_FAILED_DATA_TOO_LARGE:
                    reason = "ADVERTISE_FAILED_DATA_TOO_LARGE";
                    isAdvertising = false;
                    charLength--;
                    break;

                default:
                    reason = "UNDOCUMENTED";
            }

            Log.e(TAG, "Advertising onStartFailure: " + errorCode + " " + reason);
        }
    };

Параллельно запускаем наш сканер:

 public void StartScan(ScanCallback scanCallback) {
        ScanFilter filter = new ScanFilter.Builder()
                .setServiceUuid(new ParcelUuid(UUID.fromString(serviceUUID)))
                .build();

        List<ScanFilter> filters = new ArrayList<>();
        filters.add(filter);

        ScanSettings settings = new ScanSettings.Builder()
                .setReportDelay(reportDelay)
                .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
                .build();

        this.scanCallback = scanCallback;
        //try to get a scanner if there isn't anything
        scanner = (scanner != null) ? scanner : BluetoothAdapter.getDefaultAdapter().getBluetoothLeScanner();
        scanner.startScan(filters, settings, scanCallback);
        Log.d(TAG, "scanning started");
    }

ServiceUUID должен быть такой же, как при вещании, иначе вы не будете получать вообще никакой информации. Он уникален для вашего приложения. Сгенерировать можно вот тут https://www.uuidgenerator.net.

В самой службе следует установить удобные интервалы опроса, у нас это раз в 30 секунд. Это позволяет нам беречь заряд батареи.

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

После нескольких месяцев разработки появились iOS и Android версии приложения. Мы отправили их на проверку в Google и Apple.

И вот тут началось самое интересное, оказывается разработать решение – это самое лёгкое. Сложности начались с размещением в сторах.

В Google у нас получилось пройти, предоставив бумагу официальных гос. органов, что они не против приложения. Получить её в России само по себе подвиг, но это уже другая история.
С Apple была тишина долгое время. Мы писали повторно, оправляли им разрешение гос. органов, но ответа не было.

И вот, в один прекрасный день, звонок. Мне позвонили из Apple! Я так переволновался, что забыл английский язык. В Apple были крайне вежливы и после пяти минут общения сказали, что найдут переводчика.

Через некоторое время, мне позвонили опять и уже на русском с акцентом стали спрашивать о приложении.

Сказали, что их медицинский департамент заинтересовался нашим приложением, что без их одобрения мы не пройдем проверку. Что если мы ответим на их вопросы, они готовы даже посотрудничать. После этого мы какое-то время покорно отвечали на все их вопросы и ждали решения и долгожданного сотрудничества.

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

Отправляя письма во все инстанции, мы получали один ответ – полное отсутствие ответа.
Минздрав России мы решили атаковать активнее как Энди Дюфрейн. Но Минздрав был непробиваем, нам даже Госдепартамент США ответил на обращение, а вот Минздрав…

Нашему удивлению не было предела, когда Apple выпустила своё API ExposureNotification с формулировками точь-в-точь, повторяющими наши! Может быть это совпадение, но это настораживает и заставляет задуматься.

Хочу обратиться к вам за помощью в оценке приложения, в его распространении и донесении до наших властей, что можно и по-другому. Давайте попробуем вместе что-то поменять!

https://mysafe.ai

Google Play