Android狀態列禁止下拉異常分析
最近做一個專案,需要在進入極致省電模式的時候,禁止狀態列的下拉,退出極致省電模式時,恢復狀態列的下拉,功能很容易就實現了,但是卻發現在極致省電狀態列出現異常後,狀態列仍然處於禁止下拉狀態,此時呼叫恢復下拉的程式碼,仍然不能恢復狀態列下拉,在此記錄一下我的解決過程。
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更是我的短板,會在後續的工作中深入研究它們。如果本文有問題,歡迎指正!