diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl index caae5188b6a06..77c7ce8cf5c43 100644 --- a/core/java/com/android/internal/statusbar/IStatusBar.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl @@ -101,6 +101,13 @@ oneway interface IStatusBar */ void onCameraLaunchGestureDetected(int source); + /** + * Notifies the status bar that the Emergency Action launch gesture has been detected. + * + * TODO(b/169175022) Update method name and docs when feature name is locked. + */ + void onEmergencyActionLaunchGestureDetected(); + /** * Shows the picture-in-picture menu if an activity is in picture-in-picture mode. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java index 0184fa7a5bfd2..dcee9fa9e648b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java @@ -140,6 +140,8 @@ public class CommandQueue extends IStatusBar.Stub implements CallbackController< private static final int MSG_SUPPRESS_AMBIENT_DISPLAY = 56 << MSG_SHIFT; private static final int MSG_REQUEST_WINDOW_MAGNIFICATION_CONNECTION = 57 << MSG_SHIFT; private static final int MSG_HANDLE_WINDOW_MANAGER_LOGGING_COMMAND = 58 << MSG_SHIFT; + //TODO(b/169175022) Update name and when feature name is locked. + private static final int MSG_EMERGENCY_ACTION_LAUNCH_GESTURE = 59 << MSG_SHIFT; public static final int FLAG_EXCLUDE_NONE = 0; public static final int FLAG_EXCLUDE_SEARCH_PANEL = 1 << 0; @@ -258,6 +260,11 @@ public class CommandQueue extends IStatusBar.Stub implements CallbackController< default void showAssistDisclosure() { } default void startAssist(Bundle args) { } default void onCameraLaunchGestureDetected(int source) { } + + /** + * Notifies SysUI that the emergency action gesture was detected. + */ + default void onEmergencyActionLaunchGestureDetected() { } default void showPictureInPictureMenu() { } default void setTopAppHidesStatusBar(boolean topAppHidesStatusBar) { } @@ -729,6 +736,14 @@ public class CommandQueue extends IStatusBar.Stub implements CallbackController< } } + @Override + public void onEmergencyActionLaunchGestureDetected() { + synchronized (mLock) { + mHandler.removeMessages(MSG_EMERGENCY_ACTION_LAUNCH_GESTURE); + mHandler.obtainMessage(MSG_EMERGENCY_ACTION_LAUNCH_GESTURE).sendToTarget(); + } + } + @Override public void addQsTile(ComponentName tile) { synchronized (mLock) { @@ -1186,6 +1201,10 @@ public class CommandQueue extends IStatusBar.Stub implements CallbackController< mCallbacks.get(i).onCameraLaunchGestureDetected(msg.arg1); } break; + case MSG_EMERGENCY_ACTION_LAUNCH_GESTURE: + for (int i = 0; i < mCallbacks.size(); i++) { + mCallbacks.get(i).onEmergencyActionLaunchGestureDetected(); + } case MSG_SHOW_PICTURE_IN_PICTURE_MENU: for (int i = 0; i < mCallbacks.size(); i++) { mCallbacks.get(i).showPictureInPictureMenu(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java index e7c29b6b54b01..b5d32711daf20 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java @@ -69,6 +69,7 @@ import android.content.IntentFilter; import android.content.pm.IPackageManager; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ResolveInfo; import android.content.res.Configuration; import android.graphics.Point; import android.graphics.PointF; @@ -152,6 +153,7 @@ import com.android.systemui.dagger.qualifiers.UiBackground; import com.android.systemui.demomode.DemoMode; import com.android.systemui.demomode.DemoModeCommandReceiver; import com.android.systemui.demomode.DemoModeController; +import com.android.systemui.emergency.EmergencyGesture; import com.android.systemui.fragments.ExtensionFragmentListener; import com.android.systemui.fragments.FragmentHostManager; import com.android.systemui.keyguard.DismissCallbackRegistry; @@ -819,7 +821,7 @@ public class StatusBar extends SystemUI implements DemoMode, updateScrimController(); }; - + mActivityIntentHelper = new ActivityIntentHelper(mContext); DateTimeView.setReceiverHandler(timeTickHandler); } @@ -833,8 +835,6 @@ public class StatusBar extends SystemUI implements DemoMode, mBubblesOptional.get().setExpandListener(mBubbleExpandListener); } - mActivityIntentHelper = new ActivityIntentHelper(mContext); - mColorExtractor.addOnColorsChangedListener(this); mStatusBarStateController.addCallback(this, SysuiStatusBarStateController.RANK_STATUS_BAR); @@ -3982,6 +3982,27 @@ public class StatusBar extends SystemUI implements DemoMode, } } + @Override + public void onEmergencyActionLaunchGestureDetected() { + // TODO (b/169793384) Polish the panic gesture to be just like its older brother, camera. + Intent emergencyIntent = new Intent(EmergencyGesture.ACTION_LAUNCH_EMERGENCY); + PackageManager pm = mContext.getPackageManager(); + ResolveInfo resolveInfo = pm.resolveActivity(emergencyIntent, /*flags=*/0); + if (resolveInfo == null) { + Log.wtf(TAG, "Couldn't find an app to process the emergency intent."); + return; + } + + if (mVibrator != null && mVibrator.hasVibrator()) { + mVibrator.vibrate(500L); + } + + emergencyIntent.setComponent(new ComponentName(resolveInfo.activityInfo.packageName, + resolveInfo.activityInfo.name)); + emergencyIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(emergencyIntent, /*dismissShade=*/true); + } + boolean isCameraAllowedByAdmin() { if (mDevicePolicyManager.getCameraDisabled(null, mLockscreenUserManager.getCurrentUserId())) { diff --git a/packages/SystemUI/tests/AndroidManifest.xml b/packages/SystemUI/tests/AndroidManifest.xml index db878458721e7..21d39429411b4 100644 --- a/packages/SystemUI/tests/AndroidManifest.xml +++ b/packages/SystemUI/tests/AndroidManifest.xml @@ -70,6 +70,13 @@ android:exported="false" android:resizeableActivity="true" /> + + + + + + intentCaptor = ArgumentCaptor.forClass(Intent.class); + StatusBar statusBarSpy = spy(mStatusBar); + + statusBarSpy.onEmergencyActionLaunchGestureDetected(); + + verify(statusBarSpy).startActivity(intentCaptor.capture(), eq(true)); + Intent sentIntent = intentCaptor.getValue(); + assertEquals(sentIntent.getAction(), EmergencyGesture.ACTION_LAUNCH_EMERGENCY); + + } + public static class TestableNotificationInterruptStateProviderImpl extends NotificationInterruptStateProviderImpl { diff --git a/services/core/java/com/android/server/GestureLauncherService.java b/services/core/java/com/android/server/GestureLauncherService.java index b3d4085288dd4..95a7a221697a2 100644 --- a/services/core/java/com/android/server/GestureLauncherService.java +++ b/services/core/java/com/android/server/GestureLauncherService.java @@ -38,7 +38,6 @@ import android.os.SystemProperties; import android.os.Trace; import android.os.UserHandle; import android.provider.Settings; -import android.telecom.TelecomManager; import android.util.MutableBoolean; import android.util.Slog; import android.view.KeyEvent; @@ -46,7 +45,6 @@ import android.view.KeyEvent; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.server.LocalServices; import com.android.server.statusbar.StatusBarManagerInternal; import com.android.server.wm.WindowManagerInternal; @@ -529,15 +527,9 @@ public class GestureLauncherService extends SystemService { "userSetupComplete = %s, performing panic gesture.", userSetupComplete)); } - // TODO(b/160006048): Not all devices have telephony. Check system feature first. - TelecomManager telecomManager = (TelecomManager) mContext.getSystemService( - Context.TELECOM_SERVICE); - mContext.startActivity(telecomManager.createLaunchEmergencyDialerIntent(null).addFlags( - Intent.FLAG_ACTIVITY_NEW_TASK - | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS - | Intent.FLAG_ACTIVITY_SINGLE_TOP).putExtra( - "com.android.phone.EmergencyDialer.extra.ENTRY_TYPE", - 2)); // 2 maps to power button, forcing into fast emergency dialer experience. + StatusBarManagerInternal service = LocalServices.getService( + StatusBarManagerInternal.class); + service.onEmergencyActionLaunchGestureDetected(); return true; } finally { Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java index e9215f9793d26..ebfffec1774ea 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java @@ -86,6 +86,13 @@ public interface StatusBarManagerInternal { void toggleSplitScreen(); void appTransitionFinished(int displayId); + /** + * Notifies the status bar that a Emergency Action launch gesture has been detected. + * + * TODO (b/169175022) Update method name and docs when feature name is locked. + */ + void onEmergencyActionLaunchGestureDetected(); + void toggleRecentApps(); void setCurrentUser(int newUserId); diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java index 6a68dc1fb2a2e..55cb7f32e1b7e 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java @@ -264,6 +264,23 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D } } + /** + * Notifies the status bar that a Emergency Action launch gesture has been detected. + * + * TODO (b/169175022) Update method name and docs when feature name is locked. + */ + @Override + public void onEmergencyActionLaunchGestureDetected() { + if (SPEW) Slog.d(TAG, "Launching emergency action"); + if (mBar != null) { + try { + mBar.onEmergencyActionLaunchGestureDetected(); + } catch (RemoteException e) { + if (SPEW) Slog.d(TAG, "Failed to launch emergency action"); + } + } + } + @Override public void topAppWindowChanged(int displayId, boolean isFullscreen, boolean isImmersive) { StatusBarManagerService.this.topAppWindowChanged(displayId, isFullscreen, isImmersive); diff --git a/services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java b/services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java index c91bb93fc559c..9a2ce3c598a41 100644 --- a/services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/GestureLauncherServiceTest.java @@ -222,6 +222,25 @@ public class GestureLauncherServiceTest { verify(mMetricsLogger).histogram("power_double_tap_interval", (int) eventTime); } + @Test + public void testInterceptPowerKeyDown_firstPowerDown_panicGestureNotLaunched() { + withPanicGestureEnabledSettingValue(true); + mGestureLauncherService.updatePanicButtonGestureEnabled(); + + long eventTime = INITIAL_EVENT_TIME_MILLIS + + GestureLauncherService.POWER_SHORT_TAP_SEQUENCE_MAX_INTERVAL_MS - 1; + KeyEvent keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, + IGNORED_REPEAT); + boolean interactive = true; + MutableBoolean outLaunched = new MutableBoolean(true); + boolean intercepted = mGestureLauncherService.interceptPowerKeyDown(keyEvent, interactive, + outLaunched); + + assertFalse(intercepted); + assertFalse(outLaunched.value); + verify(mMetricsLogger).histogram("power_double_tap_interval", (int) eventTime); + } + @Test public void testInterceptPowerKeyDown_intervalInBoundsCameraPowerGestureOffInteractive() { withCameraDoubleTapPowerEnableConfigValue(false); @@ -404,6 +423,146 @@ public class GestureLauncherServiceTest { assertEquals(2, tapCounts.get(1).intValue()); } + @Test + public void + testInterceptPowerKeyDown_fiveInboundPresses_cameraAndPanicEnabled_bothLaunch() { + withCameraDoubleTapPowerEnableConfigValue(true); + withCameraDoubleTapPowerDisableSettingValue(0); + withPanicGestureEnabledSettingValue(true); + mGestureLauncherService.updateCameraDoubleTapPowerEnabled(); + mGestureLauncherService.updatePanicButtonGestureEnabled(); + withUserSetupCompleteValue(true); + + // First button press does nothing + long eventTime = INITIAL_EVENT_TIME_MILLIS; + KeyEvent keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, + IGNORED_REPEAT); + boolean interactive = true; + MutableBoolean outLaunched = new MutableBoolean(true); + boolean intercepted = mGestureLauncherService.interceptPowerKeyDown(keyEvent, interactive, + outLaunched); + assertFalse(intercepted); + assertFalse(outLaunched.value); + + final long interval = GestureLauncherService.CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + + // 2nd button triggers camera + eventTime += interval; + keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, + IGNORED_REPEAT); + outLaunched.value = false; + intercepted = mGestureLauncherService.interceptPowerKeyDown(keyEvent, interactive, + outLaunched); + assertTrue(intercepted); + assertTrue(outLaunched.value); + + // Camera checks + verify(mStatusBarManagerInternal).onCameraLaunchGestureDetected( + StatusBarManager.CAMERA_LAUNCH_SOURCE_POWER_DOUBLE_TAP); + verify(mMetricsLogger) + .action(MetricsEvent.ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE, (int) interval); + + final ArgumentCaptor cameraIntervalCaptor = ArgumentCaptor.forClass(Integer.class); + verify(mMetricsLogger, times(2)).histogram( + eq("power_double_tap_interval"), cameraIntervalCaptor.capture()); + List cameraIntervals = cameraIntervalCaptor.getAllValues(); + assertEquals((int) INITIAL_EVENT_TIME_MILLIS, cameraIntervals.get(0).intValue()); + assertEquals((int) interval, cameraIntervals.get(1).intValue()); + + final ArgumentCaptor tapCountCaptor = ArgumentCaptor.forClass(Integer.class); + verify(mMetricsLogger, times(2)).histogram( + eq("power_consecutive_short_tap_count"), tapCountCaptor.capture()); + List tapCounts = tapCountCaptor.getAllValues(); + assertEquals(1, tapCounts.get(0).intValue()); + assertEquals(2, tapCounts.get(1).intValue()); + + // Continue the button presses for the panic gesture. + + // Presses 3 and 4 should not trigger any gesture + for (int i = 0; i < 2; i++) { + eventTime += interval; + keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, + IGNORED_REPEAT); + outLaunched.value = false; + intercepted = mGestureLauncherService.interceptPowerKeyDown(keyEvent, interactive, + outLaunched); + assertFalse(intercepted); + assertFalse(outLaunched.value); + } + + // Fifth button press should trigger the panic flow + eventTime += interval; + keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, + IGNORED_REPEAT); + outLaunched.value = false; + intercepted = mGestureLauncherService.interceptPowerKeyDown(keyEvent, interactive, + outLaunched); + assertTrue(intercepted); + assertTrue(outLaunched.value); + + // TODO (b/169960245) Verify metric event equiv. to ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE + verify(mStatusBarManagerInternal).onEmergencyActionLaunchGestureDetected(); + + final ArgumentCaptor intervalCaptor = ArgumentCaptor.forClass(Integer.class); + verify(mMetricsLogger, times(5)).histogram( + eq("power_double_tap_interval"), intervalCaptor.capture()); + List intervals = intervalCaptor.getAllValues(); + assertEquals((int) INITIAL_EVENT_TIME_MILLIS, intervals.get(0).intValue()); + assertEquals((int) interval, intervals.get(1).intValue()); + } + + @Test + public void + testInterceptPowerKeyDown_fiveInboundPresses_panicGestureEnabled_launchesPanicFlow() { + withPanicGestureEnabledSettingValue(true); + mGestureLauncherService.updatePanicButtonGestureEnabled(); + withUserSetupCompleteValue(true); + + // First button press does nothing + long eventTime = INITIAL_EVENT_TIME_MILLIS; + KeyEvent keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, + IGNORED_REPEAT); + boolean interactive = true; + MutableBoolean outLaunched = new MutableBoolean(true); + boolean intercepted = mGestureLauncherService.interceptPowerKeyDown(keyEvent, interactive, + outLaunched); + assertFalse(intercepted); + assertFalse(outLaunched.value); + + final long interval = GestureLauncherService.CAMERA_POWER_DOUBLE_TAP_MAX_TIME_MS - 1; + // 3 more button presses which should not trigger any gesture (camera gesture disabled) + for (int i = 0; i < 3; i++) { + eventTime += interval; + keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, + IGNORED_REPEAT); + outLaunched.value = false; + intercepted = mGestureLauncherService.interceptPowerKeyDown(keyEvent, interactive, + outLaunched); + assertFalse(intercepted); + assertFalse(outLaunched.value); + } + + // Fifth button press should trigger the panic flow + eventTime += interval; + keyEvent = new KeyEvent(IGNORED_DOWN_TIME, eventTime, IGNORED_ACTION, IGNORED_CODE, + IGNORED_REPEAT); + outLaunched.value = false; + intercepted = mGestureLauncherService.interceptPowerKeyDown(keyEvent, interactive, + outLaunched); + assertTrue(outLaunched.value); + assertTrue(intercepted); + + // TODO (b/169960245) Verify metric event equiv. to ACTION_DOUBLE_TAP_POWER_CAMERA_GESTURE + verify(mStatusBarManagerInternal).onEmergencyActionLaunchGestureDetected(); + + final ArgumentCaptor intervalCaptor = ArgumentCaptor.forClass(Integer.class); + verify(mMetricsLogger, times(5)).histogram( + eq("power_double_tap_interval"), intervalCaptor.capture()); + List intervals = intervalCaptor.getAllValues(); + assertEquals((int) INITIAL_EVENT_TIME_MILLIS, intervals.get(0).intValue()); + assertEquals((int) interval, intervals.get(1).intValue()); + } + @Test public void testInterceptPowerKeyDown_longpress() { withCameraDoubleTapPowerEnableConfigValue(true);