Support global key can be dispatched from non-interactive

Add adddional element 'dispatchWhenNonInteractive' in global key xml to
allow it could be dispatched from non-interactive mode if the key is a
wake key. That could help user press button to wake device and send the
global button broadcast without pressing the same button twice.

The receiver could use 'GlobalKeyIntent.from(Intent)' to get the
detail global key information from the received Intent.

Bug: 183071267
Test: manual
Change-Id: I4eef4a5fd92baece995fa7ddf535f0791923f694
diff --git a/services/core/java/com/android/server/policy/GlobalKeyIntent.java b/services/core/java/com/android/server/policy/GlobalKeyIntent.java
new file mode 100644
index 0000000..f8682be
--- /dev/null
+++ b/services/core/java/com/android/server/policy/GlobalKeyIntent.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.policy;
+
+import android.annotation.NonNull;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.util.Log;
+import android.view.KeyEvent;
+
+/**
+ * This class wrapped the Intent for global key ops.
+ */
+public final class GlobalKeyIntent {
+    private static final String EXTRA_BEGAN_FROM_NON_INTERACTIVE =
+            "EXTRA_BEGAN_FROM_NON_INTERACTIVE";
+
+    private final ComponentName mComponentName;
+    private final KeyEvent mKeyEvent;
+    private final boolean mBeganFromNonInteractive;
+
+    GlobalKeyIntent(@NonNull ComponentName componentName, @NonNull KeyEvent event,
+            boolean beganFromNonInteractive) {
+        mComponentName = componentName;
+        mKeyEvent = new KeyEvent(event);
+        mBeganFromNonInteractive = beganFromNonInteractive;
+    }
+
+    Intent getIntent() {
+        final Intent intent = new Intent(Intent.ACTION_GLOBAL_BUTTON)
+                .setComponent(mComponentName)
+                .setFlags(Intent.FLAG_RECEIVER_FOREGROUND)
+                .putExtra(Intent.EXTRA_KEY_EVENT, mKeyEvent)
+                .putExtra(EXTRA_BEGAN_FROM_NON_INTERACTIVE, mBeganFromNonInteractive);
+        return intent;
+    }
+
+    /**
+     * Get the {@link KeyEvent} information of {@link Intent#ACTION_GLOBAL_BUTTON}.
+     */
+    public KeyEvent getKeyEvent() {
+        return mKeyEvent;
+    }
+
+    /**
+     * Indicate if the global key is dispatched from non-interactive mode.
+     * Information of {@link Intent#ACTION_GLOBAL_BUTTON}.
+     */
+    public boolean beganFromNonInteractive() {
+        return mBeganFromNonInteractive;
+    }
+
+    /**
+     * Generate a GlobalKeyIntent from {@link Intent}, the action must be
+     * {@link Intent#ACTION_GLOBAL_BUTTON}.
+     *
+     * @param intent The received intent of the global key.
+     */
+    public static GlobalKeyIntent from(@NonNull Intent intent) {
+        if (intent.getAction() != Intent.ACTION_GLOBAL_BUTTON) {
+            Log.wtf("GlobalKeyIntent", "Intent should be ACTION_GLOBAL_BUTTON");
+            return null;
+        }
+
+        final KeyEvent event = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
+        final boolean fromNonInteractive =
+                intent.getBooleanExtra(EXTRA_BEGAN_FROM_NON_INTERACTIVE, false);
+        return new GlobalKeyIntent(intent.getComponent(), event, fromNonInteractive);
+    }
+}
diff --git a/services/core/java/com/android/server/policy/GlobalKeyManager.java b/services/core/java/com/android/server/policy/GlobalKeyManager.java
index 157f825..2d48452 100644
--- a/services/core/java/com/android/server/policy/GlobalKeyManager.java
+++ b/services/core/java/com/android/server/policy/GlobalKeyManager.java
@@ -39,6 +39,9 @@
  * broadcast to the specified component. The action of the intent will be
  * {@link Intent#ACTION_GLOBAL_BUTTON} and the KeyEvent will be included in the intent with
  * {@link Intent#EXTRA_KEY_EVENT}.
+ *
+ * Use {@link GlobalKeyIntent} to get detail information from received {@link Intent}, includes
+ * {@link KeyEvent} and the information about if the key is dispatched from non-interactive mode.
  */
 final class GlobalKeyManager {
 
@@ -49,13 +52,15 @@
     private static final String TAG_KEY = "key";
     private static final String ATTR_KEY_CODE = "keyCode";
     private static final String ATTR_COMPONENT = "component";
+    private static final String ATTR_DISPATCH_WHEN_NON_INTERACTIVE = "dispatchWhenNonInteractive";
 
     private static final int GLOBAL_KEY_FILE_VERSION = 1;
 
-    private SparseArray<ComponentName> mKeyMapping;
+    private SparseArray<GlobalKeyAction> mKeyMapping;
+    private boolean mBeganFromNonInteractive = false;
 
     public GlobalKeyManager(Context context) {
-        mKeyMapping = new SparseArray<ComponentName>();
+        mKeyMapping = new SparseArray<>();
         loadGlobalKeys(context);
     }
 
@@ -69,13 +74,15 @@
      */
     boolean handleGlobalKey(Context context, int keyCode, KeyEvent event) {
         if (mKeyMapping.size() > 0) {
-            ComponentName component = mKeyMapping.get(keyCode);
-            if (component != null) {
-                Intent intent = new Intent(Intent.ACTION_GLOBAL_BUTTON)
-                        .setComponent(component)
-                        .setFlags(Intent.FLAG_RECEIVER_FOREGROUND)
-                        .putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(event));
+            GlobalKeyAction action = mKeyMapping.get(keyCode);
+            if (action != null) {
+                final Intent intent = new GlobalKeyIntent(action.mComponentName, event,
+                        mBeganFromNonInteractive).getIntent();
                 context.sendBroadcastAsUser(intent, UserHandle.CURRENT, null);
+
+                if (event.getAction() == KeyEvent.ACTION_UP) {
+                    mBeganFromNonInteractive = false;
+                }
                 return true;
             }
         }
@@ -85,10 +92,35 @@
     /**
      * Returns {@code true} if the key will be handled globally.
      */
-    boolean shouldHandleGlobalKey(int keyCode, KeyEvent event) {
+    boolean shouldHandleGlobalKey(int keyCode) {
         return mKeyMapping.get(keyCode) != null;
     }
 
+    /**
+     * Returns {@code true} if the key will be handled globally.
+     */
+    boolean shouldDispatchFromNonInteractive(int keyCode) {
+        final GlobalKeyAction action = mKeyMapping.get(keyCode);
+        if (action == null) {
+            return false;
+        }
+
+        return action.mDispatchWhenNonInteractive;
+    }
+
+    void setBeganFromNonInteractive() {
+        mBeganFromNonInteractive = true;
+    }
+
+    class GlobalKeyAction {
+        private ComponentName mComponentName;
+        private boolean mDispatchWhenNonInteractive;
+        GlobalKeyAction(String componentName, String dispatchWhenNonInteractive) {
+            mComponentName = ComponentName.unflattenFromString(componentName);
+            mDispatchWhenNonInteractive = Boolean.valueOf(dispatchWhenNonInteractive);
+        }
+    }
+
     private void loadGlobalKeys(Context context) {
         XmlResourceParser parser = null;
         try {
@@ -105,10 +137,12 @@
                     if (TAG_KEY.equals(element)) {
                         String keyCodeName = parser.getAttributeValue(null, ATTR_KEY_CODE);
                         String componentName = parser.getAttributeValue(null, ATTR_COMPONENT);
+                        String dispatchWhenNonInteractive =
+                                parser.getAttributeValue(null, ATTR_DISPATCH_WHEN_NON_INTERACTIVE);
                         int keyCode = KeyEvent.keyCodeFromString(keyCodeName);
                         if (keyCode != KeyEvent.KEYCODE_UNKNOWN) {
-                            mKeyMapping.put(keyCode, ComponentName.unflattenFromString(
-                                    componentName));
+                            mKeyMapping.put(keyCode, new GlobalKeyAction(
+                                    componentName, dispatchWhenNonInteractive));
                         }
                     }
                 }
@@ -138,7 +172,9 @@
             pw.print(prefix);
             pw.print(KeyEvent.keyCodeToString(mKeyMapping.keyAt(i)));
             pw.print("=");
-            pw.println(mKeyMapping.valueAt(i).flattenToString());
+            pw.print(mKeyMapping.valueAt(i).mComponentName.flattenToString());
+            pw.print(",dispatchWhenNonInteractive=");
+            pw.println(mKeyMapping.valueAt(i).mDispatchWhenNonInteractive);
         }
         pw.print(prefix); pw.println("}");
     }
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 27f5350..92f6891 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -3427,7 +3427,16 @@
         // If the key would be handled globally, just return the result, don't worry about special
         // key processing.
         if (isValidGlobalKey(keyCode)
-                && mGlobalKeyManager.shouldHandleGlobalKey(keyCode, event)) {
+                && mGlobalKeyManager.shouldHandleGlobalKey(keyCode)) {
+            // Dispatch if global key defined dispatchWhenNonInteractive.
+            if (!interactive && isWakeKey && down
+                    && mGlobalKeyManager.shouldDispatchFromNonInteractive(keyCode)) {
+                mGlobalKeyManager.setBeganFromNonInteractive();
+                result = ACTION_PASS_TO_USER;
+                // Since we're dispatching the input, reset the pending key
+                mPendingWakeKey = PENDING_KEY_NULL;
+            }
+
             if (isWakeKey) {
                 wakeUpFromWakeKey(event);
             }
@@ -3981,6 +3990,7 @@
                 Slog.e(TAG, "RemoteException when checking if dreaming", e);
             }
         }
+
         // Otherwise, consume events since the user can't see what is being
         // interacted with.
         return false;