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
This commit is contained in:
Matthew Fritze
2020-09-24 14:53:40 -07:00
parent f76ffeaff1
commit 9394bd84bd
10 changed files with 295 additions and 14 deletions

View File

@@ -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.
*/

View File

@@ -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();

View File

@@ -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())) {

View File

@@ -70,6 +70,13 @@
android:exported="false"
android:resizeableActivity="true" />
<activity android:name="com.android.systemui.emergency.EmergencyActivityTest"
android:exported="true">
<intent-filter>
<action android:name="com.android.systemui.action.LAUNCH_EMERGENCY"/>
</intent-filter>
</activity>
<activity
android:name="com.android.systemui.globalactions.GlobalActionsImeTest$TestActivity"
android:excludeFromRecents="true"

View File

@@ -0,0 +1,34 @@
/*
* Copyright (C) 2020 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.systemui.emergency;
import android.app.Activity;
import android.os.Bundle;
import com.android.systemui.R;
/**
* Test activity for resolving {@link EmergencyGesture#ACTION_LAUNCH_EMERGENCY} action.
*/
public class EmergencyActivityTest extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
}
}

View File

@@ -24,6 +24,7 @@ import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
import static junit.framework.TestCase.fail;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
@@ -34,6 +35,7 @@ import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -43,6 +45,7 @@ import android.app.StatusBarManager;
import android.app.trust.TrustManager;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Intent;
import android.content.IntentFilter;
import android.hardware.display.AmbientDisplayConfiguration;
import android.hardware.fingerprint.FingerprintManager;
@@ -84,6 +87,7 @@ import com.android.systemui.bubbles.Bubbles;
import com.android.systemui.classifier.FalsingManagerFake;
import com.android.systemui.colorextraction.SysuiColorExtractor;
import com.android.systemui.demomode.DemoModeController;
import com.android.systemui.emergency.EmergencyGesture;
import com.android.systemui.keyguard.DismissCallbackRegistry;
import com.android.systemui.keyguard.KeyguardViewMediator;
import com.android.systemui.keyguard.ScreenLifecycle;
@@ -146,6 +150,7 @@ import com.android.wm.shell.splitscreen.SplitScreen;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@@ -873,6 +878,19 @@ public class StatusBarTest extends SysuiTestCase {
verify(mDozeServiceHost).setDozeSuppressed(false);
}
@Test
public void onEmergencyActionLaunchGesture_launchesEmergencyIntent() {
ArgumentCaptor<Intent> 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 {

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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<Integer> cameraIntervalCaptor = ArgumentCaptor.forClass(Integer.class);
verify(mMetricsLogger, times(2)).histogram(
eq("power_double_tap_interval"), cameraIntervalCaptor.capture());
List<Integer> cameraIntervals = cameraIntervalCaptor.getAllValues();
assertEquals((int) INITIAL_EVENT_TIME_MILLIS, cameraIntervals.get(0).intValue());
assertEquals((int) interval, cameraIntervals.get(1).intValue());
final ArgumentCaptor<Integer> tapCountCaptor = ArgumentCaptor.forClass(Integer.class);
verify(mMetricsLogger, times(2)).histogram(
eq("power_consecutive_short_tap_count"), tapCountCaptor.capture());
List<Integer> 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<Integer> intervalCaptor = ArgumentCaptor.forClass(Integer.class);
verify(mMetricsLogger, times(5)).histogram(
eq("power_double_tap_interval"), intervalCaptor.capture());
List<Integer> 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<Integer> intervalCaptor = ArgumentCaptor.forClass(Integer.class);
verify(mMetricsLogger, times(5)).histogram(
eq("power_double_tap_interval"), intervalCaptor.capture());
List<Integer> 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);