Как найти размеры для строки состояния, панели навигации и экрана в Android AccessibilityService

У меня есть приложение Java (API 23). В нем у меня есть MainActivity и AccessibilityService. В службе доступности:

@Override
public void onServiceConnected() {
    oView = new LinearLayout(this);
    oView.setBackgroundColor(0x88ff0000); // The translucent red color
    WindowManager.LayoutParams params = new WindowManager.LayoutParams(
            width,
            height,
            xPos, yPos,
            WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY,
            WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE |
                    WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
                    WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
            PixelFormat.TRANSLUCENT
    );
    WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
    wm.addView(oView, params);
} 

Мне нужно настроить width, height, xPos и yPos, чтобы они покрывали весь экран, включая строку состояния и панель навигации. Это означает, что мне нужно найти размеры для строки состояния, экрана и панели навигации (я могу определить нужные мне значения, когда они у меня будут).

Я нашел высоту строки состояния? - лучший ответ, похоже,

ViewCompat.setOnApplyWindowInsetsListener(rootView, new OnApplyWindowInsetsListener() {
                @Override
                public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) {
                    //THIS is the value you want.
                    int statusBarHeight = insets.getSystemWindowInsetTop();
                    int navigationBar = insets.getSystemWindowInsetBottom();

                    // Let the view handle insets as it likes.
                    return ViewCompat.onApplyWindowInsets(v, insets);
                }
            });

Но что rootView я могу использовать, поскольку нет гарантии, что моя активность будет видна? И нужно ли переносить создание представления из onServiceConnected() внутрь onApplyWindowInsets()?

Это действительно лучший способ сделать это?

Дополнительные вопросы, которые следует учитывать в связи с вышеизложенным - в зависимости от вышеизложенного и от того, отвечает ли он на них, я задам здесь или в отдельном вопросе:

Что происходит при повороте экрана? Что делать, если строка состояния и/или панель навигации исчезают (например, если они переходят в полноэкранный режим)? Похоже, мне нужно справиться с этим, перечитать высоты и заново создать представление. Любые мысли или рекомендации о том, как это сделать, будут приветствоваться.

=== Обновление - Прогресс на данный момент ===

Я думал, что мне нужен только размер экрана плюс информация о строке состояния (размер, местоположение), но панель навигации иногда находится в левой части экрана, поэтому мне это нужно, и клавиатура тоже может влиять на вещи.

Я могу получить размер экрана с помощью

    Display myDisplay = wm.getDefaultDisplay();
    Display.Mode mode = myDisplay.getMode();
    int height = mode.getPhysicalHeight();
    int width = mode.getPhysicalWidth();

Я все еще работаю над различными локациями и размерами. Я попытался создать представление, а затем прочитать вставки, но это дало мне неправильные ответы. Поэтому, если не появится лучший ответ, мне, вероятно, придется использовать

    int result = 0;
    int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
    if (resourceId > 0) {
        result = getResources().getDimensionPixelSize(resourceId);
    }

Но в некоторых случаях это может давать неправильные ответы, если строка состояния скрыта, и она не говорит мне, где она находится на экране. Я все еще работаю над этим; есть несколько вещей, чтобы попробовать, так что мы посмотрим.

Обработка ротации в службе может быть выполнена с помощью onConfigurationChanged() - см. -in-android">Как использовать сервис для отслеживания изменения ориентации в Android

Надеюсь, это поможет кому-то - пожалуйста, отчаянно ищу эти последние части.


person James Carlyle-Clarke    schedule 02.01.2021    source источник
comment
WindowInsets применяются к представлению. Если у вас нет представления, вы не можете их получить. Есть и другие способы получить status-bar или navigation-bar высоты, но они не на 100% надежны.   -  person Ahmad Sattout    schedule 02.01.2021
comment
Это было моей заботой. Если это окажется правдой, то я подозреваю, что мне придется либо прочитать представление моей активности и каким-то образом передать его, а затем иногда продолжать проверять его, либо создать окно с родительскими размерами (т.е. полный размер, кроме состояния/ панели навигации), прочитайте представления под ним, найдите полноэкранное, используйте его для чтения высоты, затем удалите окно и воссоздайте его с правильной высотой. Последнее кажется более сложным, но немного более последовательным. Я должен посмотреть, что говорят другие.   -  person James Carlyle-Clarke    schedule 02.01.2021
comment
@AhmadSattout, какой из альтернативных методов вы бы порекомендовали?   -  person James Carlyle-Clarke    schedule 15.01.2021


Ответы (1)


Обновление: есть проблема, когда экран переворачивается вверх ногами. См. Найдите размещение панели навигации Android без вставок (внутри службы )

В настоящее время я работаю над решением с моим собственным исследованием в сочетании с https://stackoverflow.com/a/55348825/1910690

Но я действительно хотел бы просто использовать Insets или что-то в этом роде.

==============

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

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

Дело в том, что в конце onGlobalLayout() внутри CreateLayoutHelperWnd() у вас есть полная информация о размере экрана, ориентации, видимости и размере строки состояния и панели навигации, и вы знаете, что либо вы только что запустили свой сервис, либо что-то изменилось. Я обнаружил, что иногда он вызывается дважды, хотя ничего не изменилось, вероятно, потому, что иногда он «скрывает панель состояния + панель навигации», а затем «меняет ориентацию» (одинаковые результаты для обоих), а иногда наоборот (разные результаты ), поэтому я обернул результаты в класс и сравниваю новые результаты с результатами, полученными в прошлый раз, и делаю что-то только в том случае, если результат изменился (вот почему я использую так много глобальных переменных, потому что позже они были перемещены в специальные сорт).

Обратите внимание, что это мой первый серьезный опыт работы с Android и Java, так что извините, если что-то не так — комментарии приветствуются. Он также не обрабатывает проверку ошибок (например, StatusBar не найден). Также обратите внимание, что я не тестировал это на нескольких устройствах, я понятия не имею, что он делает со складными устройствами, несколькими дисплеями или даже с разделенным экраном; но я попытался учесть проблемы, которые я видел в различных решениях (например, чтение высоты строки состояния иногда дает неправильный ответ, когда она закрыта на некоторых устройствах, поэтому я пытаюсь проверить, открыта она или закрыта). Я буду обновлять, если это необходимо, поскольку я тестирую больше.

В службе доступности:

// global variables
int statusBarHeight = -1;
int navigationBarHeight = -1;
int screenWidth = -1;
int screenHeight = -1;
View layoutHelperWnd;

@Override
public void onServiceConnected() {
    WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
    CreateLayoutHelperWnd(wm);
}

public void onUnbind() {
    WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
    DestroyLayoutHelperWnd(wm);
}

private void DestroyLayoutHelperWnd(WindowManager wm) {
    wm.removeView(layoutHelperWnd);
}
private void CreateLayoutHelperWnd(WindowManager wm) {
    final WindowManager.LayoutParams p = new WindowManager.LayoutParams();
    p.type = Build.VERSION.SDK_INT < Build.VERSION_CODES.O ?
            WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY :
            WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
    p.gravity = Gravity.RIGHT | Gravity.TOP;
    p.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
    p.width = WindowManager.LayoutParams.MATCH_PARENT;
    p.height = WindowManager.LayoutParams.MATCH_PARENT;
    p.format = PixelFormat.TRANSPARENT;
    layoutHelperWnd = new View(this); //View helperWnd;

    wm.addView(layoutHelperWnd, p);
    final ViewTreeObserver vto = layoutHelperWnd.getViewTreeObserver();
    vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {

        @Override
        public void onGlobalLayout() {
            // catches orientation change and fullscreen/not fullscreen change

            // read basic screen setup - not sure if needed every time, but can't hurt
            WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
            ReadScreenDimensions(wm);   // sets up screenWidth and screenHeight
            statusBarHeight = GetStatusBarHeight();
            navigationBarHeight = GetNavigationBarHeight();

            int windowHeight = layoutHelperWnd.getHeight();
            int windowWidth = layoutHelperWnd.getWidth();

            Boolean isFullScreen, isStatusBar, isNavigationBar;
            isFullScreen = isStatusBar = isNavigationBar = false;

            Configuration config = getResources().getConfiguration();
            if (config.orientation == ORIENTATION_LANDSCAPE) {
                // landscape - screenWidth is for comparison to windowHeight (top to bottom), status bar may be on top, nav bar may be on right
                if (screenWidth != windowHeight)
                    isStatusBar = true;

                if (screenHeight != windowWidth)
                    isNavigationBar = true;
            }
            else {
                // portrait, square, unknown - screenHeight is for comparison to windowHeight (top to bottom), status bar may be on top, nav bar may be on bottom
                if (screenHeight != windowHeight) {
                    int difference = screenHeight - windowHeight;
                    if (difference == statusBarHeight)
                        isStatusBar = true;
                    else if (difference == navigationBarHeight)
                        isNavigationBar = true;
                    else {
                        isStatusBar = true;
                        isNavigationBar = true;
                    }
                }
            }

            if (!isStatusBar && !isNavigationBar)
                isFullScreen = true;

            Log.v(TAG,"Screen change:\nScreen W,H: (" + screenWidth + ", " + screenHeight + ")\nOrientation: " + (config.orientation == ORIENTATION_LANDSCAPE ? "Landscape" : config.orientation == ORIENTATION_PORTRAIT ? "Portrait" : "Unknown") +
                    "\nWindow W,H: (" + windowWidth + ", " + windowHeight + ")\n" + "Status bar H: " + statusBarHeight + ", present: " + isStatusBar + "\nNavigation bar H: " + navigationBarHeight + ", present: " + isNavigationBar + "\nFullScreen: " + isFullScreen);

            // do any updates required to the service's assets here
        }
    });
}

public void ReadScreenDimensions(WindowManager wm) {
    Display myDisplay = wm.getDefaultDisplay();
    Display.Mode mode = myDisplay.getMode();
    screenHeight = mode.getPhysicalHeight();
    screenWidth = mode.getPhysicalWidth();
}

public int GetStatusBarHeight() {
    // returns 0 for no result found
    int result = 0;
    int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
    if (resourceId > 0) {
        result = getResources().getDimensionPixelSize(resourceId);
    }
    return result;
}
public int GetNavigationBarHeight() {
    // returns 0 for no result found
    int result = 0;
    int resourceId = getResources().getIdentifier("navigation_bar_height", "dimen", "android");
    if (resourceId > 0) {
        return getResources().getDimensionPixelSize(resourceId);
    }
    return result;
}

Вам также нужно будет добавить в свой манифест (для приложения, а не службы):

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

и в вашей основной деятельности вызовите isSystemAlertPermissionGranted(), а затем сделайте что-то значимое в зависимости от того, что возвращается (возможно, в некоторых случаях ожидание до onActivityResult()). Э-э, я в основном схватил это оптом из разных мест и почти не изменил его. :)

public static int ACTION_MANAGE_OVERLAY_PERMISSION_REQUEST_CODE= 1234;

public boolean isSystemAlertPermissionGranted() {
    // if this doesn't work, try https://stackoverflow.com/questions/46208897/android-permission-denied-for-window-type-2038-using-type-application-overlay
    if (Build.VERSION.SDK_INT >= 23) {
        if (!Settings.canDrawOverlays(this)) {
            Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
                    Uri.parse("package:" + getPackageName()));
            startActivityForResult(intent, ACTION_MANAGE_OVERLAY_PERMISSION_REQUEST_CODE);
            return false;
        }
        else
        {
            Log.v(TAG,"Permission is granted");
            return true;
        }
    }
    else { //permission is automatically granted on sdk<23 upon installation
        Log.v(TAG, "Permission is granted");
        return true;
    }
}

@Override
protected void onActivityResult(int requestCode, int resultCode,  Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == ACTION_MANAGE_OVERLAY_PERMISSION_REQUEST_CODE) {
        // Check if the app get the permission
        if (Settings.canDrawOverlays(this)) {
            // Run your logic with newly-granted permission.
        } else {
            // Permission not granted. Change your logic accordingly.
            // App can re-request permission anytime.
        }
    }
}
person James Carlyle-Clarke    schedule 20.01.2021