diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 489a79f5f5e73..b063140d4efd9 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -1051,6 +1051,39 @@
-->
0
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
3500
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index d96b1de32c82e..846ec602385f5 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -459,6 +459,10 @@
+
+
+
+
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 193b27d4e37b3..1890a1f7573a1 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -93,8 +93,10 @@ import static com.android.server.wm.WindowManagerPolicyProto.ROTATION_MODE;
import static com.android.server.wm.WindowManagerPolicyProto.SCREEN_ON_FULLY;
import static com.android.server.wm.WindowManagerPolicyProto.WINDOW_MANAGER_DRAW_COMPLETE;
+import android.accessibilityservice.AccessibilityServiceInfo;
import android.annotation.Nullable;
import android.app.ActivityManager;
+import android.app.ActivityManager.RecentTaskInfo;
import android.app.ActivityManagerInternal;
import android.app.ActivityTaskManager;
import android.app.AppOpsManager;
@@ -105,6 +107,7 @@ import android.app.SearchManager;
import android.app.UiModeManager;
import android.content.ActivityNotFoundException;
import android.content.BroadcastReceiver;
+import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
@@ -113,6 +116,7 @@ import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
import android.content.res.CompatibilityInfo;
import android.content.res.Configuration;
import android.content.res.Resources;
@@ -191,6 +195,7 @@ import android.view.autofill.AutofillManagerInternal;
import com.android.internal.R;
import com.android.internal.accessibility.AccessibilityShortcutController;
+import com.android.internal.accessibility.util.AccessibilityUtils;
import com.android.internal.app.AssistUtils;
import com.android.internal.inputmethod.SoftInputShowHideReason;
import com.android.internal.logging.MetricsLogger;
@@ -230,6 +235,7 @@ import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashSet;
import java.util.List;
+import java.util.Set;
/**
* WindowManagerPolicy implementation for the Android phone UI. This
@@ -308,6 +314,22 @@ public class PhoneWindowManager implements WindowManagerPolicy {
static final int PENDING_KEY_NULL = -1;
+ // Must match: config_shortPressOnStemPrimaryBehavior in config.xml
+ static final int SHORT_PRESS_PRIMARY_NOTHING = 0;
+ static final int SHORT_PRESS_PRIMARY_LAUNCH_ALL_APPS = 1;
+
+ // Must match: config_longPressOnStemPrimaryBehavior in config.xml
+ static final int LONG_PRESS_PRIMARY_NOTHING = 0;
+ static final int LONG_PRESS_PRIMARY_LAUNCH_VOICE_ASSISTANT = 1;
+
+ // Must match: config_doublePressOnStemPrimaryBehavior in config.xml
+ static final int DOUBLE_PRESS_PRIMARY_NOTHING = 0;
+ static final int DOUBLE_PRESS_PRIMARY_SWITCH_RECENT_APP = 1;
+
+ // Must match: config_triplePressOnStemPrimaryBehavior in config.xml
+ static final int TRIPLE_PRESS_PRIMARY_NOTHING = 0;
+ static final int TRIPLE_PRESS_PRIMARY_TOGGLE_ACCESSIBILITY = 1;
+
static public final String SYSTEM_DIALOG_REASON_KEY = "reason";
static public final String SYSTEM_DIALOG_REASON_GLOBAL_ACTIONS = "globalactions";
static public final String SYSTEM_DIALOG_REASON_RECENT_APPS = "recentapps";
@@ -315,6 +337,8 @@ public class PhoneWindowManager implements WindowManagerPolicy {
static public final String SYSTEM_DIALOG_REASON_ASSIST = "assist";
static public final String SYSTEM_DIALOG_REASON_SCREENSHOT = "screenshot";
+ private static final String TALKBACK_LABEL = "TalkBack";
+
private static final int POWER_BUTTON_SUPPRESSION_DELAY_DEFAULT_MILLIS = 800;
private static final AudioAttributes VIBRATION_ATTRIBUTES = new AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
@@ -487,6 +511,10 @@ public class PhoneWindowManager implements WindowManagerPolicy {
int mShortPressOnSleepBehavior;
int mShortPressOnWindowBehavior;
int mPowerVolUpBehavior;
+ int mShortPressOnStemPrimaryBehavior;
+ int mDoublePressOnStemPrimaryBehavior;
+ int mTriplePressOnStemPrimaryBehavior;
+ int mLongPressOnStemPrimaryBehavior;
boolean mHasSoftInput = false;
boolean mHapticTextHandleEnabled;
boolean mUseTvRouting;
@@ -1192,6 +1220,170 @@ public class PhoneWindowManager implements WindowManagerPolicy {
return mLongPressOnPowerBehavior;
}
+ private void stemPrimaryPress(int count) {
+ if (DEBUG_INPUT) {
+ Slog.d(TAG, "stemPrimaryPress: " + count);
+ }
+ if (count == 3) {
+ stemPrimaryTriplePressAction(mTriplePressOnStemPrimaryBehavior);
+ } else if (count == 2) {
+ stemPrimaryDoublePressAction(mDoublePressOnStemPrimaryBehavior);
+ } else if (count == 1) {
+ stemPrimarySinglePressAction(mShortPressOnStemPrimaryBehavior);
+ }
+ }
+
+ private void stemPrimarySinglePressAction(int behavior) {
+ switch (behavior) {
+ case SHORT_PRESS_PRIMARY_NOTHING:
+ break;
+ case SHORT_PRESS_PRIMARY_LAUNCH_ALL_APPS:
+ if (DEBUG_INPUT) {
+ Slog.d(TAG, "Executing stem primary short press action behavior.");
+ }
+ final boolean keyguardActive =
+ mKeyguardDelegate != null && mKeyguardDelegate.isShowing();
+ if (!keyguardActive) {
+ Intent intent = new Intent(Intent.ACTION_ALL_APPS);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
+ | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
+ startActivityAsUser(intent, UserHandle.CURRENT_OR_SELF);
+ }
+ break;
+ }
+ }
+
+ private void stemPrimaryDoublePressAction(int behavior) {
+ switch (behavior) {
+ case DOUBLE_PRESS_PRIMARY_NOTHING:
+ break;
+ case DOUBLE_PRESS_PRIMARY_SWITCH_RECENT_APP:
+ if (DEBUG_INPUT) {
+ Slog.d(TAG, "Executing stem primary double press action behavior.");
+ }
+ final boolean keyguardActive = mKeyguardDelegate == null
+ ? false
+ : mKeyguardDelegate.isShowing();
+ if (!keyguardActive) {
+ switchRecentTask();
+ }
+ break;
+ }
+ }
+
+ private void stemPrimaryTriplePressAction(int behavior) {
+ switch (behavior) {
+ case TRIPLE_PRESS_PRIMARY_NOTHING:
+ break;
+ case TRIPLE_PRESS_PRIMARY_TOGGLE_ACCESSIBILITY:
+ if (DEBUG_INPUT) {
+ Slog.d(TAG, "Executing stem primary triple press action behavior.");
+ }
+ toggleTalkBack();
+ break;
+ }
+ }
+
+ private void stemPrimaryLongPress() {
+ if (DEBUG_INPUT) {
+ Slog.d(TAG, "Executing stem primary long press action behavior.");
+ }
+
+ switch (mLongPressOnStemPrimaryBehavior) {
+ case LONG_PRESS_PRIMARY_NOTHING:
+ break;
+ case LONG_PRESS_PRIMARY_LAUNCH_VOICE_ASSISTANT:
+ launchVoiceAssist(/* allowDuringSetup= */false);
+ break;
+ }
+ }
+
+ private void toggleTalkBack() {
+ final ComponentName componentName = getTalkbackComponent();
+ if (componentName == null) {
+ return;
+ }
+
+ final Set enabledServices =
+ AccessibilityUtils.getEnabledServicesFromSettings(mContext, mCurrentUserId);
+
+ AccessibilityUtils.setAccessibilityServiceState(mContext, componentName,
+ !enabledServices.contains(componentName));
+ }
+
+ private ComponentName getTalkbackComponent() {
+ AccessibilityManager accessibilityManager = mContext.getSystemService(
+ AccessibilityManager.class);
+ List serviceInfos =
+ accessibilityManager.getInstalledAccessibilityServiceList();
+
+ for (AccessibilityServiceInfo service : serviceInfos) {
+ final ServiceInfo serviceInfo = service.getResolveInfo().serviceInfo;
+ if (isTalkback(serviceInfo)) {
+ return new ComponentName(serviceInfo.packageName, serviceInfo.name);
+ }
+ }
+ return null;
+ }
+
+ private boolean isTalkback(ServiceInfo info) {
+ String label = info.loadLabel(mPackageManager).toString();
+ return label.equals(TALKBACK_LABEL);
+ }
+
+ /**
+ * Load most recent task (expect current task) and bring it to the front.
+ */
+ private void switchRecentTask() {
+ RecentTaskInfo targetTask = mActivityTaskManagerInternal.getMostRecentTaskFromBackground();
+ if (targetTask == null) {
+ if (DEBUG_INPUT) {
+ Slog.w(TAG, "No recent task available! Show watch face.");
+ }
+ goHome();
+ return;
+ }
+
+ if (DEBUG_INPUT) {
+ Slog.d(
+ TAG,
+ "Starting task from recents. id="
+ + targetTask.id
+ + ", persistentId="
+ + targetTask.persistentId
+ + ", topActivity="
+ + targetTask.topActivity
+ + ", baseIntent="
+ + targetTask.baseIntent);
+ }
+ try {
+ ActivityManager.getService().startActivityFromRecents(targetTask.persistentId, null);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to start task " + targetTask.persistentId + " from recents", e);
+ }
+ }
+
+ private int getMaxMultiPressStemPrimaryCount() {
+ switch (mTriplePressOnStemPrimaryBehavior) {
+ case TRIPLE_PRESS_PRIMARY_NOTHING:
+ break;
+ case TRIPLE_PRESS_PRIMARY_TOGGLE_ACCESSIBILITY:
+ if (Settings.System.getIntForUser(
+ mContext.getContentResolver(),
+ Settings.System.WEAR_ACCESSIBILITY_GESTURE_ENABLED,
+ /* def= */ 0,
+ UserHandle.USER_CURRENT)
+ == 1) {
+ return 3;
+ }
+ break;
+ }
+ if (mDoublePressOnStemPrimaryBehavior != DOUBLE_PRESS_PRIMARY_NOTHING) {
+ return 2;
+ }
+ return 1;
+ }
+
private boolean hasLongPressOnPowerBehavior() {
return getResolvedLongPressOnPowerBehavior() != LONG_PRESS_POWER_NOTHING;
}
@@ -1204,6 +1396,16 @@ public class PhoneWindowManager implements WindowManagerPolicy {
return mLongPressOnBackBehavior != LONG_PRESS_BACK_NOTHING;
}
+ private boolean hasLongPressOnStemPrimaryBehavior() {
+ return mLongPressOnStemPrimaryBehavior != LONG_PRESS_PRIMARY_NOTHING;
+ }
+
+ private boolean hasStemPrimaryBehavior() {
+ return getMaxMultiPressStemPrimaryCount() > 1
+ || hasLongPressOnStemPrimaryBehavior()
+ || mShortPressOnStemPrimaryBehavior != SHORT_PRESS_PRIMARY_NOTHING;
+ }
+
private void interceptScreenshotChord() {
mHandler.removeCallbacks(mScreenshotRunnable);
mScreenshotRunnable.setScreenshotType(TAKE_SCREENSHOT_FULLSCREEN);
@@ -2037,6 +2239,35 @@ public class PhoneWindowManager implements WindowManagerPolicy {
}
}
+ /**
+ * Rule for single stem primary key gesture.
+ */
+ private final class StemPrimaryKeyRule extends SingleKeyGestureDetector.SingleKeyRule {
+ StemPrimaryKeyRule(int gestures) {
+ super(mContext, KeyEvent.KEYCODE_STEM_PRIMARY, gestures);
+ }
+
+ @Override
+ int getMaxMultiPressCount() {
+ return getMaxMultiPressStemPrimaryCount();
+ }
+
+ @Override
+ void onPress(long downTime) {
+ stemPrimaryPress(1 /*count*/);
+ }
+
+ @Override
+ void onLongPress(long eventTime) {
+ stemPrimaryLongPress();
+ }
+
+ @Override
+ void onMultiPress(long downTime, int count) {
+ stemPrimaryPress(count);
+ }
+ }
+
private void initSingleKeyGestureRules() {
mSingleKeyGestureDetector = new SingleKeyGestureDetector();
@@ -2052,6 +2283,13 @@ public class PhoneWindowManager implements WindowManagerPolicy {
if (hasLongPressOnBackBehavior()) {
mSingleKeyGestureDetector.addRule(new BackKeyRule(KEY_LONGPRESS));
}
+ if (hasStemPrimaryBehavior()) {
+ int stemPrimaryKeyGestures = 0;
+ if (hasLongPressOnStemPrimaryBehavior()) {
+ stemPrimaryKeyGestures |= KEY_LONGPRESS;
+ }
+ mSingleKeyGestureDetector.addRule(new StemPrimaryKeyRule(stemPrimaryKeyGestures));
+ }
}
/**
@@ -2080,6 +2318,14 @@ public class PhoneWindowManager implements WindowManagerPolicy {
if (mPackageManager.hasSystemFeature(FEATURE_PICTURE_IN_PICTURE)) {
mShortPressOnWindowBehavior = SHORT_PRESS_WINDOW_PICTURE_IN_PICTURE;
}
+ mShortPressOnStemPrimaryBehavior = mContext.getResources().getInteger(
+ com.android.internal.R.integer.config_shortPressOnStemPrimaryBehavior);
+ mLongPressOnStemPrimaryBehavior = mContext.getResources().getInteger(
+ com.android.internal.R.integer.config_longPressOnStemPrimaryBehavior);
+ mDoublePressOnStemPrimaryBehavior = mContext.getResources().getInteger(
+ com.android.internal.R.integer.config_doublePressOnStemPrimaryBehavior);
+ mTriplePressOnStemPrimaryBehavior = mContext.getResources().getInteger(
+ com.android.internal.R.integer.config_triplePressOnStemPrimaryBehavior);
}
public void updateSettings() {
@@ -5380,6 +5626,22 @@ public class PhoneWindowManager implements WindowManagerPolicy {
pw.print(prefix);
pw.print("mShortPressOnWindowBehavior=");
pw.println(shortPressOnWindowBehaviorToString(mShortPressOnWindowBehavior));
+ pw.print(prefix);
+ pw.print("mShortPressOnStemPrimaryBehavior=");
+ pw.println(shortPressOnStemPrimaryBehaviorToString(
+ mShortPressOnStemPrimaryBehavior));
+ pw.print(prefix);
+ pw.print("mDoublePressOnStemPrimaryBehavior=");
+ pw.println(doublePressOnStemPrimaryBehaviorToString(
+ mDoublePressOnStemPrimaryBehavior));
+ pw.print(prefix);
+ pw.print("mTriplePressOnStemPrimaryBehavior=");
+ pw.println(triplePressOnStemPrimaryBehaviorToString(
+ mTriplePressOnStemPrimaryBehavior));
+ pw.print(prefix);
+ pw.print("mLongPressOnStemPrimaryBehavior=");
+ pw.println(longPressOnStemPrimaryBehaviorToString(
+ mLongPressOnStemPrimaryBehavior));
pw.print(prefix);
pw.print("mAllowStartActivityForLongPressOnPowerDuringSetup=");
pw.println(mAllowStartActivityForLongPressOnPowerDuringSetup);
@@ -5596,6 +5858,50 @@ public class PhoneWindowManager implements WindowManagerPolicy {
}
}
+ private static String shortPressOnStemPrimaryBehaviorToString(int behavior) {
+ switch (behavior) {
+ case SHORT_PRESS_PRIMARY_NOTHING:
+ return "SHORT_PRESS_PRIMARY_NOTHING";
+ case SHORT_PRESS_PRIMARY_LAUNCH_ALL_APPS:
+ return "SHORT_PRESS_PRIMARY_LAUNCH_ALL_APPS";
+ default:
+ return Integer.toString(behavior);
+ }
+ }
+
+ private static String doublePressOnStemPrimaryBehaviorToString(int behavior) {
+ switch (behavior) {
+ case DOUBLE_PRESS_PRIMARY_NOTHING:
+ return "DOUBLE_PRESS_PRIMARY_NOTHING";
+ case DOUBLE_PRESS_PRIMARY_SWITCH_RECENT_APP:
+ return "DOUBLE_PRESS_PRIMARY_SWITCH_RECENT_APP";
+ default:
+ return Integer.toString(behavior);
+ }
+ }
+
+ private static String triplePressOnStemPrimaryBehaviorToString(int behavior) {
+ switch (behavior) {
+ case TRIPLE_PRESS_PRIMARY_NOTHING:
+ return "TRIPLE_PRESS_PRIMARY_NOTHING";
+ case TRIPLE_PRESS_PRIMARY_TOGGLE_ACCESSIBILITY:
+ return "TRIPLE_PRESS_PRIMARY_TOGGLE_ACCESSIBILITY";
+ default:
+ return Integer.toString(behavior);
+ }
+ }
+
+ private static String longPressOnStemPrimaryBehaviorToString(int behavior) {
+ switch (behavior) {
+ case LONG_PRESS_PRIMARY_NOTHING:
+ return "LONG_PRESS_PRIMARY_NOTHING";
+ case LONG_PRESS_PRIMARY_LAUNCH_VOICE_ASSISTANT:
+ return "LONG_PRESS_PRIMARY_LAUNCH_VOICE_ASSISTANT";
+ default:
+ return Integer.toString(behavior);
+ }
+ }
+
private static String lidBehaviorToString(int behavior) {
switch (behavior) {
case LID_BEHAVIOR_LOCK:
diff --git a/services/core/java/com/android/server/policy/SingleKeyGestureDetector.java b/services/core/java/com/android/server/policy/SingleKeyGestureDetector.java
index 1d4943fc1b034..b7fb6e53cc272 100644
--- a/services/core/java/com/android/server/policy/SingleKeyGestureDetector.java
+++ b/services/core/java/com/android/server/policy/SingleKeyGestureDetector.java
@@ -53,6 +53,7 @@ public final class SingleKeyGestureDetector {
// Key code of current key down event, reset when key up.
private int mDownKeyCode = KeyEvent.KEYCODE_UNKNOWN;
private volatile boolean mHandledByLongPress = false;
+ private volatile boolean mHandledByMultiPress = false;
private final Handler mHandler;
private long mLastDownTime = 0;
private static final long MULTI_PRESS_TIMEOUT = ViewConfiguration.getMultiPressTimeout();
@@ -271,6 +272,7 @@ public final class SingleKeyGestureDetector {
mKeyPressCounter + 1, mActiveRule);
msg.setAsynchronous(true);
mHandler.sendMessage(msg);
+ mHandledByMultiPress = true;
mKeyPressCounter = 0;
}
}
@@ -284,8 +286,9 @@ public final class SingleKeyGestureDetector {
return false;
}
- if (mHandledByLongPress) {
+ if (mHandledByLongPress || mHandledByMultiPress) {
mHandledByLongPress = false;
+ mHandledByMultiPress = false;
mKeyPressCounter = 0;
return true;
}
@@ -339,6 +342,7 @@ public final class SingleKeyGestureDetector {
}
mHandledByLongPress = false;
+ mHandledByMultiPress = false;
mDownKeyCode = KeyEvent.KEYCODE_UNKNOWN;
}
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java
index 9c68f2df8b83e..e505d8529383d 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java
@@ -19,6 +19,7 @@ package com.android.server.wm;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
+import android.app.ActivityManager;
import android.app.AppProtoEnums;
import android.app.IActivityManager;
import android.app.IApplicationThread;
@@ -688,4 +689,7 @@ public abstract class ActivityTaskManagerInternal {
public abstract void registerActivityStartInterceptor(
@ActivityInterceptorCallback.OrderedId int id,
ActivityInterceptorCallback callback);
+
+ /** Get the most recent task excluding the first running task (the one on the front most). */
+ public abstract ActivityManager.RecentTaskInfo getMostRecentTaskFromBackground();
}
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 886b05f25dc86..a1a357e91b8f9 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -6609,5 +6609,33 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub {
mActivityInterceptorCallbacks.put(id, callback);
}
}
+
+ @Override
+ public ActivityManager.RecentTaskInfo getMostRecentTaskFromBackground() {
+ List runningTaskInfoList = getTasks(1);
+ ActivityManager.RunningTaskInfo runningTaskInfo;
+ if (runningTaskInfoList.size() > 0) {
+ runningTaskInfo = runningTaskInfoList.get(0);
+ } else {
+ Slog.i(TAG, "No running task found!");
+ return null;
+ }
+ // Get 2 most recent tasks.
+ List recentTaskInfoList =
+ getRecentTasks(
+ 2,
+ ActivityManager.RECENT_IGNORE_UNAVAILABLE,
+ mContext.getUserId())
+ .getList();
+ ActivityManager.RecentTaskInfo targetTask = null;
+ for (ActivityManager.RecentTaskInfo info : recentTaskInfoList) {
+ // Find a recent task that is not the current running task on screen.
+ if (info.id != runningTaskInfo.id) {
+ targetTask = info;
+ break;
+ }
+ }
+ return targetTask;
+ }
}
}