Адаптация приложений для Android 10

Некоторые операции в Android 10 невозможно выполнять так же, как в Android 6. В этой статье мы собрали несколько советов для адаптации вашего приложения под Эвоторы с ОС на базе Android 10 (EvotorOS 5.x.x).

Получить IMEI

Чтобы получить IMEI устройства, используйте метод getImei():

public String getImei (int slotIndex)

В актуальных версиях Android этот метод отмечен как устаревший (deprecated) и запрещён для пользовательских приложений. В ОС Эвотор этот метод доступен, но нужны разрешения READ_PHONE_STATE и READ_PRIVILEGED_PHONE_STATE.

Runtime разрешения приложений

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

Для корректной работы приложения на Android 10 разработчик должен адаптировать его самостоятельно. Если приложению нужен доступ только к камере, местоположению, микрофону и календарю, то ничего делать не нужно — разрешения выдаются автоматически.

Доступ к /sdcard

В Android 10 по умолчанию нет доступа к /sdcard, его нужно включать вручную. В ОС Эвотор для Android 10 это делать не нужно, /sdcard будет доступен без дополнительных настроек так же, как на ОС Эвотор версии 4.х.

Работа приложений в фоне

Андроид 10 ограничивает работу приложений в фоновом режиме для максимальной автономности устройства. Чтобы приложение могло работать в фоновом режим, используйте метод startForeground():

public final void startForeground (int id,
  Notification notification)

Также обязательное требование — push-уведомление о том, что в фоновом режиме работает приложение.

Адаптировать приложение под требования Android 10 разработчику нужно самостоятельно. В ОС Эвотор 5.х возможно только скрыть все уведомления через флаг FLAG_HIDE_NOTIFICATION:

private const val FLAG_HIDE_NOTIFICATION = 0x10000000
notification.flags = notification.flags or FLAG_HIDE_NOTIFICATION

Если пропали уведомления

Используйте IMPORTANCE_DEFAULT вместо IMPORTANCE_NONE. Фрагмент кода для примера:

@RequiresApi(Build.VERSION_CODES.O) // API >= 26
private static String createNotificationChannel(Context context, String channelId, String channelName) {
    NotificationManager nm = (NotificationManager) context.getSystemService(NOTIFICATION_SERVICE);
    if (nm != null) {
        if (nm.getNotificationChannel(channelId) != null) {
            return channelId;
        }
        NotificationChannel nc = new NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_DEFAULT);
        nc.setLightColor(BLUE);
        nc.setLockscreenVisibility(VISIBILITY_PRIVATE);
        nm.createNotificationChannel(nc);
    }
    return channelId;
}

HDMI и два экрана на Эвотор Power

Отключение/включение HDMI

В Эвотор ОС есть широковещательное сообщение (broadcast) при подключении и отключении HDMI. Возможно два состояния: "newState" = "connected" / "disconnected"

Широковещательное сообщение:

ru.evotor.action.HDMI_STATUS_CHANGED

Переключение основного экрана

Эвотор Power может выводить информацию на встроенный или внешний экран. Встроенный экран — internal, используется по умолчанию, внешний экран — external. На выбранном экране будут отображаться панели уведомлений и навигации.

Узнать текущий основной экран

Метод для определения текущего основного экрана:

getWindowManager().getDefaultDisplay().getName()

Возможный результат:

Закрепить NavBar и StatusBar для приложений

Пример кода для захвата окна на дополнительном экране без блокирования панели навигации:

public static final int SYSTEM_UI_FLAG_IMMERSIVE_HIDDEN = 64; //0x40

getWindow().getDecorView().setSystemUiVisibility(
       View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
          | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
          | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // скрыть NavBar
          | View.SYSTEM_UI_FLAG_FULLSCREEN // скрыть StatusBar
          | View.SYSTEM_UI_FLAG_IMMERSIVE
          | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
          | SYSTEM_UI_FLAG_IMMERSIVE_HIDDEN); //0x40 <-- блокирует NavBar и StatusBar от вытягивания если они спрятаны предыдущими флагами

Отключить уменьшение яркости экрана при простое

Пример кода для отключения функции автоматического уменьшения яркости:

window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)

Режим работы Эвотор.Power как ФР или СТ

Эвотор Power может работать в режиме ФР или СТ. Текущий режим работы нужно учитывать в приложении. ОС один раз отправляет в приложения intent с обозначением текущего режима. После перезагрузки текущий режим нужно получать из API. Пример получения текущего режима из API:

package ru.evotor.framework.system.mode

object DeviceModeApi

............

/**
* Возвращает текущий режим работы терминала, если применимо, иначе - null
*/
@JvmStatic
fun getCurrentDeviceMode(context: Context): String? {

Пример реализации автофокуса на Эвотор Power

private fun startCameraX() {

        val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

        cameraProviderFuture.addListener(
            {
                // Used to bind the lifecycle of cameras to the lifecycle owner
                val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

                val profile = CamcorderProfile.get(CamcorderProfile.QUALITY_1080P)

                // Preview
                val preview = Preview.Builder()
                    .setTargetResolution(
                        Size(
                            profile.videoFrameHeight,
                            profile.videoFrameWidth
                        )
                    )
                    .build().also {
                        it.setSurfaceProvider(cameraPreviewContainer.surfaceProvider)
                    }

                val scannerAnalyzer = ScannerAnalyzer {
                    val rectangle = Rect()
                    val window = window
                    window.decorView.getWindowVisibleDisplayFrame(rectangle)
                    val statusBarHeight = rectangle.top
                    val contentViewTop =
                        window.findViewById<View>(Window.ID_ANDROID_CONTENT).top
                    val titleBarHeight = contentViewTop - statusBarHeight

                    it.boundingBox?.toRectF()?.also {
                        it.offset(0f, titleBarHeight.toFloat())
                        val rectView = RectView(this, it)
                        cameraPreviewContainer.addView(rectView)
                    }

                    it.rawValue?.let {
                        setResult(Activity.RESULT_OK, putScannerResult(Intent(), it))
                        finish()
                    }
                }

                val imageAnalyzer = ImageAnalysis.Builder()
                    .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
                    .setOutputImageFormat(OUTPUT_IMAGE_FORMAT_YUV_420_888)
                    .setTargetResolution(
                        Size(
                            profile.videoFrameHeight,
                            profile.videoFrameWidth
                        )
                    )
                    .build().also {
                        it.setAnalyzer(cameraExecutor, scannerAnalyzer)
                    }

                // Select back camera as a default
                val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

                try {
                    // Unbind use cases before rebinding
                    cameraProvider.unbindAll()
                    camera = cameraProvider.bindToLifecycle(
                        this, cameraSelector, preview, imageAnalyzer
                    )

                    val point = SurfaceOrientedMeteringPointFactory(
                        profile.videoFrameHeight.toFloat(),
                        profile.videoFrameWidth.toFloat(),
                        imageAnalyzer
                    ).createPoint(
                        profile.videoFrameHeight / 2f,
                        profile.videoFrameWidth / 2f
                    )
                    autoFocus(point)
                } catch (exc: Exception) {
                    Log.e(TAG, "Use case binding failed", exc)
                }
            },
            ContextCompat.getMainExecutor(this)
        )
    }

    private fun autoFocus(point: MeteringPoint) {
        camera?.cameraControl?.startFocusAndMetering(
            FocusMeteringAction.Builder(point).setAutoCancelDuration(2, TimeUnit.SECONDS).build()
        )?.addListener(
            {
                Thread.sleep(500)
                autoFocus(point)
            },
            Executors.newSingleThreadExecutor()
        )
    }