Merge "Add owners for controls" into tm-qpr-dev
diff --git a/core/java/android/app/ActivityClient.java b/core/java/android/app/ActivityClient.java
index 324b8e7..0074a0d 100644
--- a/core/java/android/app/ActivityClient.java
+++ b/core/java/android/app/ActivityClient.java
@@ -58,6 +58,15 @@
}
}
+ /** Reports {@link android.app.servertransaction.RefreshCallbackItem} is executed. */
+ public void activityRefreshed(IBinder token) {
+ try {
+ getActivityClientController().activityRefreshed(token);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
/**
* Reports after {@link Activity#onTopResumedActivityChanged(boolean)} is called for losing the
* top most position.
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index ef6c5a6..2beb64d 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -5249,6 +5249,11 @@
}
}
+ @Override
+ public void reportRefresh(ActivityClientRecord r) {
+ ActivityClient.getInstance().activityRefreshed(r.token);
+ }
+
private void handleSetCoreSettings(Bundle coreSettings) {
synchronized (mCoreSettingsLock) {
mCoreSettings = coreSettings;
diff --git a/core/java/android/app/ClientTransactionHandler.java b/core/java/android/app/ClientTransactionHandler.java
index a7566fd..2c70c4e 100644
--- a/core/java/android/app/ClientTransactionHandler.java
+++ b/core/java/android/app/ClientTransactionHandler.java
@@ -140,6 +140,9 @@
/** Restart the activity after it was stopped. */
public abstract void performRestartActivity(@NonNull ActivityClientRecord r, boolean start);
+ /** Report that activity was refreshed to server. */
+ public abstract void reportRefresh(@NonNull ActivityClientRecord r);
+
/** Set pending activity configuration in case it will be updated by other transaction item. */
public abstract void updatePendingActivityConfiguration(@NonNull IBinder token,
Configuration overrideConfig);
diff --git a/core/java/android/app/IActivityClientController.aidl b/core/java/android/app/IActivityClientController.aidl
index 8b655b9..969f975 100644
--- a/core/java/android/app/IActivityClientController.aidl
+++ b/core/java/android/app/IActivityClientController.aidl
@@ -38,6 +38,7 @@
interface IActivityClientController {
oneway void activityIdle(in IBinder token, in Configuration config, in boolean stopProfiling);
oneway void activityResumed(in IBinder token, in boolean handleSplashScreenExit);
+ oneway void activityRefreshed(in IBinder token);
/**
* This call is not one-way because {@link #activityPaused()) is not one-way, or
* the top-resumed-lost could be reported after activity paused.
diff --git a/core/java/android/app/servertransaction/ClientTransactionItem.java b/core/java/android/app/servertransaction/ClientTransactionItem.java
index d94f08b..b159f33 100644
--- a/core/java/android/app/servertransaction/ClientTransactionItem.java
+++ b/core/java/android/app/servertransaction/ClientTransactionItem.java
@@ -38,6 +38,9 @@
return UNDEFINED;
}
+ boolean shouldHaveDefinedPreExecutionState() {
+ return true;
+ }
// Parcelable
diff --git a/core/java/android/app/servertransaction/RefreshCallbackItem.java b/core/java/android/app/servertransaction/RefreshCallbackItem.java
new file mode 100644
index 0000000..74abab2
--- /dev/null
+++ b/core/java/android/app/servertransaction/RefreshCallbackItem.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2022 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 android.app.servertransaction;
+
+import static android.app.servertransaction.ActivityLifecycleItem.LifecycleState;
+import static android.app.servertransaction.ActivityLifecycleItem.ON_PAUSE;
+import static android.app.servertransaction.ActivityLifecycleItem.ON_STOP;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActivityThread.ActivityClientRecord;
+import android.app.ClientTransactionHandler;
+import android.os.IBinder;
+import android.os.Parcel;
+
+/**
+ * Callback that allows to {@link TransactionExecutor#cycleToPath} to {@link ON_PAUSE} or
+ * {@link ON_STOP} in {@link TransactionExecutor#executeCallbacks} for activity "refresh" flow
+ * that goes through "paused -> resumed" or "stopped -> resumed" cycle.
+ *
+ * <p>This is used in combination with {@link com.android.server.wm.DisplayRotationCompatPolicy}
+ * for camera compatibility treatment that handles orientation mismatch between camera buffers and
+ * an app window. This allows to clear cached values in apps (e.g. display or camera rotation) that
+ * influence camera preview and can lead to sideways or stretching issues.
+ *
+ * @hide
+ */
+public class RefreshCallbackItem extends ActivityTransactionItem {
+
+ // Whether refresh should happen using the "stopped -> resumed" cycle or
+ // "paused -> resumed" cycle.
+ @LifecycleState
+ private int mPostExecutionState;
+
+ @Override
+ public void execute(@NonNull ClientTransactionHandler client,
+ @NonNull ActivityClientRecord r, PendingTransactionActions pendingActions) {}
+
+ @Override
+ public void postExecute(ClientTransactionHandler client, IBinder token,
+ PendingTransactionActions pendingActions) {
+ final ActivityClientRecord r = getActivityClientRecord(client, token);
+ client.reportRefresh(r);
+ }
+
+ @Override
+ public int getPostExecutionState() {
+ return mPostExecutionState;
+ }
+
+ @Override
+ boolean shouldHaveDefinedPreExecutionState() {
+ return false;
+ }
+
+ // ObjectPoolItem implementation
+
+ @Override
+ public void recycle() {
+ ObjectPool.recycle(this);
+ }
+
+ /**
+ * Obtain an instance initialized with provided params.
+ * @param postExecutionState indicating whether refresh should happen using the
+ * "stopped -> resumed" cycle or "paused -> resumed" cycle.
+ */
+ public static RefreshCallbackItem obtain(@LifecycleState int postExecutionState) {
+ if (postExecutionState != ON_STOP && postExecutionState != ON_PAUSE) {
+ throw new IllegalArgumentException(
+ "Only ON_STOP or ON_PAUSE are allowed as a post execution state for "
+ + "RefreshCallbackItem but got " + postExecutionState);
+ }
+ RefreshCallbackItem instance =
+ ObjectPool.obtain(RefreshCallbackItem.class);
+ if (instance == null) {
+ instance = new RefreshCallbackItem();
+ }
+ instance.mPostExecutionState = postExecutionState;
+ return instance;
+ }
+
+ private RefreshCallbackItem() {}
+
+ // Parcelable implementation
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mPostExecutionState);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ final RefreshCallbackItem other = (RefreshCallbackItem) o;
+ return mPostExecutionState == other.mPostExecutionState;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + mPostExecutionState;
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "RefreshCallbackItem{mPostExecutionState=" + mPostExecutionState + "}";
+ }
+
+ private RefreshCallbackItem(Parcel in) {
+ mPostExecutionState = in.readInt();
+ }
+
+ public static final @NonNull Creator<RefreshCallbackItem> CREATOR =
+ new Creator<RefreshCallbackItem>() {
+
+ public RefreshCallbackItem createFromParcel(Parcel in) {
+ return new RefreshCallbackItem(in);
+ }
+
+ public RefreshCallbackItem[] newArray(int size) {
+ return new RefreshCallbackItem[size];
+ }
+ };
+}
diff --git a/core/java/android/app/servertransaction/TransactionExecutor.java b/core/java/android/app/servertransaction/TransactionExecutor.java
index de1d38a..1ff0b79 100644
--- a/core/java/android/app/servertransaction/TransactionExecutor.java
+++ b/core/java/android/app/servertransaction/TransactionExecutor.java
@@ -126,10 +126,13 @@
final ClientTransactionItem item = callbacks.get(i);
if (DEBUG_RESOLVER) Slog.d(TAG, tId(transaction) + "Resolving callback: " + item);
final int postExecutionState = item.getPostExecutionState();
- final int closestPreExecutionState = mHelper.getClosestPreExecutionState(r,
- item.getPostExecutionState());
- if (closestPreExecutionState != UNDEFINED) {
- cycleToPath(r, closestPreExecutionState, transaction);
+
+ if (item.shouldHaveDefinedPreExecutionState()) {
+ final int closestPreExecutionState = mHelper.getClosestPreExecutionState(r,
+ item.getPostExecutionState());
+ if (closestPreExecutionState != UNDEFINED) {
+ cycleToPath(r, closestPreExecutionState, transaction);
+ }
}
item.execute(mTransactionHandler, token, mPendingActions);
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 50761bf..a0d6e47 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -5344,6 +5344,12 @@
TODO(b/255532890) Enable when ignoreOrientationRequest is set -->
<bool name="config_letterboxIsEnabledForTranslucentActivities">false</bool>
+ <!-- Whether camera compat treatment is enabled for issues caused by orientation mismatch
+ between camera buffers and an app window. This includes force rotation of fixed
+ orientation activities connected to the camera in fullscreen and showing a tooltip in
+ split screen. -->
+ <bool name="config_isWindowManagerCameraCompatTreatmentEnabled">false</bool>
+
<!-- Whether a camera compat controller is enabled to allow the user to apply or revert
treatment for stretched issues in camera viewfinder. -->
<bool name="config_isCameraCompatControlForStretchedIssuesEnabled">false</bool>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index fb77b3b..371dbfb 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -4460,6 +4460,7 @@
<java-symbol type="bool" name="config_letterboxIsEducationEnabled" />
<java-symbol type="dimen" name="config_letterboxDefaultMinAspectRatioForUnresizableApps" />
<java-symbol type="bool" name="config_letterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled" />
+ <java-symbol type="bool" name="config_isWindowManagerCameraCompatTreatmentEnabled" />
<java-symbol type="bool" name="config_isCameraCompatControlForStretchedIssuesEnabled" />
<java-symbol type="bool" name="config_hideDisplayCutoutWithDisplayArea" />
diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index 49704d9..f47d9c6 100644
--- a/data/etc/services.core.protolog.json
+++ b/data/etc/services.core.protolog.json
@@ -295,6 +295,12 @@
"group": "WM_DEBUG_IME",
"at": "com\/android\/server\/wm\/DisplayContent.java"
},
+ "-1812743677": {
+ "message": "Display id=%d is ignoring all orientation requests, camera is active and the top activity is eligible for force rotation, return %s,portrait activity: %b, is natural orientation portrait: %b.",
+ "level": "VERBOSE",
+ "group": "WM_DEBUG_ORIENTATION",
+ "at": "com\/android\/server\/wm\/DisplayRotationCompatPolicy.java"
+ },
"-1810446914": {
"message": "Trying to update display configuration for system\/invalid process.",
"level": "WARN",
@@ -481,6 +487,12 @@
"group": "WM_DEBUG_ORIENTATION",
"at": "com\/android\/server\/wm\/WindowManagerService.java"
},
+ "-1631991057": {
+ "message": "Display id=%d is notified that Camera %s is closed but activity is still refreshing. Rescheduling an update.",
+ "level": "VERBOSE",
+ "group": "WM_DEBUG_ORIENTATION",
+ "at": "com\/android\/server\/wm\/DisplayRotationCompatPolicy.java"
+ },
"-1630752478": {
"message": "removeLockedTask: removed %s",
"level": "DEBUG",
@@ -619,6 +631,12 @@
"group": "WM_DEBUG_WINDOW_INSETS",
"at": "com\/android\/server\/wm\/InsetsSourceProvider.java"
},
+ "-1480918485": {
+ "message": "Refreshed activity: %s",
+ "level": "INFO",
+ "group": "WM_DEBUG_STATES",
+ "at": "com\/android\/server\/wm\/ActivityRecord.java"
+ },
"-1480772131": {
"message": "No app or window is requesting an orientation, return %d for display id=%d",
"level": "VERBOSE",
@@ -1321,6 +1339,12 @@
"group": "WM_DEBUG_CONFIGURATION",
"at": "com\/android\/server\/wm\/ActivityRecord.java"
},
+ "-799396645": {
+ "message": "Display id=%d is notified that Camera %s is closed, updating rotation.",
+ "level": "VERBOSE",
+ "group": "WM_DEBUG_ORIENTATION",
+ "at": "com\/android\/server\/wm\/DisplayRotationCompatPolicy.java"
+ },
"-799003045": {
"message": "Set animatingExit: reason=remove\/replaceWindow win=%s",
"level": "VERBOSE",
@@ -1543,6 +1567,12 @@
"group": "WM_DEBUG_SCREEN_ON",
"at": "com\/android\/server\/wm\/DisplayContent.java"
},
+ "-627759820": {
+ "message": "Display id=%d is notified that Camera %s is open for package %s",
+ "level": "VERBOSE",
+ "group": "WM_DEBUG_ORIENTATION",
+ "at": "com\/android\/server\/wm\/DisplayRotationCompatPolicy.java"
+ },
"-622997754": {
"message": "postWindowRemoveCleanupLocked: %s",
"level": "VERBOSE",
@@ -2101,6 +2131,12 @@
"group": "WM_SHOW_TRANSACTIONS",
"at": "com\/android\/server\/wm\/Session.java"
},
+ "-81260230": {
+ "message": "Display id=%d is notified that Camera %s is closed, scheduling rotation update.",
+ "level": "VERBOSE",
+ "group": "WM_DEBUG_ORIENTATION",
+ "at": "com\/android\/server\/wm\/DisplayRotationCompatPolicy.java"
+ },
"-81121442": {
"message": "ImeContainer just became organized but it doesn't have a parent or the parent doesn't have a surface control. mSurfaceControl=%s imeParentSurfaceControl=%s",
"level": "ERROR",
@@ -4201,6 +4237,12 @@
"group": "WM_DEBUG_REMOTE_ANIMATIONS",
"at": "com\/android\/server\/wm\/RemoteAnimationController.java"
},
+ "1967643923": {
+ "message": "Refershing activity for camera compatibility treatment, activityRecord=%s",
+ "level": "VERBOSE",
+ "group": "WM_DEBUG_STATES",
+ "at": "com\/android\/server\/wm\/DisplayRotationCompatPolicy.java"
+ },
"1967975839": {
"message": "Changing app %s visible=%b performLayout=%b",
"level": "VERBOSE",
diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt
index f55fb97..9058510 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt
@@ -169,11 +169,9 @@
setFloatUniform("in_progress", value)
val curvedProg = 1 - (1 - value) * (1 - value) * (1 - value)
- setFloatUniform(
- "in_size",
- /* width= */ maxSize.x * curvedProg,
- /* height= */ maxSize.y * curvedProg
- )
+ currentWidth = maxSize.x * curvedProg
+ currentHeight = maxSize.y * curvedProg
+ setFloatUniform("in_size", /* width= */ currentWidth, /* height= */ currentHeight)
setFloatUniform("in_thickness", maxSize.y * curvedProg * 0.5f)
// radius should not exceed width and height values.
setFloatUniform("in_cornerRadius", Math.min(maxSize.x, maxSize.y) * curvedProg)
@@ -237,4 +235,10 @@
* False for a ring effect.
*/
var rippleFill: Boolean = false
+
+ var currentWidth: Float = 0f
+ private set
+
+ var currentHeight: Float = 0f
+ private set
}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt
index ae28a8b..b37c734 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleView.kt
@@ -36,7 +36,7 @@
*/
open class RippleView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
- private lateinit var rippleShader: RippleShader
+ protected lateinit var rippleShader: RippleShader
lateinit var rippleShape: RippleShape
private set
diff --git a/packages/SystemUI/res/layout/screenshot_static.xml b/packages/SystemUI/res/layout/screenshot_static.xml
index efd683f..e4e0bd4 100644
--- a/packages/SystemUI/res/layout/screenshot_static.xml
+++ b/packages/SystemUI/res/layout/screenshot_static.xml
@@ -190,7 +190,6 @@
app:layout_constraintBottom_toBottomOf="parent"
android:contentDescription="@string/screenshot_dismiss_work_profile">
<ImageView
- android:id="@+id/screenshot_dismiss_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="@dimen/overlay_dismiss_button_margin"
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 87d2c51..5848237 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1272,6 +1272,12 @@
translate into their final position. -->
<dimen name="lockscreen_shade_keyguard_transition_distance">@dimen/lockscreen_shade_media_transition_distance</dimen>
+ <!-- DREAMING -> LOCKSCREEN transition: Amount to shift lockscreen content on entering -->
+ <dimen name="dreaming_to_lockscreen_transition_lockscreen_translation_y">40dp</dimen>
+
+ <!-- OCCLUDED -> LOCKSCREEN transition: Amount to shift lockscreen content on entering -->
+ <dimen name="occluded_to_lockscreen_transition_lockscreen_translation_y">40dp</dimen>
+
<!-- The amount of vertical offset for the keyguard during the full shade transition. -->
<dimen name="lockscreen_shade_keyguard_transition_vertical_offset">0dp</dimen>
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java
index 8b9823b..b8e196f 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java
@@ -16,6 +16,8 @@
package com.android.keyguard;
+import static java.util.Collections.emptySet;
+
import android.content.Context;
import android.os.Trace;
import android.util.AttributeSet;
@@ -88,8 +90,9 @@
}
/** Sets a translationY value on every child view except for the media view. */
- public void setChildrenTranslationYExcludingMediaView(float translationY) {
- setChildrenTranslationYExcluding(translationY, Set.of(mMediaHostContainer));
+ public void setChildrenTranslationY(float translationY, boolean excludeMedia) {
+ setChildrenTranslationYExcluding(translationY,
+ excludeMedia ? Set.of(mMediaHostContainer) : emptySet());
}
/** Sets a translationY value on every view except for the views in the provided set. */
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
index 7849747..aec3063 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
@@ -20,6 +20,8 @@
import android.util.Slog;
import com.android.keyguard.KeyguardClockSwitch.ClockSize;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
import com.android.systemui.plugins.ClockAnimations;
import com.android.systemui.statusbar.notification.AnimatableProperty;
import com.android.systemui.statusbar.notification.PropertyAnimator;
@@ -59,6 +61,7 @@
KeyguardUpdateMonitor keyguardUpdateMonitor,
ConfigurationController configurationController,
DozeParameters dozeParameters,
+ FeatureFlags featureFlags,
ScreenOffAnimationController screenOffAnimationController) {
super(keyguardStatusView);
mKeyguardSliceViewController = keyguardSliceViewController;
@@ -67,6 +70,8 @@
mConfigurationController = configurationController;
mKeyguardVisibilityHelper = new KeyguardVisibilityHelper(mView, keyguardStateController,
dozeParameters, screenOffAnimationController, /* animateYPos= */ true);
+ mKeyguardVisibilityHelper.setOcclusionTransitionFlagEnabled(
+ featureFlags.isEnabled(Flags.UNOCCLUSION_TRANSITION));
}
@Override
@@ -115,8 +120,8 @@
/**
* Sets a translationY on the views on the keyguard, except on the media view.
*/
- public void setTranslationYExcludingMedia(float translationY) {
- mView.setChildrenTranslationYExcludingMediaView(translationY);
+ public void setTranslationY(float translationY, boolean excludeMedia) {
+ mView.setChildrenTranslationY(translationY, excludeMedia);
}
/**
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUnfoldTransition.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardUnfoldTransition.kt
index f974e27..edd150c 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUnfoldTransition.kt
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUnfoldTransition.kt
@@ -19,6 +19,8 @@
import android.content.Context
import android.view.ViewGroup
import com.android.systemui.R
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.statusbar.StatusBarState.KEYGUARD
import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator
import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator.Direction.END
import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator.Direction.START
@@ -36,27 +38,29 @@
@Inject
constructor(
private val context: Context,
- unfoldProgressProvider: NaturalRotationUnfoldProgressProvider
+ statusBarStateController: StatusBarStateController,
+ unfoldProgressProvider: NaturalRotationUnfoldProgressProvider,
) {
/** Certain views only need to move if they are not currently centered */
var statusViewCentered = false
- private val filterSplitShadeOnly = { !statusViewCentered }
- private val filterNever = { true }
+ private val filterKeyguardAndSplitShadeOnly: () -> Boolean = {
+ statusBarStateController.getState() == KEYGUARD && !statusViewCentered }
+ private val filterKeyguard: () -> Boolean = { statusBarStateController.getState() == KEYGUARD }
private val translateAnimator by lazy {
UnfoldConstantTranslateAnimator(
viewsIdToTranslate =
setOf(
- ViewIdToTranslate(R.id.keyguard_status_area, START, filterNever),
+ ViewIdToTranslate(R.id.keyguard_status_area, START, filterKeyguard),
ViewIdToTranslate(
- R.id.lockscreen_clock_view_large, START, filterSplitShadeOnly),
- ViewIdToTranslate(R.id.lockscreen_clock_view, START, filterNever),
+ R.id.lockscreen_clock_view_large, START, filterKeyguardAndSplitShadeOnly),
+ ViewIdToTranslate(R.id.lockscreen_clock_view, START, filterKeyguard),
ViewIdToTranslate(
- R.id.notification_stack_scroller, END, filterSplitShadeOnly),
- ViewIdToTranslate(R.id.start_button, START, filterNever),
- ViewIdToTranslate(R.id.end_button, END, filterNever)),
+ R.id.notification_stack_scroller, END, filterKeyguardAndSplitShadeOnly),
+ ViewIdToTranslate(R.id.start_button, START, filterKeyguard),
+ ViewIdToTranslate(R.id.end_button, END, filterKeyguard)),
progressProvider = unfoldProgressProvider)
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java b/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java
index 498304b..bde0692 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java
@@ -44,6 +44,7 @@
private boolean mAnimateYPos;
private boolean mKeyguardViewVisibilityAnimating;
private boolean mLastOccludedState = false;
+ private boolean mIsUnoccludeTransitionFlagEnabled = false;
private final AnimationProperties mAnimationProperties = new AnimationProperties();
public KeyguardVisibilityHelper(View view,
@@ -62,6 +63,10 @@
return mKeyguardViewVisibilityAnimating;
}
+ public void setOcclusionTransitionFlagEnabled(boolean enabled) {
+ mIsUnoccludeTransitionFlagEnabled = enabled;
+ }
+
/**
* Set the visibility of a keyguard view based on some new state.
*/
@@ -129,7 +134,7 @@
// since it may need to be cancelled due to keyguard lifecycle events.
mScreenOffAnimationController.animateInKeyguard(
mView, mAnimateKeyguardStatusViewVisibleEndRunnable);
- } else if (mLastOccludedState && !isOccluded) {
+ } else if (!mIsUnoccludeTransitionFlagEnabled && mLastOccludedState && !isOccluded) {
// An activity was displayed over the lock screen, and has now gone away
mView.setVisibility(View.VISIBLE);
mView.setAlpha(0f);
@@ -142,7 +147,9 @@
.start();
} else {
mView.setVisibility(View.VISIBLE);
- mView.setAlpha(1f);
+ if (!mIsUnoccludeTransitionFlagEnabled) {
+ mView.setAlpha(1f);
+ }
}
} else {
mView.setVisibility(View.GONE);
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
index 833ff3f..37183e8 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
@@ -22,6 +22,7 @@
import static com.android.systemui.plugins.SensorManagerPlugin.Sensor.TYPE_WAKE_LOCK_SCREEN;
import android.annotation.AnyThread;
+import android.content.res.Resources;
import android.database.ContentObserver;
import android.hardware.Sensor;
import android.hardware.SensorManager;
@@ -40,6 +41,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
+import com.android.internal.R;
import com.android.internal.logging.UiEvent;
import com.android.internal.logging.UiEventLogger;
import com.android.internal.logging.UiEventLoggerImpl;
@@ -140,6 +142,7 @@
}
DozeSensors(
+ Resources resources,
AsyncSensorManager sensorManager,
DozeParameters dozeParameters,
AmbientDisplayConfiguration config,
@@ -185,7 +188,8 @@
new TriggerSensor(
mSensorManager.getDefaultSensor(Sensor.TYPE_PICK_UP_GESTURE),
Settings.Secure.DOZE_PICK_UP_GESTURE,
- true /* settingDef */,
+ resources.getBoolean(
+ R.bool.config_dozePickupGestureEnabled) /* settingDef */,
config.dozePickupSensorAvailable(),
DozeLog.REASON_SENSOR_PICKUP, false /* touchCoords */,
false /* touchscreen */,
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
index 3f9f14c..b95c3f3 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
@@ -210,7 +210,7 @@
mAllowPulseTriggers = true;
mSessionTracker = sessionTracker;
- mDozeSensors = new DozeSensors(mSensorManager, dozeParameters,
+ mDozeSensors = new DozeSensors(mContext.getResources(), mSensorManager, dozeParameters,
config, wakeLock, this::onSensor, this::onProximityFar, dozeLog, proximitySensor,
secureSettings, authController, devicePostureController, userTracker);
mDockManager = dockManager;
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamCallbackController.kt b/packages/SystemUI/src/com/android/systemui/dreams/DreamCallbackController.kt
deleted file mode 100644
index ab4632b..0000000
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamCallbackController.kt
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright (C) 2022 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.dreams
-
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.statusbar.policy.CallbackController
-import javax.inject.Inject
-
-/** Dream-related callback information */
-@SysUISingleton
-class DreamCallbackController @Inject constructor() :
- CallbackController<DreamCallbackController.DreamCallback> {
-
- private val callbacks = mutableSetOf<DreamCallbackController.DreamCallback>()
-
- override fun addCallback(callback: DreamCallbackController.DreamCallback) {
- callbacks.add(callback)
- }
-
- override fun removeCallback(callback: DreamCallbackController.DreamCallback) {
- callbacks.remove(callback)
- }
-
- fun onWakeUp() {
- callbacks.forEach { it.onWakeUp() }
- }
-
- interface DreamCallback {
- fun onWakeUp()
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt
index abe9355..c882f8a 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt
@@ -101,13 +101,11 @@
transitionViewModel.dreamOverlayTranslationY(it.translationYPx)
}
.collect { px ->
- setElementsTranslationYAtPosition(
- px,
- ComplicationLayoutParams.POSITION_TOP
- )
- setElementsTranslationYAtPosition(
- px,
- ComplicationLayoutParams.POSITION_BOTTOM
+ ComplicationLayoutParams.iteratePositions(
+ { position: Int ->
+ setElementsTranslationYAtPosition(px, position)
+ },
+ POSITION_TOP or POSITION_BOTTOM
)
}
}
@@ -115,15 +113,15 @@
/* Alpha animations, when moving from DREAMING->LOCKSCREEN state */
launch {
transitionViewModel.dreamOverlayAlpha.collect { alpha ->
- setElementsAlphaAtPosition(
- alpha = alpha,
- position = ComplicationLayoutParams.POSITION_TOP,
- fadingOut = true,
- )
- setElementsAlphaAtPosition(
- alpha = alpha,
- position = ComplicationLayoutParams.POSITION_BOTTOM,
- fadingOut = true,
+ ComplicationLayoutParams.iteratePositions(
+ { position: Int ->
+ setElementsAlphaAtPosition(
+ alpha = alpha,
+ position = position,
+ fadingOut = true,
+ )
+ },
+ POSITION_TOP or POSITION_BOTTOM
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayCallbackController.kt b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayCallbackController.kt
new file mode 100644
index 0000000..d5ff8f2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayCallbackController.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2022 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.dreams
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.policy.CallbackController
+import javax.inject.Inject
+
+/** Dream overlay-related callback information */
+@SysUISingleton
+class DreamOverlayCallbackController @Inject constructor() :
+ CallbackController<DreamOverlayCallbackController.Callback> {
+
+ private val callbacks = mutableSetOf<DreamOverlayCallbackController.Callback>()
+
+ var isDreaming = false
+ private set
+
+ override fun addCallback(callback: DreamOverlayCallbackController.Callback) {
+ callbacks.add(callback)
+ }
+
+ override fun removeCallback(callback: DreamOverlayCallbackController.Callback) {
+ callbacks.remove(callback)
+ }
+
+ fun onWakeUp() {
+ isDreaming = false
+ callbacks.forEach { it.onWakeUp() }
+ }
+
+ fun onStartDream() {
+ isDreaming = true
+ callbacks.forEach { it.onStartDream() }
+ }
+
+ interface Callback {
+ /** Dream overlay has ended */
+ fun onWakeUp()
+
+ /** Dream overlay has started */
+ fun onStartDream()
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
index 1be9cd1..eedef65 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
@@ -64,7 +64,7 @@
// A controller for the dream overlay container view (which contains both the status bar and the
// content area).
private DreamOverlayContainerViewController mDreamOverlayContainerViewController;
- private final DreamCallbackController mDreamCallbackController;
+ private final DreamOverlayCallbackController mDreamOverlayCallbackController;
private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
@Nullable
private final ComponentName mLowLightDreamComponent;
@@ -134,7 +134,7 @@
UiEventLogger uiEventLogger,
@Nullable @Named(LowLightDreamModule.LOW_LIGHT_DREAM_COMPONENT)
ComponentName lowLightDreamComponent,
- DreamCallbackController dreamCallbackController) {
+ DreamOverlayCallbackController dreamOverlayCallbackController) {
mContext = context;
mExecutor = executor;
mWindowManager = windowManager;
@@ -143,7 +143,7 @@
mKeyguardUpdateMonitor.registerCallback(mKeyguardCallback);
mStateController = stateController;
mUiEventLogger = uiEventLogger;
- mDreamCallbackController = dreamCallbackController;
+ mDreamOverlayCallbackController = dreamOverlayCallbackController;
final ViewModelStore viewModelStore = new ViewModelStore();
final Complication.Host host =
@@ -203,6 +203,7 @@
dreamComponent != null && dreamComponent.equals(mLowLightDreamComponent));
mUiEventLogger.log(DreamOverlayEvent.DREAM_OVERLAY_COMPLETE_START);
+ mDreamOverlayCallbackController.onStartDream();
mStarted = true;
});
}
@@ -219,7 +220,7 @@
public void onWakeUp(@NonNull Runnable onCompletedCallback) {
mExecutor.execute(() -> {
if (mDreamOverlayContainerViewController != null) {
- mDreamCallbackController.onWakeUp();
+ mDreamOverlayCallbackController.onWakeUp();
mDreamOverlayContainerViewController.wakeUp(onCompletedCallback, mExecutor);
}
});
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index 339fd13..1f80b5a 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -181,10 +181,6 @@
@JvmField
val LIGHT_REVEAL_MIGRATION = unreleasedFlag(218, "light_reveal_migration", teamfood = false)
- // TODO(b/262780002): Tracking Bug
- @JvmField
- val REVAMPED_WALLPAPER_UI = unreleasedFlag(222, "revamped_wallpaper_ui", teamfood = false)
-
/** Flag to control the migration of face auth to modern architecture. */
// TODO(b/262838215): Tracking bug
@JvmField val FACE_AUTH_REFACTOR = unreleasedFlag(220, "face_auth_refactor")
@@ -193,6 +189,15 @@
// TODO(b/244313043): Tracking bug
@JvmField val BIOMETRICS_ANIMATION_REVAMP = unreleasedFlag(221, "biometrics_animation_revamp")
+ // TODO(b/262780002): Tracking Bug
+ @JvmField
+ val REVAMPED_WALLPAPER_UI = unreleasedFlag(222, "revamped_wallpaper_ui", teamfood = false)
+
+ /** A different path for unocclusion transitions back to keyguard */
+ // TODO(b/262859270): Tracking Bug
+ @JvmField
+ val UNOCCLUSION_TRANSITION = unreleasedFlag(223, "unocclusion_transition", teamfood = false)
+
// 300 - power menu
// TODO(b/254512600): Tracking Bug
@JvmField val POWER_MENU_LITE = releasedFlag(300, "power_menu_lite")
@@ -310,6 +315,10 @@
// TODO(b/261734857): Tracking Bug
@JvmField val UMO_TURBULENCE_NOISE = unreleasedFlag(909, "umo_turbulence_noise")
+ // TODO(b/263272731): Tracking Bug
+ val MEDIA_TTT_RECEIVER_SUCCESS_RIPPLE =
+ unreleasedFlag(910, "media_ttt_receiver_success_ripple", teamfood = true)
+
// 1000 - dock
val SIMULATE_DOCK_THROUGH_CHARGING = releasedFlag(1000, "simulate_dock_through_charging")
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index ae714fb..8aada1f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -33,7 +33,7 @@
import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT;
import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE;
import static com.android.systemui.DejankUtils.whitelistIpcs;
-import static com.android.systemui.keyguard.domain.interactor.DreamingTransitionInteractor.TO_LOCKSCREEN_DURATION_MS;
+import static com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.LOCKSCREEN_ANIMATION_DURATION_MS;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
@@ -1232,7 +1232,7 @@
mDreamOpenAnimationDuration = context.getResources().getInteger(
com.android.internal.R.integer.config_dreamOpenAnimationDuration);
- mDreamCloseAnimationDuration = (int) TO_LOCKSCREEN_DURATION_MS;
+ mDreamCloseAnimationDuration = (int) LOCKSCREEN_ANIMATION_DURATION_MS;
}
public void userActivity() {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
index 9a0fbbf..a4fd087 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
@@ -29,8 +29,7 @@
import com.android.systemui.doze.DozeMachine
import com.android.systemui.doze.DozeTransitionCallback
import com.android.systemui.doze.DozeTransitionListener
-import com.android.systemui.dreams.DreamCallbackController
-import com.android.systemui.dreams.DreamCallbackController.DreamCallback
+import com.android.systemui.dreams.DreamOverlayCallbackController
import com.android.systemui.keyguard.WakefulnessLifecycle
import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
import com.android.systemui.keyguard.shared.model.BiometricUnlockSource
@@ -49,7 +48,6 @@
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.merge
/** Defines interface for classes that encapsulate application state for the keyguard. */
interface KeyguardRepository {
@@ -81,6 +79,9 @@
*/
val isKeyguardShowing: Flow<Boolean>
+ /** Is an activity showing over the keyguard? */
+ val isKeyguardOccluded: Flow<Boolean>
+
/** Observable for the signal that keyguard is about to go away. */
val isKeyguardGoingAway: Flow<Boolean>
@@ -107,6 +108,9 @@
*/
val isDreaming: Flow<Boolean>
+ /** Observable for whether the device is dreaming with an overlay, see [DreamOverlayService] */
+ val isDreamingWithOverlay: Flow<Boolean>
+
/**
* Observable for the amount of doze we are currently in.
*
@@ -179,7 +183,7 @@
private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
private val dozeTransitionListener: DozeTransitionListener,
private val authController: AuthController,
- private val dreamCallbackController: DreamCallbackController,
+ private val dreamOverlayCallbackController: DreamOverlayCallbackController,
) : KeyguardRepository {
private val _animateBottomAreaDozingTransitions = MutableStateFlow(false)
override val animateBottomAreaDozingTransitions =
@@ -191,28 +195,55 @@
private val _clockPosition = MutableStateFlow(Position(0, 0))
override val clockPosition = _clockPosition.asStateFlow()
- override val isKeyguardShowing: Flow<Boolean> = conflatedCallbackFlow {
- val callback =
- object : KeyguardStateController.Callback {
- override fun onKeyguardShowingChanged() {
- trySendWithFailureLogging(
- keyguardStateController.isShowing,
- TAG,
- "updated isKeyguardShowing"
- )
- }
+ override val isKeyguardShowing: Flow<Boolean> =
+ conflatedCallbackFlow {
+ val callback =
+ object : KeyguardStateController.Callback {
+ override fun onKeyguardShowingChanged() {
+ trySendWithFailureLogging(
+ keyguardStateController.isShowing,
+ TAG,
+ "updated isKeyguardShowing"
+ )
+ }
+ }
+
+ keyguardStateController.addCallback(callback)
+ // Adding the callback does not send an initial update.
+ trySendWithFailureLogging(
+ keyguardStateController.isShowing,
+ TAG,
+ "initial isKeyguardShowing"
+ )
+
+ awaitClose { keyguardStateController.removeCallback(callback) }
}
+ .distinctUntilChanged()
- keyguardStateController.addCallback(callback)
- // Adding the callback does not send an initial update.
- trySendWithFailureLogging(
- keyguardStateController.isShowing,
- TAG,
- "initial isKeyguardShowing"
- )
+ override val isKeyguardOccluded: Flow<Boolean> =
+ conflatedCallbackFlow {
+ val callback =
+ object : KeyguardStateController.Callback {
+ override fun onKeyguardShowingChanged() {
+ trySendWithFailureLogging(
+ keyguardStateController.isOccluded,
+ TAG,
+ "updated isKeyguardOccluded"
+ )
+ }
+ }
- awaitClose { keyguardStateController.removeCallback(callback) }
- }
+ keyguardStateController.addCallback(callback)
+ // Adding the callback does not send an initial update.
+ trySendWithFailureLogging(
+ keyguardStateController.isOccluded,
+ TAG,
+ "initial isKeyguardOccluded"
+ )
+
+ awaitClose { keyguardStateController.removeCallback(callback) }
+ }
+ .distinctUntilChanged()
override val isKeyguardGoingAway: Flow<Boolean> = conflatedCallbackFlow {
val callback =
@@ -279,36 +310,45 @@
}
.distinctUntilChanged()
+ override val isDreamingWithOverlay: Flow<Boolean> =
+ conflatedCallbackFlow {
+ val callback =
+ object : DreamOverlayCallbackController.Callback {
+ override fun onStartDream() {
+ trySendWithFailureLogging(true, TAG, "updated isDreamingWithOverlay")
+ }
+ override fun onWakeUp() {
+ trySendWithFailureLogging(false, TAG, "updated isDreamingWithOverlay")
+ }
+ }
+ dreamOverlayCallbackController.addCallback(callback)
+ trySendWithFailureLogging(
+ dreamOverlayCallbackController.isDreaming,
+ TAG,
+ "initial isDreamingWithOverlay",
+ )
+
+ awaitClose { dreamOverlayCallbackController.removeCallback(callback) }
+ }
+ .distinctUntilChanged()
+
override val isDreaming: Flow<Boolean> =
- merge(
- conflatedCallbackFlow {
- val callback =
- object : KeyguardUpdateMonitorCallback() {
- override fun onDreamingStateChanged(isDreaming: Boolean) {
- trySendWithFailureLogging(isDreaming, TAG, "updated isDreaming")
- }
+ conflatedCallbackFlow {
+ val callback =
+ object : KeyguardUpdateMonitorCallback() {
+ override fun onDreamingStateChanged(isDreaming: Boolean) {
+ trySendWithFailureLogging(isDreaming, TAG, "updated isDreaming")
}
- keyguardUpdateMonitor.registerCallback(callback)
- trySendWithFailureLogging(
- keyguardUpdateMonitor.isDreaming,
- TAG,
- "initial isDreaming",
- )
+ }
+ keyguardUpdateMonitor.registerCallback(callback)
+ trySendWithFailureLogging(
+ keyguardUpdateMonitor.isDreaming,
+ TAG,
+ "initial isDreaming",
+ )
- awaitClose { keyguardUpdateMonitor.removeCallback(callback) }
- },
- conflatedCallbackFlow {
- val callback =
- object : DreamCallback {
- override fun onWakeUp() {
- trySendWithFailureLogging(false, TAG, "updated isDreaming")
- }
- }
- dreamCallbackController.addCallback(callback)
-
- awaitClose { dreamCallbackController.removeCallback(callback) }
- }
- )
+ awaitClose { keyguardUpdateMonitor.removeCallback(callback) }
+ }
.distinctUntilChanged()
override val linearDozeAmount: Flow<Float> = conflatedCallbackFlow {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
index d72d7183b..343c2dc 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
@@ -131,6 +131,10 @@
}
override fun startTransition(info: TransitionInfo): UUID? {
+ if (lastStep.from == info.from && lastStep.to == info.to) {
+ Log.i(TAG, "Duplicate call to start the transition, rejecting: $info")
+ return null
+ }
if (lastStep.transitionState != TransitionState.FINISHED) {
Log.i(TAG, "Transition still active: $lastStep, canceling")
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodToGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodToGoneTransitionInteractor.kt
deleted file mode 100644
index dad166f..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodToGoneTransitionInteractor.kt
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright (C) 2022 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.keyguard.domain.interactor
-
-import android.animation.ValueAnimator
-import com.android.systemui.animation.Interpolators
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
-import com.android.systemui.keyguard.shared.model.BiometricUnlockModel.Companion.isWakeAndUnlock
-import com.android.systemui.keyguard.shared.model.KeyguardState
-import com.android.systemui.keyguard.shared.model.TransitionInfo
-import com.android.systemui.util.kotlin.sample
-import javax.inject.Inject
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.collect
-import kotlinx.coroutines.launch
-
-@SysUISingleton
-class AodToGoneTransitionInteractor
-@Inject
-constructor(
- @Application private val scope: CoroutineScope,
- private val keyguardInteractor: KeyguardInteractor,
- private val keyguardTransitionRepository: KeyguardTransitionRepository,
- private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
-) : TransitionInteractor(AodToGoneTransitionInteractor::class.simpleName!!) {
-
- override fun start() {
- scope.launch {
- keyguardInteractor.biometricUnlockState
- .sample(keyguardTransitionInteractor.finishedKeyguardState, { a, b -> Pair(a, b) })
- .collect { pair ->
- val (biometricUnlockState, keyguardState) = pair
- if (
- keyguardState == KeyguardState.AOD && isWakeAndUnlock(biometricUnlockState)
- ) {
- keyguardTransitionRepository.startTransition(
- TransitionInfo(
- name,
- KeyguardState.AOD,
- KeyguardState.GONE,
- getAnimator(),
- )
- )
- }
- }
- }
- }
-
- private fun getAnimator(): ValueAnimator {
- return ValueAnimator().apply {
- setInterpolator(Interpolators.LINEAR)
- setDuration(TRANSITION_DURATION_MS)
- }
- }
-
- companion object {
- private const val TRANSITION_DURATION_MS = 500L
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
similarity index 76%
rename from packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt
rename to packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
index f3d2905..c2d139c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
@@ -21,6 +21,7 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.BiometricUnlockModel.Companion.isWakeAndUnlock
import com.android.systemui.keyguard.shared.model.DozeStateModel
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionInfo
@@ -30,33 +31,33 @@
import kotlinx.coroutines.launch
@SysUISingleton
-class AodLockscreenTransitionInteractor
+class FromAodTransitionInteractor
@Inject
constructor(
@Application private val scope: CoroutineScope,
private val keyguardInteractor: KeyguardInteractor,
private val keyguardTransitionRepository: KeyguardTransitionRepository,
private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
-) : TransitionInteractor(AodLockscreenTransitionInteractor::class.simpleName!!) {
+) : TransitionInteractor(FromAodTransitionInteractor::class.simpleName!!) {
override fun start() {
- listenForTransitionToAodFromLockscreen()
- listenForTransitionToLockscreenFromDozeStates()
+ listenForAodToLockscreen()
+ listenForAodToGone()
}
- private fun listenForTransitionToAodFromLockscreen() {
+ private fun listenForAodToLockscreen() {
scope.launch {
keyguardInteractor
- .dozeTransitionTo(DozeStateModel.DOZE_AOD)
+ .dozeTransitionTo(DozeStateModel.FINISH)
.sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
.collect { pair ->
val (dozeToAod, lastStartedStep) = pair
- if (lastStartedStep.to == KeyguardState.LOCKSCREEN) {
+ if (lastStartedStep.to == KeyguardState.AOD) {
keyguardTransitionRepository.startTransition(
TransitionInfo(
name,
- KeyguardState.LOCKSCREEN,
KeyguardState.AOD,
+ KeyguardState.LOCKSCREEN,
getAnimator(),
)
)
@@ -65,20 +66,20 @@
}
}
- private fun listenForTransitionToLockscreenFromDozeStates() {
- val canGoToLockscreen = setOf(KeyguardState.AOD, KeyguardState.DOZING)
+ private fun listenForAodToGone() {
scope.launch {
- keyguardInteractor
- .dozeTransitionTo(DozeStateModel.FINISH)
- .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
+ keyguardInteractor.biometricUnlockState
+ .sample(keyguardTransitionInteractor.finishedKeyguardState, ::Pair)
.collect { pair ->
- val (dozeToAod, lastStartedStep) = pair
- if (canGoToLockscreen.contains(lastStartedStep.to)) {
+ val (biometricUnlockState, keyguardState) = pair
+ if (
+ keyguardState == KeyguardState.AOD && isWakeAndUnlock(biometricUnlockState)
+ ) {
keyguardTransitionRepository.startTransition(
TransitionInfo(
name,
- lastStartedStep.to,
- KeyguardState.LOCKSCREEN,
+ KeyguardState.AOD,
+ KeyguardState.GONE,
getAnimator(),
)
)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerToGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromBouncerTransitionInteractor.kt
similarity index 60%
rename from packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerToGoneTransitionInteractor.kt
rename to packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromBouncerTransitionInteractor.kt
index 056c44d..0e9c447 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerToGoneTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromBouncerTransitionInteractor.kt
@@ -23,16 +23,18 @@
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionInfo
+import com.android.systemui.keyguard.shared.model.WakefulnessState
import com.android.systemui.shade.data.repository.ShadeRepository
import com.android.systemui.util.kotlin.sample
import java.util.UUID
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
@SysUISingleton
-class BouncerToGoneTransitionInteractor
+class FromBouncerTransitionInteractor
@Inject
constructor(
@Application private val scope: CoroutineScope,
@@ -40,15 +42,54 @@
private val shadeRepository: ShadeRepository,
private val keyguardTransitionRepository: KeyguardTransitionRepository,
private val keyguardTransitionInteractor: KeyguardTransitionInteractor
-) : TransitionInteractor(BouncerToGoneTransitionInteractor::class.simpleName!!) {
+) : TransitionInteractor(FromBouncerTransitionInteractor::class.simpleName!!) {
private var transitionId: UUID? = null
override fun start() {
- listenForKeyguardGoingAway()
+ listenForBouncerToGone()
+ listenForBouncerToLockscreenOrAod()
}
- private fun listenForKeyguardGoingAway() {
+ private fun listenForBouncerToLockscreenOrAod() {
+ scope.launch {
+ keyguardInteractor.isBouncerShowing
+ .sample(
+ combine(
+ keyguardInteractor.wakefulnessModel,
+ keyguardTransitionInteractor.startedKeyguardTransitionStep,
+ ::Pair
+ ),
+ ::toTriple
+ )
+ .collect { triple ->
+ val (isBouncerShowing, wakefulnessState, lastStartedTransitionStep) = triple
+ if (
+ !isBouncerShowing && lastStartedTransitionStep.to == KeyguardState.BOUNCER
+ ) {
+ val to =
+ if (
+ wakefulnessState.state == WakefulnessState.STARTING_TO_SLEEP ||
+ wakefulnessState.state == WakefulnessState.ASLEEP
+ ) {
+ KeyguardState.AOD
+ } else {
+ KeyguardState.LOCKSCREEN
+ }
+ keyguardTransitionRepository.startTransition(
+ TransitionInfo(
+ ownerName = name,
+ from = KeyguardState.BOUNCER,
+ to = to,
+ animator = getAnimator(),
+ )
+ )
+ }
+ }
+ }
+ }
+
+ private fun listenForBouncerToGone() {
scope.launch {
keyguardInteractor.isKeyguardGoingAway
.sample(keyguardTransitionInteractor.finishedKeyguardState, { a, b -> Pair(a, b) })
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
similarity index 70%
rename from packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenGoneTransitionInteractor.kt
rename to packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
index 95d9602..fd2d271 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenGoneTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
@@ -21,36 +21,46 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.DozeStateModel.Companion.isDozeOff
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionInfo
import com.android.systemui.util.kotlin.sample
import javax.inject.Inject
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
@SysUISingleton
-class LockscreenGoneTransitionInteractor
+class FromDozingTransitionInteractor
@Inject
constructor(
@Application private val scope: CoroutineScope,
private val keyguardInteractor: KeyguardInteractor,
- private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
private val keyguardTransitionRepository: KeyguardTransitionRepository,
-) : TransitionInteractor(LockscreenGoneTransitionInteractor::class.simpleName!!) {
+ private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
+) : TransitionInteractor(FromDozingTransitionInteractor::class.simpleName!!) {
override fun start() {
+ listenForDozingToLockscreen()
+ }
+
+ private fun listenForDozingToLockscreen() {
scope.launch {
- keyguardInteractor.isKeyguardGoingAway
+ keyguardInteractor.dozeTransitionModel
.sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
.collect { pair ->
- val (isKeyguardGoingAway, lastStartedStep) = pair
- if (isKeyguardGoingAway && lastStartedStep.to == KeyguardState.LOCKSCREEN) {
+ val (dozeTransitionModel, lastStartedTransition) = pair
+ if (
+ isDozeOff(dozeTransitionModel.to) &&
+ lastStartedTransition.to == KeyguardState.DOZING
+ ) {
keyguardTransitionRepository.startTransition(
TransitionInfo(
name,
+ KeyguardState.DOZING,
KeyguardState.LOCKSCREEN,
- KeyguardState.GONE,
getAnimator(),
)
)
@@ -59,14 +69,14 @@
}
}
- private fun getAnimator(): ValueAnimator {
+ private fun getAnimator(duration: Duration = DEFAULT_DURATION): ValueAnimator {
return ValueAnimator().apply {
setInterpolator(Interpolators.LINEAR)
- setDuration(TRANSITION_DURATION_MS)
+ setDuration(duration.inWholeMilliseconds)
}
}
companion object {
- private const val TRANSITION_DURATION_MS = 10L
+ private val DEFAULT_DURATION = 500.milliseconds
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DreamingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
similarity index 81%
rename from packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DreamingTransitionInteractor.kt
rename to packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
index 4d60579..3b09ae7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/DreamingTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
@@ -36,75 +36,40 @@
import kotlinx.coroutines.launch
@SysUISingleton
-class DreamingTransitionInteractor
+class FromDreamingTransitionInteractor
@Inject
constructor(
@Application private val scope: CoroutineScope,
private val keyguardInteractor: KeyguardInteractor,
private val keyguardTransitionRepository: KeyguardTransitionRepository,
private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
-) : TransitionInteractor(DreamingTransitionInteractor::class.simpleName!!) {
-
- private val canDreamFrom =
- setOf(KeyguardState.LOCKSCREEN, KeyguardState.GONE, KeyguardState.DOZING)
+) : TransitionInteractor(FromDreamingTransitionInteractor::class.simpleName!!) {
override fun start() {
- listenForEntryToDreaming()
listenForDreamingToLockscreen()
+ listenForDreamingToOccluded()
listenForDreamingToGone()
listenForDreamingToDozing()
}
- private fun listenForEntryToDreaming() {
- scope.launch {
- keyguardInteractor.isDreaming
- .sample(
- combine(
- keyguardInteractor.dozeTransitionModel,
- keyguardTransitionInteractor.finishedKeyguardState,
- ::Pair
- ),
- ::toTriple
- )
- .collect { triple ->
- val (isDreaming, dozeTransitionModel, keyguardState) = triple
- // Dozing/AOD and dreaming have overlapping events. If the state remains in
- // FINISH, it means that doze mode is not running and DREAMING is ok to
- // commence.
- if (
- isDozeOff(dozeTransitionModel.to) &&
- isDreaming &&
- canDreamFrom.contains(keyguardState)
- ) {
- keyguardTransitionRepository.startTransition(
- TransitionInfo(
- name,
- keyguardState,
- KeyguardState.DREAMING,
- getAnimator(),
- )
- )
- }
- }
- }
- }
-
private fun listenForDreamingToLockscreen() {
scope.launch {
- keyguardInteractor.isDreaming
+ // Using isDreamingWithOverlay provides an optimized path to LOCKSCREEN state, which
+ // otherwise would have gone through OCCLUDED first
+ keyguardInteractor.isDreamingWithOverlay
.sample(
combine(
keyguardInteractor.dozeTransitionModel,
keyguardTransitionInteractor.startedKeyguardTransitionStep,
- ::Pair,
+ ::Pair
),
::toTriple
)
.collect { triple ->
val (isDreaming, dozeTransitionModel, lastStartedTransition) = triple
if (
- isDozeOff(dozeTransitionModel.to) &&
- !isDreaming &&
+ !isDreaming &&
+ isDozeOff(dozeTransitionModel.to) &&
lastStartedTransition.to == KeyguardState.DREAMING
) {
keyguardTransitionRepository.startTransition(
@@ -120,6 +85,42 @@
}
}
+ private fun listenForDreamingToOccluded() {
+ scope.launch {
+ keyguardInteractor.isDreaming
+ .sample(
+ combine(
+ keyguardInteractor.isKeyguardOccluded,
+ keyguardTransitionInteractor.startedKeyguardTransitionStep,
+ ::Pair,
+ ),
+ ::toTriple
+ )
+ .collect { triple ->
+ val (isDreaming, isOccluded, lastStartedTransition) = triple
+ if (
+ isOccluded &&
+ !isDreaming &&
+ (lastStartedTransition.to == KeyguardState.DREAMING ||
+ lastStartedTransition.to == KeyguardState.LOCKSCREEN)
+ ) {
+ // At the moment, checking for LOCKSCREEN state above provides a corrective
+ // action. There's no great signal to determine when the dream is ending
+ // and a transition to OCCLUDED is beginning directly. For now, the solution
+ // is DREAMING->LOCKSCREEN->OCCLUDED
+ keyguardTransitionRepository.startTransition(
+ TransitionInfo(
+ name,
+ lastStartedTransition.to,
+ KeyguardState.OCCLUDED,
+ getAnimator(),
+ )
+ )
+ }
+ }
+ }
+ }
+
private fun listenForDreamingToGone() {
scope.launch {
keyguardInteractor.biometricUnlockState
@@ -179,6 +180,5 @@
companion object {
private val DEFAULT_DURATION = 500.milliseconds
val TO_LOCKSCREEN_DURATION = 1183.milliseconds
- @JvmField val TO_LOCKSCREEN_DURATION_MS = TO_LOCKSCREEN_DURATION.inWholeMilliseconds
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GoneAodTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
similarity index 72%
rename from packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GoneAodTransitionInteractor.kt
rename to packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
index a50e759..553fafe 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GoneAodTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
@@ -30,19 +30,44 @@
import kotlinx.coroutines.launch
@SysUISingleton
-class GoneAodTransitionInteractor
+class FromGoneTransitionInteractor
@Inject
constructor(
@Application private val scope: CoroutineScope,
private val keyguardInteractor: KeyguardInteractor,
private val keyguardTransitionRepository: KeyguardTransitionRepository,
private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
-) : TransitionInteractor(GoneAodTransitionInteractor::class.simpleName!!) {
+) : TransitionInteractor(FromGoneTransitionInteractor::class.simpleName!!) {
override fun start() {
+ listenForGoneToAod()
+ listenForGoneToDreaming()
+ }
+
+ private fun listenForGoneToDreaming() {
+ scope.launch {
+ keyguardInteractor.isAbleToDream
+ .sample(keyguardTransitionInteractor.finishedKeyguardState, ::Pair)
+ .collect { pair ->
+ val (isAbleToDream, keyguardState) = pair
+ if (isAbleToDream && keyguardState == KeyguardState.GONE) {
+ keyguardTransitionRepository.startTransition(
+ TransitionInfo(
+ name,
+ KeyguardState.GONE,
+ KeyguardState.DREAMING,
+ getAnimator(),
+ )
+ )
+ }
+ }
+ }
+ }
+
+ private fun listenForGoneToAod() {
scope.launch {
keyguardInteractor.wakefulnessModel
- .sample(keyguardTransitionInteractor.finishedKeyguardState, { a, b -> Pair(a, b) })
+ .sample(keyguardTransitionInteractor.finishedKeyguardState, ::Pair)
.collect { pair ->
val (wakefulnessState, keyguardState) = pair
if (
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
new file mode 100644
index 0000000..326acc9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
@@ -0,0 +1,236 @@
+/*
+ * Copyright (C) 2022 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.keyguard.domain.interactor
+
+import android.animation.ValueAnimator
+import com.android.systemui.animation.Interpolators
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.DozeStateModel
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.StatusBarState.KEYGUARD
+import com.android.systemui.keyguard.shared.model.TransitionInfo
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.shade.data.repository.ShadeRepository
+import com.android.systemui.util.kotlin.sample
+import java.util.UUID
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.launch
+
+@SysUISingleton
+class FromLockscreenTransitionInteractor
+@Inject
+constructor(
+ @Application private val scope: CoroutineScope,
+ private val keyguardInteractor: KeyguardInteractor,
+ private val shadeRepository: ShadeRepository,
+ private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
+ private val keyguardTransitionRepository: KeyguardTransitionRepository,
+) : TransitionInteractor(FromLockscreenTransitionInteractor::class.simpleName!!) {
+
+ private var transitionId: UUID? = null
+
+ override fun start() {
+ listenForLockscreenToGone()
+ listenForLockscreenToOccluded()
+ listenForLockscreenToAod()
+ listenForLockscreenToBouncer()
+ listenForLockscreenToDreaming()
+ listenForLockscreenToBouncerDragging()
+ }
+
+ private fun listenForLockscreenToDreaming() {
+ scope.launch {
+ keyguardInteractor.isAbleToDream
+ .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
+ .collect { pair ->
+ val (isAbleToDream, lastStartedTransition) = pair
+ if (isAbleToDream && lastStartedTransition.to == KeyguardState.LOCKSCREEN) {
+ keyguardTransitionRepository.startTransition(
+ TransitionInfo(
+ name,
+ KeyguardState.LOCKSCREEN,
+ KeyguardState.DREAMING,
+ getAnimator(),
+ )
+ )
+ }
+ }
+ }
+ }
+
+ private fun listenForLockscreenToBouncer() {
+ scope.launch {
+ keyguardInteractor.isBouncerShowing
+ .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
+ .collect { pair ->
+ val (isBouncerShowing, lastStartedTransitionStep) = pair
+ if (
+ isBouncerShowing && lastStartedTransitionStep.to == KeyguardState.LOCKSCREEN
+ ) {
+ keyguardTransitionRepository.startTransition(
+ TransitionInfo(
+ ownerName = name,
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.BOUNCER,
+ animator = getAnimator(),
+ )
+ )
+ }
+ }
+ }
+ }
+
+ /* Starts transitions when manually dragging up the bouncer from the lockscreen. */
+ private fun listenForLockscreenToBouncerDragging() {
+ scope.launch {
+ shadeRepository.shadeModel
+ .sample(
+ combine(
+ keyguardTransitionInteractor.finishedKeyguardState,
+ keyguardInteractor.statusBarState,
+ ::Pair
+ ),
+ ::toTriple
+ )
+ .collect { triple ->
+ val (shadeModel, keyguardState, statusBarState) = triple
+
+ val id = transitionId
+ if (id != null) {
+ // An existing `id` means a transition is started, and calls to
+ // `updateTransition` will control it until FINISHED
+ keyguardTransitionRepository.updateTransition(
+ id,
+ 1f - shadeModel.expansionAmount,
+ if (
+ shadeModel.expansionAmount == 0f || shadeModel.expansionAmount == 1f
+ ) {
+ transitionId = null
+ TransitionState.FINISHED
+ } else {
+ TransitionState.RUNNING
+ }
+ )
+ } else {
+ // TODO (b/251849525): Remove statusbarstate check when that state is
+ // integrated into KeyguardTransitionRepository
+ if (
+ keyguardState == KeyguardState.LOCKSCREEN &&
+ shadeModel.isUserDragging &&
+ statusBarState == KEYGUARD
+ ) {
+ transitionId =
+ keyguardTransitionRepository.startTransition(
+ TransitionInfo(
+ ownerName = name,
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.BOUNCER,
+ animator = null,
+ )
+ )
+ }
+ }
+ }
+ }
+ }
+
+ private fun listenForLockscreenToGone() {
+ scope.launch {
+ keyguardInteractor.isKeyguardGoingAway
+ .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
+ .collect { pair ->
+ val (isKeyguardGoingAway, lastStartedStep) = pair
+ if (isKeyguardGoingAway && lastStartedStep.to == KeyguardState.LOCKSCREEN) {
+ keyguardTransitionRepository.startTransition(
+ TransitionInfo(
+ name,
+ KeyguardState.LOCKSCREEN,
+ KeyguardState.GONE,
+ getAnimator(),
+ )
+ )
+ }
+ }
+ }
+ }
+
+ private fun listenForLockscreenToOccluded() {
+ scope.launch {
+ keyguardInteractor.isKeyguardOccluded
+ .sample(
+ combine(
+ keyguardTransitionInteractor.finishedKeyguardState,
+ keyguardInteractor.isDreaming,
+ ::Pair
+ ),
+ ::toTriple
+ )
+ .collect { triple ->
+ val (isOccluded, keyguardState, isDreaming) = triple
+ // Occlusion signals come from the framework, and should interrupt any
+ // existing transition
+ if (isOccluded && !isDreaming) {
+ keyguardTransitionRepository.startTransition(
+ TransitionInfo(
+ name,
+ keyguardState,
+ KeyguardState.OCCLUDED,
+ getAnimator(),
+ )
+ )
+ }
+ }
+ }
+ }
+
+ private fun listenForLockscreenToAod() {
+ scope.launch {
+ keyguardInteractor
+ .dozeTransitionTo(DozeStateModel.DOZE_AOD)
+ .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
+ .collect { pair ->
+ val (dozeToAod, lastStartedStep) = pair
+ if (lastStartedStep.to == KeyguardState.LOCKSCREEN) {
+ keyguardTransitionRepository.startTransition(
+ TransitionInfo(
+ name,
+ KeyguardState.LOCKSCREEN,
+ KeyguardState.AOD,
+ getAnimator(),
+ )
+ )
+ }
+ }
+ }
+ }
+
+ private fun getAnimator(): ValueAnimator {
+ return ValueAnimator().apply {
+ setInterpolator(Interpolators.LINEAR)
+ setDuration(TRANSITION_DURATION_MS)
+ }
+ }
+
+ companion object {
+ private const val TRANSITION_DURATION_MS = 500L
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
similarity index 62%
copy from packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt
copy to packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
index f3d2905..8878901 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodLockscreenTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
@@ -21,42 +21,43 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
-import com.android.systemui.keyguard.shared.model.DozeStateModel
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionInfo
import com.android.systemui.util.kotlin.sample
import javax.inject.Inject
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
@SysUISingleton
-class AodLockscreenTransitionInteractor
+class FromOccludedTransitionInteractor
@Inject
constructor(
@Application private val scope: CoroutineScope,
private val keyguardInteractor: KeyguardInteractor,
private val keyguardTransitionRepository: KeyguardTransitionRepository,
private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
-) : TransitionInteractor(AodLockscreenTransitionInteractor::class.simpleName!!) {
+) : TransitionInteractor(FromOccludedTransitionInteractor::class.simpleName!!) {
override fun start() {
- listenForTransitionToAodFromLockscreen()
- listenForTransitionToLockscreenFromDozeStates()
+ listenForOccludedToLockscreen()
+ listenForOccludedToDreaming()
}
- private fun listenForTransitionToAodFromLockscreen() {
+ private fun listenForOccludedToDreaming() {
scope.launch {
- keyguardInteractor
- .dozeTransitionTo(DozeStateModel.DOZE_AOD)
- .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
+ keyguardInteractor.isAbleToDream
+ .sample(keyguardTransitionInteractor.finishedKeyguardState, ::Pair)
.collect { pair ->
- val (dozeToAod, lastStartedStep) = pair
- if (lastStartedStep.to == KeyguardState.LOCKSCREEN) {
+ val (isAbleToDream, keyguardState) = pair
+ if (isAbleToDream && keyguardState == KeyguardState.OCCLUDED) {
keyguardTransitionRepository.startTransition(
TransitionInfo(
name,
- KeyguardState.LOCKSCREEN,
- KeyguardState.AOD,
+ KeyguardState.OCCLUDED,
+ KeyguardState.DREAMING,
getAnimator(),
)
)
@@ -65,21 +66,21 @@
}
}
- private fun listenForTransitionToLockscreenFromDozeStates() {
- val canGoToLockscreen = setOf(KeyguardState.AOD, KeyguardState.DOZING)
+ private fun listenForOccludedToLockscreen() {
scope.launch {
- keyguardInteractor
- .dozeTransitionTo(DozeStateModel.FINISH)
+ keyguardInteractor.isKeyguardOccluded
.sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair)
.collect { pair ->
- val (dozeToAod, lastStartedStep) = pair
- if (canGoToLockscreen.contains(lastStartedStep.to)) {
+ val (isOccluded, lastStartedKeyguardState) = pair
+ // Occlusion signals come from the framework, and should interrupt any
+ // existing transition
+ if (!isOccluded && lastStartedKeyguardState.to == KeyguardState.OCCLUDED) {
keyguardTransitionRepository.startTransition(
TransitionInfo(
name,
- lastStartedStep.to,
+ KeyguardState.OCCLUDED,
KeyguardState.LOCKSCREEN,
- getAnimator(),
+ getAnimator(TO_LOCKSCREEN_DURATION),
)
)
}
@@ -87,14 +88,15 @@
}
}
- private fun getAnimator(): ValueAnimator {
+ private fun getAnimator(duration: Duration = DEFAULT_DURATION): ValueAnimator {
return ValueAnimator().apply {
setInterpolator(Interpolators.LINEAR)
- setDuration(TRANSITION_DURATION_MS)
+ setDuration(duration.inWholeMilliseconds)
}
}
companion object {
- private const val TRANSITION_DURATION_MS = 500L
+ private val DEFAULT_DURATION = 500.milliseconds
+ val TO_LOCKSCREEN_DURATION = 933.milliseconds
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
index 6912e1d..402c179 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
@@ -22,12 +22,16 @@
import com.android.systemui.keyguard.data.repository.KeyguardRepository
import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
import com.android.systemui.keyguard.shared.model.DozeStateModel
+import com.android.systemui.keyguard.shared.model.DozeStateModel.Companion.isDozeOff
import com.android.systemui.keyguard.shared.model.DozeTransitionModel
import com.android.systemui.keyguard.shared.model.StatusBarState
import com.android.systemui.keyguard.shared.model.WakefulnessModel
+import com.android.systemui.util.kotlin.sample
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.merge
/**
* Encapsulates business-logic related to the keyguard but not to a more specific part within it.
@@ -52,8 +56,27 @@
* but not vice-versa.
*/
val isDreaming: Flow<Boolean> = repository.isDreaming
+ /** Whether the system is dreaming with an overlay active */
+ val isDreamingWithOverlay: Flow<Boolean> = repository.isDreamingWithOverlay
+
+ /**
+ * Dozing and dreaming have overlapping events. If the doze state remains in FINISH, it means
+ * that doze mode is not running and DREAMING is ok to commence.
+ */
+ val isAbleToDream: Flow<Boolean> =
+ merge(isDreaming, isDreamingWithOverlay)
+ .sample(
+ dozeTransitionModel,
+ { isDreaming, dozeTransitionModel ->
+ isDreaming && isDozeOff(dozeTransitionModel.to)
+ }
+ )
+ .distinctUntilChanged()
+
/** Whether the keyguard is showing or not. */
val isKeyguardShowing: Flow<Boolean> = repository.isKeyguardShowing
+ /** Whether the keyguard is occluded (covered by an activity). */
+ val isKeyguardOccluded: Flow<Boolean> = repository.isKeyguardOccluded
/** Whether the keyguard is going away. */
val isKeyguardGoingAway: Flow<Boolean> = repository.isKeyguardGoingAway
/** Whether the bouncer is showing or not. */
@@ -74,8 +97,8 @@
/** The approximate location on the screen of the face unlock sensor, if one is available. */
val faceSensorLocation: Flow<Point?> = repository.faceSensorLocation
- fun dozeTransitionTo(state: DozeStateModel): Flow<DozeTransitionModel> {
- return dozeTransitionModel.filter { it.to == state }
+ fun dozeTransitionTo(vararg states: DozeStateModel): Flow<DozeTransitionModel> {
+ return dozeTransitionModel.filter { states.contains(it.to) }
}
fun isKeyguardShowing(): Boolean {
return repository.isKeyguardShowing()
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt
index bb8b79a..fbed446 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt
@@ -37,13 +37,13 @@
// exhaustive
val ret =
when (it) {
- is LockscreenBouncerTransitionInteractor -> Log.d(TAG, "Started $it")
- is AodLockscreenTransitionInteractor -> Log.d(TAG, "Started $it")
- is GoneAodTransitionInteractor -> Log.d(TAG, "Started $it")
- is LockscreenGoneTransitionInteractor -> Log.d(TAG, "Started $it")
- is AodToGoneTransitionInteractor -> Log.d(TAG, "Started $it")
- is BouncerToGoneTransitionInteractor -> Log.d(TAG, "Started $it")
- is DreamingTransitionInteractor -> Log.d(TAG, "Started $it")
+ is FromBouncerTransitionInteractor -> Log.d(TAG, "Started $it")
+ is FromAodTransitionInteractor -> Log.d(TAG, "Started $it")
+ is FromGoneTransitionInteractor -> Log.d(TAG, "Started $it")
+ is FromLockscreenTransitionInteractor -> Log.d(TAG, "Started $it")
+ is FromDreamingTransitionInteractor -> Log.d(TAG, "Started $it")
+ is FromOccludedTransitionInteractor -> Log.d(TAG, "Started $it")
+ is FromDozingTransitionInteractor -> Log.d(TAG, "Started $it")
}
it.start()
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
index 3b9d6f5..04024be 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
@@ -24,7 +24,9 @@
import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING
import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
+import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED
import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionState.STARTED
import com.android.systemui.keyguard.shared.model.TransitionStep
import javax.inject.Inject
import kotlin.time.Duration
@@ -50,6 +52,10 @@
val dreamingToLockscreenTransition: Flow<TransitionStep> =
repository.transition(DREAMING, LOCKSCREEN)
+ /** OCCLUDED->LOCKSCREEN transition information. */
+ val occludedToLockscreenTransition: Flow<TransitionStep> =
+ repository.transition(OCCLUDED, LOCKSCREEN)
+
/** (any)->AOD transition information */
val anyStateToAodTransition: Flow<TransitionStep> =
repository.transitions.filter { step -> step.to == KeyguardState.AOD }
@@ -93,7 +99,14 @@
val start = (params.startTime / totalDuration).toFloat()
val chunks = (totalDuration / params.duration).toFloat()
return flow
- .map { step -> (step.value - start) * chunks }
+ // When starting, emit a value of 0f to give animations a chance to set initial state
+ .map { step ->
+ if (step.transitionState == STARTED) {
+ 0f
+ } else {
+ (step.value - start) * chunks
+ }
+ }
.filter { value -> value >= 0f && value <= 1f }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt
index 6e25200..a59c407 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt
@@ -89,6 +89,7 @@
KeyguardState.BOUNCER -> true
KeyguardState.LOCKSCREEN -> true
KeyguardState.GONE -> true
+ KeyguardState.OCCLUDED -> true
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt
deleted file mode 100644
index 5cb7d70..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt
+++ /dev/null
@@ -1,170 +0,0 @@
-/*
- * Copyright (C) 2022 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.keyguard.domain.interactor
-
-import android.animation.ValueAnimator
-import com.android.systemui.animation.Interpolators
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
-import com.android.systemui.keyguard.shared.model.KeyguardState
-import com.android.systemui.keyguard.shared.model.StatusBarState.KEYGUARD
-import com.android.systemui.keyguard.shared.model.TransitionInfo
-import com.android.systemui.keyguard.shared.model.TransitionState
-import com.android.systemui.keyguard.shared.model.WakefulnessState
-import com.android.systemui.shade.data.repository.ShadeRepository
-import com.android.systemui.util.kotlin.sample
-import java.util.UUID
-import javax.inject.Inject
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.collect
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.launch
-
-@SysUISingleton
-class LockscreenBouncerTransitionInteractor
-@Inject
-constructor(
- @Application private val scope: CoroutineScope,
- private val keyguardInteractor: KeyguardInteractor,
- private val shadeRepository: ShadeRepository,
- private val keyguardTransitionRepository: KeyguardTransitionRepository,
- private val keyguardTransitionInteractor: KeyguardTransitionInteractor
-) : TransitionInteractor(LockscreenBouncerTransitionInteractor::class.simpleName!!) {
-
- private var transitionId: UUID? = null
-
- override fun start() {
- listenForDraggingUpToBouncer()
- listenForBouncer()
- }
-
- private fun listenForBouncer() {
- scope.launch {
- keyguardInteractor.isBouncerShowing
- .sample(
- combine(
- keyguardInteractor.wakefulnessModel,
- keyguardTransitionInteractor.startedKeyguardTransitionStep,
- ::Pair
- ),
- ::toTriple
- )
- .collect { triple ->
- val (isBouncerShowing, wakefulnessState, lastStartedTransitionStep) = triple
- if (
- !isBouncerShowing && lastStartedTransitionStep.to == KeyguardState.BOUNCER
- ) {
- val to =
- if (
- wakefulnessState.state == WakefulnessState.STARTING_TO_SLEEP ||
- wakefulnessState.state == WakefulnessState.ASLEEP
- ) {
- KeyguardState.AOD
- } else {
- KeyguardState.LOCKSCREEN
- }
- keyguardTransitionRepository.startTransition(
- TransitionInfo(
- ownerName = name,
- from = KeyguardState.BOUNCER,
- to = to,
- animator = getAnimator(),
- )
- )
- } else if (
- isBouncerShowing && lastStartedTransitionStep.to == KeyguardState.LOCKSCREEN
- ) {
- keyguardTransitionRepository.startTransition(
- TransitionInfo(
- ownerName = name,
- from = KeyguardState.LOCKSCREEN,
- to = KeyguardState.BOUNCER,
- animator = getAnimator(),
- )
- )
- }
- Unit
- }
- }
- }
-
- /* Starts transitions when manually dragging up the bouncer from the lockscreen. */
- private fun listenForDraggingUpToBouncer() {
- scope.launch {
- shadeRepository.shadeModel
- .sample(
- combine(
- keyguardTransitionInteractor.finishedKeyguardState,
- keyguardInteractor.statusBarState,
- ::Pair
- ),
- ::toTriple
- )
- .collect { triple ->
- val (shadeModel, keyguardState, statusBarState) = triple
-
- val id = transitionId
- if (id != null) {
- // An existing `id` means a transition is started, and calls to
- // `updateTransition` will control it until FINISHED
- keyguardTransitionRepository.updateTransition(
- id,
- 1f - shadeModel.expansionAmount,
- if (
- shadeModel.expansionAmount == 0f || shadeModel.expansionAmount == 1f
- ) {
- transitionId = null
- TransitionState.FINISHED
- } else {
- TransitionState.RUNNING
- }
- )
- } else {
- // TODO (b/251849525): Remove statusbarstate check when that state is
- // integrated into KeyguardTransitionRepository
- if (
- keyguardState == KeyguardState.LOCKSCREEN &&
- shadeModel.isUserDragging &&
- statusBarState == KEYGUARD
- ) {
- transitionId =
- keyguardTransitionRepository.startTransition(
- TransitionInfo(
- ownerName = name,
- from = KeyguardState.LOCKSCREEN,
- to = KeyguardState.BOUNCER,
- animator = null,
- )
- )
- }
- }
- }
- }
- }
-
- private fun getAnimator(): ValueAnimator {
- return ValueAnimator().apply {
- setInterpolator(Interpolators.LINEAR)
- setDuration(TRANSITION_DURATION_MS)
- }
- }
-
- companion object {
- private const val TRANSITION_DURATION_MS = 300L
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt
index 5f63ae7..81fa233 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt
@@ -32,25 +32,25 @@
@Binds
@IntoSet
- abstract fun lockscreenBouncer(
- impl: LockscreenBouncerTransitionInteractor
- ): TransitionInteractor
+ abstract fun fromBouncer(impl: FromBouncerTransitionInteractor): TransitionInteractor
@Binds
@IntoSet
- abstract fun aodLockscreen(impl: AodLockscreenTransitionInteractor): TransitionInteractor
+ abstract fun fromLockscreen(impl: FromLockscreenTransitionInteractor): TransitionInteractor
- @Binds @IntoSet abstract fun goneAod(impl: GoneAodTransitionInteractor): TransitionInteractor
+ @Binds @IntoSet abstract fun fromAod(impl: FromAodTransitionInteractor): TransitionInteractor
- @Binds @IntoSet abstract fun aodGone(impl: AodToGoneTransitionInteractor): TransitionInteractor
+ @Binds @IntoSet abstract fun fromGone(impl: FromGoneTransitionInteractor): TransitionInteractor
@Binds
@IntoSet
- abstract fun bouncerGone(impl: BouncerToGoneTransitionInteractor): TransitionInteractor
+ abstract fun fromDreaming(impl: FromDreamingTransitionInteractor): TransitionInteractor
@Binds
@IntoSet
- abstract fun lockscreenGone(impl: LockscreenGoneTransitionInteractor): TransitionInteractor
+ abstract fun fromOccluded(impl: FromOccludedTransitionInteractor): TransitionInteractor
- @Binds @IntoSet abstract fun dreaming(impl: DreamingTransitionInteractor): TransitionInteractor
+ @Binds
+ @IntoSet
+ abstract fun fromDozing(impl: FromDozingTransitionInteractor): TransitionInteractor
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt
index 08ad3d5..4d24c14 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/TransitionInteractor.kt
@@ -31,4 +31,6 @@
abstract fun start()
fun <A, B, C> toTriple(a: A, bc: Pair<B, C>) = Triple(a, bc.first, bc.second)
+
+ fun <A, B, C> toTriple(ab: Pair<A, B>, c: C) = Triple(ab.first, ab.second, c)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt
index dd908c4..c757986 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardState.kt
@@ -57,4 +57,8 @@
* with SWIPE security method or face unlock without bypass.
*/
GONE,
+ /*
+ * An activity is displaying over the keyguard.
+ */
+ OCCLUDED,
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt
index 9b36e8c..e164f5d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt
@@ -17,8 +17,9 @@
package com.android.systemui.keyguard.ui.viewmodel
import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE
+import com.android.systemui.animation.Interpolators.EMPHASIZED_DECELERATE
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.keyguard.domain.interactor.DreamingTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
+import com.android.systemui.keyguard.domain.interactor.FromDreamingTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.AnimationParams
import javax.inject.Inject
@@ -46,12 +47,11 @@
/** Dream overlay views alpha - fade out */
val dreamOverlayAlpha: Flow<Float> = flowForAnimation(DREAM_OVERLAY_ALPHA).map { 1f - it }
- /** Dream background alpha - fade out */
- val dreamBackgroundAlpha: Flow<Float> = flowForAnimation(DREAM_BACKGROUND_ALPHA).map { 1f - it }
-
/** Lockscreen views y-translation */
fun lockscreenTranslationY(translatePx: Int): Flow<Float> {
- return flowForAnimation(LOCKSCREEN_TRANSLATION_Y).map { it * translatePx }
+ return flowForAnimation(LOCKSCREEN_TRANSLATION_Y).map { value ->
+ -translatePx + (EMPHASIZED_DECELERATE.getInterpolation(value) * translatePx)
+ }
}
/** Lockscreen views alpha */
@@ -68,10 +68,12 @@
companion object {
/* Length of time before ending the dream activity, in order to start unoccluding */
val DREAM_ANIMATION_DURATION = 250.milliseconds
+ @JvmField
+ val LOCKSCREEN_ANIMATION_DURATION_MS =
+ (TO_LOCKSCREEN_DURATION - DREAM_ANIMATION_DURATION).inWholeMilliseconds
- val DREAM_OVERLAY_TRANSLATION_Y = AnimationParams(duration = 500.milliseconds)
+ val DREAM_OVERLAY_TRANSLATION_Y = AnimationParams(duration = 600.milliseconds)
val DREAM_OVERLAY_ALPHA = AnimationParams(duration = 250.milliseconds)
- val DREAM_BACKGROUND_ALPHA = AnimationParams(duration = 250.milliseconds)
val LOCKSCREEN_TRANSLATION_Y = AnimationParams(duration = TO_LOCKSCREEN_DURATION)
val LOCKSCREEN_ALPHA =
AnimationParams(startTime = 233.milliseconds, duration = 250.milliseconds)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt
new file mode 100644
index 0000000..e804562
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2022 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.keyguard.ui.viewmodel
+
+import com.android.systemui.animation.Interpolators.EMPHASIZED_DECELERATE
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.FromOccludedTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.AnimationParams
+import javax.inject.Inject
+import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+/**
+ * Breaks down OCCLUDED->LOCKSCREEN transition into discrete steps for corresponding views to
+ * consume.
+ */
+@SysUISingleton
+class OccludedToLockscreenTransitionViewModel
+@Inject
+constructor(
+ private val interactor: KeyguardTransitionInteractor,
+) {
+ /** Lockscreen views y-translation */
+ fun lockscreenTranslationY(translatePx: Int): Flow<Float> {
+ return flowForAnimation(LOCKSCREEN_TRANSLATION_Y).map { value ->
+ -translatePx + (EMPHASIZED_DECELERATE.getInterpolation(value) * translatePx)
+ }
+ }
+
+ /** Lockscreen views alpha */
+ val lockscreenAlpha: Flow<Float> = flowForAnimation(LOCKSCREEN_ALPHA)
+
+ private fun flowForAnimation(params: AnimationParams): Flow<Float> {
+ return interactor.transitionStepAnimation(
+ interactor.occludedToLockscreenTransition,
+ params,
+ totalDuration = TO_LOCKSCREEN_DURATION
+ )
+ }
+
+ companion object {
+ @JvmField val LOCKSCREEN_ANIMATION_DURATION_MS = TO_LOCKSCREEN_DURATION.inWholeMilliseconds
+ val LOCKSCREEN_TRANSLATION_Y = AnimationParams(duration = TO_LOCKSCREEN_DURATION)
+ val LOCKSCREEN_ALPHA =
+ AnimationParams(startTime = 233.milliseconds, duration = 250.milliseconds)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttFlags.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttFlags.kt
index 03bc935..8a565fa 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/MediaTttFlags.kt
@@ -26,4 +26,8 @@
class MediaTttFlags @Inject constructor(private val featureFlags: FeatureFlags) {
/** */
fun isMediaTttEnabled(): Boolean = featureFlags.isEnabled(Flags.MEDIA_TAP_TO_TRANSFER)
+
+ /** Check whether the flag for the receiver success state is enabled. */
+ fun isMediaTttReceiverSuccessRippleEnabled(): Boolean =
+ featureFlags.isEnabled(Flags.MEDIA_TTT_RECEIVER_SUCCESS_RIPPLE)
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
index 358534c..889147b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
@@ -114,6 +114,9 @@
}
}
+ private var maxRippleWidth: Float = 0f
+ private var maxRippleHeight: Float = 0f
+
private fun updateMediaTapToTransferReceiverDisplay(
@StatusBarManager.MediaTransferReceiverState displayState: Int,
routeInfo: MediaRoute2Info,
@@ -216,7 +219,7 @@
expandRipple(view.requireViewById(R.id.ripple))
}
- override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) {
+ override fun animateViewOut(view: ViewGroup, removalReason: String?, onAnimationEnd: Runnable) {
val appIconView = view.getAppIconView()
appIconView.animate()
.translationYBy(getTranslationAmount().toFloat())
@@ -226,7 +229,14 @@
.alpha(0f)
.setDuration(ICON_ALPHA_ANIM_DURATION)
.start()
- (view.requireViewById(R.id.ripple) as ReceiverChipRippleView).collapseRipple(onAnimationEnd)
+
+ val rippleView: ReceiverChipRippleView = view.requireViewById(R.id.ripple)
+ if (removalReason == ChipStateReceiver.TRANSFER_TO_RECEIVER_SUCCEEDED.name &&
+ mediaTttFlags.isMediaTttReceiverSuccessRippleEnabled()) {
+ expandRippleToFull(rippleView, onAnimationEnd)
+ } else {
+ rippleView.collapseRipple(onAnimationEnd)
+ }
}
override fun getTouchableRegion(view: View, outRect: Rect) {
@@ -271,12 +281,19 @@
})
}
- private fun layoutRipple(rippleView: ReceiverChipRippleView) {
+ private fun layoutRipple(rippleView: ReceiverChipRippleView, isFullScreen: Boolean = false) {
val windowBounds = windowManager.currentWindowMetrics.bounds
val height = windowBounds.height().toFloat()
val width = windowBounds.width().toFloat()
- rippleView.setMaxSize(width / 2f, height / 2f)
+ if (isFullScreen) {
+ maxRippleHeight = height * 2f
+ maxRippleWidth = width * 2f
+ } else {
+ maxRippleHeight = height / 2f
+ maxRippleWidth = width / 2f
+ }
+ rippleView.setMaxSize(maxRippleWidth, maxRippleHeight)
// Center the ripple on the bottom of the screen in the middle.
rippleView.setCenter(width * 0.5f, height)
val color = Utils.getColorAttrDefaultColor(context, R.attr.wallpaperTextColorAccent)
@@ -286,6 +303,11 @@
private fun View.getAppIconView(): CachingIconView {
return this.requireViewById(R.id.app_icon)
}
+
+ private fun expandRippleToFull(rippleView: ReceiverChipRippleView, onAnimationEnd: Runnable?) {
+ layoutRipple(rippleView, true)
+ rippleView.expandToFull(maxRippleHeight, onAnimationEnd)
+ }
}
val ICON_TRANSLATION_ANIM_DURATION = 30.frames
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt
index 6e9fc5c..87b2528 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt
@@ -22,6 +22,7 @@
import android.util.AttributeSet
import com.android.systemui.surfaceeffects.ripple.RippleShader
import com.android.systemui.surfaceeffects.ripple.RippleView
+import kotlin.math.pow
/**
* An expanding ripple effect for the media tap-to-transfer receiver chip.
@@ -59,4 +60,44 @@
})
animator.reverse()
}
+
+ // Expands the ripple to cover full screen.
+ fun expandToFull(newHeight: Float, onAnimationEnd: Runnable? = null) {
+ if (!isStarted) {
+ return
+ }
+ // Reset all listeners to animator.
+ animator.removeAllListeners()
+ animator.removeAllUpdateListeners()
+
+ // Only show the outline as ripple expands and disappears when animation ends.
+ setRippleFill(false)
+
+ val startingPercentage = calculateStartingPercentage(newHeight)
+ animator.addUpdateListener { updateListener ->
+ val now = updateListener.currentPlayTime
+ val progress = updateListener.animatedValue as Float
+ rippleShader.progress = startingPercentage + (progress * (1 - startingPercentage))
+ rippleShader.distortionStrength = 1 - rippleShader.progress
+ rippleShader.pixelDensity = 1 - rippleShader.progress
+ rippleShader.time = now.toFloat()
+ invalidate()
+ }
+ animator.addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator?) {
+ animation?.let { visibility = GONE }
+ onAnimationEnd?.run()
+ isStarted = false
+ }
+ })
+ animator.start()
+ }
+
+ // Calculates the actual starting percentage according to ripple shader progress set method.
+ // Check calculations in [RippleShader.progress]
+ fun calculateStartingPercentage(newHeight: Float): Float {
+ val ratio = rippleShader.currentHeight / newHeight
+ val remainingPercentage = (1 - ratio).toDouble().pow(1 / 3.toDouble()).toFloat()
+ return 1 - remainingPercentage
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelUnfoldAnimationController.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelUnfoldAnimationController.kt
index ba779c6..639172f 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelUnfoldAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelUnfoldAnimationController.kt
@@ -19,6 +19,9 @@
import android.content.Context
import android.view.ViewGroup
import com.android.systemui.R
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.statusbar.StatusBarState.SHADE
+import com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED
import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator
import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator.Direction.END
import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator.Direction.START
@@ -30,17 +33,24 @@
@SysUIUnfoldScope
class NotificationPanelUnfoldAnimationController
@Inject
-constructor(private val context: Context, progressProvider: NaturalRotationUnfoldProgressProvider) {
+constructor(
+ private val context: Context,
+ statusBarStateController: StatusBarStateController,
+ progressProvider: NaturalRotationUnfoldProgressProvider,
+) {
+
+ private val filterShade: () -> Boolean = { statusBarStateController.getState() == SHADE ||
+ statusBarStateController.getState() == SHADE_LOCKED }
private val translateAnimator by lazy {
UnfoldConstantTranslateAnimator(
viewsIdToTranslate =
setOf(
- ViewIdToTranslate(R.id.quick_settings_panel, START),
- ViewIdToTranslate(R.id.notification_stack_scroller, END),
- ViewIdToTranslate(R.id.rightLayout, END),
- ViewIdToTranslate(R.id.clock, START),
- ViewIdToTranslate(R.id.date, START)),
+ ViewIdToTranslate(R.id.quick_settings_panel, START, filterShade),
+ ViewIdToTranslate(R.id.notification_stack_scroller, END, filterShade),
+ ViewIdToTranslate(R.id.rightLayout, END, filterShade),
+ ViewIdToTranslate(R.id.clock, START, filterShade),
+ ViewIdToTranslate(R.id.date, START, filterShade)),
progressProvider = progressProvider)
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index fcdde79..fe9b733 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -44,6 +44,7 @@
import static com.android.systemui.statusbar.VibratorHelper.TOUCH_VIBRATION_ATTRIBUTES;
import static com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_FOLD_TO_AOD;
import static com.android.systemui.util.DumpUtilsKt.asIndenting;
+import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
import static java.lang.Float.isNaN;
@@ -138,7 +139,12 @@
import com.android.systemui.fragments.FragmentService;
import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
+import com.android.systemui.keyguard.shared.model.TransitionState;
+import com.android.systemui.keyguard.shared.model.TransitionStep;
+import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel;
import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel;
+import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel;
import com.android.systemui.media.controls.pipeline.MediaDataManager;
import com.android.systemui.media.controls.ui.KeyguardMediaController;
import com.android.systemui.media.controls.ui.MediaHierarchyManager;
@@ -233,6 +239,8 @@
import javax.inject.Inject;
import javax.inject.Provider;
+import kotlinx.coroutines.CoroutineDispatcher;
+
@CentralSurfacesComponent.CentralSurfacesScope
public final class NotificationPanelViewController implements Dumpable {
@@ -677,6 +685,15 @@
private boolean mGestureWaitForTouchSlop;
private boolean mIgnoreXTouchSlop;
private boolean mExpandLatencyTracking;
+ private DreamingToLockscreenTransitionViewModel mDreamingToLockscreenTransitionViewModel;
+ private OccludedToLockscreenTransitionViewModel mOccludedToLockscreenTransitionViewModel;
+
+ private KeyguardTransitionInteractor mKeyguardTransitionInteractor;
+ private CoroutineDispatcher mMainDispatcher;
+ private boolean mIsToLockscreenTransitionRunning = false;
+ private int mDreamingToLockscreenTransitionTranslationY;
+ private int mOccludedToLockscreenTransitionTranslationY;
+ private boolean mUnocclusionTransitionFlagEnabled = false;
private final Runnable mFlingCollapseRunnable = () -> fling(0, false /* expand */,
mNextCollapseSpeedUpFactor, false /* expandBecauseOfFalsing */);
@@ -692,6 +709,18 @@
}
};
+ private final Consumer<TransitionStep> mDreamingToLockscreenTransition =
+ (TransitionStep step) -> {
+ mIsToLockscreenTransitionRunning =
+ step.getTransitionState() == TransitionState.RUNNING;
+ };
+
+ private final Consumer<TransitionStep> mOccludedToLockscreenTransition =
+ (TransitionStep step) -> {
+ mIsToLockscreenTransitionRunning =
+ step.getTransitionState() == TransitionState.RUNNING;
+ };
+
@Inject
public NotificationPanelViewController(NotificationPanelView view,
@Main Handler handler,
@@ -760,6 +789,10 @@
SystemClock systemClock,
KeyguardBottomAreaViewModel keyguardBottomAreaViewModel,
KeyguardBottomAreaInteractor keyguardBottomAreaInteractor,
+ DreamingToLockscreenTransitionViewModel dreamingToLockscreenTransitionViewModel,
+ OccludedToLockscreenTransitionViewModel occludedToLockscreenTransitionViewModel,
+ @Main CoroutineDispatcher mainDispatcher,
+ KeyguardTransitionInteractor keyguardTransitionInteractor,
DumpManager dumpManager) {
keyguardStateController.addCallback(new KeyguardStateController.Callback() {
@Override
@@ -775,6 +808,9 @@
mShadeLog = shadeLogger;
mShadeHeightLogger = shadeHeightLogger;
mGutsManager = gutsManager;
+ mDreamingToLockscreenTransitionViewModel = dreamingToLockscreenTransitionViewModel;
+ mOccludedToLockscreenTransitionViewModel = occludedToLockscreenTransitionViewModel;
+ mKeyguardTransitionInteractor = keyguardTransitionInteractor;
mView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
@@ -852,6 +888,7 @@
mFalsingCollector = falsingCollector;
mPowerManager = powerManager;
mWakeUpCoordinator = coordinator;
+ mMainDispatcher = mainDispatcher;
mAccessibilityManager = accessibilityManager;
mView.setAccessibilityPaneTitle(determineAccessibilityPaneTitle());
setPanelAlpha(255, false /* animate */);
@@ -920,6 +957,8 @@
mNotificationPanelUnfoldAnimationController = unfoldComponent.map(
SysUIUnfoldComponent::getNotificationPanelUnfoldAnimationController);
+ mUnocclusionTransitionFlagEnabled = featureFlags.isEnabled(Flags.UNOCCLUSION_TRANSITION);
+
mQsFrameTranslateController = qsFrameTranslateController;
updateUserSwitcherFlags();
mKeyguardBottomAreaViewModel = keyguardBottomAreaViewModel;
@@ -1072,6 +1111,30 @@
mKeyguardUnfoldTransition.ifPresent(u -> u.setup(mView));
mNotificationPanelUnfoldAnimationController.ifPresent(controller ->
controller.setup(mNotificationContainerParent));
+
+ if (mUnocclusionTransitionFlagEnabled) {
+ // Dreaming->Lockscreen
+ collectFlow(mView, mKeyguardTransitionInteractor.getDreamingToLockscreenTransition(),
+ mDreamingToLockscreenTransition, mMainDispatcher);
+ collectFlow(mView, mDreamingToLockscreenTransitionViewModel.getLockscreenAlpha(),
+ toLockscreenTransitionAlpha(mNotificationStackScrollLayoutController),
+ mMainDispatcher);
+ collectFlow(mView, mDreamingToLockscreenTransitionViewModel.lockscreenTranslationY(
+ mDreamingToLockscreenTransitionTranslationY),
+ toLockscreenTransitionY(mNotificationStackScrollLayoutController),
+ mMainDispatcher);
+
+ // Occluded->Lockscreen
+ collectFlow(mView, mKeyguardTransitionInteractor.getOccludedToLockscreenTransition(),
+ mOccludedToLockscreenTransition, mMainDispatcher);
+ collectFlow(mView, mOccludedToLockscreenTransitionViewModel.getLockscreenAlpha(),
+ toLockscreenTransitionAlpha(mNotificationStackScrollLayoutController),
+ mMainDispatcher);
+ collectFlow(mView, mOccludedToLockscreenTransitionViewModel.lockscreenTranslationY(
+ mOccludedToLockscreenTransitionTranslationY),
+ toLockscreenTransitionY(mNotificationStackScrollLayoutController),
+ mMainDispatcher);
+ }
}
@VisibleForTesting
@@ -1106,6 +1169,10 @@
mUdfpsMaxYBurnInOffset = mResources.getDimensionPixelSize(R.dimen.udfps_burn_in_offset_y);
mSplitShadeScrimTransitionDistance = mResources.getDimensionPixelSize(
R.dimen.split_shade_scrim_transition_distance);
+ mDreamingToLockscreenTransitionTranslationY = mResources.getDimensionPixelSize(
+ R.dimen.dreaming_to_lockscreen_transition_lockscreen_translation_y);
+ mOccludedToLockscreenTransitionTranslationY = mResources.getDimensionPixelSize(
+ R.dimen.occluded_to_lockscreen_transition_lockscreen_translation_y);
}
private void updateViewControllers(KeyguardStatusView keyguardStatusView,
@@ -1769,10 +1836,14 @@
}
private void updateClock() {
+ if (mIsToLockscreenTransitionRunning) {
+ return;
+ }
float alpha = mClockPositionResult.clockAlpha * mKeyguardOnlyContentAlpha;
mKeyguardStatusViewController.setAlpha(alpha);
mKeyguardStatusViewController
- .setTranslationYExcludingMedia(mKeyguardOnlyTransitionTranslationY);
+ .setTranslationY(mKeyguardOnlyTransitionTranslationY, /* excludeMedia= */true);
+
if (mKeyguardQsUserSwitchController != null) {
mKeyguardQsUserSwitchController.setAlpha(alpha);
}
@@ -2656,7 +2727,9 @@
} else if (statusBarState == KEYGUARD
|| statusBarState == StatusBarState.SHADE_LOCKED) {
mKeyguardBottomArea.setVisibility(View.VISIBLE);
- mKeyguardBottomArea.setAlpha(1f);
+ if (!mIsToLockscreenTransitionRunning) {
+ mKeyguardBottomArea.setAlpha(1f);
+ }
} else {
mKeyguardBottomArea.setVisibility(View.GONE);
}
@@ -3523,6 +3596,9 @@
}
private void updateNotificationTranslucency() {
+ if (mIsToLockscreenTransitionRunning) {
+ return;
+ }
float alpha = 1f;
if (mClosingWithAlphaFadeOut && !mExpandingFromHeadsUp
&& !mHeadsUpManager.hasPinnedHeadsUp()) {
@@ -3578,6 +3654,9 @@
}
private void updateKeyguardBottomAreaAlpha() {
+ if (mIsToLockscreenTransitionRunning) {
+ return;
+ }
// There are two possible panel expansion behaviors:
// • User dragging up to unlock: we want to fade out as quick as possible
// (ALPHA_EXPANSION_THRESHOLD) to avoid seeing the bouncer over the bottom area.
@@ -3610,7 +3689,9 @@
}
private void onExpandingFinished() {
- mScrimController.onExpandingFinished();
+ if (!mUnocclusionTransitionFlagEnabled) {
+ mScrimController.onExpandingFinished();
+ }
mNotificationStackScrollLayoutController.onExpansionStopped();
mHeadsUpManager.onExpandingFinished();
mConversationNotificationManager.onNotificationPanelExpandStateChanged(isFullyCollapsed());
@@ -5805,6 +5886,32 @@
mCurrentPanelState = state;
}
+ private Consumer<Float> toLockscreenTransitionAlpha(
+ NotificationStackScrollLayoutController stackScroller) {
+ return (Float alpha) -> {
+ mKeyguardStatusViewController.setAlpha(alpha);
+ stackScroller.setAlpha(alpha);
+
+ mKeyguardBottomAreaInteractor.setAlpha(alpha);
+ mLockIconViewController.setAlpha(alpha);
+
+ if (mKeyguardQsUserSwitchController != null) {
+ mKeyguardQsUserSwitchController.setAlpha(alpha);
+ }
+ if (mKeyguardUserSwitcherController != null) {
+ mKeyguardUserSwitcherController.setAlpha(alpha);
+ }
+ };
+ }
+
+ private Consumer<Float> toLockscreenTransitionY(
+ NotificationStackScrollLayoutController stackScroller) {
+ return (Float translationY) -> {
+ mKeyguardStatusViewController.setTranslationY(translationY, /* excludeMedia= */false);
+ stackScroller.setTranslationY(translationY);
+ };
+ }
+
@VisibleForTesting
StatusBarStateController getStatusBarStateController() {
return mStatusBarStateController;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
index 3516037..80a8913 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
@@ -464,9 +464,6 @@
riv.getController().setRemoteInputs(inputs);
riv.getController().setEditedSuggestionInfo(editedSuggestionInfo);
ViewGroup parent = view.getParent() != null ? (ViewGroup) view.getParent() : null;
- if (parent != null) {
- riv.setDefocusTargetHeight(parent.getHeight());
- }
riv.focusAnimated(parent);
if (userMessageContent != null) {
riv.setEditTextContent(userMessageContent);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index 4bcc0b6..c2c38a7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -916,6 +916,11 @@
return mView.getTranslationX();
}
+ /** Set view y-translation */
+ public void setTranslationY(float translationY) {
+ mView.setTranslationY(translationY);
+ }
+
public int indexOfChild(View view) {
return mView.indexOfChild(view);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
index d500f99..ee8b861 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
@@ -232,7 +232,6 @@
private boolean mExpansionAffectsAlpha = true;
private boolean mAnimateChange;
private boolean mUpdatePending;
- private boolean mTracking;
private long mAnimationDuration = -1;
private long mAnimationDelay;
private Animator.AnimatorListener mAnimatorListener;
@@ -526,7 +525,6 @@
}
public void onTrackingStarted() {
- mTracking = true;
mDarkenWhileDragging = !mKeyguardStateController.canDismissLockScreen();
if (!mKeyguardUnlockAnimationController.isPlayingCannedUnlockAnimation()) {
mAnimatingPanelExpansionOnUnlock = false;
@@ -534,7 +532,6 @@
}
public void onExpandingFinished() {
- mTracking = false;
setUnocclusionAnimationRunning(false);
}
@@ -1450,8 +1447,6 @@
pw.print(" expansionProgress=");
pw.println(mTransitionToLockScreenFullShadeNotificationsProgress);
- pw.print(" mTracking=");
- pw.println(mTracking);
pw.print(" mDefaultScrimAlpha=");
pw.println(mDefaultScrimAlpha);
pw.print(" mPanelExpansionFraction=");
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index f196505..d480fab 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -135,7 +135,7 @@
private final PrimaryBouncerCallbackInteractor mPrimaryBouncerCallbackInteractor;
private final PrimaryBouncerInteractor mPrimaryBouncerInteractor;
private final BouncerView mPrimaryBouncerView;
- private final Lazy<com.android.systemui.shade.ShadeController> mShadeController;
+ private final Lazy<ShadeController> mShadeController;
// Local cache of expansion events, to avoid duplicates
private float mFraction = -1f;
@@ -252,6 +252,7 @@
private float mQsExpansion;
final Set<KeyguardViewManagerCallback> mCallbacks = new HashSet<>();
private boolean mIsModernBouncerEnabled;
+ private boolean mIsUnoccludeTransitionFlagEnabled;
private OnDismissAction mAfterKeyguardGoneAction;
private Runnable mKeyguardGoneCancelAction;
@@ -329,6 +330,7 @@
mFoldAodAnimationController = sysUIUnfoldComponent
.map(SysUIUnfoldComponent::getFoldAodAnimationController).orElse(null);
mIsModernBouncerEnabled = featureFlags.isEnabled(Flags.MODERN_BOUNCER);
+ mIsUnoccludeTransitionFlagEnabled = featureFlags.isEnabled(Flags.UNOCCLUSION_TRANSITION);
}
@Override
@@ -867,8 +869,10 @@
// by a FLAG_DISMISS_KEYGUARD_ACTIVITY.
reset(isOccluding /* hideBouncerWhenShowing*/);
}
- if (animate && !isOccluded && isShowing && !primaryBouncerIsShowing()) {
- mCentralSurfaces.animateKeyguardUnoccluding();
+ if (!mIsUnoccludeTransitionFlagEnabled) {
+ if (animate && !isOccluded && isShowing && !primaryBouncerIsShowing()) {
+ mCentralSurfaces.animateKeyguardUnoccluding();
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
index d8a8c5d..c9ed0cb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
@@ -47,6 +47,7 @@
import android.view.View;
import android.view.ViewAnimationUtils;
import android.view.ViewGroup;
+import android.view.ViewGroupOverlay;
import android.view.ViewRootImpl;
import android.view.WindowInsets;
import android.view.WindowInsetsAnimation;
@@ -57,7 +58,6 @@
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
-import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
@@ -133,6 +133,7 @@
private RevealParams mRevealParams;
private Rect mContentBackgroundBounds;
private boolean mIsFocusAnimationFlagActive;
+ private boolean mIsAnimatingAppearance = false;
// TODO(b/193539698): move these to a Controller
private RemoteInputController mController;
@@ -142,10 +143,6 @@
private boolean mSending;
private NotificationViewWrapper mWrapper;
- private Integer mDefocusTargetHeight = null;
- private boolean mIsAnimatingAppearance = false;
-
-
// TODO(b/193539698): remove this; views shouldn't have access to their controller, and places
// that need the controller shouldn't have access to the view
private RemoteInputViewController mViewController;
@@ -423,18 +420,6 @@
return mIsAnimatingAppearance;
}
- /**
- * View will ensure to use at most the provided defocusTargetHeight, when defocusing animated.
- * This is to ensure that the parent can resize itself to the targetHeight while the defocus
- * animation of the RemoteInputView is running.
- *
- * @param defocusTargetHeight The target height the parent will resize itself to. If null, the
- * RemoteInputView will not resize itself.
- */
- public void setDefocusTargetHeight(Integer defocusTargetHeight) {
- mDefocusTargetHeight = defocusTargetHeight;
- }
-
@VisibleForTesting
void onDefocus(boolean animate, boolean logClose) {
mController.removeRemoteInput(mEntry, mToken);
@@ -443,35 +428,28 @@
// During removal, we get reattached and lose focus. Not hiding in that
// case to prevent flicker.
if (!mRemoved) {
- if (animate && mIsFocusAnimationFlagActive) {
- Animator animator = getDefocusAnimator();
+ ViewGroup parent = (ViewGroup) getParent();
+ if (animate && parent != null && mIsFocusAnimationFlagActive) {
- // When defocusing, the notification needs to shrink. Therefore, we need to free
- // up the space that is needed for the RemoteInputView. This is done by setting
- // a negative top margin of the height difference of the RemoteInputView and its
- // sibling (the actions_container_layout containing the Reply button)
- if (mDefocusTargetHeight != null && mDefocusTargetHeight < getHeight()
- && mDefocusTargetHeight >= 0
- && getLayoutParams() instanceof FrameLayout.LayoutParams) {
- int heightToShrink = getHeight() - mDefocusTargetHeight;
- FrameLayout.LayoutParams layoutParams =
- (FrameLayout.LayoutParams) getLayoutParams();
- layoutParams.topMargin = -heightToShrink;
- setLayoutParams(layoutParams);
- ((ViewGroup) getParent().getParent()).setClipChildren(false);
- }
+ ViewGroup grandParent = (ViewGroup) parent.getParent();
+ ViewGroupOverlay overlay = parent.getOverlay();
+
+ // After adding this RemoteInputView to the overlay of the parent (and thus removing
+ // it from the parent itself), the parent will shrink in height. This causes the
+ // overlay to be moved. To correct the position of the overlay we need to offset it.
+ int overlayOffsetY = getMaxSiblingHeight() - getHeight();
+ overlay.add(this);
+ if (grandParent != null) grandParent.setClipChildren(false);
+
+ Animator animator = getDefocusAnimator(overlayOffsetY);
+ View self = this;
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
- //reset top margin after the animation
- if (getLayoutParams() instanceof FrameLayout.LayoutParams) {
- FrameLayout.LayoutParams layoutParams =
- (FrameLayout.LayoutParams) getLayoutParams();
- layoutParams.topMargin = 0;
- setLayoutParams(layoutParams);
- ((ViewGroup) getParent().getParent()).setClipChildren(true);
- }
+ overlay.remove(self);
+ parent.addView(self);
+ if (grandParent != null) grandParent.setClipChildren(true);
setVisibility(GONE);
if (mWrapper != null) {
mWrapper.setRemoteInputVisible(false);
@@ -609,7 +587,7 @@
}
/**
- * Sets whether the feature flag for the updated inline reply animation is active or not.
+ * Sets whether the feature flag for the revised inline reply animation is active or not.
* @param active
*/
public void setIsFocusAnimationFlagActive(boolean active) {
@@ -846,6 +824,23 @@
}
}
+ /**
+ * @return max sibling height (0 in case of no siblings)
+ */
+ public int getMaxSiblingHeight() {
+ ViewGroup parentView = (ViewGroup) getParent();
+ int maxHeight = 0;
+ if (parentView == null) return 0;
+ for (int i = 0; i < parentView.getChildCount(); i++) {
+ View siblingView = parentView.getChildAt(i);
+ if (siblingView != this) maxHeight = Math.max(maxHeight, siblingView.getHeight());
+ }
+ return maxHeight;
+ }
+
+ /**
+ * Creates an animator for the focus animation.
+ */
private Animator getFocusAnimator(View crossFadeView) {
final Animator alphaAnimator = ObjectAnimator.ofFloat(this, View.ALPHA, 0f, 1f);
alphaAnimator.setStartDelay(FOCUS_ANIMATION_FADE_IN_DELAY);
@@ -854,7 +849,7 @@
ValueAnimator scaleAnimator = ValueAnimator.ofFloat(FOCUS_ANIMATION_MIN_SCALE, 1f);
scaleAnimator.addUpdateListener(valueAnimator -> {
- setFocusAnimationScaleY((float) scaleAnimator.getAnimatedValue());
+ setFocusAnimationScaleY((float) scaleAnimator.getAnimatedValue(), 0);
});
scaleAnimator.setDuration(FOCUS_ANIMATION_TOTAL_DURATION);
scaleAnimator.setInterpolator(InterpolatorsAndroidX.FAST_OUT_SLOW_IN);
@@ -875,21 +870,26 @@
return animatorSet;
}
- private Animator getDefocusAnimator() {
+ /**
+ * Creates an animator for the defocus animation.
+ *
+ * @param offsetY The RemoteInputView will be offset by offsetY during the animation
+ */
+ private Animator getDefocusAnimator(int offsetY) {
final Animator alphaAnimator = ObjectAnimator.ofFloat(this, View.ALPHA, 1f, 0f);
alphaAnimator.setDuration(FOCUS_ANIMATION_CROSSFADE_DURATION);
alphaAnimator.setInterpolator(InterpolatorsAndroidX.LINEAR);
ValueAnimator scaleAnimator = ValueAnimator.ofFloat(1f, FOCUS_ANIMATION_MIN_SCALE);
scaleAnimator.addUpdateListener(valueAnimator -> {
- setFocusAnimationScaleY((float) scaleAnimator.getAnimatedValue());
+ setFocusAnimationScaleY((float) scaleAnimator.getAnimatedValue(), offsetY);
});
scaleAnimator.setDuration(FOCUS_ANIMATION_TOTAL_DURATION);
scaleAnimator.setInterpolator(InterpolatorsAndroidX.FAST_OUT_SLOW_IN);
scaleAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation, boolean isReverse) {
- setFocusAnimationScaleY(1f);
+ setFocusAnimationScaleY(1f /* scaleY */, 0 /* verticalOffset */);
}
});
@@ -901,15 +901,21 @@
/**
* Sets affected view properties for a vertical scale animation
*
- * @param scaleY desired vertical view scale
+ * @param scaleY desired vertical view scale
+ * @param verticalOffset vertical offset to apply to the RemoteInputView during the animation
*/
- private void setFocusAnimationScaleY(float scaleY) {
+ private void setFocusAnimationScaleY(float scaleY, int verticalOffset) {
int verticalBoundOffset = (int) ((1f - scaleY) * 0.5f * mContentView.getHeight());
- mContentBackgroundBounds = new Rect(0, verticalBoundOffset, mContentView.getWidth(),
+ Rect contentBackgroundBounds = new Rect(0, verticalBoundOffset, mContentView.getWidth(),
mContentView.getHeight() - verticalBoundOffset);
- mContentBackground.setBounds(mContentBackgroundBounds);
+ mContentBackground.setBounds(contentBackgroundBounds);
mContentView.setBackground(mContentBackground);
- setTranslationY(verticalBoundOffset);
+ if (scaleY == 1f) {
+ mContentBackgroundBounds = null;
+ } else {
+ mContentBackgroundBounds = contentBackgroundBounds;
+ }
+ setTranslationY(verticalBoundOffset + verticalOffset);
}
/** Handler for button click on send action in IME. */
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
index 532fbaa..ad48e21 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt
@@ -331,7 +331,7 @@
return
}
- removeViewFromWindow(displayInfo)
+ removeViewFromWindow(displayInfo, removalReason)
// Prune anything that's already timed out before determining if we should re-display a
// different chipbar.
@@ -358,14 +358,14 @@
removeViewFromWindow(displayInfo)
}
- private fun removeViewFromWindow(displayInfo: DisplayInfo) {
+ private fun removeViewFromWindow(displayInfo: DisplayInfo, removalReason: String? = null) {
val view = displayInfo.view
if (view == null) {
logger.logViewRemovalIgnored(displayInfo.info.id, "View is null")
return
}
displayInfo.view = null // Need other places??
- animateViewOut(view) {
+ animateViewOut(view, removalReason) {
windowManager.removeView(view)
displayInfo.wakeLock?.release(displayInfo.info.wakeReason)
}
@@ -428,7 +428,11 @@
*
* @param onAnimationEnd an action that *must* be run once the animation finishes successfully.
*/
- internal open fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) {
+ internal open fun animateViewOut(
+ view: ViewGroup,
+ removalReason: String? = null,
+ onAnimationEnd: Runnable
+ ) {
onAnimationEnd.run()
}
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
index fd2c705..52980c3 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
@@ -211,7 +211,7 @@
)
}
- override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) {
+ override fun animateViewOut(view: ViewGroup, removalReason: String?, onAnimationEnd: Runnable) {
val innerView = view.getInnerView()
innerView.accessibilityLiveRegion = ACCESSIBILITY_LIVE_REGION_NONE
ViewHierarchyAnimator.animateRemoval(
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt
index 9653985..d6b3b22 100644
--- a/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt
@@ -21,6 +21,8 @@
import androidx.lifecycle.repeatOnLifecycle
import com.android.systemui.lifecycle.repeatWhenAttached
import java.util.function.Consumer
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
@@ -34,7 +36,10 @@
view: View,
flow: Flow<T>,
consumer: Consumer<T>,
+ coroutineContext: CoroutineContext = EmptyCoroutineContext,
state: Lifecycle.State = Lifecycle.State.CREATED,
) {
- view.repeatWhenAttached { repeatOnLifecycle(state) { flow.collect { consumer.accept(it) } } }
+ view.repeatWhenAttached(coroutineContext) {
+ repeatOnLifecycle(state) { flow.collect { consumer.accept(it) } }
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java
index c94c97c..be4bbdf 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewControllerTest.java
@@ -25,6 +25,7 @@
import android.testing.AndroidTestingRunner;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.plugins.ClockAnimations;
import com.android.systemui.statusbar.phone.DozeParameters;
import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
@@ -59,6 +60,8 @@
@Mock
DozeParameters mDozeParameters;
@Mock
+ FeatureFlags mFeatureFlags;
+ @Mock
ScreenOffAnimationController mScreenOffAnimationController;
@Captor
private ArgumentCaptor<KeyguardUpdateMonitorCallback> mKeyguardUpdateMonitorCallbackCaptor;
@@ -77,6 +80,7 @@
mKeyguardUpdateMonitor,
mConfigurationController,
mDozeParameters,
+ mFeatureFlags,
mScreenOffAnimationController);
}
@@ -96,9 +100,9 @@
public void setTranslationYExcludingMedia_forwardsCallToView() {
float translationY = 123f;
- mController.setTranslationYExcludingMedia(translationY);
+ mController.setTranslationY(translationY, /* excludeMedia= */true);
- verify(mKeyguardStatusView).setChildrenTranslationYExcludingMediaView(translationY);
+ verify(mKeyguardStatusView).setChildrenTranslationY(translationY, /* excludeMedia= */true);
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewTest.kt
index ce44f4d..508aea5 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardStatusViewTest.kt
@@ -37,19 +37,23 @@
fun setChildrenTranslationYExcludingMediaView_mediaViewIsNotTranslated() {
val translationY = 1234f
- keyguardStatusView.setChildrenTranslationYExcludingMediaView(translationY)
+ keyguardStatusView.setChildrenTranslationY(translationY, /* excludeMedia= */true)
assertThat(mediaView.translationY).isEqualTo(0)
- }
-
- @Test
- fun setChildrenTranslationYExcludingMediaView_childrenAreTranslated() {
- val translationY = 1234f
-
- keyguardStatusView.setChildrenTranslationYExcludingMediaView(translationY)
childrenExcludingMedia.forEach {
assertThat(it.translationY).isEqualTo(translationY)
}
}
-}
\ No newline at end of file
+
+ @Test
+ fun setChildrenTranslationYIncludeMediaView() {
+ val translationY = 1234f
+
+ keyguardStatusView.setChildrenTranslationY(translationY, /* excludeMedia= */false)
+
+ statusViewContainer.children.forEach {
+ assertThat(it.translationY).isEqualTo(translationY)
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUnfoldTransitionTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUnfoldTransitionTest.kt
index 6c1f008..bb03f28 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUnfoldTransitionTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUnfoldTransitionTest.kt
@@ -22,9 +22,13 @@
import androidx.test.filters.SmallTest
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.statusbar.StatusBarState.KEYGUARD
+import com.android.systemui.statusbar.StatusBarState.SHADE
import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
import com.android.systemui.unfold.util.NaturalRotationUnfoldProgressProvider
import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
@@ -33,7 +37,6 @@
import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
/**
@@ -50,7 +53,9 @@
@Mock private lateinit var parent: ViewGroup
- private lateinit var keyguardUnfoldTransition: KeyguardUnfoldTransition
+ @Mock private lateinit var statusBarStateController: StatusBarStateController
+
+ private lateinit var underTest: KeyguardUnfoldTransition
private lateinit var progressListener: TransitionProgressListener
private var xTranslationMax = 0f
@@ -61,10 +66,10 @@
xTranslationMax =
context.resources.getDimensionPixelSize(R.dimen.keyguard_unfold_translation_x).toFloat()
- keyguardUnfoldTransition = KeyguardUnfoldTransition(context, progressProvider)
+ underTest = KeyguardUnfoldTransition(context, statusBarStateController, progressProvider)
- keyguardUnfoldTransition.setup(parent)
- keyguardUnfoldTransition.statusViewCentered = false
+ underTest.setup(parent)
+ underTest.statusViewCentered = false
verify(progressProvider).addCallback(capture(progressListenerCaptor))
progressListener = progressListenerCaptor.value
@@ -72,10 +77,11 @@
@Test
fun onTransition_centeredViewDoesNotMove() {
- keyguardUnfoldTransition.statusViewCentered = true
+ whenever(statusBarStateController.getState()).thenReturn(KEYGUARD)
+ underTest.statusViewCentered = true
val view = View(context)
- `when`(parent.findViewById<View>(R.id.lockscreen_clock_view_large)).thenReturn(view)
+ whenever(parent.findViewById<View>(R.id.lockscreen_clock_view_large)).thenReturn(view)
progressListener.onTransitionStarted()
assertThat(view.translationX).isZero()
@@ -89,4 +95,44 @@
progressListener.onTransitionFinished()
assertThat(view.translationX).isZero()
}
+
+ @Test
+ fun whenInShadeState_viewDoesNotMove() {
+ whenever(statusBarStateController.getState()).thenReturn(SHADE)
+
+ val view = View(context)
+ whenever(parent.findViewById<View>(R.id.lockscreen_clock_view_large)).thenReturn(view)
+
+ progressListener.onTransitionStarted()
+ assertThat(view.translationX).isZero()
+
+ progressListener.onTransitionProgress(0f)
+ assertThat(view.translationX).isZero()
+
+ progressListener.onTransitionProgress(0.5f)
+ assertThat(view.translationX).isZero()
+
+ progressListener.onTransitionFinished()
+ assertThat(view.translationX).isZero()
+ }
+
+ @Test
+ fun whenInKeyguardState_viewDoesMove() {
+ whenever(statusBarStateController.getState()).thenReturn(KEYGUARD)
+
+ val view = View(context)
+ whenever(parent.findViewById<View>(R.id.lockscreen_clock_view_large)).thenReturn(view)
+
+ progressListener.onTransitionStarted()
+ assertThat(view.translationX).isZero()
+
+ progressListener.onTransitionProgress(0f)
+ assertThat(view.translationX).isEqualTo(xTranslationMax)
+
+ progressListener.onTransitionProgress(0.5f)
+ assertThat(view.translationX).isEqualTo(0.5f * xTranslationMax)
+
+ progressListener.onTransitionFinished()
+ assertThat(view.translationX).isZero()
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java
index 20d3cd5..d910a27 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeSensorsTest.java
@@ -35,6 +35,7 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.content.res.Resources;
import android.database.ContentObserver;
import android.hardware.Sensor;
import android.hardware.display.AmbientDisplayConfiguration;
@@ -76,7 +77,8 @@
@RunWithLooper
@SmallTest
public class DozeSensorsTest extends SysuiTestCase {
-
+ @Mock
+ private Resources mResources;
@Mock
private AsyncSensorManager mSensorManager;
@Mock
@@ -426,7 +428,7 @@
@Test
public void testGesturesAllInitiallyRespectSettings() {
- DozeSensors dozeSensors = new DozeSensors(mSensorManager, mDozeParameters,
+ DozeSensors dozeSensors = new DozeSensors(mResources, mSensorManager, mDozeParameters,
mAmbientDisplayConfiguration, mWakeLock, mCallback, mProxCallback, mDozeLog,
mProximitySensor, mFakeSettings, mAuthController,
mDevicePostureController, mUserTracker);
@@ -436,9 +438,47 @@
}
}
+ @Test
+ public void liftToWake_defaultSetting_configDefaultFalse() {
+ // WHEN the default lift to wake gesture setting is false
+ when(mResources.getBoolean(
+ com.android.internal.R.bool.config_dozePickupGestureEnabled)).thenReturn(false);
+
+ DozeSensors dozeSensors = new DozeSensors(mResources, mSensorManager, mDozeParameters,
+ mAmbientDisplayConfiguration, mWakeLock, mCallback, mProxCallback, mDozeLog,
+ mProximitySensor, mFakeSettings, mAuthController,
+ mDevicePostureController, mUserTracker);
+
+ for (TriggerSensor sensor : dozeSensors.mTriggerSensors) {
+ // THEN lift to wake's TriggerSensor enabledBySettings is false
+ if (sensor.mPulseReason == DozeLog.REASON_SENSOR_PICKUP) {
+ assertFalse(sensor.enabledBySetting());
+ }
+ }
+ }
+
+ @Test
+ public void liftToWake_defaultSetting_configDefaultTrue() {
+ // WHEN the default lift to wake gesture setting is true
+ when(mResources.getBoolean(
+ com.android.internal.R.bool.config_dozePickupGestureEnabled)).thenReturn(true);
+
+ DozeSensors dozeSensors = new DozeSensors(mResources, mSensorManager, mDozeParameters,
+ mAmbientDisplayConfiguration, mWakeLock, mCallback, mProxCallback, mDozeLog,
+ mProximitySensor, mFakeSettings, mAuthController,
+ mDevicePostureController, mUserTracker);
+
+ for (TriggerSensor sensor : dozeSensors.mTriggerSensors) {
+ // THEN lift to wake's TriggerSensor enabledBySettings is true
+ if (sensor.mPulseReason == DozeLog.REASON_SENSOR_PICKUP) {
+ assertTrue(sensor.enabledBySetting());
+ }
+ }
+ }
+
private class TestableDozeSensors extends DozeSensors {
TestableDozeSensors() {
- super(mSensorManager, mDozeParameters,
+ super(mResources, mSensorManager, mDozeParameters,
mAmbientDisplayConfiguration, mWakeLock, mCallback, mProxCallback, mDozeLog,
mProximitySensor, mFakeSettings, mAuthController,
mDevicePostureController, mUserTracker);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamCallbackControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamCallbackControllerTest.kt
deleted file mode 100644
index 003efbf..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamCallbackControllerTest.kt
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Copyright (C) 2022 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.dreams
-
-import android.testing.AndroidTestingRunner
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.Mockito.never
-import org.mockito.Mockito.reset
-import org.mockito.Mockito.times
-import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
-
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-class DreamCallbackControllerTest : SysuiTestCase() {
-
- @Mock private lateinit var callback: DreamCallbackController.DreamCallback
-
- private lateinit var underTest: DreamCallbackController
-
- @Before
- fun setUp() {
- MockitoAnnotations.initMocks(this)
- underTest = DreamCallbackController()
- }
-
- @Test
- fun testOnWakeUpInvokesCallback() {
- underTest.addCallback(callback)
- underTest.onWakeUp()
- verify(callback).onWakeUp()
-
- // Adding twice should not invoke twice
- reset(callback)
- underTest.addCallback(callback)
- underTest.onWakeUp()
- verify(callback, times(1)).onWakeUp()
-
- // After remove, no call to callback
- reset(callback)
- underTest.removeCallback(callback)
- underTest.onWakeUp()
- verify(callback, never()).onWakeUp()
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayCallbackControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayCallbackControllerTest.kt
new file mode 100644
index 0000000..9f534ef
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayCallbackControllerTest.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2022 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.dreams
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class DreamOverlayCallbackControllerTest : SysuiTestCase() {
+
+ @Mock private lateinit var callback: DreamOverlayCallbackController.Callback
+
+ private lateinit var underTest: DreamOverlayCallbackController
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ underTest = DreamOverlayCallbackController()
+ }
+
+ @Test
+ fun onWakeUpInvokesCallback() {
+ underTest.onStartDream()
+ assertThat(underTest.isDreaming).isEqualTo(true)
+
+ underTest.addCallback(callback)
+ underTest.onWakeUp()
+ verify(callback).onWakeUp()
+ assertThat(underTest.isDreaming).isEqualTo(false)
+
+ // Adding twice should not invoke twice
+ reset(callback)
+ underTest.addCallback(callback)
+ underTest.onWakeUp()
+ verify(callback, times(1)).onWakeUp()
+
+ // After remove, no call to callback
+ reset(callback)
+ underTest.removeCallback(callback)
+ underTest.onWakeUp()
+ verify(callback, never()).onWakeUp()
+ }
+
+ @Test
+ fun onStartDreamInvokesCallback() {
+ underTest.addCallback(callback)
+
+ assertThat(underTest.isDreaming).isEqualTo(false)
+
+ underTest.onStartDream()
+ verify(callback).onStartDream()
+ assertThat(underTest.isDreaming).isEqualTo(true)
+
+ // Adding twice should not invoke twice
+ reset(callback)
+ underTest.addCallback(callback)
+ underTest.onStartDream()
+ verify(callback, times(1)).onStartDream()
+
+ // After remove, no call to callback
+ reset(callback)
+ underTest.removeCallback(callback)
+ underTest.onStartDream()
+ verify(callback, never()).onStartDream()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
index d6f8dea..4568d1e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
@@ -114,7 +114,7 @@
UiEventLogger mUiEventLogger;
@Mock
- DreamCallbackController mDreamCallbackController;
+ DreamOverlayCallbackController mDreamOverlayCallbackController;
@Captor
ArgumentCaptor<View> mViewCaptor;
@@ -145,7 +145,7 @@
mKeyguardUpdateMonitor,
mUiEventLogger,
LOW_LIGHT_COMPONENT,
- mDreamCallbackController);
+ mDreamOverlayCallbackController);
}
@Test
@@ -357,7 +357,7 @@
mService.onWakeUp(callback);
mMainExecutor.runAllReady();
verify(mDreamOverlayContainerViewController).wakeUp(callback, mMainExecutor);
- verify(mDreamCallbackController).onWakeUp();
+ verify(mDreamOverlayCallbackController).onWakeUp();
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
index 563d44d3..be712f6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
@@ -28,8 +28,7 @@
import com.android.systemui.doze.DozeMachine
import com.android.systemui.doze.DozeTransitionCallback
import com.android.systemui.doze.DozeTransitionListener
-import com.android.systemui.dreams.DreamCallbackController
-import com.android.systemui.dreams.DreamCallbackController.DreamCallback
+import com.android.systemui.dreams.DreamOverlayCallbackController
import com.android.systemui.keyguard.WakefulnessLifecycle
import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
import com.android.systemui.keyguard.shared.model.BiometricUnlockSource
@@ -68,7 +67,7 @@
@Mock private lateinit var dozeTransitionListener: DozeTransitionListener
@Mock private lateinit var authController: AuthController
@Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
- @Mock private lateinit var dreamCallbackController: DreamCallbackController
+ @Mock private lateinit var dreamOverlayCallbackController: DreamOverlayCallbackController
private lateinit var underTest: KeyguardRepositoryImpl
@@ -86,7 +85,7 @@
keyguardUpdateMonitor,
dozeTransitionListener,
authController,
- dreamCallbackController,
+ dreamOverlayCallbackController,
)
}
@@ -171,6 +170,29 @@
}
@Test
+ fun isKeyguardOccluded() =
+ runTest(UnconfinedTestDispatcher()) {
+ whenever(keyguardStateController.isOccluded).thenReturn(false)
+ var latest: Boolean? = null
+ val job = underTest.isKeyguardOccluded.onEach { latest = it }.launchIn(this)
+
+ assertThat(latest).isFalse()
+
+ val captor = argumentCaptor<KeyguardStateController.Callback>()
+ verify(keyguardStateController).addCallback(captor.capture())
+
+ whenever(keyguardStateController.isOccluded).thenReturn(true)
+ captor.value.onKeyguardShowingChanged()
+ assertThat(latest).isTrue()
+
+ whenever(keyguardStateController.isOccluded).thenReturn(false)
+ captor.value.onKeyguardShowingChanged()
+ assertThat(latest).isFalse()
+
+ job.cancel()
+ }
+
+ @Test
fun isDozing() =
runTest(UnconfinedTestDispatcher()) {
var latest: Boolean? = null
@@ -343,19 +365,22 @@
}
@Test
- fun isDreamingFromDreamCallbackController() =
+ fun isDreamingFromDreamOverlayCallbackController() =
runTest(UnconfinedTestDispatcher()) {
- whenever(keyguardUpdateMonitor.isDreaming()).thenReturn(true)
+ whenever(dreamOverlayCallbackController.isDreaming).thenReturn(false)
var latest: Boolean? = null
- val job = underTest.isDreaming.onEach { latest = it }.launchIn(this)
+ val job = underTest.isDreamingWithOverlay.onEach { latest = it }.launchIn(this)
- assertThat(latest).isTrue()
+ assertThat(latest).isFalse()
val listener =
- withArgCaptor<DreamCallbackController.DreamCallback> {
- verify(dreamCallbackController).addCallback(capture())
+ withArgCaptor<DreamOverlayCallbackController.Callback> {
+ verify(dreamOverlayCallbackController).addCallback(capture())
}
+ listener.onStartDream()
+ assertThat(latest).isTrue()
+
listener.onWakeUp()
assertThat(latest).isFalse()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
index a6cf840..d2b7838 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
@@ -23,6 +23,8 @@
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepositoryImpl
+import com.android.systemui.keyguard.shared.model.DozeStateModel
+import com.android.systemui.keyguard.shared.model.DozeTransitionModel
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionInfo
import com.android.systemui.keyguard.shared.model.WakeSleepReason
@@ -42,6 +44,7 @@
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.mockito.Mock
+import org.mockito.Mockito.reset
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@@ -64,8 +67,8 @@
// Used to verify transition requests for test output
@Mock private lateinit var mockTransitionRepository: KeyguardTransitionRepository
- private lateinit var lockscreenBouncerTransitionInteractor:
- LockscreenBouncerTransitionInteractor
+ private lateinit var fromLockscreenTransitionInteractor: FromLockscreenTransitionInteractor
+ private lateinit var fromDreamingTransitionInteractor: FromDreamingTransitionInteractor
@Before
fun setUp() {
@@ -79,25 +82,82 @@
transitionRepository = KeyguardTransitionRepositoryImpl()
runner = KeyguardTransitionRunner(transitionRepository)
- lockscreenBouncerTransitionInteractor =
- LockscreenBouncerTransitionInteractor(
+ fromLockscreenTransitionInteractor =
+ FromLockscreenTransitionInteractor(
scope = testScope,
keyguardInteractor = KeyguardInteractor(keyguardRepository),
shadeRepository = shadeRepository,
keyguardTransitionRepository = mockTransitionRepository,
keyguardTransitionInteractor = KeyguardTransitionInteractor(transitionRepository),
)
- lockscreenBouncerTransitionInteractor.start()
+ fromLockscreenTransitionInteractor.start()
+
+ fromDreamingTransitionInteractor =
+ FromDreamingTransitionInteractor(
+ scope = testScope,
+ keyguardInteractor = KeyguardInteractor(keyguardRepository),
+ keyguardTransitionRepository = mockTransitionRepository,
+ keyguardTransitionInteractor = KeyguardTransitionInteractor(transitionRepository),
+ )
+ fromDreamingTransitionInteractor.start()
}
@Test
+ fun `DREAMING to LOCKSCREEN`() =
+ testScope.runTest {
+ // GIVEN a device is dreaming and occluded
+ keyguardRepository.setDreamingWithOverlay(true)
+ keyguardRepository.setKeyguardOccluded(true)
+ runCurrent()
+
+ // GIVEN a prior transition has run to DREAMING
+ runner.startTransition(
+ testScope,
+ TransitionInfo(
+ ownerName = "",
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.DREAMING,
+ animator =
+ ValueAnimator().apply {
+ duration = 10
+ interpolator = Interpolators.LINEAR
+ },
+ )
+ )
+ runCurrent()
+ reset(mockTransitionRepository)
+
+ // WHEN doze is complete
+ keyguardRepository.setDozeTransitionModel(
+ DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH)
+ )
+ // AND dreaming has stopped
+ keyguardRepository.setDreamingWithOverlay(false)
+ // AND occluded has stopped
+ keyguardRepository.setKeyguardOccluded(false)
+ runCurrent()
+
+ val info =
+ withArgCaptor<TransitionInfo> {
+ verify(mockTransitionRepository).startTransition(capture())
+ }
+ // THEN a transition to BOUNCER should occur
+ assertThat(info.ownerName).isEqualTo("FromDreamingTransitionInteractor")
+ assertThat(info.from).isEqualTo(KeyguardState.DREAMING)
+ assertThat(info.to).isEqualTo(KeyguardState.LOCKSCREEN)
+ assertThat(info.animator).isNotNull()
+
+ coroutineContext.cancelChildren()
+ }
+
+ @Test
fun `LOCKSCREEN to BOUNCER via bouncer showing call`() =
testScope.runTest {
// GIVEN a device that has at least woken up
keyguardRepository.setWakefulnessModel(startingToWake())
runCurrent()
- // GIVEN a transition has run to LOCKSCREEN
+ // GIVEN a prior transition has run to LOCKSCREEN
runner.startTransition(
testScope,
TransitionInfo(
@@ -122,7 +182,7 @@
verify(mockTransitionRepository).startTransition(capture())
}
// THEN a transition to BOUNCER should occur
- assertThat(info.ownerName).isEqualTo("LockscreenBouncerTransitionInteractor")
+ assertThat(info.ownerName).isEqualTo("FromLockscreenTransitionInteractor")
assertThat(info.from).isEqualTo(KeyguardState.LOCKSCREEN)
assertThat(info.to).isEqualTo(KeyguardState.BOUNCER)
assertThat(info.animator).isNotNull()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt
index 81950f1..5571663 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt
@@ -19,8 +19,9 @@
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE
+import com.android.systemui.animation.Interpolators.EMPHASIZED_DECELERATE
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.domain.interactor.DreamingTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
+import com.android.systemui.keyguard.domain.interactor.FromDreamingTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.AnimationParams
import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -28,6 +29,8 @@
import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.Companion.DREAM_OVERLAY_ALPHA
import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.Companion.DREAM_OVERLAY_TRANSLATION_Y
+import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.Companion.LOCKSCREEN_ALPHA
+import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.Companion.LOCKSCREEN_TRANSLATION_Y
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -65,10 +68,9 @@
repository.sendTransitionStep(step(0.5f))
repository.sendTransitionStep(step(1f))
- // Only two values should be present, since the dream overlay runs for a small fraction
- // of
- // the overall animation time
- assertThat(values.size).isEqualTo(2)
+ // Only 3 values should be present, since the dream overlay runs for a small fraction
+ // of the overall animation time
+ assertThat(values.size).isEqualTo(3)
assertThat(values[0])
.isEqualTo(
EMPHASIZED_ACCELERATE.getInterpolation(
@@ -81,6 +83,12 @@
animValue(0.3f, DREAM_OVERLAY_TRANSLATION_Y)
) * pixels
)
+ assertThat(values[2])
+ .isEqualTo(
+ EMPHASIZED_ACCELERATE.getInterpolation(
+ animValue(0.5f, DREAM_OVERLAY_TRANSLATION_Y)
+ ) * pixels
+ )
job.cancel()
}
@@ -98,8 +106,7 @@
repository.sendTransitionStep(step(1f))
// Only two values should be present, since the dream overlay runs for a small fraction
- // of
- // the overall animation time
+ // of the overall animation time
assertThat(values.size).isEqualTo(2)
assertThat(values[0]).isEqualTo(1f - animValue(0f, DREAM_OVERLAY_ALPHA))
assertThat(values[1]).isEqualTo(1f - animValue(0.1f, DREAM_OVERLAY_ALPHA))
@@ -107,6 +114,77 @@
job.cancel()
}
+ @Test
+ fun lockscreenFadeIn() =
+ runTest(UnconfinedTestDispatcher()) {
+ val values = mutableListOf<Float>()
+
+ val job = underTest.lockscreenAlpha.onEach { values.add(it) }.launchIn(this)
+
+ repository.sendTransitionStep(step(0f))
+ repository.sendTransitionStep(step(0.1f))
+ // Should start running here...
+ repository.sendTransitionStep(step(0.2f))
+ repository.sendTransitionStep(step(0.3f))
+ // ...up to here
+ repository.sendTransitionStep(step(1f))
+
+ // Only two values should be present, since the dream overlay runs for a small fraction
+ // of the overall animation time
+ assertThat(values.size).isEqualTo(2)
+ assertThat(values[0]).isEqualTo(animValue(0.2f, LOCKSCREEN_ALPHA))
+ assertThat(values[1]).isEqualTo(animValue(0.3f, LOCKSCREEN_ALPHA))
+
+ job.cancel()
+ }
+
+ @Test
+ fun lockscreenTranslationY() =
+ runTest(UnconfinedTestDispatcher()) {
+ val values = mutableListOf<Float>()
+
+ val pixels = 100
+ val job =
+ underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this)
+
+ repository.sendTransitionStep(step(0f))
+ repository.sendTransitionStep(step(0.3f))
+ repository.sendTransitionStep(step(0.5f))
+ repository.sendTransitionStep(step(1f))
+
+ assertThat(values.size).isEqualTo(4)
+ assertThat(values[0])
+ .isEqualTo(
+ -pixels +
+ EMPHASIZED_DECELERATE.getInterpolation(
+ animValue(0f, LOCKSCREEN_TRANSLATION_Y)
+ ) * pixels
+ )
+ assertThat(values[1])
+ .isEqualTo(
+ -pixels +
+ EMPHASIZED_DECELERATE.getInterpolation(
+ animValue(0.3f, LOCKSCREEN_TRANSLATION_Y)
+ ) * pixels
+ )
+ assertThat(values[2])
+ .isEqualTo(
+ -pixels +
+ EMPHASIZED_DECELERATE.getInterpolation(
+ animValue(0.5f, LOCKSCREEN_TRANSLATION_Y)
+ ) * pixels
+ )
+ assertThat(values[3])
+ .isEqualTo(
+ -pixels +
+ EMPHASIZED_DECELERATE.getInterpolation(
+ animValue(1f, LOCKSCREEN_TRANSLATION_Y)
+ ) * pixels
+ )
+
+ job.cancel()
+ }
+
private fun animValue(stepValue: Float, params: AnimationParams): Float {
val totalDuration = TO_LOCKSCREEN_DURATION
val startValue = (params.startTime / totalDuration).toFloat()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt
new file mode 100644
index 0000000..98d292d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2022 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.keyguard.ui.viewmodel
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.Interpolators.EMPHASIZED_DECELERATE
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.domain.interactor.FromOccludedTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.AnimationParams
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel.Companion.LOCKSCREEN_ALPHA
+import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel.Companion.LOCKSCREEN_TRANSLATION_Y
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@SmallTest
+@RunWith(JUnit4::class)
+class OccludedToLockscreenTransitionViewModelTest : SysuiTestCase() {
+ private lateinit var underTest: OccludedToLockscreenTransitionViewModel
+ private lateinit var repository: FakeKeyguardTransitionRepository
+
+ @Before
+ fun setUp() {
+ repository = FakeKeyguardTransitionRepository()
+ val interactor = KeyguardTransitionInteractor(repository)
+ underTest = OccludedToLockscreenTransitionViewModel(interactor)
+ }
+
+ @Test
+ fun lockscreenFadeIn() =
+ runTest(UnconfinedTestDispatcher()) {
+ val values = mutableListOf<Float>()
+
+ val job = underTest.lockscreenAlpha.onEach { values.add(it) }.launchIn(this)
+
+ repository.sendTransitionStep(step(0f))
+ // Should start running here...
+ repository.sendTransitionStep(step(0.3f))
+ repository.sendTransitionStep(step(0.4f))
+ repository.sendTransitionStep(step(0.5f))
+ // ...up to here
+ repository.sendTransitionStep(step(0.6f))
+ repository.sendTransitionStep(step(1f))
+
+ // Only two values should be present, since the dream overlay runs for a small fraction
+ // of the overall animation time
+ assertThat(values.size).isEqualTo(3)
+ assertThat(values[0]).isEqualTo(animValue(0.3f, LOCKSCREEN_ALPHA))
+ assertThat(values[1]).isEqualTo(animValue(0.4f, LOCKSCREEN_ALPHA))
+ assertThat(values[2]).isEqualTo(animValue(0.5f, LOCKSCREEN_ALPHA))
+
+ job.cancel()
+ }
+
+ @Test
+ fun lockscreenTranslationY() =
+ runTest(UnconfinedTestDispatcher()) {
+ val values = mutableListOf<Float>()
+
+ val pixels = 100
+ val job =
+ underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this)
+
+ repository.sendTransitionStep(step(0f))
+ repository.sendTransitionStep(step(0.3f))
+ repository.sendTransitionStep(step(0.5f))
+ repository.sendTransitionStep(step(1f))
+
+ assertThat(values.size).isEqualTo(4)
+ assertThat(values[0])
+ .isEqualTo(
+ -pixels +
+ EMPHASIZED_DECELERATE.getInterpolation(
+ animValue(0f, LOCKSCREEN_TRANSLATION_Y)
+ ) * pixels
+ )
+ assertThat(values[1])
+ .isEqualTo(
+ -pixels +
+ EMPHASIZED_DECELERATE.getInterpolation(
+ animValue(0.3f, LOCKSCREEN_TRANSLATION_Y)
+ ) * pixels
+ )
+ assertThat(values[2])
+ .isEqualTo(
+ -pixels +
+ EMPHASIZED_DECELERATE.getInterpolation(
+ animValue(0.5f, LOCKSCREEN_TRANSLATION_Y)
+ ) * pixels
+ )
+ assertThat(values[3])
+ .isEqualTo(
+ -pixels +
+ EMPHASIZED_DECELERATE.getInterpolation(
+ animValue(1f, LOCKSCREEN_TRANSLATION_Y)
+ ) * pixels
+ )
+
+ job.cancel()
+ }
+
+ private fun animValue(stepValue: Float, params: AnimationParams): Float {
+ val totalDuration = TO_LOCKSCREEN_DURATION
+ val startValue = (params.startTime / totalDuration).toFloat()
+
+ val multiplier = (totalDuration / params.duration).toFloat()
+ return (stepValue - startValue) * multiplier
+ }
+
+ private fun step(value: Float): TransitionStep {
+ return TransitionStep(
+ from = KeyguardState.OCCLUDED,
+ to = KeyguardState.LOCKSCREEN,
+ value = value,
+ transitionState = TransitionState.RUNNING,
+ ownerName = "OccludedToLockscreenTransitionViewModelTest"
+ )
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/FakeMediaTttChipControllerReceiver.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/FakeMediaTttChipControllerReceiver.kt
index 07a3109..9c4e849 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/FakeMediaTttChipControllerReceiver.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/FakeMediaTttChipControllerReceiver.kt
@@ -66,7 +66,7 @@
wakeLockBuilder,
systemClock,
) {
- override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) {
+ override fun animateViewOut(view: ViewGroup, removalReason: String?, onAnimationEnd: Runnable) {
// Just bypass the animation in tests
onAnimationEnd.run()
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt
index 03ba3d3..cefc742 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt
@@ -98,6 +98,7 @@
fun setUp() {
MockitoAnnotations.initMocks(this)
whenever(mediaTttFlags.isMediaTttEnabled()).thenReturn(true)
+ whenever(mediaTttFlags.isMediaTttReceiverSuccessRippleEnabled()).thenReturn(true)
fakeAppIconDrawable = context.getDrawable(R.drawable.ic_cake)!!
whenever(packageManager.getApplicationIcon(PACKAGE_NAME)).thenReturn(fakeAppIconDrawable)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelUnfoldAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelUnfoldAnimationControllerTest.kt
new file mode 100644
index 0000000..db6fc13
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelUnfoldAnimationControllerTest.kt
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2021 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.shade
+
+import android.testing.AndroidTestingRunner
+import android.view.View
+import android.view.ViewGroup
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.statusbar.StatusBarState.KEYGUARD
+import com.android.systemui.statusbar.StatusBarState.SHADE
+import com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED
+import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
+import com.android.systemui.unfold.util.NaturalRotationUnfoldProgressProvider
+import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+/**
+ * Translates items away/towards the hinge when the device is opened/closed. This is controlled by
+ * the set of ids, which also dictact which direction to move and when, via a filter fn.
+ */
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class NotificationPanelUnfoldAnimationControllerTest : SysuiTestCase() {
+
+ @Mock private lateinit var progressProvider: NaturalRotationUnfoldProgressProvider
+
+ @Captor private lateinit var progressListenerCaptor: ArgumentCaptor<TransitionProgressListener>
+
+ @Mock private lateinit var parent: ViewGroup
+
+ @Mock private lateinit var statusBarStateController: StatusBarStateController
+
+ private lateinit var underTest: NotificationPanelUnfoldAnimationController
+ private lateinit var progressListener: TransitionProgressListener
+ private var xTranslationMax = 0f
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+
+ xTranslationMax =
+ context.resources.getDimensionPixelSize(R.dimen.notification_side_paddings).toFloat()
+
+ underTest =
+ NotificationPanelUnfoldAnimationController(
+ context,
+ statusBarStateController,
+ progressProvider
+ )
+ underTest.setup(parent)
+
+ verify(progressProvider).addCallback(capture(progressListenerCaptor))
+ progressListener = progressListenerCaptor.value
+ }
+
+ @Test
+ fun whenInKeyguardState_viewDoesNotMove() {
+ whenever(statusBarStateController.getState()).thenReturn(KEYGUARD)
+
+ val view = View(context)
+ whenever(parent.findViewById<View>(R.id.quick_settings_panel)).thenReturn(view)
+
+ progressListener.onTransitionStarted()
+ assertThat(view.translationX).isZero()
+
+ progressListener.onTransitionProgress(0f)
+ assertThat(view.translationX).isZero()
+
+ progressListener.onTransitionProgress(0.5f)
+ assertThat(view.translationX).isZero()
+
+ progressListener.onTransitionFinished()
+ assertThat(view.translationX).isZero()
+ }
+
+ @Test
+ fun whenInShadeState_viewDoesMove() {
+ whenever(statusBarStateController.getState()).thenReturn(SHADE)
+
+ val view = View(context)
+ whenever(parent.findViewById<View>(R.id.quick_settings_panel)).thenReturn(view)
+
+ progressListener.onTransitionStarted()
+ assertThat(view.translationX).isZero()
+
+ progressListener.onTransitionProgress(0f)
+ assertThat(view.translationX).isEqualTo(xTranslationMax)
+
+ progressListener.onTransitionProgress(0.5f)
+ assertThat(view.translationX).isEqualTo(0.5f * xTranslationMax)
+
+ progressListener.onTransitionFinished()
+ assertThat(view.translationX).isZero()
+ }
+
+ @Test
+ fun whenInShadeLockedState_viewDoesMove() {
+ whenever(statusBarStateController.getState()).thenReturn(SHADE_LOCKED)
+
+ val view = View(context)
+ whenever(parent.findViewById<View>(R.id.quick_settings_panel)).thenReturn(view)
+
+ progressListener.onTransitionStarted()
+ assertThat(view.translationX).isZero()
+
+ progressListener.onTransitionProgress(0f)
+ assertThat(view.translationX).isEqualTo(xTranslationMax)
+
+ progressListener.onTransitionProgress(0.5f)
+ assertThat(view.translationX).isEqualTo(0.5f * xTranslationMax)
+
+ progressListener.onTransitionFinished()
+ assertThat(view.translationX).isZero()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
index 01f13e66..65b2ac0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
@@ -104,7 +104,10 @@
import com.android.systemui.fragments.FragmentService;
import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
+import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel;
import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel;
+import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel;
import com.android.systemui.media.controls.pipeline.MediaDataManager;
import com.android.systemui.media.controls.ui.KeyguardMediaController;
import com.android.systemui.media.controls.ui.MediaHierarchyManager;
@@ -186,6 +189,8 @@
import java.util.List;
import java.util.Optional;
+import kotlinx.coroutines.CoroutineDispatcher;
+
@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
@@ -286,6 +291,10 @@
@Mock private ViewTreeObserver mViewTreeObserver;
@Mock private KeyguardBottomAreaViewModel mKeyguardBottomAreaViewModel;
@Mock private KeyguardBottomAreaInteractor mKeyguardBottomAreaInteractor;
+ @Mock private DreamingToLockscreenTransitionViewModel mDreamingToLockscreenTransitionViewModel;
+ @Mock private OccludedToLockscreenTransitionViewModel mOccludedToLockscreenTransitionViewModel;
+ @Mock private KeyguardTransitionInteractor mKeyguardTransitionInteractor;
+ @Mock private CoroutineDispatcher mMainDispatcher;
@Mock private MotionEvent mDownMotionEvent;
@Captor
private ArgumentCaptor<NotificationStackScrollLayout.OnEmptySpaceClickListener>
@@ -502,6 +511,10 @@
systemClock,
mKeyguardBottomAreaViewModel,
mKeyguardBottomAreaInteractor,
+ mDreamingToLockscreenTransitionViewModel,
+ mOccludedToLockscreenTransitionViewModel,
+ mMainDispatcher,
+ mKeyguardTransitionInteractor,
mDumpManager);
mNotificationPanelViewController.initDependencies(
mCentralSurfaces,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
index 2c47204..4b32ee2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
@@ -55,6 +55,7 @@
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.widget.EditText;
+import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.window.OnBackInvokedCallback;
import android.window.OnBackInvokedDispatcher;
@@ -414,7 +415,9 @@
mDependency,
TestableLooper.get(this));
ExpandableNotificationRow row = helper.createRow();
+ FrameLayout remoteInputViewParent = new FrameLayout(mContext);
RemoteInputView view = RemoteInputView.inflate(mContext, null, row.getEntry(), mController);
+ remoteInputViewParent.addView(view);
bindController(view, row.getEntry());
// Start defocus animation
diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt
index b9a5bd7..4ef4e6c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt
@@ -64,7 +64,7 @@
wakeLockBuilder,
systemClock,
) {
- override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) {
+ override fun animateViewOut(view: ViewGroup, removalReason: String?, onAnimationEnd: Runnable) {
// Just bypass the animation in tests
onAnimationEnd.run()
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
index 5501949..39d2eca 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
@@ -46,12 +46,18 @@
private val _isKeyguardShowing = MutableStateFlow(false)
override val isKeyguardShowing: Flow<Boolean> = _isKeyguardShowing
+ private val _isKeyguardOccluded = MutableStateFlow(false)
+ override val isKeyguardOccluded: Flow<Boolean> = _isKeyguardOccluded
+
private val _isDozing = MutableStateFlow(false)
override val isDozing: Flow<Boolean> = _isDozing
private val _isDreaming = MutableStateFlow(false)
override val isDreaming: Flow<Boolean> = _isDreaming
+ private val _isDreamingWithOverlay = MutableStateFlow(false)
+ override val isDreamingWithOverlay: Flow<Boolean> = _isDreamingWithOverlay
+
private val _dozeAmount = MutableStateFlow(0f)
override val linearDozeAmount: Flow<Float> = _dozeAmount
@@ -112,10 +118,18 @@
_isKeyguardShowing.value = isShowing
}
+ fun setKeyguardOccluded(isOccluded: Boolean) {
+ _isKeyguardOccluded.value = isOccluded
+ }
+
fun setDozing(isDozing: Boolean) {
_isDozing.value = isDozing
}
+ fun setDreamingWithOverlay(isDreaming: Boolean) {
+ _isDreamingWithOverlay.value = isDreaming
+ }
+
fun setDozeAmount(dozeAmount: Float) {
_dozeAmount.value = dozeAmount
}
@@ -144,6 +158,10 @@
_fingerprintSensorLocation.tryEmit(location)
}
+ fun setDozeTransitionModel(model: DozeTransitionModel) {
+ _dozeTransitionModel.value = model
+ }
+
override fun isUdfpsSupported(): Boolean {
return _isUdfpsSupported.value
}
diff --git a/services/core/java/com/android/server/camera/CameraServiceProxy.java b/services/core/java/com/android/server/camera/CameraServiceProxy.java
index aec60de..7bbc604 100644
--- a/services/core/java/com/android/server/camera/CameraServiceProxy.java
+++ b/services/core/java/com/android/server/camera/CameraServiceProxy.java
@@ -74,6 +74,7 @@
import android.view.WindowManagerGlobal;
import com.android.framework.protobuf.nano.MessageNano;
+import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.FrameworkStatsLog;
import com.android.server.LocalServices;
@@ -389,6 +390,16 @@
return CaptureRequest.SCALER_ROTATE_AND_CROP_NONE;
}
+ // When config_isWindowManagerCameraCompatTreatmentEnabled is true,
+ // DisplayRotationCompatPolicy in WindowManager force rotates fullscreen activities with
+ // fixed orientation to align them with the natural orientation of the device.
+ if (ctx.getResources().getBoolean(
+ R.bool.config_isWindowManagerCameraCompatTreatmentEnabled)) {
+ Slog.v(TAG, "Disable Rotate and Crop to avoid conflicts with"
+ + " WM force rotation treatment.");
+ return CaptureRequest.SCALER_ROTATE_AND_CROP_NONE;
+ }
+
// External cameras do not need crop-rotate-scale.
if (lensFacing != CameraMetadata.LENS_FACING_FRONT
&& lensFacing != CameraMetadata.LENS_FACING_BACK) {
diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java
index fa6f4ee..890c891 100644
--- a/services/core/java/com/android/server/pm/ShortcutPackage.java
+++ b/services/core/java/com/android/server/pm/ShortcutPackage.java
@@ -1470,15 +1470,9 @@
}
// Then make sure none of the activities have more than the max number of shortcuts.
- int total = 0;
for (int i = counts.size() - 1; i >= 0; i--) {
- int count = counts.valueAt(i);
- service.enforceMaxActivityShortcuts(count);
- total += count;
+ service.enforceMaxActivityShortcuts(counts.valueAt(i));
}
-
- // Finally make sure that the app doesn't have more than the max number of shortcuts.
- service.enforceMaxAppShortcuts(total);
}
/**
diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java
index 014a77b..0b20683 100644
--- a/services/core/java/com/android/server/pm/ShortcutService.java
+++ b/services/core/java/com/android/server/pm/ShortcutService.java
@@ -181,9 +181,6 @@
static final int DEFAULT_MAX_SHORTCUTS_PER_ACTIVITY = 15;
@VisibleForTesting
- static final int DEFAULT_MAX_SHORTCUTS_PER_APP = 60;
-
- @VisibleForTesting
static final int DEFAULT_MAX_ICON_DIMENSION_DP = 96;
@VisibleForTesting
@@ -260,11 +257,6 @@
String KEY_MAX_SHORTCUTS = "max_shortcuts";
/**
- * Key name for the max dynamic shortcuts per app. (int)
- */
- String KEY_MAX_SHORTCUTS_PER_APP = "max_shortcuts_per_app";
-
- /**
* Key name for icon compression quality, 0-100.
*/
String KEY_ICON_QUALITY = "icon_quality";
@@ -337,14 +329,9 @@
new SparseArray<>();
/**
- * Max number of dynamic + manifest shortcuts that each activity can have at a time.
- */
- private int mMaxShortcutsPerActivity;
-
- /**
* Max number of dynamic + manifest shortcuts that each application can have at a time.
*/
- private int mMaxShortcutsPerApp;
+ private int mMaxShortcuts;
/**
* Max number of updating API calls that each application can make during the interval.
@@ -817,12 +804,9 @@
mMaxUpdatesPerInterval = Math.max(0, (int) parser.getLong(
ConfigConstants.KEY_MAX_UPDATES_PER_INTERVAL, DEFAULT_MAX_UPDATES_PER_INTERVAL));
- mMaxShortcutsPerActivity = Math.max(0, (int) parser.getLong(
+ mMaxShortcuts = Math.max(0, (int) parser.getLong(
ConfigConstants.KEY_MAX_SHORTCUTS, DEFAULT_MAX_SHORTCUTS_PER_ACTIVITY));
- mMaxShortcutsPerApp = Math.max(0, (int) parser.getLong(
- ConfigConstants.KEY_MAX_SHORTCUTS_PER_APP, DEFAULT_MAX_SHORTCUTS_PER_APP));
-
final int iconDimensionDp = Math.max(1, injectIsLowRamDevice()
? (int) parser.getLong(
ConfigConstants.KEY_MAX_ICON_DIMENSION_DP_LOWRAM,
@@ -1762,33 +1746,16 @@
* {@link #getMaxActivityShortcuts()}.
*/
void enforceMaxActivityShortcuts(int numShortcuts) {
- if (numShortcuts > mMaxShortcutsPerActivity) {
+ if (numShortcuts > mMaxShortcuts) {
throw new IllegalArgumentException("Max number of dynamic shortcuts exceeded");
}
}
/**
- * @throws IllegalArgumentException if {@code numShortcuts} is bigger than
- * {@link #getMaxAppShortcuts()}.
- */
- void enforceMaxAppShortcuts(int numShortcuts) {
- if (numShortcuts > mMaxShortcutsPerApp) {
- throw new IllegalArgumentException("Max number of dynamic shortcuts per app exceeded");
- }
- }
-
- /**
* Return the max number of dynamic + manifest shortcuts for each launcher icon.
*/
int getMaxActivityShortcuts() {
- return mMaxShortcutsPerActivity;
- }
-
- /**
- * Return the max number of dynamic + manifest shortcuts for each launcher icon.
- */
- int getMaxAppShortcuts() {
- return mMaxShortcutsPerApp;
+ return mMaxShortcuts;
}
/**
@@ -2221,8 +2188,6 @@
ps.ensureNotImmutable(shortcut.getId(), /*ignoreInvisible=*/ true);
fillInDefaultActivity(Arrays.asList(shortcut));
- enforceMaxAppShortcuts(ps.getShortcutCount());
-
if (!shortcut.hasRank()) {
shortcut.setRank(0);
}
@@ -2610,7 +2575,7 @@
throws RemoteException {
verifyCaller(packageName, userId);
- return mMaxShortcutsPerActivity;
+ return mMaxShortcuts;
}
@Override
@@ -4758,7 +4723,7 @@
pw.print(" maxUpdatesPerInterval: ");
pw.println(mMaxUpdatesPerInterval);
pw.print(" maxShortcutsPerActivity: ");
- pw.println(mMaxShortcutsPerActivity);
+ pw.println(mMaxShortcuts);
pw.println();
mStatLogger.dump(pw, " ");
@@ -5245,7 +5210,7 @@
@VisibleForTesting
int getMaxShortcutsForTest() {
- return mMaxShortcutsPerActivity;
+ return mMaxShortcuts;
}
@VisibleForTesting
diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java
index da6e7e8..7252545 100644
--- a/services/core/java/com/android/server/wm/ActivityClientController.java
+++ b/services/core/java/com/android/server/wm/ActivityClientController.java
@@ -163,6 +163,15 @@
}
@Override
+ public void activityRefreshed(IBinder token) {
+ final long origId = Binder.clearCallingIdentity();
+ synchronized (mGlobalLock) {
+ ActivityRecord.activityRefreshedLocked(token);
+ }
+ Binder.restoreCallingIdentity(origId);
+ }
+
+ @Override
public void activityTopResumedStateLost() {
final long origId = Binder.clearCallingIdentity();
synchronized (mGlobalLock) {
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 57eeb9a..50169b4 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -736,7 +736,6 @@
*/
private boolean mWillCloseOrEnterPip;
- @VisibleForTesting
final LetterboxUiController mLetterboxUiController;
/**
@@ -6111,6 +6110,19 @@
r.mDisplayContent.mUnknownAppVisibilityController.notifyAppResumedFinished(r);
}
+ static void activityRefreshedLocked(IBinder token) {
+ final ActivityRecord r = ActivityRecord.forTokenLocked(token);
+ ProtoLog.i(WM_DEBUG_STATES, "Refreshed activity: %s", r);
+ if (r == null) {
+ // In case the record on server side has been removed (e.g. destroy timeout)
+ // and the token could be null.
+ return;
+ }
+ if (r.mDisplayContent.mDisplayRotationCompatPolicy != null) {
+ r.mDisplayContent.mDisplayRotationCompatPolicy.onActivityRefreshed(r);
+ }
+ }
+
static void splashScreenAttachedLocked(IBinder token) {
final ActivityRecord r = ActivityRecord.forTokenLocked(token);
if (r == null) {
@@ -9143,6 +9155,8 @@
} else {
scheduleConfigurationChanged(newMergedOverrideConfig);
}
+ notifyDisplayCompatPolicyAboutConfigurationChange(
+ mLastReportedConfiguration.getMergedConfiguration(), mTmpConfig);
return true;
}
@@ -9211,11 +9225,24 @@
} else {
scheduleConfigurationChanged(newMergedOverrideConfig);
}
+ notifyDisplayCompatPolicyAboutConfigurationChange(
+ mLastReportedConfiguration.getMergedConfiguration(), mTmpConfig);
+
stopFreezingScreenLocked(false);
return true;
}
+ private void notifyDisplayCompatPolicyAboutConfigurationChange(
+ Configuration newConfig, Configuration lastReportedConfig) {
+ if (mDisplayContent.mDisplayRotationCompatPolicy == null
+ || !shouldBeResumed(/* activeActivity */ null)) {
+ return;
+ }
+ mDisplayContent.mDisplayRotationCompatPolicy.onActivityConfigurationChanging(
+ this, newConfig, lastReportedConfig);
+ }
+
/** Get process configuration, or global config if the process is not set. */
private Configuration getProcessGlobalConfiguration() {
return app != null ? app.getConfiguration() : mAtmService.getGlobalConfiguration();
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 4c19322a..36f86d1 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -431,6 +431,7 @@
private final DisplayMetrics mDisplayMetrics = new DisplayMetrics();
private final DisplayPolicy mDisplayPolicy;
private final DisplayRotation mDisplayRotation;
+ @Nullable final DisplayRotationCompatPolicy mDisplayRotationCompatPolicy;
DisplayFrames mDisplayFrames;
private final RemoteCallbackList<ISystemGestureExclusionListener>
@@ -1158,6 +1159,13 @@
onDisplayChanged(this);
updateDisplayAreaOrganizers();
+ mDisplayRotationCompatPolicy =
+ // Not checking DeviceConfig value here to allow enabling via DeviceConfig
+ // without the need to restart the device.
+ mWmService.mLetterboxConfiguration.isCameraCompatTreatmentEnabled(
+ /* checkDeviceConfig */ false)
+ ? new DisplayRotationCompatPolicy(this) : null;
+
mInputMonitor = new InputMonitor(mWmService, this);
mInsetsPolicy = new InsetsPolicy(mInsetsStateController, this);
mMinSizeOfResizeableTaskDp = getMinimalTaskSizeDp();
@@ -2704,6 +2712,14 @@
}
}
+ if (mDisplayRotationCompatPolicy != null) {
+ int compatOrientation = mDisplayRotationCompatPolicy.getOrientation();
+ if (compatOrientation != SCREEN_ORIENTATION_UNSPECIFIED) {
+ mLastOrientationSource = null;
+ return compatOrientation;
+ }
+ }
+
final int orientation = super.getOrientation();
if (!handlesOrientationChangeFromDescendant(orientation)) {
@@ -3260,6 +3276,10 @@
// on the next traversal if it's removed from RootWindowContainer child list.
getPendingTransaction().apply();
mWmService.mWindowPlacerLocked.requestTraversal();
+
+ if (mDisplayRotationCompatPolicy != null) {
+ mDisplayRotationCompatPolicy.dispose();
+ }
}
/** Returns true if a removal action is still being deferred. */
diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java
index 34bdb7a..cf3a688 100644
--- a/services/core/java/com/android/server/wm/DisplayRotation.java
+++ b/services/core/java/com/android/server/wm/DisplayRotation.java
@@ -1805,6 +1805,7 @@
final int mHalfFoldSavedRotation;
final boolean mInHalfFoldTransition;
final DeviceStateController.FoldState mFoldState;
+ @Nullable final String mDisplayRotationCompatPolicySummary;
Record(DisplayRotation dr, int fromRotation, int toRotation) {
mFromRotation = fromRotation;
@@ -1839,6 +1840,10 @@
mInHalfFoldTransition = false;
mFoldState = DeviceStateController.FoldState.UNKNOWN;
}
+ mDisplayRotationCompatPolicySummary = dc.mDisplayRotationCompatPolicy == null
+ ? null
+ : dc.mDisplayRotationCompatPolicy
+ .getSummaryForDisplayRotationHistoryRecord();
}
void dump(String prefix, PrintWriter pw) {
@@ -1861,6 +1866,9 @@
+ " mInHalfFoldTransition=" + mInHalfFoldTransition
+ " mFoldState=" + mFoldState);
}
+ if (mDisplayRotationCompatPolicySummary != null) {
+ pw.println(prefix + mDisplayRotationCompatPolicySummary);
+ }
}
}
diff --git a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
new file mode 100644
index 0000000..18c5c3b
--- /dev/null
+++ b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
@@ -0,0 +1,433 @@
+/*
+ * Copyright (C) 2022 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.server.wm;
+
+import static android.app.servertransaction.ActivityLifecycleItem.ON_PAUSE;
+import static android.app.servertransaction.ActivityLifecycleItem.ON_STOP;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LOCKED;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSET;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+import static android.content.pm.ActivityInfo.screenOrientationToString;
+import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+import static android.content.res.Configuration.ORIENTATION_UNDEFINED;
+import static android.view.Display.TYPE_INTERNAL;
+
+import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ORIENTATION;
+import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_STATES;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.servertransaction.ClientTransaction;
+import android.app.servertransaction.RefreshCallbackItem;
+import android.app.servertransaction.ResumeActivityItem;
+import android.content.pm.ActivityInfo.ScreenOrientation;
+import android.content.res.Configuration;
+import android.hardware.camera2.CameraManager;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.protolog.common.ProtoLog;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Controls camera compatibility treatment that handles orientation mismatch between camera
+ * buffers and an app window for a particular display that can lead to camera issues like sideways
+ * or stretched viewfinder.
+ *
+ * <p>This includes force rotation of fixed orientation activities connected to the camera.
+ *
+ * <p>The treatment is enabled for internal displays that have {@code ignoreOrientationRequest}
+ * display setting enabled and when {@code
+ * R.bool.config_isWindowManagerCameraCompatTreatmentEnabled} is {@code true}.
+ */
+ // TODO(b/261444714): Consider moving Camera-specific logic outside of the WM Core path
+final class DisplayRotationCompatPolicy {
+
+ // Delay for updating display rotation after Camera connection is closed. Needed to avoid
+ // rotation flickering when an app is flipping between front and rear cameras or when size
+ // compat mode is restarted.
+ // TODO(b/263114289): Consider associating this delay with a specific activity so that if
+ // the new non-camera activity started on top of the camer one we can rotate faster.
+ private static final int CAMERA_CLOSED_ROTATION_UPDATE_DELAY_MS = 2000;
+ // Delay for updating display rotation after Camera connection is opened. This delay is
+ // selected to be long enough to avoid conflicts with transitions on the app's side.
+ // Using a delay < CAMERA_CLOSED_ROTATION_UPDATE_DELAY_MS to avoid flickering when an app
+ // is flipping between front and rear cameras (in case requested orientation changes at
+ // runtime at the same time) or when size compat mode is restarted.
+ private static final int CAMERA_OPENED_ROTATION_UPDATE_DELAY_MS =
+ CAMERA_CLOSED_ROTATION_UPDATE_DELAY_MS / 2;
+ // Delay for ensuring that onActivityRefreshed is always called after an activity refresh. The
+ // client process may not always report the event back to the server, such as process is
+ // crashed or got killed.
+ private static final int REFRESH_CALLBACK_TIMEOUT_MS = 2000;
+
+ private final DisplayContent mDisplayContent;
+ private final WindowManagerService mWmService;
+ private final CameraManager mCameraManager;
+ private final Handler mHandler;
+
+ // Bi-directional map between package names and active camera IDs since we need to 1) get a
+ // camera id by a package name when determining rotation; 2) get a package name by a camera id
+ // when camera connection is closed and we need to clean up our records.
+ @GuardedBy("this")
+ private final CameraIdPackageNameBiMap mCameraIdPackageBiMap = new CameraIdPackageNameBiMap();
+ @GuardedBy("this")
+ private final Set<String> mScheduledToBeRemovedCameraIdSet = new ArraySet<>();
+ @GuardedBy("this")
+ private final Set<String> mScheduledOrientationUpdateCameraIdSet = new ArraySet<>();
+
+ private final CameraManager.AvailabilityCallback mAvailabilityCallback =
+ new CameraManager.AvailabilityCallback() {
+ @Override
+ public void onCameraOpened(@NonNull String cameraId, @NonNull String packageId) {
+ notifyCameraOpened(cameraId, packageId);
+ }
+
+ @Override
+ public void onCameraClosed(@NonNull String cameraId) {
+ notifyCameraClosed(cameraId);
+ }
+ };
+
+ @ScreenOrientation
+ private int mLastReportedOrientation = SCREEN_ORIENTATION_UNSET;
+
+ DisplayRotationCompatPolicy(@NonNull DisplayContent displayContent) {
+ this(displayContent, displayContent.mWmService.mH);
+ }
+
+ @VisibleForTesting
+ DisplayRotationCompatPolicy(@NonNull DisplayContent displayContent, Handler handler) {
+ // This constructor is called from DisplayContent constructor. Don't use any fields in
+ // DisplayContent here since they aren't guaranteed to be set.
+ mHandler = handler;
+ mDisplayContent = displayContent;
+ mWmService = displayContent.mWmService;
+ mCameraManager = mWmService.mContext.getSystemService(CameraManager.class);
+ mCameraManager.registerAvailabilityCallback(
+ mWmService.mContext.getMainExecutor(), mAvailabilityCallback);
+ }
+
+ void dispose() {
+ mCameraManager.unregisterAvailabilityCallback(mAvailabilityCallback);
+ }
+
+ /**
+ * Determines orientation for Camera compatibility.
+ *
+ * <p>The goal of this function is to compute a orientation which would align orientations of
+ * portrait app window and natural orientation of the device and set opposite to natural
+ * orientation for a landscape app window. This is one of the strongest assumptions that apps
+ * make when they implement camera previews. Since app and natural display orientations aren't
+ * guaranteed to match, the rotation can cause letterboxing.
+ *
+ * <p>If treatment isn't applicable returns {@link SCREEN_ORIENTATION_UNSPECIFIED}. See {@link
+ * #shouldComputeCameraCompatOrientation} for conditions enabling the treatment.
+ */
+ @ScreenOrientation
+ int getOrientation() {
+ mLastReportedOrientation = getOrientationInternal();
+ return mLastReportedOrientation;
+ }
+
+ @ScreenOrientation
+ private synchronized int getOrientationInternal() {
+ if (!isTreatmentEnabledForDisplay()) {
+ return SCREEN_ORIENTATION_UNSPECIFIED;
+ }
+ ActivityRecord topActivity = mDisplayContent.topRunningActivity(
+ /* considerKeyguardState= */ true);
+ if (!isTreatmentEnabledForActivity(topActivity)) {
+ return SCREEN_ORIENTATION_UNSPECIFIED;
+ }
+ boolean isPortraitActivity =
+ topActivity.getRequestedConfigurationOrientation() == ORIENTATION_PORTRAIT;
+ boolean isNaturalDisplayOrientationPortrait =
+ mDisplayContent.getNaturalOrientation() == ORIENTATION_PORTRAIT;
+ // Rotate portrait-only activity in the natural orientation of the displays (and in the
+ // opposite to natural orientation for landscape-only) since many apps assume that those
+ // are aligned when they compute orientation of the preview.
+ // This means that even for a landscape-only activity and a device with landscape natural
+ // orientation this would return SCREEN_ORIENTATION_PORTRAIT because an assumption that
+ // natural orientation = portrait window = portait camera is the main wrong assumption
+ // that apps make when they implement camera previews so landscape windows need be
+ // rotated in the orientation oposite to the natural one even if it's portrait.
+ // TODO(b/261475895): Consider allowing more rotations for "sensor" and "user" versions
+ // of the portrait and landscape orientation requests.
+ int orientation = (isPortraitActivity && isNaturalDisplayOrientationPortrait)
+ || (!isPortraitActivity && !isNaturalDisplayOrientationPortrait)
+ ? SCREEN_ORIENTATION_PORTRAIT
+ : SCREEN_ORIENTATION_LANDSCAPE;
+ ProtoLog.v(WM_DEBUG_ORIENTATION,
+ "Display id=%d is ignoring all orientation requests, camera is active "
+ + "and the top activity is eligible for force rotation, return %s,"
+ + "portrait activity: %b, is natural orientation portrait: %b.",
+ mDisplayContent.mDisplayId, screenOrientationToString(orientation),
+ isPortraitActivity, isNaturalDisplayOrientationPortrait);
+ return orientation;
+ }
+
+ /**
+ * "Refreshes" activity by going through "stopped -> resumed" or "paused -> resumed" cycle.
+ * This allows to clear cached values in apps (e.g. display or camera rotation) that influence
+ * camera preview and can lead to sideways or stretching issues persisting even after force
+ * rotation.
+ */
+ void onActivityConfigurationChanging(ActivityRecord activity, Configuration newConfig,
+ Configuration lastReportedConfig) {
+ if (!isTreatmentEnabledForDisplay()
+ || !mWmService.mLetterboxConfiguration.isCameraCompatRefreshEnabled()
+ || !shouldRefreshActivity(activity, newConfig, lastReportedConfig)) {
+ return;
+ }
+ boolean cycleThroughStop = mWmService.mLetterboxConfiguration
+ .isCameraCompatRefreshCycleThroughStopEnabled();
+ try {
+ activity.mLetterboxUiController.setIsRefreshAfterRotationRequested(true);
+ ProtoLog.v(WM_DEBUG_STATES,
+ "Refershing activity for camera compatibility treatment, "
+ + "activityRecord=%s", activity);
+ final ClientTransaction transaction = ClientTransaction.obtain(
+ activity.app.getThread(), activity.token);
+ transaction.addCallback(
+ RefreshCallbackItem.obtain(cycleThroughStop ? ON_STOP : ON_PAUSE));
+ transaction.setLifecycleStateRequest(ResumeActivityItem.obtain(/* isForward */ false));
+ activity.mAtmService.getLifecycleManager().scheduleTransaction(transaction);
+ mHandler.postDelayed(
+ () -> onActivityRefreshed(activity),
+ REFRESH_CALLBACK_TIMEOUT_MS);
+ } catch (RemoteException e) {
+ activity.mLetterboxUiController.setIsRefreshAfterRotationRequested(false);
+ }
+ }
+
+ void onActivityRefreshed(@NonNull ActivityRecord activity) {
+ activity.mLetterboxUiController.setIsRefreshAfterRotationRequested(false);
+ }
+
+ String getSummaryForDisplayRotationHistoryRecord() {
+ String summaryIfEnabled = "";
+ if (isTreatmentEnabledForDisplay()) {
+ ActivityRecord topActivity = mDisplayContent.topRunningActivity(
+ /* considerKeyguardState= */ true);
+ summaryIfEnabled =
+ " mLastReportedOrientation="
+ + screenOrientationToString(mLastReportedOrientation)
+ + " topActivity="
+ + (topActivity == null ? "null" : topActivity.shortComponentName)
+ + " isTreatmentEnabledForActivity="
+ + isTreatmentEnabledForActivity(topActivity)
+ + " CameraIdPackageNameBiMap="
+ + mCameraIdPackageBiMap.getSummaryForDisplayRotationHistoryRecord();
+ }
+ return "DisplayRotationCompatPolicy{"
+ + " isTreatmentEnabledForDisplay=" + isTreatmentEnabledForDisplay()
+ + summaryIfEnabled
+ + " }";
+ }
+
+ // Refreshing only when configuration changes after rotation.
+ private boolean shouldRefreshActivity(ActivityRecord activity, Configuration newConfig,
+ Configuration lastReportedConfig) {
+ return newConfig.windowConfiguration.getDisplayRotation()
+ != lastReportedConfig.windowConfiguration.getDisplayRotation()
+ && isTreatmentEnabledForActivity(activity);
+ }
+
+ /**
+ * Whether camera compat treatment is enabled for the display.
+ *
+ * <p>Conditions that need to be met:
+ * <ul>
+ * <li>{@code R.bool.config_isWindowManagerCameraCompatTreatmentEnabled} is {@code true}.
+ * <li>Setting {@code ignoreOrientationRequest} is enabled for the display.
+ * <li>Associated {@link DisplayContent} is for internal display. See b/225928882
+ * that tracks supporting external displays in the future.
+ * </ul>
+ */
+ private boolean isTreatmentEnabledForDisplay() {
+ return mWmService.mLetterboxConfiguration.isCameraCompatTreatmentEnabled(
+ /* checkDeviceConfig */ true)
+ && mDisplayContent.getIgnoreOrientationRequest()
+ // TODO(b/225928882): Support camera compat rotation for external displays
+ && mDisplayContent.getDisplay().getType() == TYPE_INTERNAL;
+ }
+
+ /**
+ * Whether camera compat treatment is applicable for the given activity.
+ *
+ * <p>Conditions that need to be met:
+ * <ul>
+ * <li>{@link #isCameraActiveForPackage} is {@code true} for the activity.
+ * <li>The activity is in fullscreen
+ * <li>The activity has fixed orientation but not "locked" or "nosensor" one.
+ * </ul>
+ */
+ private boolean isTreatmentEnabledForActivity(@Nullable ActivityRecord activity) {
+ return activity != null && !activity.inMultiWindowMode()
+ && activity.getRequestedConfigurationOrientation() != ORIENTATION_UNDEFINED
+ // "locked" and "nosensor" values are often used by camera apps that can't
+ // handle dynamic changes so we shouldn't force rotate them.
+ && activity.getRequestedOrientation() != SCREEN_ORIENTATION_NOSENSOR
+ && activity.getRequestedOrientation() != SCREEN_ORIENTATION_LOCKED
+ && mCameraIdPackageBiMap.containsPackageName(activity.packageName);
+ }
+
+ private synchronized void notifyCameraOpened(
+ @NonNull String cameraId, @NonNull String packageName) {
+ // If an activity is restarting or camera is flipping, the camera connection can be
+ // quickly closed and reopened.
+ mScheduledToBeRemovedCameraIdSet.remove(cameraId);
+ ProtoLog.v(WM_DEBUG_ORIENTATION,
+ "Display id=%d is notified that Camera %s is open for package %s",
+ mDisplayContent.mDisplayId, cameraId, packageName);
+ // Some apps can’t handle configuration changes coming at the same time with Camera setup
+ // so delaying orientation update to accomadate for that.
+ mScheduledOrientationUpdateCameraIdSet.add(cameraId);
+ mHandler.postDelayed(
+ () -> delayedUpdateOrientationWithWmLock(cameraId, packageName),
+ CAMERA_OPENED_ROTATION_UPDATE_DELAY_MS);
+ }
+
+ private void updateOrientationWithWmLock() {
+ synchronized (mWmService.mGlobalLock) {
+ mDisplayContent.updateOrientation();
+ }
+ }
+
+ private void delayedUpdateOrientationWithWmLock(
+ @NonNull String cameraId, @NonNull String packageName) {
+ synchronized (this) {
+ if (!mScheduledOrientationUpdateCameraIdSet.remove(cameraId)) {
+ // Orientation update has happened already or was cancelled because
+ // camera was closed.
+ return;
+ }
+ mCameraIdPackageBiMap.put(packageName, cameraId);
+ }
+ updateOrientationWithWmLock();
+ }
+
+ private synchronized void notifyCameraClosed(@NonNull String cameraId) {
+ ProtoLog.v(WM_DEBUG_ORIENTATION,
+ "Display id=%d is notified that Camera %s is closed, scheduling rotation update.",
+ mDisplayContent.mDisplayId, cameraId);
+ mScheduledToBeRemovedCameraIdSet.add(cameraId);
+ // No need to update orientation for this camera if it's already closed.
+ mScheduledOrientationUpdateCameraIdSet.remove(cameraId);
+ scheduleRemoveCameraId(cameraId);
+ }
+
+ // Delay is needed to avoid rotation flickering when an app is flipping between front and
+ // rear cameras, when size compat mode is restarted or activity is being refreshed.
+ private void scheduleRemoveCameraId(@NonNull String cameraId) {
+ mHandler.postDelayed(
+ () -> removeCameraId(cameraId),
+ CAMERA_CLOSED_ROTATION_UPDATE_DELAY_MS);
+ }
+
+ private void removeCameraId(String cameraId) {
+ synchronized (this) {
+ if (!mScheduledToBeRemovedCameraIdSet.remove(cameraId)) {
+ // Already reconnected to this camera, no need to clean up.
+ return;
+ }
+ if (isActivityForCameraIdRefreshing(cameraId)) {
+ ProtoLog.v(WM_DEBUG_ORIENTATION,
+ "Display id=%d is notified that Camera %s is closed but activity is"
+ + " still refreshing. Rescheduling an update.",
+ mDisplayContent.mDisplayId, cameraId);
+ mScheduledToBeRemovedCameraIdSet.add(cameraId);
+ scheduleRemoveCameraId(cameraId);
+ return;
+ }
+ mCameraIdPackageBiMap.removeCameraId(cameraId);
+ }
+ ProtoLog.v(WM_DEBUG_ORIENTATION,
+ "Display id=%d is notified that Camera %s is closed, updating rotation.",
+ mDisplayContent.mDisplayId, cameraId);
+ updateOrientationWithWmLock();
+ }
+
+ private boolean isActivityForCameraIdRefreshing(String cameraId) {
+ ActivityRecord topActivity = mDisplayContent.topRunningActivity(
+ /* considerKeyguardState= */ true);
+ if (!isTreatmentEnabledForActivity(topActivity)) {
+ return false;
+ }
+ String activeCameraId = mCameraIdPackageBiMap.getCameraId(topActivity.packageName);
+ if (activeCameraId == null || activeCameraId != cameraId) {
+ return false;
+ }
+ return topActivity.mLetterboxUiController.isRefreshAfterRotationRequested();
+ }
+
+ private static class CameraIdPackageNameBiMap {
+
+ private final Map<String, String> mPackageToCameraIdMap = new ArrayMap<>();
+ private final Map<String, String> mCameraIdToPackageMap = new ArrayMap<>();
+
+ void put(String packageName, String cameraId) {
+ // Always using the last connected camera ID for the package even for the concurrent
+ // camera use case since we can't guess which camera is more important anyway.
+ removePackageName(packageName);
+ removeCameraId(cameraId);
+ mPackageToCameraIdMap.put(packageName, cameraId);
+ mCameraIdToPackageMap.put(cameraId, packageName);
+ }
+
+ boolean containsPackageName(String packageName) {
+ return mPackageToCameraIdMap.containsKey(packageName);
+ }
+
+ @Nullable
+ String getCameraId(String packageName) {
+ return mPackageToCameraIdMap.get(packageName);
+ }
+
+ void removeCameraId(String cameraId) {
+ String packageName = mCameraIdToPackageMap.get(cameraId);
+ if (packageName == null) {
+ return;
+ }
+ mPackageToCameraIdMap.remove(packageName, cameraId);
+ mCameraIdToPackageMap.remove(cameraId, packageName);
+ }
+
+ String getSummaryForDisplayRotationHistoryRecord() {
+ return "{ mPackageToCameraIdMap=" + mPackageToCameraIdMap + " }";
+ }
+
+ private void removePackageName(String packageName) {
+ String cameraId = mPackageToCameraIdMap.get(packageName);
+ if (cameraId == null) {
+ return;
+ }
+ mPackageToCameraIdMap.remove(packageName, cameraId);
+ mCameraIdToPackageMap.remove(cameraId, packageName);
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
index 3eca364..a7bf595f 100644
--- a/services/core/java/com/android/server/wm/LetterboxConfiguration.java
+++ b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
@@ -191,6 +191,20 @@
// Allows to enable letterboxing strategy for translucent activities ignoring flags.
private boolean mTranslucentLetterboxingOverrideEnabled;
+ // Whether camera compatibility treatment is enabled.
+ // See DisplayRotationCompatPolicy for context.
+ private final boolean mIsCameraCompatTreatmentEnabled;
+
+ // Whether activity "refresh" in camera compatibility treatment is enabled.
+ // See RefreshCallbackItem for context.
+ private boolean mIsCameraCompatTreatmentRefreshEnabled = true;
+
+ // Whether activity "refresh" in camera compatibility treatment should happen using the
+ // "stopped -> resumed" cycle rather than "paused -> resumed" cycle. Using "stop -> resumed"
+ // cycle by default due to higher success rate confirmed with app compatibility testing.
+ // See RefreshCallbackItem for context.
+ private boolean mIsCameraCompatRefreshCycleThroughStopEnabled = true;
+
LetterboxConfiguration(Context systemUiContext) {
this(systemUiContext, new LetterboxConfigurationPersister(systemUiContext,
() -> readLetterboxHorizontalReachabilityPositionFromConfig(systemUiContext,
@@ -241,6 +255,8 @@
R.bool.config_letterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled);
mTranslucentLetterboxingEnabled = mContext.getResources().getBoolean(
R.bool.config_letterboxIsEnabledForTranslucentActivities);
+ mIsCameraCompatTreatmentEnabled = mContext.getResources().getBoolean(
+ R.bool.config_isWindowManagerCameraCompatTreatmentEnabled);
mLetterboxConfigurationPersister = letterboxConfigurationPersister;
mLetterboxConfigurationPersister.start();
}
@@ -947,9 +963,65 @@
isDeviceInTabletopMode, nextVerticalPosition);
}
- // TODO(b/262378106): Cache runtime flag and implement DeviceConfig.OnPropertiesChangedListener
+ // TODO(b/262378106): Cache a runtime flag and implement
+ // DeviceConfig.OnPropertiesChangedListener
static boolean isTranslucentLetterboxingAllowed() {
return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_WINDOW_MANAGER,
"enable_translucent_activity_letterbox", false);
}
+
+ /** Whether camera compatibility treatment is enabled. */
+ boolean isCameraCompatTreatmentEnabled(boolean checkDeviceConfig) {
+ return mIsCameraCompatTreatmentEnabled
+ && (!checkDeviceConfig || isCameraCompatTreatmentAllowed());
+ }
+
+ // TODO(b/262977416): Cache a runtime flag and implement
+ // DeviceConfig.OnPropertiesChangedListener
+ private static boolean isCameraCompatTreatmentAllowed() {
+ return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_WINDOW_MANAGER,
+ "enable_camera_compat_treatment", false);
+ }
+
+ /** Whether camera compatibility refresh is enabled. */
+ boolean isCameraCompatRefreshEnabled() {
+ return mIsCameraCompatTreatmentRefreshEnabled;
+ }
+
+ /** Overrides whether camera compatibility treatment is enabled. */
+ void setCameraCompatRefreshEnabled(boolean enabled) {
+ mIsCameraCompatTreatmentRefreshEnabled = enabled;
+ }
+
+ /**
+ * Resets whether camera compatibility treatment is enabled to {@code true}.
+ */
+ void resetCameraCompatRefreshEnabled() {
+ mIsCameraCompatTreatmentRefreshEnabled = true;
+ }
+
+ /**
+ * Whether activity "refresh" in camera compatibility treatment should happen using the
+ * "stopped -> resumed" cycle rather than "paused -> resumed" cycle.
+ */
+ boolean isCameraCompatRefreshCycleThroughStopEnabled() {
+ return mIsCameraCompatRefreshCycleThroughStopEnabled;
+ }
+
+ /**
+ * Overrides whether activity "refresh" in camera compatibility treatment should happen using
+ * "stopped -> resumed" cycle rather than "paused -> resumed" cycle.
+ */
+ void setCameraCompatRefreshCycleThroughStopEnabled(boolean enabled) {
+ mIsCameraCompatRefreshCycleThroughStopEnabled = enabled;
+ }
+
+ /**
+ * Resets whether activity "refresh" in camera compatibility treatment should happen using
+ * "stopped -> resumed" cycle rather than "paused -> resumed" cycle to {@code true}.
+ */
+ void resetCameraCompatRefreshCycleThroughStopEnabled() {
+ mIsCameraCompatRefreshCycleThroughStopEnabled = true;
+ }
+
}
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index 9cb94c6..fd7e082 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -80,6 +80,7 @@
// SizeCompatTests and LetterboxTests but not all.
// TODO(b/185264020): Consider making LetterboxUiController applicable to any level of the
// hierarchy in addition to ActivityRecord (Task, DisplayArea, ...).
+// TODO(b/263021211): Consider renaming to more generic CompatUIController.
final class LetterboxUiController {
private static final String TAG = TAG_WITH_CLASS_NAME ? "LetterboxUiController" : TAG_ATM;
@@ -125,6 +126,11 @@
@Nullable
private Letterbox mLetterbox;
+ // Whether activity "refresh" was requested but not finished in
+ // ActivityRecord#activityResumedLocked following the camera compat force rotation in
+ // DisplayRotationCompatPolicy.
+ private boolean mIsRefreshAfterRotationRequested;
+
LetterboxUiController(WindowManagerService wmService, ActivityRecord activityRecord) {
mLetterboxConfiguration = wmService.mLetterboxConfiguration;
// Given activityRecord may not be fully constructed since LetterboxUiController
@@ -147,6 +153,18 @@
}
}
+ /**
+ * Whether activity "refresh" was requested but not finished in {@link #activityResumedLocked}
+ * following the camera compat force rotation in {@link DisplayRotationCompatPolicy}.
+ */
+ boolean isRefreshAfterRotationRequested() {
+ return mIsRefreshAfterRotationRequested;
+ }
+
+ void setIsRefreshAfterRotationRequested(boolean isRequested) {
+ mIsRefreshAfterRotationRequested = isRequested;
+ }
+
boolean hasWallpaperBackgroundForLetterbox() {
return mShowWallpaperForLetterboxBackground;
}
diff --git a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
index 85aa942..aef6d1d 100644
--- a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
+++ b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
@@ -53,6 +53,7 @@
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
+import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
@@ -806,54 +807,6 @@
return 0;
}
- private int runSetLetterboxIsHorizontalReachabilityEnabled(PrintWriter pw)
- throws RemoteException {
- String arg = getNextArg();
- final boolean enabled;
- switch (arg) {
- case "true":
- case "1":
- enabled = true;
- break;
- case "false":
- case "0":
- enabled = false;
- break;
- default:
- getErrPrintWriter().println("Error: expected true, 1, false, 0, but got " + arg);
- return -1;
- }
-
- synchronized (mInternal.mGlobalLock) {
- mLetterboxConfiguration.setIsHorizontalReachabilityEnabled(enabled);
- }
- return 0;
- }
-
- private int runSetLetterboxIsVerticalReachabilityEnabled(PrintWriter pw)
- throws RemoteException {
- String arg = getNextArg();
- final boolean enabled;
- switch (arg) {
- case "true":
- case "1":
- enabled = true;
- break;
- case "false":
- case "0":
- enabled = false;
- break;
- default:
- getErrPrintWriter().println("Error: expected true, 1, false, 0, but got " + arg);
- return -1;
- }
-
- synchronized (mInternal.mGlobalLock) {
- mLetterboxConfiguration.setIsVerticalReachabilityEnabled(enabled);
- }
- return 0;
- }
-
private int runSetLetterboxDefaultPositionForHorizontalReachability(PrintWriter pw)
throws RemoteException {
@LetterboxHorizontalReachabilityPosition final int position;
@@ -916,32 +869,13 @@
return 0;
}
- private int runSetLetterboxIsEducationEnabled(PrintWriter pw) throws RemoteException {
- String arg = getNextArg();
- final boolean enabled;
- switch (arg) {
- case "true":
- case "1":
- enabled = true;
- break;
- case "false":
- case "0":
- enabled = false;
- break;
- default:
- getErrPrintWriter().println("Error: expected true, 1, false, 0, but got " + arg);
- return -1;
- }
-
- synchronized (mInternal.mGlobalLock) {
- mLetterboxConfiguration.setIsEducationEnabled(enabled);
- }
- return 0;
- }
-
- private int runSetLetterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled(PrintWriter pw)
+ private int runSetBooleanFlag(PrintWriter pw, Consumer<Boolean> setter)
throws RemoteException {
String arg = getNextArg();
+ if (arg == null) {
+ getErrPrintWriter().println("Error: expected true, 1, false, 0, but got empty input.");
+ return -1;
+ }
final boolean enabled;
switch (arg) {
case "true":
@@ -958,30 +892,7 @@
}
synchronized (mInternal.mGlobalLock) {
- mLetterboxConfiguration.setIsSplitScreenAspectRatioForUnresizableAppsEnabled(enabled);
- }
- return 0;
- }
-
- private int runSetTranslucentLetterboxingEnabled(PrintWriter pw) {
- String arg = getNextArg();
- final boolean enabled;
- switch (arg) {
- case "true":
- case "1":
- enabled = true;
- break;
- case "false":
- case "0":
- enabled = false;
- break;
- default:
- getErrPrintWriter().println("Error: expected true, 1, false, 0, but got " + arg);
- return -1;
- }
-
- synchronized (mInternal.mGlobalLock) {
- mLetterboxConfiguration.setTranslucentLetterboxingOverrideEnabled(enabled);
+ setter.accept(enabled);
}
return 0;
}
@@ -1024,10 +935,12 @@
runSetLetterboxVerticalPositionMultiplier(pw);
break;
case "--isHorizontalReachabilityEnabled":
- runSetLetterboxIsHorizontalReachabilityEnabled(pw);
+ runSetBooleanFlag(pw, mLetterboxConfiguration
+ ::setIsHorizontalReachabilityEnabled);
break;
case "--isVerticalReachabilityEnabled":
- runSetLetterboxIsVerticalReachabilityEnabled(pw);
+ runSetBooleanFlag(pw, mLetterboxConfiguration
+ ::setIsVerticalReachabilityEnabled);
break;
case "--defaultPositionForHorizontalReachability":
runSetLetterboxDefaultPositionForHorizontalReachability(pw);
@@ -1036,13 +949,23 @@
runSetLetterboxDefaultPositionForVerticalReachability(pw);
break;
case "--isEducationEnabled":
- runSetLetterboxIsEducationEnabled(pw);
+ runSetBooleanFlag(pw, mLetterboxConfiguration::setIsEducationEnabled);
break;
case "--isSplitScreenAspectRatioForUnresizableAppsEnabled":
- runSetLetterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled(pw);
+ runSetBooleanFlag(pw, mLetterboxConfiguration
+ ::setIsSplitScreenAspectRatioForUnresizableAppsEnabled);
break;
case "--isTranslucentLetterboxingEnabled":
- runSetTranslucentLetterboxingEnabled(pw);
+ runSetBooleanFlag(pw, mLetterboxConfiguration
+ ::setTranslucentLetterboxingOverrideEnabled);
+ break;
+ case "--isCameraCompatRefreshEnabled":
+ runSetBooleanFlag(pw, enabled -> mLetterboxConfiguration
+ .setCameraCompatRefreshEnabled(enabled));
+ break;
+ case "--isCameraCompatRefreshCycleThroughStopEnabled":
+ runSetBooleanFlag(pw, enabled -> mLetterboxConfiguration
+ .setCameraCompatRefreshCycleThroughStopEnabled(enabled));
break;
default:
getErrPrintWriter().println(
@@ -1089,27 +1012,34 @@
mLetterboxConfiguration.resetLetterboxVerticalPositionMultiplier();
break;
case "isHorizontalReachabilityEnabled":
- mLetterboxConfiguration.getIsHorizontalReachabilityEnabled();
+ mLetterboxConfiguration.resetIsHorizontalReachabilityEnabled();
break;
case "isVerticalReachabilityEnabled":
- mLetterboxConfiguration.getIsVerticalReachabilityEnabled();
+ mLetterboxConfiguration.resetIsVerticalReachabilityEnabled();
break;
case "defaultPositionForHorizontalReachability":
- mLetterboxConfiguration.getDefaultPositionForHorizontalReachability();
+ mLetterboxConfiguration.resetDefaultPositionForHorizontalReachability();
break;
case "defaultPositionForVerticalReachability":
- mLetterboxConfiguration.getDefaultPositionForVerticalReachability();
+ mLetterboxConfiguration.resetDefaultPositionForVerticalReachability();
break;
case "isEducationEnabled":
- mLetterboxConfiguration.getIsEducationEnabled();
+ mLetterboxConfiguration.resetIsEducationEnabled();
break;
case "isSplitScreenAspectRatioForUnresizableAppsEnabled":
mLetterboxConfiguration
- .getIsSplitScreenAspectRatioForUnresizableAppsEnabled();
+ .resetIsSplitScreenAspectRatioForUnresizableAppsEnabled();
break;
case "isTranslucentLetterboxingEnabled":
mLetterboxConfiguration.resetTranslucentLetterboxingEnabled();
break;
+ case "isCameraCompatRefreshEnabled":
+ mLetterboxConfiguration.resetCameraCompatRefreshEnabled();
+ break;
+ case "isCameraCompatRefreshCycleThroughStopEnabled":
+ mLetterboxConfiguration
+ .resetCameraCompatRefreshCycleThroughStopEnabled();
+ break;
default:
getErrPrintWriter().println(
"Error: Unrecognized letterbox style option: " + arg);
@@ -1211,6 +1141,8 @@
mLetterboxConfiguration.resetIsEducationEnabled();
mLetterboxConfiguration.resetIsSplitScreenAspectRatioForUnresizableAppsEnabled();
mLetterboxConfiguration.resetTranslucentLetterboxingEnabled();
+ mLetterboxConfiguration.resetCameraCompatRefreshEnabled();
+ mLetterboxConfiguration.resetCameraCompatRefreshCycleThroughStopEnabled();
}
}
@@ -1255,6 +1187,12 @@
pw.println("Is using split screen aspect ratio as aspect ratio for unresizable apps: "
+ mLetterboxConfiguration
.getIsSplitScreenAspectRatioForUnresizableAppsEnabled());
+
+ pw.println(" Is activity \"refresh\" in camera compatibility treatment enabled: "
+ + mLetterboxConfiguration.isCameraCompatRefreshEnabled());
+ pw.println(" Refresh using \"stopped -> resumed\" cycle: "
+ + mLetterboxConfiguration.isCameraCompatRefreshCycleThroughStopEnabled());
+
pw.println("Background type: "
+ LetterboxConfiguration.letterboxBackgroundTypeToString(
mLetterboxConfiguration.getLetterboxBackgroundType()));
@@ -1464,7 +1402,12 @@
pw.println(" unresizable apps.");
pw.println(" --isTranslucentLetterboxingEnabled [true|1|false|0]");
pw.println(" Whether letterboxing for translucent activities is enabled.");
-
+ pw.println(" --isCameraCompatRefreshEnabled [true|1|false|0]");
+ pw.println(" Whether camera compatibility refresh is enabled.");
+ pw.println(" --isCameraCompatRefreshCycleThroughStopEnabled [true|1|false|0]");
+ pw.println(" Whether activity \"refresh\" in camera compatibility treatment should");
+ pw.println(" happen using the \"stopped -> resumed\" cycle rather than");
+ pw.println(" \"paused -> resumed\" cycle.");
pw.println(" reset-letterbox-style [aspectRatio|cornerRadius|backgroundType");
pw.println(" |backgroundColor|wallpaperBlurRadius|wallpaperDarkScrimAlpha");
pw.println(" |horizontalPositionMultiplier|verticalPositionMultiplier");
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java
new file mode 100644
index 0000000..d1234e3
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java
@@ -0,0 +1,428 @@
+/*
+ * Copyright (C) 2022 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.server.wm;
+
+import static android.app.servertransaction.ActivityLifecycleItem.ON_PAUSE;
+import static android.app.servertransaction.ActivityLifecycleItem.ON_STOP;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LOCKED;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
+import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+import static android.view.Surface.ROTATION_0;
+import static android.view.Surface.ROTATION_90;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.app.servertransaction.ClientTransaction;
+import android.app.servertransaction.RefreshCallbackItem;
+import android.app.servertransaction.ResumeActivityItem;
+import android.content.ComponentName;
+import android.content.pm.ActivityInfo.ScreenOrientation;
+import android.content.res.Configuration;
+import android.content.res.Configuration.Orientation;
+import android.hardware.camera2.CameraManager;
+import android.os.Handler;
+import android.platform.test.annotations.Presubmit;
+import android.view.Display;
+import android.view.Surface.Rotation;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Tests for {@link DisplayRotationCompatPolicy}.
+ *
+ * Build/Install/Run:
+ * atest WmTests:DisplayRotationCompatPolicyTests
+ */
+@SmallTest
+@Presubmit
+@RunWith(WindowTestRunner.class)
+public final class DisplayRotationCompatPolicyTests extends WindowTestsBase {
+
+ private static final String TEST_PACKAGE_1 = "com.test.package.one";
+ private static final String TEST_PACKAGE_2 = "com.test.package.two";
+ private static final String CAMERA_ID_1 = "camera-1";
+ private static final String CAMERA_ID_2 = "camera-2";
+
+ private CameraManager mMockCameraManager;
+ private Handler mMockHandler;
+ private LetterboxConfiguration mLetterboxConfiguration;
+
+ private DisplayRotationCompatPolicy mDisplayRotationCompatPolicy;
+ private CameraManager.AvailabilityCallback mCameraAvailabilityCallback;
+
+ private ActivityRecord mActivity;
+ private Task mTask;
+
+ @Before
+ public void setUp() throws Exception {
+ mLetterboxConfiguration = mDisplayContent.mWmService.mLetterboxConfiguration;
+ spyOn(mLetterboxConfiguration);
+ when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled(
+ /* checkDeviceConfig */ anyBoolean()))
+ .thenReturn(true);
+ when(mLetterboxConfiguration.isCameraCompatRefreshEnabled())
+ .thenReturn(true);
+ when(mLetterboxConfiguration.isCameraCompatRefreshCycleThroughStopEnabled())
+ .thenReturn(true);
+
+ mMockCameraManager = mock(CameraManager.class);
+ doAnswer(invocation -> {
+ mCameraAvailabilityCallback = invocation.getArgument(1);
+ return null;
+ }).when(mMockCameraManager).registerAvailabilityCallback(
+ any(Executor.class), any(CameraManager.AvailabilityCallback.class));
+
+ spyOn(mContext);
+ when(mContext.getSystemService(CameraManager.class)).thenReturn(mMockCameraManager);
+
+ spyOn(mDisplayContent);
+
+ mDisplayContent.setIgnoreOrientationRequest(true);
+
+ mMockHandler = mock(Handler.class);
+
+ when(mMockHandler.postDelayed(any(Runnable.class), anyLong())).thenAnswer(
+ invocation -> {
+ ((Runnable) invocation.getArgument(0)).run();
+ return null;
+ });
+ mDisplayRotationCompatPolicy = new DisplayRotationCompatPolicy(
+ mDisplayContent, mMockHandler);
+ }
+
+ @Test
+ public void testTreatmentNotEnabled_noForceRotationOrRefresh() throws Exception {
+ when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled(
+ /* checkDeviceConfig */ anyBoolean()))
+ .thenReturn(false);
+
+ configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+ assertEquals(mDisplayRotationCompatPolicy.getOrientation(),
+ SCREEN_ORIENTATION_UNSPECIFIED);
+
+ assertNoForceRotationOrRefresh();
+ }
+
+ @Test
+ public void testTreatmentDisabledViaDeviceConfig_noForceRotationOrRefresh() throws Exception {
+ when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled(
+ /* checkDeviceConfig */ true))
+ .thenReturn(false);
+
+ configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+ assertNoForceRotationOrRefresh();
+ }
+
+ @Test
+ public void testMultiWindowMode_returnUnspecified_noForceRotationOrRefresh() throws Exception {
+ configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ final TestSplitOrganizer organizer = new TestSplitOrganizer(mAtm, mDisplayContent);
+ mActivity.getTask().reparent(organizer.mPrimary, WindowContainer.POSITION_TOP,
+ false /* moveParents */, "test" /* reason */);
+
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+ assertTrue(mActivity.inMultiWindowMode());
+ assertNoForceRotationOrRefresh();
+ }
+
+ @Test
+ public void testOrientationUnspecified_noForceRotationOrRefresh() throws Exception {
+ configureActivity(SCREEN_ORIENTATION_UNSPECIFIED);
+
+ assertNoForceRotationOrRefresh();
+ }
+
+ @Test
+ public void testOrientationLocked_noForceRotationOrRefresh() throws Exception {
+ configureActivity(SCREEN_ORIENTATION_LOCKED);
+
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+ assertNoForceRotationOrRefresh();
+ }
+
+ @Test
+ public void testOrientationNoSensor_noForceRotationOrRefresh() throws Exception {
+ configureActivity(SCREEN_ORIENTATION_NOSENSOR);
+
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+ assertNoForceRotationOrRefresh();
+ }
+
+ @Test
+ public void testIgnoreOrientationRequestIsFalse_noForceRotationOrRefresh() throws Exception {
+ mDisplayContent.setIgnoreOrientationRequest(false);
+
+ configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+ assertNoForceRotationOrRefresh();
+ }
+
+ @Test
+ public void testDisplayNotInternal_noForceRotationOrRefresh() throws Exception {
+ Display display = mDisplayContent.getDisplay();
+ spyOn(display);
+
+ configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+ when(display.getType()).thenReturn(Display.TYPE_EXTERNAL);
+ assertNoForceRotationOrRefresh();
+
+ when(display.getType()).thenReturn(Display.TYPE_WIFI);
+ assertNoForceRotationOrRefresh();
+
+ when(display.getType()).thenReturn(Display.TYPE_OVERLAY);
+ assertNoForceRotationOrRefresh();
+
+ when(display.getType()).thenReturn(Display.TYPE_VIRTUAL);
+ assertNoForceRotationOrRefresh();
+ }
+
+ @Test
+ public void testNoCameraConnection_noForceRotationOrRefresh() throws Exception {
+ configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+
+ assertNoForceRotationOrRefresh();
+ }
+
+ @Test
+ public void testCameraReconnected_forceRotationAndRefresh() throws Exception {
+ configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ mCameraAvailabilityCallback.onCameraClosed(CAMERA_ID_1);
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ callOnActivityConfigurationChanging(mActivity, /* isDisplayRotationChanging */ true);
+
+ assertEquals(mDisplayRotationCompatPolicy.getOrientation(),
+ SCREEN_ORIENTATION_PORTRAIT);
+ assertActivityRefreshRequested(/* refreshRequested */ true);
+ }
+
+ @Test
+ public void testReconnectedToDifferentCamera_forceRotationAndRefresh() throws Exception {
+ configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ mCameraAvailabilityCallback.onCameraClosed(CAMERA_ID_1);
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_2, TEST_PACKAGE_1);
+ callOnActivityConfigurationChanging(mActivity, /* isDisplayRotationChanging */ true);
+
+ assertEquals(mDisplayRotationCompatPolicy.getOrientation(),
+ SCREEN_ORIENTATION_PORTRAIT);
+ assertActivityRefreshRequested(/* refreshRequested */ true);
+ }
+
+ @Test
+ public void testGetOrientation_cameraConnectionClosed_returnUnspecified() {
+ configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+ assertEquals(mDisplayRotationCompatPolicy.getOrientation(),
+ SCREEN_ORIENTATION_PORTRAIT);
+
+ mCameraAvailabilityCallback.onCameraClosed(CAMERA_ID_1);
+
+ assertEquals(mDisplayRotationCompatPolicy.getOrientation(),
+ SCREEN_ORIENTATION_UNSPECIFIED);
+ }
+
+ @Test
+ public void testCameraOpenedForDifferentPackage_noForceRotationOrRefresh() throws Exception {
+ configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_2);
+
+ assertNoForceRotationOrRefresh();
+ }
+
+ @Test
+ public void testGetOrientation_portraitActivity_portraitNaturalOrientation_returnPortrait() {
+ testGetOrientationForActivityAndNaturalOrientations(
+ /* activityOrientation */ SCREEN_ORIENTATION_PORTRAIT,
+ /* naturalOrientation */ ORIENTATION_PORTRAIT,
+ /* expectedOrientation */ SCREEN_ORIENTATION_PORTRAIT);
+ }
+
+ @Test
+ public void testGetOrientation_portraitActivity_landscapeNaturalOrientation_returnLandscape() {
+ testGetOrientationForActivityAndNaturalOrientations(
+ /* activityOrientation */ SCREEN_ORIENTATION_PORTRAIT,
+ /* naturalOrientation */ ORIENTATION_LANDSCAPE,
+ /* expectedOrientation */ SCREEN_ORIENTATION_LANDSCAPE);
+ }
+
+ @Test
+ public void testGetOrientation_landscapeActivity_portraitNaturalOrientation_returnLandscape() {
+ testGetOrientationForActivityAndNaturalOrientations(
+ /* activityOrientation */ SCREEN_ORIENTATION_LANDSCAPE,
+ /* naturalOrientation */ ORIENTATION_PORTRAIT,
+ /* expectedOrientation */ SCREEN_ORIENTATION_LANDSCAPE);
+ }
+
+ @Test
+ public void testGetOrientation_landscapeActivity_landscapeNaturalOrientation_returnPortrait() {
+ testGetOrientationForActivityAndNaturalOrientations(
+ /* activityOrientation */ SCREEN_ORIENTATION_LANDSCAPE,
+ /* naturalOrientation */ ORIENTATION_LANDSCAPE,
+ /* expectedOrientation */ SCREEN_ORIENTATION_PORTRAIT);
+ }
+
+ private void testGetOrientationForActivityAndNaturalOrientations(
+ @ScreenOrientation int activityOrientation,
+ @Orientation int naturalOrientation,
+ @ScreenOrientation int expectedOrientation) {
+ configureActivityAndDisplay(activityOrientation, naturalOrientation);
+
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+ assertEquals(mDisplayRotationCompatPolicy.getOrientation(),
+ expectedOrientation);
+ }
+
+ @Test
+ public void testOnActivityConfigurationChanging_refreshDisabled_noRefresh() throws Exception {
+ when(mLetterboxConfiguration.isCameraCompatRefreshEnabled()).thenReturn(false);
+
+ configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ callOnActivityConfigurationChanging(mActivity, /* isDisplayRotationChanging */ true);
+
+ assertActivityRefreshRequested(/* refreshRequested */ false);
+ }
+
+ @Test
+ public void testOnActivityConfigurationChanging_displayRotationNotChanging_noRefresh()
+ throws Exception {
+ configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ callOnActivityConfigurationChanging(mActivity, /* isDisplayRotationChanging */ false);
+
+ assertActivityRefreshRequested(/* refreshRequested */ false);
+ }
+
+ @Test
+ public void testOnActivityConfigurationChanging_cycleThroughStopDisabled() throws Exception {
+ when(mLetterboxConfiguration.isCameraCompatRefreshCycleThroughStopEnabled())
+ .thenReturn(false);
+
+ configureActivity(SCREEN_ORIENTATION_PORTRAIT);
+
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+ callOnActivityConfigurationChanging(mActivity, /* isDisplayRotationChanging */ true);
+
+ assertActivityRefreshRequested(/* refreshRequested */ true, /* cycleThroughStop */ false);
+ }
+
+ private void configureActivity(@ScreenOrientation int activityOrientation) {
+ configureActivityAndDisplay(activityOrientation, ORIENTATION_PORTRAIT);
+ }
+
+ private void configureActivityAndDisplay(@ScreenOrientation int activityOrientation,
+ @Orientation int naturalOrientation) {
+
+ mTask = new TaskBuilder(mSupervisor)
+ .setDisplay(mDisplayContent)
+ .build();
+
+ mActivity = new ActivityBuilder(mAtm)
+ .setComponent(new ComponentName(TEST_PACKAGE_1, ".TestActivity"))
+ .setScreenOrientation(activityOrientation)
+ .setTask(mTask)
+ .build();
+
+ spyOn(mActivity.mAtmService.getLifecycleManager());
+ spyOn(mActivity.mLetterboxUiController);
+
+ doReturn(mActivity).when(mDisplayContent).topRunningActivity(anyBoolean());
+ doReturn(naturalOrientation).when(mDisplayContent).getNaturalOrientation();
+ }
+
+ private void assertActivityRefreshRequested(boolean refreshRequested) throws Exception {
+ assertActivityRefreshRequested(refreshRequested, /* cycleThroughStop*/ true);
+ }
+
+ private void assertActivityRefreshRequested(boolean refreshRequested,
+ boolean cycleThroughStop) throws Exception {
+ verify(mActivity.mLetterboxUiController, times(refreshRequested ? 1 : 0))
+ .setIsRefreshAfterRotationRequested(true);
+
+ final ClientTransaction transaction = ClientTransaction.obtain(
+ mActivity.app.getThread(), mActivity.token);
+ transaction.addCallback(RefreshCallbackItem.obtain(cycleThroughStop ? ON_STOP : ON_PAUSE));
+ transaction.setLifecycleStateRequest(ResumeActivityItem.obtain(/* isForward */ false));
+
+ verify(mActivity.mAtmService.getLifecycleManager(), times(refreshRequested ? 1 : 0))
+ .scheduleTransaction(eq(transaction));
+ }
+
+ private void assertNoForceRotationOrRefresh() throws Exception {
+ callOnActivityConfigurationChanging(mActivity, /* isDisplayRotationChanging */ true);
+
+ assertEquals(mDisplayRotationCompatPolicy.getOrientation(),
+ SCREEN_ORIENTATION_UNSPECIFIED);
+ assertActivityRefreshRequested(/* refreshRequested */ false);
+ }
+
+ private void callOnActivityConfigurationChanging(
+ ActivityRecord activity, boolean isDisplayRotationChanging) {
+ mDisplayRotationCompatPolicy.onActivityConfigurationChanging(activity,
+ /* newConfig */ createConfigurationWithDisplayRotation(ROTATION_0),
+ /* newConfig */ createConfigurationWithDisplayRotation(
+ isDisplayRotationChanging ? ROTATION_90 : ROTATION_0));
+ }
+
+ private static Configuration createConfigurationWithDisplayRotation(@Rotation int rotation) {
+ final Configuration config = new Configuration();
+ config.windowConfiguration.setDisplayRotation(rotation);
+ return config;
+ }
+}