1. 程式人生 > >Android狀態列禁止下拉異常分析

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更是我的短板,會在後續的工作中深入研究它們。如果本文有問題,歡迎指正!