From 9394bd84bda99dd08127dc1bf4cc439b5a7575db Mon Sep 17 00:00:00 2001 From: Matthew Fritze Date: Thu, 24 Sep 2020 14:53:40 -0700 Subject: [PATCH] Launch Panic gesture from SysUI - Add StatusBar interface for launching panic gesture - Add basic method to launch the panic gesture in sysui - Swap call to launch telephony to launching eshare intent Tested in Pixel 3a using the Settings Panel intent (since it resolves) and found that it does indeed launch ontop of the camera (see screenshot) and turns off the screen. These can be fixed in a later CL. Screenshot: https://screenshot.googleplex.com/8KKEEXbyBLkJssy Test: atest StatusBarTest GestureLauncherServiceTest CommandQueueTest Bug: 169087010 Change-Id: I9efccffd06edbf37c15ea92ef5935ab7091a7f80 --- .../internal/statusbar/IStatusBar.aidl | 7 + .../systemui/statusbar/CommandQueue.java | 19 +++ .../systemui/statusbar/phone/StatusBar.java | 27 ++- packages/SystemUI/tests/AndroidManifest.xml | 7 + .../emergency/EmergencyActivityTest.java | 34 ++++ .../statusbar/phone/StatusBarTest.java | 18 ++ .../server/GestureLauncherService.java | 14 +- .../statusbar/StatusBarManagerInternal.java | 7 + .../statusbar/StatusBarManagerService.java | 17 ++ .../server/GestureLauncherServiceTest.java | 159 ++++++++++++++++++ 10 files changed, 295 insertions(+), 14 deletions(-) create mode 100644 packages/SystemUI/tests/src/com/android/systemui/emergency/EmergencyActivityTest.java 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);