diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index a29fcb17b083f..9603f4d43d8bf 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -13580,6 +13580,28 @@ public final class Settings {
public static final String POWER_BUTTON_VERY_LONG_PRESS =
"power_button_very_long_press";
+
+ /**
+ * Keyguard should be on the left hand side of the screen, for wide screen layouts.
+ *
+ * @hide
+ */
+ public static final int ONE_HANDED_KEYGUARD_SIDE_LEFT = 0;
+
+ /**
+ * Keyguard should be on the right hand side of the screen, for wide screen layouts.
+ *
+ * @hide
+ */
+ public static final int ONE_HANDED_KEYGUARD_SIDE_RIGHT = 1;
+ /**
+ * In one handed mode, which side the keyguard should be on. Allowable values are one of
+ * the ONE_HANDED_KEYGUARD_SIDE_* constants.
+ *
+ * @hide
+ */
+ public static final String ONE_HANDED_KEYGUARD_SIDE = "one_handed_keyguard_side";
+
/**
* Keys we no longer back up under the current schema, but want to continue to
* process when restoring historical backup datasets.
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 367c9b93c4389..416bc84478693 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -4708,4 +4708,7 @@
false
+
+
+ false
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 4d3f5eb36e54f..672aec62f6a6c 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -4174,4 +4174,6 @@
+
+
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
index 66165b6d1ff2e..ad6a5312f1562 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
@@ -147,5 +147,10 @@ public class GlobalSettingsValidators {
VALIDATORS.put(Global.DEVELOPMENT_SETTINGS_ENABLED, BOOLEAN_VALIDATOR);
VALIDATORS.put(Global.NOTIFICATION_FEEDBACK_ENABLED, BOOLEAN_VALIDATOR);
VALIDATORS.put(Global.RESTRICTED_NETWORKING_MODE, BOOLEAN_VALIDATOR);
+ VALIDATORS.put(
+ Global.ONE_HANDED_KEYGUARD_SIDE,
+ new InclusiveIntegerRangeValidator(
+ /* first= */Global.ONE_HANDED_KEYGUARD_SIDE_LEFT,
+ /* last= */Global.ONE_HANDED_KEYGUARD_SIDE_RIGHT));
}
}
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index 438cec8ef68f6..c11877a8a9631 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -283,6 +283,7 @@ public class SettingsBackupTest {
Settings.Global.EUICC_REMOVING_INVISIBLE_PROFILES_TIMEOUT_MILLIS,
Settings.Global.EUICC_SWITCH_SLOT_TIMEOUT_MILLIS,
Settings.Global.FANCY_IME_ANIMATIONS,
+ Settings.Global.ONE_HANDED_KEYGUARD_SIDE,
Settings.Global.FORCE_ALLOW_ON_EXTERNAL,
Settings.Global.FORCED_APP_STANDBY_ENABLED,
Settings.Global.FORCED_APP_STANDBY_FOR_SMALL_BATTERY_ENABLED,
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_bouncer.xml b/packages/SystemUI/res-keyguard/layout/keyguard_bouncer.xml
index 79868093fb127..b5f55af50bff5 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_bouncer.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_bouncer.xml
@@ -24,7 +24,7 @@
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_host_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_host_view.xml
index 04e645bd0a321..c75ee51517d1d 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_host_view.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_host_view.xml
@@ -41,13 +41,14 @@
android:layout_gravity="center">
diff --git a/packages/SystemUI/res-keyguard/values-sw600dp-land/bools.xml b/packages/SystemUI/res-keyguard/values-sw600dp-land/bools.xml
new file mode 100644
index 0000000000000..e09bf7e37ed0c
--- /dev/null
+++ b/packages/SystemUI/res-keyguard/values-sw600dp-land/bools.xml
@@ -0,0 +1,20 @@
+
+
+
+
+ true
+
diff --git a/packages/SystemUI/res-keyguard/values/config.xml b/packages/SystemUI/res-keyguard/values/config.xml
index 8d9d6ee68c67d..6176f7c1dd0ac 100644
--- a/packages/SystemUI/res-keyguard/values/config.xml
+++ b/packages/SystemUI/res-keyguard/values/config.xml
@@ -22,4 +22,5 @@
false
+ false
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
index 30db341a061c8..a7ed67287fc73 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
@@ -29,12 +29,17 @@ import android.app.AlertDialog;
import android.content.Context;
import android.graphics.Insets;
import android.graphics.Rect;
+import android.provider.Settings;
import android.util.AttributeSet;
import android.util.MathUtils;
import android.util.TypedValue;
+import android.view.Gravity;
import android.view.MotionEvent;
+import android.view.OrientationEventListener;
import android.view.VelocityTracker;
+import android.view.View;
import android.view.ViewConfiguration;
+import android.view.ViewPropertyAnimator;
import android.view.WindowInsets;
import android.view.WindowInsetsAnimation;
import android.view.WindowInsetsAnimationControlListener;
@@ -55,6 +60,7 @@ import com.android.internal.widget.LockPatternUtils;
import com.android.keyguard.KeyguardSecurityModel.SecurityMode;
import com.android.systemui.Interpolators;
import com.android.systemui.R;
+import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
import java.util.List;
@@ -99,6 +105,12 @@ public class KeyguardSecurityContainer extends FrameLayout {
private boolean mDisappearAnimRunning;
private SwipeListener mSwipeListener;
+ private boolean mIsSecurityViewLeftAligned = true;
+ private boolean mOneHandedMode = false;
+ private SecurityMode mSecurityMode = SecurityMode.Invalid;
+ private ViewPropertyAnimator mRunningOneHandedAnimator;
+ private final OrientationEventListener mOrientationEventListener;
+
private final WindowInsetsAnimation.Callback mWindowInsetsAnimationCallback =
new WindowInsetsAnimation.Callback(DISPATCH_MODE_STOP) {
@@ -157,16 +169,20 @@ public class KeyguardSecurityContainer extends FrameLayout {
// Used to notify the container when something interesting happens.
public interface SecurityCallback {
boolean dismiss(boolean authenticated, int targetUserId, boolean bypassSecondaryLockScreen);
+
void userActivity();
+
void onSecurityModeChanged(SecurityMode securityMode, boolean needsInput);
/**
- * @param strongAuth wheher the user has authenticated with strong authentication like
- * pattern, password or PIN but not by trust agents or fingerprint
+ * @param strongAuth wheher the user has authenticated with strong authentication like
+ * pattern, password or PIN but not by trust agents or fingerprint
* @param targetUserId a user that needs to be the foreground user at the finish completion.
*/
void finish(boolean strongAuth, int targetUserId);
+
void reset();
+
void onCancelClicked();
}
@@ -224,11 +240,119 @@ public class KeyguardSecurityContainer extends FrameLayout {
super(context, attrs, defStyle);
mSpringAnimation = new SpringAnimation(this, DynamicAnimation.Y);
mViewConfiguration = ViewConfiguration.get(context);
+
+ mOrientationEventListener = new OrientationEventListener(context) {
+ @Override
+ public void onOrientationChanged(int orientation) {
+ updateLayoutForSecurityMode(mSecurityMode);
+ }
+ };
}
void onResume(SecurityMode securityMode, boolean faceAuthEnabled) {
+ mSecurityMode = securityMode;
mSecurityViewFlipper.setWindowInsetsAnimationCallback(mWindowInsetsAnimationCallback);
updateBiometricRetry(securityMode, faceAuthEnabled);
+
+ updateLayoutForSecurityMode(securityMode);
+ mOrientationEventListener.enable();
+ }
+
+ void updateLayoutForSecurityMode(SecurityMode securityMode) {
+ mSecurityMode = securityMode;
+ mOneHandedMode = canUseOneHandedBouncer();
+
+ if (mOneHandedMode) {
+ mIsSecurityViewLeftAligned = isOneHandedKeyguardLeftAligned(mContext);
+ }
+
+ updateSecurityViewGravity();
+ updateSecurityViewLocation(false);
+ }
+
+ /** Return whether the one-handed keyguard should be enabled. */
+ private boolean canUseOneHandedBouncer() {
+ // Is it enabled?
+ if (!getResources().getBoolean(
+ com.android.internal.R.bool.config_enableOneHandedKeyguard)) {
+ return false;
+ }
+
+ if (!KeyguardSecurityModel.isSecurityViewOneHanded(mSecurityMode)) {
+ return false;
+ }
+
+ return getResources().getBoolean(R.bool.can_use_one_handed_bouncer);
+ }
+
+ /** Read whether the one-handed keyguard should be on the left/right from settings. */
+ private boolean isOneHandedKeyguardLeftAligned(Context context) {
+ try {
+ return Settings.Global.getInt(context.getContentResolver(),
+ Settings.Global.ONE_HANDED_KEYGUARD_SIDE)
+ == Settings.Global.ONE_HANDED_KEYGUARD_SIDE_LEFT;
+ } catch (Settings.SettingNotFoundException ex) {
+ return true;
+ }
+ }
+
+ private void updateSecurityViewGravity() {
+ View securityView = getChildAt(0);
+
+ if (securityView == null) {
+ return;
+ }
+
+ FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) securityView.getLayoutParams();
+
+ if (mOneHandedMode) {
+ lp.gravity = Gravity.LEFT | Gravity.BOTTOM;
+ } else {
+ lp.gravity = Gravity.CENTER;
+ }
+
+ securityView.setLayoutParams(lp);
+ }
+
+ /**
+ * Moves the inner security view to the correct location (in one handed mode) with animation.
+ * This is triggered when the user taps on the side of the screen that is not currently occupied
+ * by the security view .
+ */
+ private void updateSecurityViewLocation(boolean animate) {
+ View securityView = getChildAt(0);
+
+ if (securityView == null) {
+ return;
+ }
+
+ if (!mOneHandedMode) {
+ securityView.setTranslationX(0);
+ return;
+ }
+
+ if (mRunningOneHandedAnimator != null) {
+ mRunningOneHandedAnimator.cancel();
+ mRunningOneHandedAnimator = null;
+ }
+
+ int targetTranslation = mIsSecurityViewLeftAligned ? 0 : (int) (getMeasuredWidth() / 2f);
+
+ if (animate) {
+ mRunningOneHandedAnimator = securityView.animate().translationX(targetTranslation);
+ mRunningOneHandedAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
+ mRunningOneHandedAnimator.setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mRunningOneHandedAnimator = null;
+ }
+ });
+
+ mRunningOneHandedAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
+ mRunningOneHandedAnimator.start();
+ } else {
+ securityView.setTranslationX(targetTranslation);
+ }
}
public void onPause() {
@@ -237,6 +361,7 @@ public class KeyguardSecurityContainer extends FrameLayout {
mAlertDialog = null;
}
mSecurityViewFlipper.setWindowInsetsAnimationCallback(null);
+ mOrientationEventListener.disable();
}
@Override
@@ -318,19 +443,44 @@ public class KeyguardSecurityContainer extends FrameLayout {
if (mSwipeListener != null) {
mSwipeListener.onSwipeUp();
}
+ } else {
+ if (!mIsDragging) {
+ handleTap(event);
+ }
}
}
return true;
}
+ private void handleTap(MotionEvent event) {
+ // If we're using a fullscreen security mode, skip
+ if (!mOneHandedMode) {
+ return;
+ }
+
+ // Did the tap hit the "other" side of the bouncer?
+ if ((mIsSecurityViewLeftAligned && (event.getX() > getWidth() / 2f))
+ || (!mIsSecurityViewLeftAligned && (event.getX() < getWidth() / 2f))) {
+ mIsSecurityViewLeftAligned = !mIsSecurityViewLeftAligned;
+
+ Settings.Global.putInt(
+ mContext.getContentResolver(),
+ Settings.Global.ONE_HANDED_KEYGUARD_SIDE,
+ mIsSecurityViewLeftAligned ? Settings.Global.ONE_HANDED_KEYGUARD_SIDE_LEFT
+ : Settings.Global.ONE_HANDED_KEYGUARD_SIDE_RIGHT);
+
+ updateSecurityViewLocation(true);
+ }
+ }
+
void setSwipeListener(SwipeListener swipeListener) {
mSwipeListener = swipeListener;
}
private void startSpringAnimation(float startVelocity) {
mSpringAnimation
- .setStartVelocity(startVelocity)
- .animateToFinalPosition(0);
+ .setStartVelocity(startVelocity)
+ .animateToFinalPosition(0);
}
public void startDisappearAnimation(SecurityMode securitySelection) {
@@ -440,18 +590,17 @@ public class KeyguardSecurityContainer extends FrameLayout {
return insets.inset(0, 0, 0, inset);
}
-
private void showDialog(String title, String message) {
if (mAlertDialog != null) {
mAlertDialog.dismiss();
}
mAlertDialog = new AlertDialog.Builder(mContext)
- .setTitle(title)
- .setMessage(message)
- .setCancelable(false)
- .setNeutralButton(R.string.ok, null)
- .create();
+ .setTitle(title)
+ .setMessage(message)
+ .setCancelable(false)
+ .setNeutralButton(R.string.ok, null)
+ .create();
if (!(mContext instanceof Activity)) {
mAlertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG);
}
@@ -489,6 +638,53 @@ public class KeyguardSecurityContainer extends FrameLayout {
}
}
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ // This is a little hacky, but this element only ever has one wrap_content child, and is
+ // itself set to match_parent, so we can take a couple of shortcuts compared to
+ // FrameLayout#onMeasure
+ int maxHeight = 0;
+ int maxWidth = 0;
+ int childState = 0;
+
+ int count = getChildCount();
+
+ int halfWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
+ MeasureSpec.getSize(widthMeasureSpec) / 2,
+ MeasureSpec.getMode(widthMeasureSpec));
+
+ if (count > 1) {
+ throw new IllegalStateException("KeyguardSecurityContainer should only have one child");
+ }
+
+ if (count > 0) {
+ final View securityView = getChildAt(0);
+ if (securityView.getVisibility() != GONE) {
+ if (mOneHandedMode) {
+ measureChildWithMargins(securityView, halfWidthMeasureSpec, 0,
+ heightMeasureSpec, 0);
+ } else {
+ measureChildWithMargins(securityView, widthMeasureSpec, 0,
+ heightMeasureSpec, 0);
+ }
+ final LayoutParams lp = (LayoutParams) securityView.getLayoutParams();
+ maxWidth = Math.max(maxWidth,
+ securityView.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
+ maxHeight = Math.max(maxHeight,
+ securityView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
+ childState = combineMeasuredStates(childState, securityView.getMeasuredState());
+ }
+ }
+
+ // Check against our minimum height and width
+ maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
+ maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
+
+ setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
+ resolveSizeAndState(maxHeight, heightMeasureSpec,
+ childState << MEASURED_HEIGHT_STATE_SHIFT));
+ }
+
void showAlmostAtWipeDialog(int attempts, int remaining, int userType) {
String message = null;
switch (userType) {
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
index 1a8d420fb3945..fdab8db67431f 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
@@ -404,6 +404,7 @@ public class KeyguardSecurityContainerController extends ViewController