Android Hide & Disable Notification (Status) Bar - android

I've been able to hide the notification bar by going full screen, by using the code below
android:theme="#android:style/Theme.Holo.NoActionBar.Fullscreen"
or
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
But what I am trying to do is completely disable the status bar. I'm in what's known as a "kiosk mode" and I'd like to make sure that a user can not slide their finger down from the top bezel. Both of the solutions above work for hiding the notification bar, but it does not work for disabling it completely within the app.
Is this possible?

Instead of following links to other answers, here's what I did.
This solution does not disallow a user to "view" the status bar in it's 'preview' state if pulled down (even in a full screen app), but it DOES disallow a user from pulling the status bar down to it's full state to see settings, notifications, etc.
You must first add the permissions in your AndroidManifest.xml
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
Then add another class (Java file) called customViewGroup.java and place this code in it:
import android.content.Context;
import android.util.Log;
import android.view.MotionEvent;
import android.view.ViewGroup;
public class customViewGroup extends ViewGroup {
public customViewGroup(Context context) {
super(context);
}
#Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
}
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.v("customViewGroup", "**********Intercepted");
return true;
}
}
After you have both of those set up, you can then add this into your main onCreate()
WindowManager manager = ((WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE));
WindowManager.LayoutParams localLayoutParams = new WindowManager.LayoutParams();
localLayoutParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ERROR;
localLayoutParams.gravity = Gravity.TOP;
localLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE|
// this is to enable the notification to recieve touch events
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL |
// Draws over status bar
WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
localLayoutParams.width = WindowManager.LayoutParams.MATCH_PARENT;
localLayoutParams.height = (int) (50 * getResources().getDisplayMetrics().scaledDensity);
localLayoutParams.format = PixelFormat.TRANSPARENT;
customViewGroup view = new customViewGroup(this);
manager.addView(view, localLayoutParams);
This solution disables the ability to pull the status bar down always, until your app is closed. You'll have to remove this action on pause if you don't want to close your app every time.
Credit goes to #Abhimaan Madhav from This Answer

I think permanently disable the status bar is difficult. I am also working on the same concept and did lots of R&D and found that below code can be useful. if the user tries to expand the status bar then within a sec it will pull back it and it will work on oreo as well. I have tried on different OS.
public class BlockStatusBar {
Context context;
// To keep track of activity's window focus
boolean currentFocus;
// To keep track of activity's foreground/background status
boolean isPaused;
public Handler collapseNotificationHandler;
Method collapseStatusBar = null;
public BlockStatusBar(Context context, boolean isPaused) {
this.context = context;
this.isPaused = isPaused;
collapseNow();
}
public void collapseNow() {
// Initialize 'collapseNotificationHandler'
if (collapseNotificationHandler == null) {
collapseNotificationHandler = new Handler();
}
// If window focus has been lost && activity is not in a paused state
// Its a valid check because showing of notification panel
// steals the focus from current activity's window, but does not
// 'pause' the activity
if (!currentFocus && !isPaused) {
Runnable myRunnable = new Runnable() {
public void run() {
// do something
try {
// Use reflection to trigger a method from 'StatusBarManager'
Object statusBarService = context.getSystemService("statusbar");
Class<?> statusBarManager = null;
try {
statusBarManager = Class.forName("android.app.StatusBarManager");
} catch (ClassNotFoundException e) {
Log.e(LOG_TAG, "" + e.getMessage());
}
try {
// Prior to API 17, the method to call is 'collapse()'
// API 17 onwards, the method to call is `collapsePanels()`
if (Build.VERSION.SDK_INT > 16) {
collapseStatusBar = statusBarManager.getMethod("collapsePanels");
} else {
collapseStatusBar = statusBarManager.getMethod("collapse");
}
} catch (NoSuchMethodException e) {
Log.e(LOG_TAG, "" + e.getMessage());
}
collapseStatusBar.setAccessible(true);
try {
collapseStatusBar.invoke(statusBarService);
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
// Check if the window focus has been returned
// If it hasn'kioskthread been returned, post this Runnable again
// Currently, the delay is 100 ms. You can change this
// value to suit your needs.
if (!currentFocus && !isPaused) {
collapseNotificationHandler.postDelayed(this, 100L);
}
if (!currentFocus && isPaused) {
collapseNotificationHandler.removeCallbacksAndMessages(null);
}
} catch (Exception e) {
Log.e("MSG", "" + e.getMessage());
}
}
};
// Post a Runnable with some delay - currently set to 300 ms
collapseNotificationHandler.postDelayed(myRunnable, 1L);
}
}
}

Related

toolbar button disappearing when dialog closed

my app is fairly simple and always works IF my IOT device is up.
i need to load a popup and show the ReScan button on the toolbar if the device cannot be found.
the app preloads IPaddress="-" and loads 2 asyncTask(s)
one uses NsdManager.DiscoveryListener to find the mDNS name and loads the IP into IPaddress
this task watches to see IPaddress change and gets the presets from the device by JSON and sets up the UI or pops up the error dialog with instructions if not found.
MY PROBLEM:
when counter >= 15 , i show the "Rescan" Button on the toolbar with setMenuVisible() then popup the error dialog but when the button in the dialog is pressed to close the dialog, the "Rescan" Button disappears again.
Also times out in about 5 seconds.
how do i get the "Rescan" Button to stay?
.
private class getSettingsFromClock extends AsyncTask<Void, Void, Void> {
#Override
protected Void doInBackground(Void... params) {
String mlooper = IPaddress;
Log.i(TAG, "LOG getSettingsFromClock doInBackground started ");
int counter = 0;
while ( mlooper.equals("-") ) {
mlooper = IPaddress;
try {
Thread.sleep(600);
} catch (InterruptedException e) {
e.printStackTrace();
}
counter++;
if (counter >= 15) // in normal operation counter never goes above 3
{
Log.i(TAG, "LOG getSettingsFromClock - NO IP Found, count= " + counter );
runOnUiThread(new Runnable() {
#Override
public void run() {
setMenuVisible( true, R.id.action_rescan); // show rescan button on toolbar
try { // delay is debugging only
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
//scanning failed Popup Dialog
final Dialog dialog = new Dialog(context );
dialog.setContentView(R.layout.popup);
dialog.setTitle("Scan Error");
Button button = dialog.findViewById(R.id.Button01);
button.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View view) {
dialog.dismiss();
}
});
dialog.show();
Toast.makeText(getApplicationContext(),
"Could Not get presets from clock. \n check Clock is on and on WiFi\n and reload app.",
Toast.LENGTH_LONG).show();
}
});
break;
}
}
if( IPaddress != "-" )
{
// gets JSON here
} else
{
// add popup - IOT Not found
}
// JSON starts here
if (JSON_return != null) {
try {
// loads presets from JSON to UI here
} catch (final JSONException e) {
Log.e(TAG, "LOG, JSON parsing error: " + e.getMessage());
}
} else
{
Log.e(TAG, "LOG, Could Not get JSON from Clock.");
}
return null;
}
} // end asyncTask class
// remember to run on main thread
// NOTE; private Menu option_Menu; declared in MainActivity
// ie; setMenuVisible( true, R.id.action_rescan);
public void setMenuVisible(boolean visible, int id) {
if (option_Menu != null) {
option_Menu.findItem(id).setVisible(visible);
}
}
Mike M. had it, Thank You Mike
added onPrepareOptionsMenu()
added showRescan = visible; and invalidateOptionsMenu(); to setMenuVisible()
all work perfectly now.
#Override
public boolean onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
try {
if( showRescan )
{
option_Menu.findItem(R.id.action_rescan).setVisible( true );
} else
{
option_Menu.findItem(R.id.action_rescan).setVisible( false );
}
}
catch(Exception e) {
Log.e(TAG, "onPrepareOptionsMenu error");
}
return true;
}
// when task is completed you can show your menu just by calling this method
// remember to run on main thread
// ie; setMenuVisible( true, R.id.action_rescan);
public void setMenuVisible(boolean visible, int id) {
if (option_Menu != null) {
option_Menu.findItem(id).setVisible(visible);
showRescan = visible;
invalidateOptionsMenu();
}
}

Restrict Notification Bar from pulling

#Override
public void onWindowFocusChanged(boolean hasFocus){
super.onWindowFocusChanged(hasFocus);
try{
if(!hasFocus && enableKioskMode){
Intent closeDialog = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
sendBroadcast(closeDialog);
ActivityManager am = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE);
am.moveTaskToFront(getTaskId(), ActivityManager.MOVE_TASK_WITH_HOME);
// sametime required to close opened notification area
Timer timer = new Timer();
timer.schedule(new TimerTask(){
public void run() {
Intent closeDialog = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
sendBroadcast(closeDialog);
}
}, 500);
}
}catch(Exception ex){
ex.printStackTrace();
}
}
private class CustomViewGroup extends ViewGroup {
public CustomViewGroup(Context context) {
super(context);
}
#Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
}
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return true;
}
}
private void addBlockingViews() {
try {
WindowManager manager = ((WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE));
//For Bottom View
WindowManager.LayoutParams bottomlocalLayoutParams = new WindowManager.LayoutParams();
bottomlocalLayoutParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ERROR;
bottomlocalLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL |
WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
bottomlocalLayoutParams.width = WindowManager.LayoutParams.MATCH_PARENT;
bottomlocalLayoutParams.height = (int) (50 * getResources().getDisplayMetrics().scaledDensity);
bottomlocalLayoutParams.format = PixelFormat.RGBX_8888;
bottomlocalLayoutParams.gravity = Gravity.BOTTOM;
bottomView = new CustomViewGroup(BaseActivity.this);
ViewGroup.LayoutParams layoutParams1 = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 50);
bottomView.setLayoutParams(layoutParams1);
manager.addView(bottomView, bottomlocalLayoutParams);
WindowManager.LayoutParams toplocalLayoutParams = new WindowManager.LayoutParams();
toplocalLayoutParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ERROR;
toplocalLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL |
WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
int resId = getResources().getIdentifier("status_bar_height", "dimen", "android");
int result = 0;
if (resId > 0) {
result = getResources().getDimensionPixelSize(resId);
} else {
// Use Fallback size:
result = 60; // 60px Fallback
}
//toplocalLayoutParams.height = result;
toplocalLayoutParams.height = (int) (50 * getResources().getDisplayMetrics().scaledDensity);
toplocalLayoutParams.gravity = Gravity.TOP;
toplocalLayoutParams.format = PixelFormat.TRANSPARENT;
topView = new CustomViewGroup(BaseActivity.this);
ViewGroup.LayoutParams layoutParams2 = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
25);
topView.setLayoutParams(layoutParams2);
manager.addView(topView, toplocalLayoutParams);
} catch (Exception e) {
e.printStackTrace();
}
}
My aim is to create a Kiosk app. I checked many codes for that like this and this. With their help I have achieved navigation bar hiding. Now I want to block user from dragging the notification bar down just like Surelock does. I've tried the common answers given in SO posts like here. But it does not work in my Redmi Note 5 Pro with Android Pie. Is there any other way to accomplish this?
In this solution notification bar is not blocked entirely but it gets closed after user opens it. You need a service that checks if the notification bar is open repeatedly. This service uses reflection to get needed method to close the notification bar after it gets opened so I think it wont work on android 9 devices(just checked it working fine with compilesdk 28 on a 7.1.1 device). you need to use this permission:
<uses-permission android:name="android.permission.EXPAND_STATUS_BAR"/>
this is the code for the service:
public class CollapseService extends Service {
ScheduledExecutorService scheduler;
#Nullable
#Override
public IBinder onBind(Intent intent) {
return null;
}
#Override
public void onCreate() {
super.onCreate();
MyRunnable runnable = new MyRunnable(this);
scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate
(runnable, 0, 100, TimeUnit.MILLISECONDS);
}
#Override
public int onStartCommand(Intent intent, int flags, int startId) {
return Service.START_STICKY;
}
private void collapseNow() {
Object statusBarService = getSystemService("statusbar");
Class<?> statusBarManager = null;
try {
statusBarManager = Class.forName("android.app.StatusBarManager");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
Method collapseStatusBar = null;
try {
collapseStatusBar = statusBarManager.getMethod("collapsePanels");
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
collapseStatusBar.setAccessible(true);
try {
collapseStatusBar.invoke(statusBarService);
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
#Override
public void onDestroy() {
super.onDestroy();
scheduler.shutdown();
}
static class MyRunnable implements Runnable{
CollapseService aService = null;
MyRunnable(CollapseService service){
WeakReference<CollapseService> weakReference = new WeakReference<>(service);
aService = weakReference.get();
}
#Override
public void run(){
if(aService != null) {
aService.collapseNow();
}
}
}
}
Weak reference is to remove the possibility of occurrence of memory leaks.
If your kiosk app is a device owner (as explained in one of your referenced examples) you can use DevicePolicyManager.setStatusBarDisabled() :
Disabling the status bar blocks notifications, quick settings and
other screen overlays that allow escaping from a single use device.
It is available since Android 6 (API 23), the status bar is still displayed (with the time, wifi level, bluetooth indicator, ...) but the notifications are not and you cannot expand it.

Disable status bar pull in Oreo

The method for kiosking an application by disabling pull and click of the status bar does not work on android 8. As anserwed on How to disable status bar click and pull down in Android?
You can lay a window over the status bar to disable any touch or pulling down.
As described by this answer, this method of doing it does works on android 7 and below however this method does not work on android 8(oreo).
I have tested it on android 7 and less and it works, but the status bar still pulls down when pulled on android 8.
If you have a solution on this please assist.
Thank you all.
For and 8 and above you cant realy fully overylay a view over other apps, so what you have to do is, when you pull the drawer down, return the drawer back up so fast that the user wont be able to click anything on it. This is the code that does that. Make sure you are doing this on an activity.
#Override
public void onWindowFocusChanged(boolean hasFocus) {
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (!hasFocus) {
Intent closeDialog = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
sendBroadcast(closeDialog);
// Method that handles loss of window focus
new BlockStatusBar(this,false).collapseNow();
}
}
}
Then the helper class that is doing the job of hiding the status bar is as indicated below.
public class BlockStatusBar {
Context context;
// To keep track of activity's window focus
boolean currentFocus;
// To keep track of activity's foreground/background status
boolean isPaused;
public static Handler collapseNotificationHandler;
Method collapseStatusBar = null;
public BlockStatusBar(Context context,boolean isPaused) {
this.context=context;
this.isPaused=isPaused;
collapseNow();
}
public void collapseNow() {
// Initialize 'collapseNotificationHandler'
if (collapseNotificationHandler == null) {
collapseNotificationHandler = new Handler();
}
// If window focus has been lost && activity is not in a paused state
// Its a valid check because showing of notification panel
// steals the focus from current activity's window, but does not
// 'pause' the activity
if (!currentFocus && !isPaused) {
// Post a Runnable with some delay - currently set to 300 ms
collapseNotificationHandler.postDelayed(new Runnable() {
#Override
public void run() {
// Use reflection to trigger a method from 'StatusBarManager'
Object statusBarService = context.getSystemService("statusbar");
Class<?> statusBarManager = null;
try {
statusBarManager = Class.forName("android.app.StatusBarManager");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
try {
// Prior to API 17, the method to call is 'collapse()'
// API 17 onwards, the method to call is `collapsePanels()`
if (Build.VERSION.SDK_INT > 16) {
collapseStatusBar = statusBarManager .getMethod("collapsePanels");
} else {
collapseStatusBar = statusBarManager .getMethod("collapse");
}
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
collapseStatusBar.setAccessible(true);
try {
collapseStatusBar.invoke(statusBarService);
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
// Check if the window focus has been returned
// If it hasn't been returned, post this Runnable again
// Currently, the delay is 100 ms. You can change this
// value to suit your needs.
if (!currentFocus && !isPaused) {
collapseNotificationHandler.postDelayed(this, 100L);
}
if (!currentFocus && isPaused) {
collapseNotificationHandler.removeCallbacksAndMessages(null);
}
}
}, 1L);
}
}
}

Android: Disable and enable to pulling the status bar programmatically for API 19

I already done do to hide the status bar, but unfortunately I didn't found the way to display it back once its already hide. I did so many workaround but still not success, here and here.
Activity
WindowManager manager = ((WindowManager) getApplicationContext()
.getSystemService(Context.WINDOW_SERVICE));
WindowManager.LayoutParams localLayoutParams = new WindowManager.LayoutParams();
localLayoutParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ERROR;
localLayoutParams.gravity = Gravity.TOP;
localLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE|
// this is to enable the notification to receive touch events
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL |
// Draws over status bar
WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
localLayoutParams.width = WindowManager.LayoutParams.MATCH_PARENT;
localLayoutParams.height = (int) (50 * getResources()
.getDisplayMetrics().scaledDensity);
localLayoutParams.format = PixelFormat.TRANSPARENT;
customViewGroup view = new customViewGroup(this);
manager.addView(view, localLayoutParams);
customViewGroup class
class customViewGroup extends ViewGroup {
public customViewGroup(Context context) {
super(context);
}
#Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
}
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return true;
}
}
For Hiding Status Bar on 4.1 or Higher use
View decorView = getWindow().getDecorView();
// Hide the status bar.
int uiOptions = View.SYSTEM_UI_FLAG_FULLSCREEN;
decorView.setSystemUiVisibility(uiOptions);
// Remember that you should never show the action bar if the
// status bar is hidden, so hide that too if necessary.
ActionBar actionBar = getActionBar();
actionBar.hide();
OR
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
To make Status Bar Visible
View decorView = getWindow().getDecorView();
// Hide the status bar.
int uiOptions = View.SYSTEM_UI_FLAG_VISIBLE;
decorView.setSystemUiVisibility(uiOptions);
// Remember that you should never show the action bar if the
// status bar is hidden, so hide that too if necessary.
ActionBar actionBar = getActionBar();
actionBar.show();
OR
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
Refer Immersve View
android Exit from full screen mode
I think permanently disable the status bar is difficult. I am also working on the same concept and did lots of R&D and found that below code can be useful. if the user tries to expand the status bar then within a sec it will pull back it and it will work on oreo as well. I have tried on different OS.
public class BlockStatusBar {
Context context
// To keep track of activity's window focus
boolean currentFocus;
// To keep track of activity's foreground/background status
boolean isPaused;
public Handler collapseNotificationHandler;
Method collapseStatusBar = null;
public BlockStatusBar(Context context, boolean isPaused) {
this.context = context;
this.isPaused = isPaused;
collapseNow();
}
public void collapseNow() {
// Initialize 'collapseNotificationHandler'
if (collapseNotificationHandler == null) {
collapseNotificationHandler = new Handler();
}
// If window focus has been lost && activity is not in a paused state
// Its a valid check because showing of notification panel
// steals the focus from current activity's window, but does not
// 'pause' the activity
if (!currentFocus && !isPaused) {
Runnable myRunnable = new Runnable() {
public void run() {
// do something
try {
// Use reflection to trigger a method from 'StatusBarManager'
Object statusBarService = context.getSystemService("statusbar");
Class<?> statusBarManager = null;
try {
statusBarManager = Class.forName("android.app.StatusBarManager");
} catch (ClassNotFoundException e) {
Log.e(LOG_TAG, "" + e.getMessage());
}
try {
// Prior to API 17, the method to call is 'collapse()'
// API 17 onwards, the method to call is `collapsePanels()`
if (Build.VERSION.SDK_INT > 16) {
collapseStatusBar = statusBarManager.getMethod("collapsePanels");
} else {
collapseStatusBar = statusBarManager.getMethod("collapse");
}
} catch (NoSuchMethodException e) {
Log.e(LOG_TAG, "" + e.getMessage());
}
collapseStatusBar.setAccessible(true);
try {
collapseStatusBar.invoke(statusBarService);
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
// Check if the window focus has been returned
// If it hasn'kioskthread been returned, post this Runnable again
// Currently, the delay is 100 ms. You can change this
// value to suit your needs.
if (!currentFocus && !isPaused) {
collapseNotificationHandler.postDelayed(this, 100L);
}
if (!currentFocus && isPaused) {
collapseNotificationHandler.removeCallbacksAndMessages(null);
}
} catch (Exception e) {
Log.e("MSG", "" + e.getMessage());
}
}
};
// Post a Runnable with some delay - currently set to 300 ms
collapseNotificationHandler.postDelayed(myRunnable, 1L);
}
}
}

appcompat-v7 v23.0.0 statusbar color black when in ActionMode

UPDATE
Same issue present in the latest Gmail app. I still don't understand why would Google make such unpleasant UI change. Obsessive in me goes crazy whenever I see it
QUESTION
I have this weird issue with appcompat-v7 23. Issue I am going to describe does not happen with 22 series
You can get source code that reproduces this issuse form
https://github.com/devserv/t/
Once built, you can tap and hold an item in the list to activate ActionMode
Issue:
When in ActionMode, appcompat turns status bar to black. This does not happen if I don’t use following
<item name="android:statusBarColor">#android:color/transparent</item>
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
in my v21 style but I have to use it because I want my navigation drawer to look behind status bar.
I used to use following to avoid black status bar when ActionMode started and ended
public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
getActivity().getWindow().setStatusBarColor(getResources().getColor(R.color.appColorPrimaryDark));
}
}
public void onDestroyActionMode(ActionMode actionMode) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
getActivity().getWindow().setStatusBarColor(getResources().getColor(android.R.color.transparent));
}
mMode = null;
}
Above code did not create/avoided status bar turning black, but does not work properly on v23 of appcompat. Instead you see a short black status bar while ActionMode destroyed. It looks like related to the animation that plays when ActionMode destroyed.
I have tried to open bug reports but it has been declined with comment
Don't re-create bugs.
Am I missing something?
Here are the screenshots for normal and action mode.
In case only the color is the issue, you can change it. Only to a fixed color resource.
<color name="abc_input_method_navigation_guard" tools:override="true">#color/primary_dark</color>
Obvious ?colorPrimaryDark will not work, not even on API 21.
The view responsible for the black status bar background is stored in AppCompatDelegateImplV7.mStatusGuard. You can get the delegate by calling getDelegate() from your activity and access mStatusGuard field by reflection. After starting the action mode you can get a reference to this view and customize it however you like.
This was found in AppCompat 24.1.1.
The version 23.0.0 of v7 appcompat library introduced an animation that fades in and out the action mode when it's started and finished as you can read here:
The action mode has fades in and is working as intended.
The changes are made in the method onDestroyActionMode in AppCompatDelegateImplV7:
public void onDestroyActionMode(ActionMode mode) {
mWrapped.onDestroyActionMode(mode);
if (mActionModePopup != null) {
mWindow.getDecorView().removeCallbacks(mShowActionModePopup);
mActionModePopup.dismiss();
} else if (mActionModeView != null) {
mActionModeView.setVisibility(View.GONE);
if (mActionModeView.getParent() != null) {
ViewCompat.requestApplyInsets((View) mActionModeView.getParent());
}
}
if (mActionModeView != null) {
mActionModeView.removeAllViews();
}
if (mAppCompatCallback != null) {
mAppCompatCallback.onSupportActionModeFinished(mActionMode);
}
mActionMode = null;
}
In version 23.0.0 it was changed to:
public void onDestroyActionMode(ActionMode mode) {
mWrapped.onDestroyActionMode(mode);
if (mActionModePopup != null) {
mWindow.getDecorView().removeCallbacks(mShowActionModePopup);
}
if (mActionModeView != null) {
endOnGoingFadeAnimation();
mFadeAnim = ViewCompat.animate(mActionModeView).alpha(0f);
mFadeAnim.setListener(new ViewPropertyAnimatorListenerAdapter() {
#Override
public void onAnimationEnd(View view) {
mActionModeView.setVisibility(View.GONE);
if (mActionModePopup != null) {
mActionModePopup.dismiss();
} else if (mActionModeView.getParent() instanceof View) {
ViewCompat.requestApplyInsets((View) mActionModeView.getParent());
}
mActionModeView.removeAllViews();
mFadeAnim.setListener(null);
mFadeAnim = null;
}
});
}
if (mAppCompatCallback != null) {
mAppCompatCallback.onSupportActionModeFinished(mActionMode);
}
mActionMode = null;
}
As you can see mWrapped.onDestroyActionMode(mode); is called immediately, not when the animation ends. This is what cause the black status bar in your app and in other apps like Gmail and Keep.
The workaround that you found works, but unfortunately is not reliable, because if the animation takes longer you could see the black status bar anyway.
I think Google should correct the issue and call onDestroyActionMode only when the animation is really ended. In the mean time you can change this behaviour with a bit of reflections. It is necessary to override onSupportActionModeStarted in your activity and call the method fixActionModeCallback:
#Override
public void onSupportActionModeStarted(ActionMode mode) {
super.onSupportActionModeStarted(mode);
//Call this method
fixActionModeCallback(this, mode);
}
private void fixActionModeCallback(AppCompatActivity activity, ActionMode mode) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
return;
if (!(mode instanceof StandaloneActionMode))
return;
try {
final Field mCallbackField = mode.getClass().getDeclaredField("mCallback");
mCallbackField.setAccessible(true);
final Object mCallback = mCallbackField.get(mode);
final Field mWrappedField = mCallback.getClass().getDeclaredField("mWrapped");
mWrappedField.setAccessible(true);
final ActionMode.Callback mWrapped = (ActionMode.Callback) mWrappedField.get(mCallback);
final Field mDelegateField = AppCompatActivity.class.getDeclaredField("mDelegate");
mDelegateField.setAccessible(true);
final Object mDelegate = mDelegateField.get(activity);
mCallbackField.set(mode, new ActionMode.Callback() {
#Override
public boolean onCreateActionMode(android.support.v7.view.ActionMode mode, Menu menu) {
return mWrapped.onCreateActionMode(mode, menu);
}
#Override
public boolean onPrepareActionMode(android.support.v7.view.ActionMode mode, Menu menu) {
return mWrapped.onPrepareActionMode(mode, menu);
}
#Override
public boolean onActionItemClicked(android.support.v7.view.ActionMode mode, MenuItem item) {
return mWrapped.onActionItemClicked(mode, item);
}
#Override
public void onDestroyActionMode(final android.support.v7.view.ActionMode mode) {
Class mDelegateClass = mDelegate.getClass().getSuperclass();
Window mWindow = null;
PopupWindow mActionModePopup = null;
Runnable mShowActionModePopup = null;
ActionBarContextView mActionModeView = null;
AppCompatCallback mAppCompatCallback = null;
ViewPropertyAnimatorCompat mFadeAnim = null;
android.support.v7.view.ActionMode mActionMode = null;
Field mFadeAnimField = null;
Field mActionModeField = null;
while (mDelegateClass != null) {
try {
if (TextUtils.equals("AppCompatDelegateImplV7", mDelegateClass.getSimpleName())) {
Field mActionModePopupField = mDelegateClass.getDeclaredField("mActionModePopup");
mActionModePopupField.setAccessible(true);
mActionModePopup = (PopupWindow) mActionModePopupField.get(mDelegate);
Field mShowActionModePopupField = mDelegateClass.getDeclaredField("mShowActionModePopup");
mShowActionModePopupField.setAccessible(true);
mShowActionModePopup = (Runnable) mShowActionModePopupField.get(mDelegate);
Field mActionModeViewField = mDelegateClass.getDeclaredField("mActionModeView");
mActionModeViewField.setAccessible(true);
mActionModeView = (ActionBarContextView) mActionModeViewField.get(mDelegate);
mFadeAnimField = mDelegateClass.getDeclaredField("mFadeAnim");
mFadeAnimField.setAccessible(true);
mFadeAnim = (ViewPropertyAnimatorCompat) mFadeAnimField.get(mDelegate);
mActionModeField = mDelegateClass.getDeclaredField("mActionMode");
mActionModeField.setAccessible(true);
mActionMode = (android.support.v7.view.ActionMode) mActionModeField.get(mDelegate);
} else if (TextUtils.equals("AppCompatDelegateImplBase", mDelegateClass.getSimpleName())) {
Field mAppCompatCallbackField = mDelegateClass.getDeclaredField("mAppCompatCallback");
mAppCompatCallbackField.setAccessible(true);
mAppCompatCallback = (AppCompatCallback) mAppCompatCallbackField.get(mDelegate);
Field mWindowField = mDelegateClass.getDeclaredField("mWindow");
mWindowField.setAccessible(true);
mWindow = (Window) mWindowField.get(mDelegate);
}
mDelegateClass = mDelegateClass.getSuperclass();
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
if (mActionModePopup != null) {
mWindow.getDecorView().removeCallbacks(mShowActionModePopup);
}
if (mActionModeView != null) {
if (mFadeAnim != null) {
mFadeAnim.cancel();
}
mFadeAnim = ViewCompat.animate(mActionModeView).alpha(0.0F);
final PopupWindow mActionModePopupFinal = mActionModePopup;
final ActionBarContextView mActionModeViewFinal = mActionModeView;
final ViewPropertyAnimatorCompat mFadeAnimFinal = mFadeAnim;
final AppCompatCallback mAppCompatCallbackFinal = mAppCompatCallback;
final android.support.v7.view.ActionMode mActionModeFinal = mActionMode;
final Field mFadeAnimFieldFinal = mFadeAnimField;
final Field mActionModeFieldFinal = mActionModeField;
mFadeAnim.setListener(new ViewPropertyAnimatorListenerAdapter() {
public void onAnimationEnd(View view) {
mActionModeViewFinal.setVisibility(View.GONE);
if (mActionModePopupFinal != null) {
mActionModePopupFinal.dismiss();
} else if (mActionModeViewFinal.getParent() instanceof View) {
ViewCompat.requestApplyInsets((View) mActionModeViewFinal.getParent());
}
mActionModeViewFinal.removeAllViews();
mFadeAnimFinal.setListener((ViewPropertyAnimatorListener) null);
try {
if (mFadeAnimFieldFinal != null) {
mFadeAnimFieldFinal.set(mDelegate, null);
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
mWrapped.onDestroyActionMode(mode);
if (mAppCompatCallbackFinal != null) {
mAppCompatCallbackFinal.onSupportActionModeFinished(mActionModeFinal);
}
try {
if (mActionModeFieldFinal != null) {
mActionModeFieldFinal.set(mDelegate, null);
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
});
}
}
});
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
Nobody? Here is the workaround I came up with. Delay.
#Override
public void onDestroyActionMode(ActionMode mode) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
try {
getActivity().getWindow().setStatusBarColor(getResources().getColor(android.R.color.transparent));
}
catch(Exception e)
{
e.printStackTrace();
}
}
}, 400);
}
mActionMode = null;
}

Categories

Resources