最近做一个项目,需要在进入极致省电模式的时候,禁止状态栏的下拉,退出极致省电模式时,恢复状态栏的下拉,功能很容易就实现了,但是却发现在极致省电状态栏出现异常后,状态栏仍然处于禁止下拉状态,此时调用恢复下拉的代码,仍然不能恢复状态栏下拉,在此记录一下我的解决过程。
1.添加权限
<!-- <uses-permission android:name="android.permission.EXPAND_STATUS_BAR" /> -->
<uses-permission android:name="android.permission.STATUS_BAR" />
注:(1)我看有的文章,用的第一个权限,但是我实际用的第二个权限,这里把两个都写上,大家选一个合适的吧。
(2)这个权限在之前的Android版本中,是可以直接获取的,但是6.0以后就需要系统权限才可以获得这个权限了
2.禁止通知栏下拉
StatusBarManager mStatusBarManager = (StatusBarManager) getSystemService("statusbar");
mStatusBarManager.disable(StatusBarManager.DISABLE_EXPAND);
如果想禁止多个选项,比如禁止下拉以及隐藏虚拟按键的recent键,可用按位或的方式:
StatusBarManager mStatusBarManager = (StatusBarManager) getSystemService("statusbar");
mStatusBarManager.disable(StatusBarManager.DISABLE_EXPAND|StatusBarManager.DISABLE_RECENT);
如果此处不用按位或,而调用两次disable,则只会有最后一次的disable生效。
3.恢复通知栏下拉
StatusBarManager mStatusBarManager = (StatusBarManager) getSystemService("statusbar");
mStatusBarManager.disable(StatusBarManager.DISABLE_NONE);
4.在禁止下拉状态发生异常崩溃,不能恢复下拉原因分析
(1)根据代码,查看StatusBarManager.java
…………
import android.os.ServiceManager;
import android.view.View;
import com.android.internal.statusbar.IStatusBarService;
public class StatusBarManager {
public static final int DISABLE_EXPAND = View.STATUS_BAR_DISABLE_EXPAND;
private IStatusBarService mService;
private IBinder mToken = new Binder();
private static final String TAG = "StatusBarManager";
…………
private synchronized IStatusBarService getService() {
if (mService == null) {
mService = IStatusBarService.Stub.asInterface(
ServiceManager.getService(Context.STATUS_BAR_SERVICE));
if (mService == null) {
Slog.w("StatusBarManager", "warning: no STATUS_BAR_SERVICE");
}
}
return mService;
}
/**
* Disable some features in the status bar. Pass the bitwise-or of the DISABLE_* flags.
* To re-enable everything, pass {@link #DISABLE_NONE}.
*/
public void disable(int what) {
try {
final IStatusBarService svc = getService();
if (svc != null) {
if ((what & DISABLE_EXPAND) != 0 ) {
Slog.d("StatusBarManager", "disable status bar , call from" , new RuntimeException("disable"));
}
svc.disable(what, mToken, mContext.getPackageName());
}
} catch (RemoteException ex) {
// system process is dead anyway.
throw new RuntimeException(ex);
}
}
…………
在disable方法中,先获取service,然后调用service的disable方法。
其中有一段代码:
if (svc != null) {
if ((what & DISABLE_EXPAND) != 0 ) {
Slog.d("StatusBarManager", "disable status bar , call from" , new RuntimeException("disable"));
}
svc.disable(what, mToken, mContext.getPackageName());
}
如果
what & DISABLE_EXPAND) != 0
其中what为我们输入的禁止或者下拉的int型参数,在禁止下拉的时候是0x00010000,恢复下拉的时候是0x00000000;DISABLE_EXPAND是View.STATUS_BAR_DISABLE_EXPAND,为0x00010000。
因此,在正常情况下,禁止下拉时会有debug的log信息,在恢复下拉的时候没有。
注:本文手机使用的是YunOS系统,其log的tag与Android稍有不同,但极致未变,不影响分析
于是我们分析log信息,此处省略繁杂的系统log,在正常情况下,禁止下拉时,出现log:
10-09 11:02:01.318: D/StatusBarManager(20724): disable status bar , call from
10-09 11:02:01.318: D/StatusBarManager(20724): java.lang.RuntimeException: disable
10-09 11:02:01.318: D/StatusBarManager(20724): at android.app.StatusBarManager.disable(StatusBarManager.java:109)
10-09 11:02:01.318: D/StatusBarManager(20724): at com.changhong.batteryaidl.BatteryService$AIDLServerBinder.disableStatusBar(BatteryService.java:326)
10-09 11:02:01.318: D/StatusBarManager(20724): at com.changhong.batteryaidl.IBatteryService$Stub.onTransact(IBatteryService.java:118)
10-09 11:02:01.318: D/StatusBarManager(20724): at android.os.Binder.execTransact(Binder.java:451)
10-09 11:02:01.319: D/StatusBarManagerService(794): disable statusbar calling PID = 20724
10-09 11:02:01.320: D/SystemUI_PhoneStatusBar(1097): disable: 0x00000000 -> 0x00010000 (diff: 0x00010000)
10-09 11:02:01.320: D/SystemUI_PhoneStatusBar(1097): disable: < EXPAND* icons alerts ticker system_info back home recent clock >
10-09 11:02:01.320: D/tianPanelView(1097): setExpandedHeightInternal() h=0.0 mLeftRightEffect=false
10-09 11:02:01.321: E/BatteryService(20724): disableStatusBar state: 65536
再查看发生崩溃后:
10-09 11:16:01.983: D/StatusBarManager(20724): disable status bar , call from
10-09 11:16:01.983: D/StatusBarManager(20724): java.lang.RuntimeException: disable
10-09 11:16:01.983: D/StatusBarManager(20724): at android.app.StatusBarManager.disable(StatusBarManager.java:109)
10-09 11:16:01.983: D/StatusBarManager(20724): at com.changhong.batteryaidl.BatteryService$AIDLServerBinder.disableStatusBar(BatteryService.java:326)
10-09 11:16:01.983: D/StatusBarManager(20724): at com.changhong.batteryaidl.IBatteryService$Stub.onTransact(IBatteryService.java:118)
10-09 11:16:01.983: D/StatusBarManager(20724): at android.os.Binder.execTransact(Binder.java:451)
10-09 11:16:01.983: E/BatteryService(20724): disableStatusBar state: 65536
从log可以看出,发生异常后,依然可以获取到service,但是在调用service的disable方法时出现了问题,参见异常后,缺少下面log:
10-09 11:02:01.319: D/StatusBarManagerService(794): disable statusbar calling PID = 20724
10-09 11:02:01.320: D/SystemUI_PhoneStatusBar(1097): disable: 0x00000000 -> 0x00010000 (diff: 0x00010000)
10-09 11:02:01.320: D/SystemUI_PhoneStatusBar(1097): disable: < EXPAND* icons alerts ticker system_info back home recent clock >
从log可以看出,这段log部分的代码正是执行状态栏禁止或恢复下拉的代码。
追踪语句
svc.disable(what, mToken, mContext.getPackageName());
我们找到StatusBarManagerService.java,该类disable方法的源码为:
public void disable(int what, IBinder token, String pkg) {
disableInternal(mCurrentUserId, what, token, pkg);
}
mCurrentUserId是由方法设置的,根据名字推测是应用的ID
→继续 (第一层方法)
private void disableInternal(int userId, int what, IBinder token, String pkg) {
enforceStatusBar();
synchronized (mLock) {
disableLocked(userId, what, token, pkg);
}
}
其中enforceStatusBar方法与获取权限相关,并未仔细分析
→看disableLocked (第二层方法)
private void disableLocked(int userId, int what, IBinder token, String pkg) {
// It's important that the the callback and the call to mBar get done
// in the same order when multiple threads are calling this function
// so they are paired correctly. The messages on the handler will be
// handled in the order they were enqueued, but will be outside the lock.
manageDisableListLocked(userId, what, token, pkg);
// Ensure state for the current user is applied, even if passed a non-current user.
final int net = gatherDisableActionsLocked(mCurrentUserId);
if (net != mDisabled) {
mDisabled = net;
mHandler.post(new Runnable() {
public void run() {
mNotificationDelegate.onSetDisabled(net);
}
});
if (mBar != null) {
try {
/// M:[ALPS01673960] Fix User cannot drag down the notification bar.
if (true) Slog.d(TAG, "disable statusbar calling PID = " + Binder.getCallingPid());
mBar.disable(net);
} catch (RemoteException ex) {
}
}
}
}
翻译前面几句注释:
当多个线程调用这个函数时,回调函数和调用mbar在同一顺序里是很重要的,以便它们的配对正确。在handler中的信息将在其排队的队列中管理,但处于lock的外面。
注:纯直译,关于lock方面的知识很单薄,如果有问题,欢迎指正。
→看manageDisableListLocked(第二层方法→第三层方法1)
void manageDisableListLocked(int userId, int what, IBinder token, String pkg) {
if (SPEW) {
Slog.d(TAG, "manageDisableList userId=" + userId
+ " what=0x" + Integer.toHexString(what) + " pkg=" + pkg);
}
// update the list
final int N = mDisableRecords.size();
DisableRecord tok = null;
int i;
for (i=0; i<N; i++) {
DisableRecord t = mDisableRecords.get(i);
if (t.token == token && t.userId == userId) {
tok = t;
break;
}
}
if (what == 0 || !token.isBinderAlive()) {
if (tok != null) {
mDisableRecords.remove(i);
tok.token.unlinkToDeath(tok, 0);
}
} else {
if (tok == null) {
tok = new DisableRecord();
tok.userId = userId;
try {
token.linkToDeath(tok, 0);
}
catch (RemoteException ex) {
return; // give up
}
mDisableRecords.add(tok);
}
tok.what = what;
tok.token = token;
tok.pkg = pkg;
}
}
该方法主要是更新mDisableRecords,如果token和userId在mDisableRecords中找到了匹配的记录,则赋值给tok,对于异常发生后,token和userId均与之前不同,因此tok为空。正常情况下,当恢复通知栏下拉的时候,如果tok不为空,则会清除mDisableRecords中该条记录,并调用unlinkToDeath清除一个之前注册的死亡标识信息,很显然,如果发生异常,不会执行该操作。
→看gatherDisableActionsLocked方法(第二层方法→第三层方法2)
// lock on mDisableRecords
int gatherDisableActionsLocked(int userId) {
final int N = mDisableRecords.size();
// gather the new net flags
int net = 0;
for (int i=0; i<N; i++) {
final DisableRecord rec = mDisableRecords.get(i);
if (rec.userId == userId) {
net |= rec.what;
}
}
return net;
}
该方法主要是获取需要执行的操作,即what值,异常发生后,net值为0
→看 mBar.disable方法即PhoneStatusBar.disable(第二层方法2→第三层方法3)
public void disable(int state) {
final int old = mDisabled;
final int diff = state ^ old;
mDisabled = state;
if (DEBUG) {
Slog.d(TAG, String.format("disable: 0x%08x -> 0x%08x (diff: 0x%08x)", old, state, diff));
}
StringBuilder flagdbg = new StringBuilder();
flagdbg.append("disable: < ");
flagdbg.append(((state & StatusBarManager.DISABLE_EXPAND) != 0) ? "EXPAND" : "expand");
flagdbg.append(((diff & StatusBarManager.DISABLE_EXPAND) != 0) ? "* " : " ");
flagdbg.append(((state & StatusBarManager.DISABLE_NOTIFICATION_ICONS) != 0) ? "ICONS"
: "icons");
flagdbg.append(((diff & StatusBarManager.DISABLE_NOTIFICATION_ICONS) != 0) ? "* " : " ");
flagdbg.append(((state & StatusBarManager.DISABLE_NOTIFICATION_ALERTS) != 0) ? "ALERTS"
: "alerts");
flagdbg.append(((diff & StatusBarManager.DISABLE_NOTIFICATION_ALERTS) != 0) ? "* " : " ");
flagdbg.append(((state & StatusBarManager.DISABLE_NOTIFICATION_TICKER) != 0) ? "TICKER"
: "ticker");
flagdbg.append(((diff & StatusBarManager.DISABLE_NOTIFICATION_TICKER) != 0) ? "* " : " ");
flagdbg.append(((state & StatusBarManager.DISABLE_SYSTEM_INFO) != 0) ? "SYSTEM_INFO"
: "system_info");
flagdbg.append(((diff & StatusBarManager.DISABLE_SYSTEM_INFO) != 0) ? "* " : " ");
flagdbg.append(((state & StatusBarManager.DISABLE_BACK) != 0) ? "BACK" : "back");
flagdbg.append(((diff & StatusBarManager.DISABLE_BACK) != 0) ? "* " : " ");
flagdbg.append(((state & StatusBarManager.DISABLE_HOME) != 0) ? "HOME" : "home");
flagdbg.append(((diff & StatusBarManager.DISABLE_HOME) != 0) ? "* " : " ");
flagdbg.append(((state & StatusBarManager.DISABLE_RECENT) != 0) ? "RECENT" : "recent");
flagdbg.append(((diff & StatusBarManager.DISABLE_RECENT) != 0) ? "* " : " ");
flagdbg.append(((state & StatusBarManager.DISABLE_CLOCK) != 0) ? "CLOCK" : "clock");
flagdbg.append(((diff & StatusBarManager.DISABLE_CLOCK) != 0) ? "* " : " ");
flagdbg.append(">");
Slog.d(TAG, flagdbg.toString());
if ((diff & StatusBarManager.DISABLE_CLOCK) != 0) {
boolean show = (state & StatusBarManager.DISABLE_CLOCK) == 0;
// showClock(show);
}
if ((diff & StatusBarManager.DISABLE_EXPAND) != 0) {
if ((state & StatusBarManager.DISABLE_EXPAND) != 0) {
animateCollapsePanels();
}
}
/*
* if ((diff & StatusBarManager.DISABLE_NOTIFICATION_ICONS) != 0) { if
* ((state & StatusBarManager.DISABLE_NOTIFICATION_ICONS) != 0) {
* setNotificationIconVisibility(false,
* com.android.internal.R.anim.fade_out); } }
*/
if ((diff & (StatusBarManager.DISABLE_HOME
| StatusBarManager.DISABLE_RECENT
| StatusBarManager.DISABLE_BACK
| StatusBarManager.DISABLE_SEARCH)) != 0) {
// the nav bar will take care of these
if (mNavigationBarView != null)
mNavigationBarView.setDisabledFlags(state);
if ((state & StatusBarManager.DISABLE_RECENT) != 0) {
// close recents if it's visible
mHandler.removeMessages(MSG_CLOSE_RECENTS_PANEL);
mHandler.sendEmptyMessage(MSG_CLOSE_RECENTS_PANEL);
}
}
if ((diff & StatusBarManager.DISABLE_NOTIFICATION_ICONS) != 0) {
if ((state & StatusBarManager.DISABLE_NOTIFICATION_ICONS) != 0) {
if (mTicking) {
mTicker.halt();
} else {
setNotificationIconVisibility(false, com.android.internal.R.anim.fade_out);
}
} else {
if (!mExpandedVisible) {
setNotificationIconVisibility(true, com.android.internal.R.anim.fade_in);
}
}
} else if ((diff & StatusBarManager.DISABLE_NOTIFICATION_TICKER) != 0) {
if (mTicking && (state & StatusBarManager.DISABLE_NOTIFICATION_TICKER) != 0) {
mTicker.halt();
}
}
}
可以发现,是此处输出的
10-09 11:02:01.320: D/SystemUI_PhoneStatusBar(1097): disable: 0x00000000 -> 0x00010000 (diff: 0x00010000)
10-09 11:02:01.320: D/SystemUI_PhoneStatusBar(1097): disable: < EXPAND* icons alerts ticker system_info back home recent clock >
在发生异常后,不再输出该段log,因此可以确定,发生异常后,并没有进入该方法。 但在调用该方法前,输出了一个log:
10-09 11:02:01.319: D/StatusBarManagerService(794): disable statusbar calling PID = 20724
在试验中发现,发生异常仍然是输出了该句log的,也就是说逻辑应该没有问题。同时,当我们每次在代码中调用disable时都新建一个binder时,当成功禁止通知栏下拉后,并不能恢复通知栏下拉,因为binder改变了。此分析,发生异常后不能恢复通知栏下拉与binder、lock有关,这也就是disableLocked方法中注释说明的情况。
(2)此处需要说明一下:因为涉及到系统权限级别,我把对通知栏操作的方法放在了有系统权限的AIDLService中,然后在app中建了一个AIDLClient与其通信。
发生异常后,如果重新安装AIDLClient所在的app A,状态栏仍然无法恢复下拉,但是如果重新安装一次AIDLService所在的app B,在安装完成的时候,状态栏自行恢复下拉。
查看log,发现出现了下面log :
10-09 17:56:37.372: D/DisplayManagerService(793): Display listener for pid 22946 died.
10-09 17:56:37.373: E/BatteryClient(20551): AIDLClient.onServiceDisconnected()...
10-09 17:56:37.372: I/StatusBarManagerService(793): binder died for pkg=com.changhong.batteryaidl
10-09 17:56:37.373: E/BatteryClient(20551): AIDLClient.onServiceDisconnected()...
10-09 17:56:37.373: D/StatusBarManagerService(793): disable statusbar calling PID = 793
分析log,第三句log处是使状态栏恢复下拉的关键,查看源码:
private class DisableRecord implements IBinder.DeathRecipient {
int userId;
String pkg;
int what;
IBinder token;
public void binderDied() {
Slog.i(TAG, "binder died for pkg=" + pkg);
disableInternal(userId, 0, token, pkg);
token.unlinkToDeath(this, 0);
}
}
这是因为binger前面调用了linkToDeath方法,因此当binder死亡的时候会调用该方法,而该方法中,disableInternal方法正是上文分析的对状态栏进行设置的方法,因此可以推测,如果我们在发生异常AIDLService与其Client联系中断的时候,调用恢复通知栏下拉的代码,其binder未改变,也许可以正常恢复通知栏下拉。
在服务中断的时候,远程的AIDLservice将会调用onUnbind方法,以及一系列销毁service的方法,因此我们只需要在这些操作中执行一次恢复状态栏下拉即可,本文将该部分操作放入onUnbind中,
public boolean onUnbind(Intent intent) {
StatusBarManager mStatusBarManager = (StatusBarManager) getSystemService("statusbar");
mStatusBarManager.disable(StatusBarManager.DISABLE_NONE);
return super.onUnbind(intent);
}
至此,当再次发生程序崩溃,导致AIDLService与其Client连接中断时,通知栏能够自行恢复下拉,问题得到解决。
本人小菜鸟一枚,Android和JAVA很多知识是硬伤,文章中设计到的lock和binder更是我的短板,会在后续的工作中深入研究它们。如果本文有问题,欢迎指正!