Update: There's a problem when the screen is rotated upside-down. See Find placement of Android Navigation Bar without Insets (inside Service)
I'm currently working on a solution with my own research combined with https://stackoverflow.com/a/55348825/1910690
But I really wish I could just use Insets, or something like that.
==============
I tried a bunch of things that seemed to fail because I didn't have an activity; it's possible if I had poked around some more I would have made it work with Insets, and I still hope for an answer using them as they are more official and allow for notches etc.
In the meantime I finally pieced various answers together to make the following. It would probably work for a normal Service as well as an AccessibilityService.
The point is that at the end of onGlobalLayout()
inside CreateLayoutHelperWnd()
you have the complete information on screen size, orientation, status bar and navigation bar visibility and size, and you know that either you've just started your service or something changed. I found that it gets called twice sometimes even though nothing changed, probably because sometimes it does 'hide status+nav bar' and then 'change orientation' (same results for both), and sometimes it does it the other way around (different results) so I have wrapped the results in a class, and I compare the new results with the results from the last time and only do something if a result changed (that's why I use so many global variables, because those got moved later to the special class).
Please note that this is my first serious foray into Android and Java, so apologies if it's badly done - comments welcomed. It also doesn't handle error checking (eg StatusBar not found). Also note that I have not tested this extensively across multiple devices, I have no idea what it does with folding devices, multiple displays, or even split-screen; but I tried to account for the issues I saw in the various solutions (eg reading Status Bar height sometimes gives the wrong answer when it's closed on some devices, so I try to check if it is open or closed). I'll update if needed as I test more.
In the AccessibilityService:
// 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:
Screen W,H: (" + screenWidth + ", " + screenHeight + ")
Orientation: " + (config.orientation == ORIENTATION_LANDSCAPE ? "Landscape" : config.orientation == ORIENTATION_PORTRAIT ? "Portrait" : "Unknown") +
"
Window W,H: (" + windowWidth + ", " + windowHeight + ")
" + "Status bar H: " + statusBarHeight + ", present: " + isStatusBar + "
Navigation bar H: " + navigationBarHeight + ", present: " + isNavigationBar + "
FullScreen: " + 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;
}
You will also need to add to your manifest (for the app, not the service):
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
and in your main activity call isSystemAlertPermissionGranted()
and then do something meaningful depending on what is returned (possibly waiting until onActivityResult()
in some cases). Er, I basically grabbed this wholesale from various places and have not changed it much or at all. :)
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.
}
}
}