分析Android長按電源鍵事件並定製長按電源dialog

本文的分析基於Android官方提供的Android7.0源碼

Android設備長按電源鍵,會彈出一個對話框。
這裏寫圖片描述
現有一個需求,就是定製一個彈出的對話框。
這裏寫圖片描述

Android在Frameworks下的PhoneWindowManager對電源按鍵和Home鍵的事件做了處理,不會將這些鍵傳送到上層應用。因此,我們可以從PhoneWindowManager入手處理長按電源鍵的一系列事件。
PhoneWindowManager的源碼路徑:
frameworks/base/services/core/java/com/android/server/policy/PhoneWindowManager.java

PhoneWindowManager這個類的源碼有近8000行,我們不需要從頭到尾分析它。我們在源碼中搜索關鍵字:KeyEvent.KEYCODE_POWER,發現該關鍵字出在“interceptKeyBeforeQueueing”方法裏,這方法大概在PhoneWindowManager的5542行,從方法名大概可以猜出該方法就是在系統將事件放到隊列之前進行攔截。我們看看KeyEvent.KEYCODE_POWER相關的代碼:

case KeyEvent.KEYCODE_POWER: {
     result &= ~ACTION_PASS_TO_USER;
     isWakeKey = false; // wake-up will be handled separately
     if (down) {
         interceptPowerKeyDown(event, interactive);
     } else {
         interceptPowerKeyUp(event, interactive, canceled);
     }
     break;
 }

再看看” interceptPowerKeyDown”這個方法:該方法大概100行,處理了各種情況下按下電源按鍵的事件,就不便貼出全部代碼。在該方法的末尾,我發現了跟長按事件有關的代碼片段:

private void interceptPowerKeyDown(KeyEvent event, boolean interactive) {
    //省略代碼……

  // If the power key has still not yet been handled, then detect short
  // press, long press, or multi press and decide what to do.
  mPowerKeyHandled = hungUp || mScreenshotChordVolumeDownKeyTriggered
          || mScreenshotChordVolumeUpKeyTriggered || gesturedServiceIntercepted;
  if (!mPowerKeyHandled) {
      if (interactive) {
          // When interactive, we're already awake.
          // Wait for a long press or for the button to be released to decide what to do.
          if (hasLongPressOnPowerBehavior()) {
              Message msg = mHandler.obtainMessage(MSG_POWER_LONG_PRESS);
              msg.setAsynchronous(true);
              mHandler.sendMessageDelayed(msg,
                      //省略代碼……
          }
      } else {
        //省略代碼……
              Message msg = mHandler.obtainMessage(MSG_POWER_LONG_PRESS);
              msg.setAsynchronous(true);
              mHandler.sendMessageDelayed(msg,
                      ViewConfiguration.get(mContext).getDeviceGlobalActionKeyTimeout());
              mBeganFromNonInteractive = true;
         //省略代碼……
      }
  }
}

可以看到這裏用if加了個判斷,不過不管是if或者else包裹的代碼裏都有一個通過Handler發送MSG_POWER_LONG_PRESS消息的操作。因此我們搜索:MSG_POWER_LONG_PRESS,在之前定義好的Handler中找到了相關代碼,最後通過Handler調用了“powerLongPress()”方法。

private void powerLongPress() {
    final int behavior = getResolvedLongPressOnPowerBehavior();
    switch (behavior) {
    case LONG_PRESS_POWER_NOTHING:
        break;
    case LONG_PRESS_POWER_GLOBAL_ACTIONS:
        mPowerKeyHandled = true;
        if (!performHapticFeedbackLw(null, HapticFeedbackConstants.LONG_PRESS, false)) {
            performAuditoryFeedbackForAccessibilityIfNeed();
        }
        showGlobalActionsInternal();
        break;
    case LONG_PRESS_POWER_SHUT_OFF:
    case LONG_PRESS_POWER_SHUT_OFF_NO_CONFIRM:
        //省略代碼……
        break;
    }
}

這個方法裏處理了幾種長按的情況,其中最有可能跟我們的需求有關的便是LONG_PRESS_POWER_GLOBAL_ACTIONS,追蹤到showGlobalActionsInternal()方法

void showGlobalActionsInternal() {
    sendCloseSystemWindows(SYSTEM_DIALOG_REASON_GLOBAL_ACTIONS);
    if (mGlobalActions == null) {
        mGlobalActions = new GlobalActions(mContext, mWindowManagerFuncs);
    }
    final boolean keyguardShowing = isKeyguardShowingAndNotOccluded();
    mGlobalActions.showDialog(keyguardShowing, isDeviceProvisioned());
   //省略代碼……
}

終於在這裏找到了mGlobalActions.showDialog(),我們可以看到這裏創建了一個GlobalActions對象,並調用了該對象的showDialog()方法,我們所要定製的界面就是通過該方法顯示出來的。GlobalActions和PhoneWindowManager在同一個包下,源碼路徑:
frameworks/base/services/core/java/com/android/server/policy/GlobalActions.java
我們定位到GlobalActions# showDialog()方法,看看該方法裏到底做了什麼騷操作;

/**
  * Show the global actions dialog (creating if necessary)
  * @param keyguardShowing True if keyguard is showing
  */
 public void showDialog(boolean keyguardShowing, boolean isDeviceProvisioned) {
     mKeyguardShowing = keyguardShowing;
     mDeviceProvisioned = isDeviceProvisioned;
     if (mDialog != null) {
         mDialog.dismiss();
         mDialog = null;
         // Show delayed, so that the dismiss of the previous dialog completes
         mHandler.sendEmptyMessage(MESSAGE_SHOW);
     } else {
         handleShow();
     }
 }

這裏判斷了dialog是否已經創建,我們的需求是定製一個dialog,自然應該從創建dialog開始入手。因此將代碼定位到handleShow()方法。

private void handleShow() {
    awakenIfNecessary();
    mDialog = createDialog();
    prepareDialog();

    // If we only have 1 item and it's a simple press action, just do this action.
    if (mAdapter.getCount() == 1
            && mAdapter.getItem(0) instanceof SinglePressAction
            && !(mAdapter.getItem(0) instanceof LongPressAction)) {
        ((SinglePressAction) mAdapter.getItem(0)).onPress();
    } else {
        WindowManager.LayoutParams attrs = mDialog.getWindow().getAttributes();
        attrs.setTitle("GlobalActions");
        mDialog.getWindow().setAttributes(attrs);
        mDialog.show();
        mDialog.getWindow().getDecorView().setSystemUiVisibility(View.STATUS_BAR_DISABLE_EXPAND);
    }
}

這個方法主要做了三件事:創建dialog,準備dialog需要的數據,顯示dialog
功夫不負有心人,終於定位到定製dialog相關的代碼。
我們看到createDialog()返回的是GlobalActionsDialog,它是一個GlobalActions的內部類,繼承了Dialog,

private static final class GlobalActionsDialog extends Dialog implements DialogInterface

我們要做的就是自己實現一個Dialog,並替換掉GlobalActionsDialog。

GlobalActions內部定義了一個Action接口,像關機,重啓,截屏等操作都是繼承了Action的子類SinglePressAction,需求中有兩項功能是截圖,源碼中沒有截圖和重啓相關的Action,好在我們是在源碼中進行修改,源碼提供了一系列的方法。下面以重啓爲例,自己實現一個相關的Action。

private final class RebootAction extends SinglePressAction implements LongPressAction {
    private RebootAction() {
        super(com.android.internal.R.drawable.ic_menu_rotate,
           R.string.factorytest_reboot);
    }
    @Override
    public boolean onLongPress() {
        return true;
    }
    @Override
    public boolean showDuringKeyguard() {
        return true;
    }
    @Override
    public boolean showBeforeProvisioning() {
        return true;
    }
    @Override
    public void onPress() {
        mWindowManagerFuncs.reboot(true /* confirm */);
    }
}

當我們做相應的操作就會調用相對應Action的onPress()方法,具體的操作都是在onPress()方法裏實現。這裏 Android源碼 用了策略模式來實現。還不瞭解策略模式的童鞋可以點擊這裏:Android設計模式源碼解析之策略模式

準備好需要的Action類,我們就可以着手dialog的實現。
這裏我自定義了一個dialog佈局,本來項目要求背景高斯模糊,我覺得這種實現方式太過麻煩,我就直接將背景設爲一張圖片,用FrameLayout作爲最外層佈局。

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center">

    <ImageView
        android:scaleType="fitXY"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@drawable/miki_power_blur_img" />

    <LinearLayout
        android:id="@+id/container_miki_dialog_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:gravity="center"
        android:orientation="vertical">
        <GridLayout
            android:id="@+id/gridlayout_miki_dialog_view"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:columnCount="2"
            android:rowCount="2">
        </GridLayout>
    </LinearLayout>
</FrameLayout>

實現佈局之後,還不能讓dialog全屏。要想實現全屏,這裏借鑑了郭霖的方法:
傳送門:Android狀態欄微技巧,帶你真正理解沉浸式模式

除了設置沉浸式模式還不夠,dialog默認是有內邊距的,因此你會看到默認的dialog是無法佔滿全屏的,需使用我們自定義的style

<style name="custome_dialog" parent="Theme.AppCompat.Dialog">
        <!--是否浮現在activity之上-->
        <item name="android:windowIsFloating">true</item>
        <!-- 全屏 -->
        <item name="android:windowFullscreen">true</item>
        <!--無標題-->
        <item name="android:windowNoTitle">true</item>
        <item name="android:backgroundDimEnabled">true</item><!--灰度-->
        <item name="android:backgroundDimAmount">0.5</item>
        <item name="android:alpha">0.3</item>
</style>

同時還需要將dialog的window的佈局設爲WindowManager.LayoutParams.MATCH_PARENT

除了使用Dialog,我們還可以用Activity來替換。只需將GlobalActions的showDialog方法替換爲showActivity,自己實現一些操作即可,這裏就不再描述。

另外需要注意的是,由於我們的修改是在framework下進行,需要添加一些資源文件,這裏就不重複造輪子了,直接貼上鍊接,有興趣的可以去看。
點我跳轉:android源碼framework下添加新資源的方法

至此整個功能就完成了。不過最後還有個小問題,就是將dialog窗口的佈局設爲
WindowManager.LayoutParams.MATCH_PARENT的時候,旋轉屏幕dialog是不會消失掉的,我們豎屏的佈局可能切換到橫屏就會出現適配問題,好在項目不需要適配其他型號的機子,只需要在佈局的時候調整好橫豎屏的佈局即可。

若有實現dialog全屏更好的方法,請給我留言。謝謝閱讀!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章