Merge "Optimize DisplayRepository to reduce lock contention" into main
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 14ae3f5..d03dd16 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -46,6 +46,7 @@
field public static final String REMAP_MODIFIER_KEYS = "android.permission.REMAP_MODIFIER_KEYS";
field public static final String REMOVE_TASKS = "android.permission.REMOVE_TASKS";
field public static final String REQUEST_UNIQUE_ID_ATTESTATION = "android.permission.REQUEST_UNIQUE_ID_ATTESTATION";
+ field public static final String RESERVED_FOR_TESTING_SIGNATURE = "android.permission.RESERVED_FOR_TESTING_SIGNATURE";
field public static final String RESET_APP_ERRORS = "android.permission.RESET_APP_ERRORS";
field public static final String REVOKE_POST_NOTIFICATIONS_WITHOUT_KILL = "android.permission.REVOKE_POST_NOTIFICATIONS_WITHOUT_KILL";
field public static final String SET_AND_VERIFY_LOCKSCREEN_CREDENTIALS = "android.permission.SET_AND_VERIFY_LOCKSCREEN_CREDENTIALS";
diff --git a/core/api/test-lint-baseline.txt b/core/api/test-lint-baseline.txt
index 6cc71e5..b4a3abc 100644
--- a/core/api/test-lint-baseline.txt
+++ b/core/api/test-lint-baseline.txt
@@ -1973,6 +1973,8 @@
UnflaggedApi: android.Manifest.permission#MANAGE_REMOTE_AUTH:
New API must be flagged with @FlaggedApi: field android.Manifest.permission.MANAGE_REMOTE_AUTH
+UnflaggedApi: android.Manifest.permission#RESERVED_FOR_TESTING_SIGNATURE:
+ New API must be flagged with @FlaggedApi: field android.Manifest.permission.RESERVED_FOR_TESTING_SIGNATURE
UnflaggedApi: android.Manifest.permission#START_ACTIVITIES_FROM_SDK_SANDBOX:
New API must be flagged with @FlaggedApi: field android.Manifest.permission.START_ACTIVITIES_FROM_SDK_SANDBOX
UnflaggedApi: android.Manifest.permission#USE_REMOTE_AUTH:
diff --git a/core/java/android/app/assist/AssistContent.java b/core/java/android/app/assist/AssistContent.java
index e5316bc0..a488689 100644
--- a/core/java/android/app/assist/AssistContent.java
+++ b/core/java/android/app/assist/AssistContent.java
@@ -34,12 +34,22 @@
}
/**
+ * Create an AssistContent with extras initialized.
+ *
* @hide
+ */
+ public AssistContent(@android.annotation.NonNull Bundle extras) {
+ mExtras = extras;
+ }
+
+ /**
* Called by {@link android.app.ActivityThread} to set the default Intent based on
* {@link android.app.Activity#getIntent Activity.getIntent}.
*
* <p>Automatically populates {@link #mUri} if that Intent is an {@link Intent#ACTION_VIEW}
* of a web (http or https scheme) URI.</p>
+ *
+ * @hide
*/
public void setDefaultIntent(Intent intent) {
mIntent = intent;
diff --git a/core/java/android/hardware/display/DisplayManagerGlobal.java b/core/java/android/hardware/display/DisplayManagerGlobal.java
index 3d7b714..8519722 100644
--- a/core/java/android/hardware/display/DisplayManagerGlobal.java
+++ b/core/java/android/hardware/display/DisplayManagerGlobal.java
@@ -548,6 +548,20 @@
}
}
+ /**
+ * Request to power a display ON or OFF.
+ * @hide
+ */
+ @RequiresPermission("android.permission.MANAGE_DISPLAYS")
+ public boolean requestDisplayPower(int displayId, boolean on) {
+ try {
+ return mDm.requestDisplayPower(displayId, on);
+ } catch (RemoteException ex) {
+ Log.e(TAG, "Error trying to request display power " + on, ex);
+ return false;
+ }
+ }
+
public void startWifiDisplayScan() {
synchronized (mLock) {
if (mWifiDisplayScanNestCount++ == 0) {
diff --git a/core/java/android/hardware/display/IDisplayManager.aidl b/core/java/android/hardware/display/IDisplayManager.aidl
index 70efc6f..b7c02b0 100644
--- a/core/java/android/hardware/display/IDisplayManager.aidl
+++ b/core/java/android/hardware/display/IDisplayManager.aidl
@@ -236,6 +236,10 @@
@EnforcePermission("MANAGE_DISPLAYS")
void disableConnectedDisplay(int displayId);
+ // Request to power display ON or OFF.
+ @EnforcePermission("MANAGE_DISPLAYS")
+ boolean requestDisplayPower(int displayId, boolean on);
+
// Restricts display modes to specified modeIds.
@EnforcePermission("RESTRICT_DISPLAY_MODES")
void requestDisplayModes(in IBinder token, int displayId, in @nullable int[] modeIds);
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 05345d8..63f0b9e 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -6101,6 +6101,15 @@
public static final String POINTER_SPEED = "pointer_speed";
/**
+ * Pointer scale setting.
+ *
+ * <p>This float value represents the scale by which the size of the pointer increases.
+ * @hide
+ */
+ @Readable
+ public static final String POINTER_SCALE = "pointer_scale";
+
+ /**
* Touchpad pointer speed setting.
* This is an integer value in a range between -7 and +7, so there are 15 possible values.
* -7 = slowest
@@ -6358,6 +6367,7 @@
PRIVATE_SETTINGS.add(SIP_ASK_ME_EACH_TIME);
PRIVATE_SETTINGS.add(POINTER_SPEED);
PRIVATE_SETTINGS.add(POINTER_FILL_STYLE);
+ PRIVATE_SETTINGS.add(POINTER_SCALE);
PRIVATE_SETTINGS.add(LOCK_TO_APP_ENABLED);
PRIVATE_SETTINGS.add(EGG_MODE);
PRIVATE_SETTINGS.add(SHOW_BATTERY_PERCENT);
diff --git a/core/java/android/service/contentcapture/ContentCaptureService.java b/core/java/android/service/contentcapture/ContentCaptureService.java
index e1965ef..405fe26 100644
--- a/core/java/android/service/contentcapture/ContentCaptureService.java
+++ b/core/java/android/service/contentcapture/ContentCaptureService.java
@@ -29,6 +29,7 @@
import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.app.Service;
+import android.app.assist.AssistContent;
import android.content.ComponentName;
import android.content.ContentCaptureOptions;
import android.content.Intent;
@@ -133,6 +134,16 @@
*/
public static final String SERVICE_META_DATA = "android.content_capture";
+
+ /**
+ * Extras key to flag that the passed in {@link AssistContent} is sent only during Activity
+ * start.
+ *
+ * @hide
+ */
+ public static final String ASSIST_CONTENT_ACTIVITY_START_KEY = "activity_start_assist_content";
+
+
private final LocalDataShareAdapterResourceManager mDataShareAdapterResourceManager =
new LocalDataShareAdapterResourceManager();
diff --git a/core/java/android/view/PointerIcon.java b/core/java/android/view/PointerIcon.java
index 7c2577f..c302126 100644
--- a/core/java/android/view/PointerIcon.java
+++ b/core/java/android/view/PointerIcon.java
@@ -193,6 +193,9 @@
/** @hide */ public static final int POINTER_ICON_VECTOR_STYLE_FILL_END =
POINTER_ICON_VECTOR_STYLE_FILL_BLUE;
+ /** @hide */ public static final float DEFAULT_POINTER_SCALE = 1f;
+ /** @hide */ public static final float LARGE_POINTER_SCALE = 2.5f;
+
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
private final int mType;
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
@@ -253,7 +256,7 @@
* @hide
*/
public static @NonNull PointerIcon getLoadedSystemIcon(@NonNull Context context, int type,
- boolean useLargeIcons) {
+ boolean useLargeIcons, float pointerScale) {
if (type == TYPE_NOT_SPECIFIED) {
throw new IllegalStateException("Cannot load icon for type TYPE_NOT_SPECIFIED");
}
@@ -268,13 +271,18 @@
}
final int defStyle;
- // TODO(b/305193969): Use scaled vectors when large icons are requested.
- if (useLargeIcons) {
- defStyle = com.android.internal.R.style.LargePointer;
- } else if (android.view.flags.Flags.enableVectorCursors()) {
+ if (android.view.flags.Flags.enableVectorCursorA11ySettings()) {
defStyle = com.android.internal.R.style.VectorPointer;
} else {
- defStyle = com.android.internal.R.style.Pointer;
+ // TODO(b/346358375): Remove useLargeIcons and the legacy pointer styles when
+ // enableVectorCursorA11ySetting is rolled out.
+ if (useLargeIcons) {
+ defStyle = com.android.internal.R.style.LargePointer;
+ } else if (android.view.flags.Flags.enableVectorCursors()) {
+ defStyle = com.android.internal.R.style.VectorPointer;
+ } else {
+ defStyle = com.android.internal.R.style.Pointer;
+ }
}
TypedArray a = context.obtainStyledAttributes(null,
com.android.internal.R.styleable.Pointer,
@@ -286,11 +294,11 @@
Log.w(TAG, "Missing theme resources for pointer icon type " + type);
return type == TYPE_DEFAULT
? getSystemIcon(TYPE_NULL)
- : getLoadedSystemIcon(context, TYPE_DEFAULT, useLargeIcons);
+ : getLoadedSystemIcon(context, TYPE_DEFAULT, useLargeIcons, pointerScale);
}
final PointerIcon icon = new PointerIcon(type);
- icon.loadResource(context.getResources(), resourceId, context.getTheme());
+ icon.loadResource(context.getResources(), resourceId, context.getTheme(), pointerScale);
return icon;
}
@@ -353,7 +361,7 @@
}
PointerIcon icon = new PointerIcon(TYPE_CUSTOM);
- icon.loadResource(resources, resourceId, null);
+ icon.loadResource(resources, resourceId, null, DEFAULT_POINTER_SCALE);
return icon;
}
@@ -460,12 +468,13 @@
}
private BitmapDrawable getBitmapDrawableFromVectorDrawable(Resources resources,
- VectorDrawable vectorDrawable) {
+ VectorDrawable vectorDrawable, float pointerScale) {
// Ensure we pass the display metrics into the Bitmap constructor so that it is initialized
// with the correct density.
Bitmap bitmap = Bitmap.createBitmap(resources.getDisplayMetrics(),
- vectorDrawable.getIntrinsicWidth(),
- vectorDrawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888, true /* hasAlpha */);
+ (int) (vectorDrawable.getIntrinsicWidth() * pointerScale),
+ (int) (vectorDrawable.getIntrinsicHeight() * pointerScale),
+ Bitmap.Config.ARGB_8888, true /* hasAlpha */);
Canvas canvas = new Canvas(bitmap);
vectorDrawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
vectorDrawable.draw(canvas);
@@ -473,7 +482,7 @@
}
private void loadResource(@NonNull Resources resources, @XmlRes int resourceId,
- @Nullable Resources.Theme theme) {
+ @Nullable Resources.Theme theme, float pointerScale) {
final XmlResourceParser parser = resources.getXml(resourceId);
final int bitmapRes;
final float hotSpotX;
@@ -484,8 +493,10 @@
final TypedArray a = resources.obtainAttributes(
parser, com.android.internal.R.styleable.PointerIcon);
bitmapRes = a.getResourceId(com.android.internal.R.styleable.PointerIcon_bitmap, 0);
- hotSpotX = a.getDimension(com.android.internal.R.styleable.PointerIcon_hotSpotX, 0);
- hotSpotY = a.getDimension(com.android.internal.R.styleable.PointerIcon_hotSpotY, 0);
+ hotSpotX = a.getDimension(com.android.internal.R.styleable.PointerIcon_hotSpotX, 0)
+ * pointerScale;
+ hotSpotY = a.getDimension(com.android.internal.R.styleable.PointerIcon_hotSpotY, 0)
+ * pointerScale;
a.recycle();
} catch (Exception ex) {
throw new IllegalArgumentException("Exception parsing pointer icon resource.", ex);
@@ -534,7 +545,7 @@
}
if (isVectorAnimation) {
drawableFrame = getBitmapDrawableFromVectorDrawable(resources,
- (VectorDrawable) drawableFrame);
+ (VectorDrawable) drawableFrame, pointerScale);
}
mBitmapFrames[i - 1] = getBitmapFromDrawable((BitmapDrawable) drawableFrame);
}
@@ -542,7 +553,8 @@
}
if (drawable instanceof VectorDrawable) {
mDrawNativeDropShadow = true;
- drawable = getBitmapDrawableFromVectorDrawable(resources, (VectorDrawable) drawable);
+ drawable = getBitmapDrawableFromVectorDrawable(resources, (VectorDrawable) drawable,
+ pointerScale);
}
if (!(drawable instanceof BitmapDrawable)) {
throw new IllegalArgumentException("<pointer-icon> bitmap attribute must "
diff --git a/core/java/android/view/contentcapture/ContentCaptureManager.java b/core/java/android/view/contentcapture/ContentCaptureManager.java
index bcef37f..d74867c 100644
--- a/core/java/android/view/contentcapture/ContentCaptureManager.java
+++ b/core/java/android/view/contentcapture/ContentCaptureManager.java
@@ -366,6 +366,14 @@
"enable_content_protection_receiver";
/**
+ * Whether AssistContent snapshot should be sent on activity start.
+ *
+ * @hide
+ */
+ public static final String DEVICE_CONFIG_ENABLE_ACTIVITY_START_ASSIST_CONTENT =
+ "enable_activity_start_assist_content";
+
+ /**
* Sets the size of the in-memory ring buffer for the content protection flow.
*
* @hide
diff --git a/core/java/com/android/internal/policy/PhoneWindow.java b/core/java/com/android/internal/policy/PhoneWindow.java
index 2194c89..40d760e 100644
--- a/core/java/com/android/internal/policy/PhoneWindow.java
+++ b/core/java/com/android/internal/policy/PhoneWindow.java
@@ -537,8 +537,13 @@
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
- if (cb != null && !isDestroyed()) {
- cb.onContentChanged();
+ if (!isDestroyed()) {
+ if (cb != null) {
+ cb.onContentChanged();
+ }
+ if (mDecorContentParent != null) {
+ mDecorContentParent.notifyContentChanged();
+ }
}
mContentParentExplicitlySet = true;
}
@@ -568,8 +573,13 @@
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
- if (cb != null && !isDestroyed()) {
- cb.onContentChanged();
+ if (!isDestroyed()) {
+ if (cb != null) {
+ cb.onContentChanged();
+ }
+ if (mDecorContentParent != null) {
+ mDecorContentParent.notifyContentChanged();
+ }
}
mContentParentExplicitlySet = true;
}
@@ -586,8 +596,13 @@
mContentParent.addView(view, params);
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
- if (cb != null && !isDestroyed()) {
- cb.onContentChanged();
+ if (!isDestroyed()) {
+ if (cb != null) {
+ cb.onContentChanged();
+ }
+ if (mDecorContentParent != null) {
+ mDecorContentParent.notifyContentChanged();
+ }
}
}
diff --git a/core/java/com/android/internal/widget/ActionBarOverlayLayout.java b/core/java/com/android/internal/widget/ActionBarOverlayLayout.java
index 6832825..ff57fd4 100644
--- a/core/java/com/android/internal/widget/ActionBarOverlayLayout.java
+++ b/core/java/com/android/internal/widget/ActionBarOverlayLayout.java
@@ -898,6 +898,13 @@
mDecorToolbar.dismissPopupMenus();
}
+ @Override
+ public void notifyContentChanged() {
+ mLastBaseContentInsets.setEmpty();
+ mLastBaseInnerInsets = WindowInsets.CONSUMED;
+ mLastInnerInsets = WindowInsets.CONSUMED;
+ }
+
public static class LayoutParams extends MarginLayoutParams {
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
diff --git a/core/java/com/android/internal/widget/DecorContentParent.java b/core/java/com/android/internal/widget/DecorContentParent.java
index ac524f9..8d6cfd1 100644
--- a/core/java/com/android/internal/widget/DecorContentParent.java
+++ b/core/java/com/android/internal/widget/DecorContentParent.java
@@ -22,6 +22,7 @@
import android.util.SparseArray;
import android.view.Menu;
import android.view.Window;
+
import com.android.internal.view.menu.MenuPresenter;
/**
@@ -49,4 +50,5 @@
void saveToolbarHierarchyState(SparseArray<Parcelable> toolbarStates);
void restoreToolbarHierarchyState(SparseArray<Parcelable> toolbarStates);
void dismissPopups();
+ void notifyContentChanged();
}
diff --git a/core/proto/android/providers/settings/system.proto b/core/proto/android/providers/settings/system.proto
index 1233069..6a0ec1d 100644
--- a/core/proto/android/providers/settings/system.proto
+++ b/core/proto/android/providers/settings/system.proto
@@ -126,6 +126,7 @@
option (android.msg_privacy).dest = DEST_EXPLICIT;
optional SettingProto pointer_fill_style = 1 [ (android.privacy).dest = DEST_AUTOMATIC ];
+ optional SettingProto pointer_scale = 3 [ (android.privacy).dest = DEST_AUTOMATIC ];
}
optional Pointer pointer = 37;
optional SettingProto pointer_speed = 18 [ (android.privacy).dest = DEST_AUTOMATIC ];
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 09ffdf3..c6db371 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -3249,16 +3249,20 @@
<permission android:name="android.permission.INTERACT_ACROSS_PROFILES"
android:protectionLevel="signature|appop" />
- <!-- Allows applications to access profiles with ACCESS_HIDDEN_PROFILES user property
- <p>Protection level: normal
- @FlaggedApi("android.multiuser.enable_permission_to_access_hidden_profiles") -->
+ <!-- Allows applications to access profiles with
+ {@code android.content.pm.UserProperties#PROFILE_API_VISIBILITY_HIDDEN} user property, e.g.
+ {@link android.os.UserManager#USER_TYPE_PROFILE_PRIVATE}.
+ <p>Protection level: normal
+ @FlaggedApi("android.multiuser.enable_permission_to_access_hidden_profiles") -->
<permission android:name="android.permission.ACCESS_HIDDEN_PROFILES"
android:label="@string/permlab_accessHiddenProfile"
android:description="@string/permdesc_accessHiddenProfile"
android:protectionLevel="normal" />
- <!-- @SystemApi @hide Allows privileged applications to get details about hidden profile
- users.
+ <!-- @SystemApi @hide Allows privileged applications to get details about profiles with
+ {@code android.content.pm.UserProperties#PROFILE_API_VISIBILITY_HIDDEN} user property, e.g.
+ {@link android.os.UserManager#USER_TYPE_PROFILE_PRIVATE}. Removes extra requirements such
+ as having {@link android.app.role.RoleManager#ROLE_HOME} role for LauncherApps APIs.
@FlaggedApi("android.multiuser.enable_permission_to_access_hidden_profiles") -->
<permission
android:name="android.permission.ACCESS_HIDDEN_PROFILES_FULL"
@@ -8194,6 +8198,17 @@
<permission android:name="android.permission.SETUP_FSVERITY"
android:protectionLevel="signature|privileged"/>
+ <!--
+ @TestApi
+ Signature permission reserved for testing. This should never be used to
+ gate any actual functionality.
+ <p>
+ Protection level: signature
+ @hide
+ -->
+ <permission android:name="android.permission.RESERVED_FOR_TESTING_SIGNATURE"
+ android:protectionLevel="signature"/>
+
<!-- Attribution for Geofencing service. -->
<attribution android:tag="GeofencingService" android:label="@string/geofencing_service"/>
<!-- Attribution for Country Detector. -->
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
index 16c77d0..ecf4720 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java
@@ -24,6 +24,7 @@
import android.app.compat.CompatChanges;
import android.content.Context;
import android.hardware.devicestate.DeviceStateManager;
+import android.os.SystemProperties;
import android.util.Log;
import androidx.annotation.NonNull;
@@ -50,6 +51,11 @@
private static final String TAG = "WindowExtensionsImpl";
/**
+ * The value of the system property that indicates no override is set.
+ */
+ private static final int NO_LEVEL_OVERRIDE = -1;
+
+ /**
* The min version of the WM Extensions that must be supported in the current platform version.
*/
@VisibleForTesting
@@ -66,14 +72,30 @@
WindowExtensionsImpl() {
mIsActivityEmbeddingEnabled = isActivityEmbeddingEnabled();
- Log.i(TAG, "Initializing Window Extensions, vendor API level=" + mVersion
- + ", activity embedding enabled=" + mIsActivityEmbeddingEnabled);
+
+ Log.i(TAG, generateLogMessage());
+ }
+
+ private String generateLogMessage() {
+ final StringBuilder logBuilder = new StringBuilder("Initializing Window Extensions, "
+ + "vendor API level=" + mVersion);
+ final int levelOverride = getLevelOverride();
+ if (levelOverride != NO_LEVEL_OVERRIDE) {
+ logBuilder.append(", override to ").append(levelOverride);
+ }
+ logBuilder.append(", activity embedding enabled=").append(mIsActivityEmbeddingEnabled);
+ return logBuilder.toString();
}
// TODO(b/241126279) Introduce constants to better version functionality
@Override
public int getVendorApiLevel() {
- return mVersion;
+ final int levelOverride = getLevelOverride();
+ return (levelOverride != NO_LEVEL_OVERRIDE) ? levelOverride : mVersion;
+ }
+
+ private int getLevelOverride() {
+ return SystemProperties.getInt("persist.wm.debug.ext_version_override", NO_LEVEL_OVERRIDE);
}
@NonNull
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
index a7da07d..972dce5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
@@ -209,7 +209,7 @@
@Override
public void onDismissBubble(Bubble bubble) {
- mManager.dismissBubble(bubble, Bubbles.DISMISS_USER_REMOVED);
+ mManager.dismissBubble(bubble, Bubbles.DISMISS_USER_GESTURE);
}
});
mHandleView.setOnClickListener(view -> {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
index 57e95d6..f4ac5f2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
@@ -538,8 +538,10 @@
@Override
public void onAnimationStart(Animator animation) {
+ ValueAnimator valueAnimator = (ValueAnimator) animation;
+ float value = (float) valueAnimator.getAnimatedValue();
SurfaceControl.Transaction t = mTransactionPool.acquire();
- t.setPosition(mImeSourceControl.getLeash(), x, startY);
+ t.setPosition(mImeSourceControl.getLeash(), x, value);
if (DEBUG) {
Slog.d(TAG, "onAnimationStart d:" + mDisplayId + " top:"
+ imeTop(hiddenY) + "->" + imeTop(shownY)
@@ -549,7 +551,7 @@
imeTop(shownY), mAnimationDirection == DIRECTION_SHOW, isFloating, t);
mAnimateAlpha = (flags & ImePositionProcessor.IME_ANIMATION_NO_ALPHA) == 0;
final float alpha = (mAnimateAlpha || isFloating)
- ? (startY - hiddenY) / (shownY - hiddenY)
+ ? (value - hiddenY) / (shownY - hiddenY)
: 1.f;
t.setAlpha(mImeSourceControl.getLeash(), alpha);
if (mAnimationDirection == DIRECTION_SHOW) {
@@ -560,7 +562,7 @@
if (DEBUG_IME_VISIBILITY) {
EventLog.writeEvent(IMF_IME_REMOTE_ANIM_START,
mStatsToken != null ? mStatsToken.getTag() : ImeTracker.TOKEN_NONE,
- mDisplayId, mAnimationDirection, alpha, startY , endY,
+ mDisplayId, mAnimationDirection, alpha, value, endY,
Objects.toString(mImeSourceControl.getLeash()),
Objects.toString(mImeSourceControl.getInsetsHint()),
Objects.toString(mImeSourceControl.getSurfacePosition()),
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index 991fbaf..609e5af 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -87,6 +87,7 @@
import com.android.wm.shell.recents.RecentTasks;
import com.android.wm.shell.recents.RecentTasksController;
import com.android.wm.shell.recents.RecentsTransitionHandler;
+import com.android.wm.shell.recents.TaskStackTransitionObserver;
import com.android.wm.shell.shared.DesktopModeStatus;
import com.android.wm.shell.shared.ShellTransitions;
import com.android.wm.shell.shared.annotations.ShellAnimationThread;
@@ -619,12 +620,13 @@
TaskStackListenerImpl taskStackListener,
ActivityTaskManager activityTaskManager,
Optional<DesktopModeTaskRepository> desktopModeTaskRepository,
+ TaskStackTransitionObserver taskStackTransitionObserver,
@ShellMainThread ShellExecutor mainExecutor
) {
return Optional.ofNullable(
RecentTasksController.create(context, shellInit, shellController,
shellCommandHandler, taskStackListener, activityTaskManager,
- desktopModeTaskRepository, mainExecutor));
+ desktopModeTaskRepository, taskStackTransitionObserver, mainExecutor));
}
@BindsOptionalOf
@@ -924,6 +926,19 @@
}
//
+ // Task Stack
+ //
+
+ @WMSingleton
+ @Provides
+ static TaskStackTransitionObserver provideTaskStackTransitionObserver(
+ Lazy<Transitions> transitions,
+ ShellInit shellInit
+ ) {
+ return new TaskStackTransitionObserver(transitions, shellInit);
+ }
+
+ //
// Misc
//
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 12bbd51..87bd840 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -121,9 +121,9 @@
*/
@Module(
includes = {
- WMShellBaseModule.class,
- PipModule.class,
- ShellBackAnimationModule.class,
+ WMShellBaseModule.class,
+ PipModule.class,
+ ShellBackAnimationModule.class,
})
public abstract class WMShellModule {
@@ -664,7 +664,8 @@
@Provides
static Object provideIndependentShellComponentsToCreate(
DragAndDropController dragAndDropController,
- Optional<DesktopTasksTransitionObserver> desktopTasksTransitionObserverOptional) {
+ Optional<DesktopTasksTransitionObserver> desktopTasksTransitionObserverOptional
+ ) {
return new Object();
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl
index 62d195e..245829e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl
@@ -42,4 +42,7 @@
* Called when a running task changes.
*/
void onRunningTaskChanged(in RunningTaskInfo taskInfo);
-}
+
+ /** A task has moved to front. */
+ oneway void onTaskMovedToFront(in RunningTaskInfo taskInfo);
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/OWNERS
new file mode 100644
index 0000000..452644b
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/OWNERS
@@ -0,0 +1,6 @@
+# WM shell sub-module task stack owners
+uysalorhan@google.com
+samcackett@google.com
+alexchau@google.com
+silvajordan@google.com
+uwaisashraf@google.com
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
index 9d16246..03c8cf8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
@@ -20,6 +20,7 @@
import static android.content.pm.PackageManager.FEATURE_PC;
import static com.android.window.flags.Flags.enableDesktopWindowingTaskbarRunningApps;
+import static com.android.window.flags.Flags.enableTaskStackObserverInShell;
import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_RECENT_TASKS;
import android.app.ActivityManager;
@@ -57,6 +58,7 @@
import com.android.wm.shell.sysui.ShellCommandHandler;
import com.android.wm.shell.sysui.ShellController;
import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.transition.Transitions;
import com.android.wm.shell.util.GroupedRecentTaskInfo;
import com.android.wm.shell.util.SplitBounds;
@@ -73,7 +75,8 @@
* Manages the recent task list from the system, caching it as necessary.
*/
public class RecentTasksController implements TaskStackListenerCallback,
- RemoteCallable<RecentTasksController>, DesktopModeTaskRepository.ActiveTasksListener {
+ RemoteCallable<RecentTasksController>, DesktopModeTaskRepository.ActiveTasksListener,
+ TaskStackTransitionObserver.TaskStackTransitionObserverListener {
private static final String TAG = RecentTasksController.class.getSimpleName();
private final Context mContext;
@@ -84,6 +87,7 @@
private final TaskStackListenerImpl mTaskStackListener;
private final RecentTasksImpl mImpl = new RecentTasksImpl();
private final ActivityTaskManager mActivityTaskManager;
+ private final TaskStackTransitionObserver mTaskStackTransitionObserver;
private RecentsTransitionHandler mTransitionHandler = null;
private IRecentTasksListener mListener;
private final boolean mPcFeatureEnabled;
@@ -112,13 +116,15 @@
TaskStackListenerImpl taskStackListener,
ActivityTaskManager activityTaskManager,
Optional<DesktopModeTaskRepository> desktopModeTaskRepository,
+ TaskStackTransitionObserver taskStackTransitionObserver,
@ShellMainThread ShellExecutor mainExecutor
) {
if (!context.getResources().getBoolean(com.android.internal.R.bool.config_hasRecents)) {
return null;
}
return new RecentTasksController(context, shellInit, shellController, shellCommandHandler,
- taskStackListener, activityTaskManager, desktopModeTaskRepository, mainExecutor);
+ taskStackListener, activityTaskManager, desktopModeTaskRepository,
+ taskStackTransitionObserver, mainExecutor);
}
RecentTasksController(Context context,
@@ -128,6 +134,7 @@
TaskStackListenerImpl taskStackListener,
ActivityTaskManager activityTaskManager,
Optional<DesktopModeTaskRepository> desktopModeTaskRepository,
+ TaskStackTransitionObserver taskStackTransitionObserver,
ShellExecutor mainExecutor) {
mContext = context;
mShellController = shellController;
@@ -136,6 +143,7 @@
mPcFeatureEnabled = mContext.getPackageManager().hasSystemFeature(FEATURE_PC);
mTaskStackListener = taskStackListener;
mDesktopModeTaskRepository = desktopModeTaskRepository;
+ mTaskStackTransitionObserver = taskStackTransitionObserver;
mMainExecutor = mainExecutor;
shellInit.addInitCallback(this::onInit, this);
}
@@ -154,6 +162,10 @@
mShellCommandHandler.addDumpCallback(this::dump, this);
mTaskStackListener.addListener(this);
mDesktopModeTaskRepository.ifPresent(it -> it.addActiveTaskListener(this));
+ if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+ mTaskStackTransitionObserver.addTaskStackTransitionObserverListener(this,
+ mMainExecutor);
+ }
}
void setTransitionHandler(RecentsTransitionHandler handler) {
@@ -267,6 +279,12 @@
notifyRecentTasksChanged();
}
+ @Override
+ public void onTaskMovedToFrontThroughTransition(
+ ActivityManager.RunningTaskInfo runningTaskInfo) {
+ notifyTaskMovedToFront(runningTaskInfo);
+ }
+
@VisibleForTesting
void notifyRecentTasksChanged() {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENT_TASKS, "Notify recent tasks changed");
@@ -328,6 +346,19 @@
}
}
+ private void notifyTaskMovedToFront(ActivityManager.RunningTaskInfo taskInfo) {
+ if (mListener == null
+ || !enableTaskStackObserverInShell()
+ || taskInfo.realActivity == null) {
+ return;
+ }
+ try {
+ mListener.onTaskMovedToFront(taskInfo);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Failed call onTaskMovedToFront", e);
+ }
+ }
+
private boolean shouldEnableRunningTasksForDesktopMode() {
return mPcFeatureEnabled
|| (DesktopModeStatus.canEnterDesktopMode(mContext)
@@ -464,6 +495,7 @@
}
return null;
}
+
public void dump(@NonNull PrintWriter pw, String prefix) {
final String innerPrefix = prefix + " ";
pw.println(prefix + TAG);
@@ -547,6 +579,11 @@
public void onRunningTaskChanged(ActivityManager.RunningTaskInfo taskInfo) {
mListener.call(l -> l.onRunningTaskChanged(taskInfo));
}
+
+ @Override
+ public void onTaskMovedToFront(ActivityManager.RunningTaskInfo taskInfo) {
+ mListener.call(l -> l.onTaskMovedToFront(taskInfo));
+ }
};
public IRecentTasksImpl(RecentTasksController controller) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt
new file mode 100644
index 0000000..7c5f10a
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2024 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.wm.shell.recents
+
+import android.app.ActivityManager.RunningTaskInfo
+import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.os.IBinder
+import android.util.ArrayMap
+import android.view.SurfaceControl
+import android.view.WindowManager
+import android.window.TransitionInfo
+import com.android.window.flags.Flags.enableTaskStackObserverInShell
+import com.android.wm.shell.shared.TransitionUtil
+import com.android.wm.shell.sysui.ShellInit
+import com.android.wm.shell.transition.Transitions
+import dagger.Lazy
+import java.util.concurrent.Executor
+
+/**
+ * A [Transitions.TransitionObserver] that observes shell transitions and sends updates to listeners
+ * about task stack changes.
+ *
+ * TODO(346588978) Move split/pip signals here as well so that launcher don't need to handle it
+ */
+class TaskStackTransitionObserver(
+ private val transitions: Lazy<Transitions>,
+ shellInit: ShellInit
+) : Transitions.TransitionObserver {
+
+ private val transitionToTransitionChanges: MutableMap<IBinder, TransitionChanges> =
+ mutableMapOf()
+ private val taskStackTransitionObserverListeners =
+ ArrayMap<TaskStackTransitionObserverListener, Executor>()
+
+ init {
+ if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+ shellInit.addInitCallback(::onInit, this)
+ }
+ }
+
+ fun onInit() {
+ transitions.get().registerObserver(this)
+ }
+
+ override fun onTransitionReady(
+ transition: IBinder,
+ info: TransitionInfo,
+ startTransaction: SurfaceControl.Transaction,
+ finishTransaction: SurfaceControl.Transaction
+ ) {
+ if (enableTaskStackObserverInShell()) {
+ val taskInfoList = mutableListOf<RunningTaskInfo>()
+ val transitionTypeList = mutableListOf<Int>()
+
+ for (change in info.changes) {
+ if (change.flags and TransitionInfo.FLAG_IS_WALLPAPER != 0) {
+ continue
+ }
+
+ val taskInfo = change.taskInfo
+ if (taskInfo == null || taskInfo.taskId == -1) {
+ continue
+ }
+
+ if (change.mode == WindowManager.TRANSIT_OPEN) {
+ change.taskInfo?.let { taskInfoList.add(it) }
+ transitionTypeList.add(change.mode)
+ }
+ }
+ transitionToTransitionChanges.put(
+ transition,
+ TransitionChanges(taskInfoList, transitionTypeList)
+ )
+ }
+ }
+
+ override fun onTransitionStarting(transition: IBinder) {}
+
+ override fun onTransitionMerged(merged: IBinder, playing: IBinder) {}
+
+ override fun onTransitionFinished(transition: IBinder, aborted: Boolean) {
+ val taskInfoList =
+ transitionToTransitionChanges.getOrDefault(transition, TransitionChanges()).taskInfoList
+ val typeList =
+ transitionToTransitionChanges
+ .getOrDefault(transition, TransitionChanges())
+ .transitionTypeList
+ transitionToTransitionChanges.remove(transition)
+
+ for ((index, taskInfo) in taskInfoList.withIndex()) {
+ if (
+ TransitionUtil.isOpeningType(typeList[index]) &&
+ taskInfo.windowingMode == WINDOWING_MODE_FREEFORM
+ ) {
+ notifyTaskStackTransitionObserverListeners(taskInfo)
+ }
+ }
+ }
+
+ fun addTaskStackTransitionObserverListener(
+ taskStackTransitionObserverListener: TaskStackTransitionObserverListener,
+ executor: Executor
+ ) {
+ taskStackTransitionObserverListeners[taskStackTransitionObserverListener] = executor
+ }
+
+ fun removeTaskStackTransitionObserverListener(
+ taskStackTransitionObserverListener: TaskStackTransitionObserverListener
+ ) {
+ taskStackTransitionObserverListeners.remove(taskStackTransitionObserverListener)
+ }
+
+ private fun notifyTaskStackTransitionObserverListeners(taskInfo: RunningTaskInfo) {
+ taskStackTransitionObserverListeners.forEach { (listener, executor) ->
+ executor.execute { listener.onTaskMovedToFrontThroughTransition(taskInfo) }
+ }
+ }
+
+ /** Listener to use to get updates regarding task stack from this observer */
+ interface TaskStackTransitionObserverListener {
+ /** Called when a task is moved to front. */
+ fun onTaskMovedToFrontThroughTransition(taskInfo: RunningTaskInfo) {}
+ }
+
+ private data class TransitionChanges(
+ val taskInfoList: MutableList<RunningTaskInfo> = ArrayList(),
+ val transitionTypeList: MutableList<Int> = ArrayList()
+ )
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/Android.bp b/libs/WindowManager/Shell/tests/flicker/splitscreen/Android.bp
index f813b0d..0fe7a16b 100644
--- a/libs/WindowManager/Shell/tests/flicker/splitscreen/Android.bp
+++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/Android.bp
@@ -65,9 +65,11 @@
android_test {
name: "WMShellFlickerTestsSplitScreenGroup2",
+ defaults: ["WMShellFlickerTestsDefault"],
manifest: "AndroidManifest.xml",
package_name: "com.android.wm.shell.flicker.splitscreen",
instrumentation_target_package: "com.android.wm.shell.flicker.splitscreen",
+ test_config_template: "AndroidTestTemplate.xml",
srcs: [
":WMShellFlickerTestsSplitScreenBase-src",
":WMShellFlickerTestsSplitScreenGroup2-src",
diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/MultipleShowImeRequestsInSplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/MultipleShowImeRequestsInSplitScreen.kt
new file mode 100644
index 0000000..dad5db9
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/MultipleShowImeRequestsInSplitScreen.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2024 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.wm.shell.flicker.splitscreen
+
+import android.platform.test.annotations.Presubmit
+import android.tools.Rotation
+import android.tools.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.flicker.legacy.FlickerBuilder
+import android.tools.flicker.legacy.LegacyFlickerTest
+import android.tools.flicker.legacy.LegacyFlickerTestFactory
+import android.tools.traces.component.ComponentNameMatcher
+import androidx.test.filters.RequiresDevice
+import com.android.wm.shell.flicker.splitscreen.benchmark.MultipleShowImeRequestsInSplitScreenBenchmark
+import com.android.wm.shell.flicker.utils.ICommonAssertions
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test quick switch between two split pairs.
+ *
+ * To run this test: `atest WMShellFlickerTestsSplitScreenGroup2:MultipleShowImeRequestsInSplitScreen`
+ */
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class MultipleShowImeRequestsInSplitScreen(override val flicker: LegacyFlickerTest) :
+ MultipleShowImeRequestsInSplitScreenBenchmark(flicker), ICommonAssertions {
+ override val transition: FlickerBuilder.() -> Unit
+ get() = {
+ defaultSetup(this)
+ defaultTeardown(this)
+ thisTransition(this)
+ }
+
+ @Presubmit
+ @Test
+ fun imeLayerAlwaysVisible() =
+ flicker.assertLayers {
+ this.isVisible(ComponentNameMatcher.IME)
+ }
+
+ companion object {
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams() = LegacyFlickerTestFactory.nonRotationTests(
+ supportedRotations = listOf(Rotation.ROTATION_0)
+ )
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/UnlockKeyguardToSplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/UnlockKeyguardToSplitScreen.kt
index 9045364..d349988 100644
--- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/UnlockKeyguardToSplitScreen.kt
+++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/UnlockKeyguardToSplitScreen.kt
@@ -47,7 +47,7 @@
* To run this test: `atest WMShellFlickerTestsSplitScreen:UnlockKeyguardToSplitScreen`
*/
@RequiresDevice
-@Postsubmit
+@FlakyTest(bugId = 293578017)
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
@@ -61,7 +61,6 @@
}
@Test
- @FlakyTest(bugId = 293578017)
override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
super.visibleLayersShownMoreThanOneConsecutiveEntry()
diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/MultipleShowImeRequestsInSplitScreenBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/MultipleShowImeRequestsInSplitScreenBenchmark.kt
new file mode 100644
index 0000000..2492531
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/MultipleShowImeRequestsInSplitScreenBenchmark.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2024 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.wm.shell.flicker.splitscreen.benchmark
+
+import android.tools.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.flicker.legacy.FlickerBuilder
+import android.tools.flicker.legacy.LegacyFlickerTest
+import android.tools.flicker.legacy.LegacyFlickerTestFactory
+import androidx.test.filters.RequiresDevice
+import com.android.server.wm.flicker.helpers.ImeAppHelper
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
+import org.junit.FixMethodOrder
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+abstract class MultipleShowImeRequestsInSplitScreenBenchmark(
+ override val flicker: LegacyFlickerTest
+) : SplitScreenBase(flicker) {
+ override val primaryApp = ImeAppHelper(instrumentation)
+ override val defaultTeardown: FlickerBuilder.() -> Unit
+ get() = {
+ teardown {
+ primaryApp.closeIME(wmHelper)
+ super.defaultTeardown
+ }
+ }
+
+ protected val thisTransition: FlickerBuilder.() -> Unit
+ get() = {
+ setup {
+ SplitScreenUtils.enterSplit(
+ wmHelper,
+ tapl,
+ device,
+ primaryApp,
+ secondaryApp,
+ flicker.scenario.startRotation
+ )
+ // initially open the IME
+ primaryApp.openIME(wmHelper)
+ }
+ transitions {
+ for (i in 1..OPEN_IME_COUNT) {
+ primaryApp.openIME(wmHelper)
+ }
+ }
+ }
+
+ companion object {
+ const val OPEN_IME_COUNT = 30
+
+ @Parameterized.Parameters(name = "{0}")
+ @JvmStatic
+ fun getParams() = LegacyFlickerTestFactory.nonRotationTests()
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/SplitScreenBase.kt b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/SplitScreenBase.kt
index 4b10603..51074f6 100644
--- a/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/SplitScreenBase.kt
+++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/src/com/android/wm/shell/flicker/splitscreen/benchmark/SplitScreenBase.kt
@@ -25,7 +25,7 @@
abstract class SplitScreenBase(flicker: LegacyFlickerTest) : BaseBenchmarkTest(flicker) {
protected val context: Context = instrumentation.context
- protected val primaryApp = SplitScreenUtils.getPrimary(instrumentation)
+ protected open val primaryApp = SplitScreenUtils.getPrimary(instrumentation)
protected val secondaryApp = SplitScreenUtils.getSecondary(instrumentation)
protected open val defaultSetup: FlickerBuilder.() -> Unit = {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt
index 5f6132a..0d3cd10 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt
@@ -40,14 +40,14 @@
import com.android.wm.shell.common.ShellExecutor
import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.EnterReason
import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ExitReason
-import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON
-import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT
-import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG
-import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN
import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW
import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON
import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT
import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_UNKNOWN
+import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON
+import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT
+import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG
+import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN
import com.android.wm.shell.shared.DesktopModeStatus
import com.android.wm.shell.sysui.ShellInit
import com.android.wm.shell.transition.TransitionInfoBuilder
@@ -78,409 +78,397 @@
@RunWith(AndroidTestingRunner::class)
class DesktopModeLoggerTransitionObserverTest {
- @JvmField
- @Rule
- val extendedMockitoRule =
- ExtendedMockitoRule.Builder(this)
- .mockStatic(DesktopModeEventLogger::class.java)
- .mockStatic(DesktopModeStatus::class.java)
- .build()!!
+ @JvmField
+ @Rule
+ val extendedMockitoRule =
+ ExtendedMockitoRule.Builder(this)
+ .mockStatic(DesktopModeEventLogger::class.java)
+ .mockStatic(DesktopModeStatus::class.java)
+ .build()!!
- @Mock lateinit var testExecutor: ShellExecutor
- @Mock private lateinit var mockShellInit: ShellInit
- @Mock private lateinit var transitions: Transitions
- @Mock private lateinit var context: Context
+ @Mock lateinit var testExecutor: ShellExecutor
+ @Mock private lateinit var mockShellInit: ShellInit
+ @Mock private lateinit var transitions: Transitions
+ @Mock private lateinit var context: Context
- private lateinit var transitionObserver: DesktopModeLoggerTransitionObserver
- private lateinit var shellInit: ShellInit
- private lateinit var desktopModeEventLogger: DesktopModeEventLogger
+ private lateinit var transitionObserver: DesktopModeLoggerTransitionObserver
+ private lateinit var shellInit: ShellInit
+ private lateinit var desktopModeEventLogger: DesktopModeEventLogger
- @Before
- fun setup() {
- doReturn(true).`when` { DesktopModeStatus.canEnterDesktopMode(any()) }
- shellInit = Mockito.spy(ShellInit(testExecutor))
- desktopModeEventLogger = mock(DesktopModeEventLogger::class.java)
+ @Before
+ fun setup() {
+ doReturn(true).`when` { DesktopModeStatus.canEnterDesktopMode(any()) }
+ shellInit = Mockito.spy(ShellInit(testExecutor))
+ desktopModeEventLogger = mock(DesktopModeEventLogger::class.java)
- transitionObserver =
- DesktopModeLoggerTransitionObserver(
- context,
- mockShellInit,
- transitions,
- desktopModeEventLogger
- )
- if (Transitions.ENABLE_SHELL_TRANSITIONS) {
- val initRunnableCaptor = ArgumentCaptor.forClass(Runnable::class.java)
- verify(mockShellInit)
- .addInitCallback(initRunnableCaptor.capture(), same(transitionObserver))
- initRunnableCaptor.value.run()
- } else {
- transitionObserver.onInit()
- }
+ transitionObserver =
+ DesktopModeLoggerTransitionObserver(
+ context, mockShellInit, transitions, desktopModeEventLogger)
+ if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+ val initRunnableCaptor = ArgumentCaptor.forClass(Runnable::class.java)
+ verify(mockShellInit).addInitCallback(initRunnableCaptor.capture(), same(transitionObserver))
+ initRunnableCaptor.value.run()
+ } else {
+ transitionObserver.onInit()
+ }
+ }
+
+ @Test
+ fun testRegistersObserverAtInit() {
+ verify(transitions).registerObserver(same(transitionObserver))
+ }
+
+ @Test
+ fun transitOpen_notFreeformWindow_doesNotLogTaskAddedOrSessionEnter() {
+ val change = createChange(TRANSIT_OPEN, createTaskInfo(1, WINDOWING_MODE_FULLSCREEN))
+ val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build()
+
+ callOnTransitionReady(transitionInfo)
+
+ verify(desktopModeEventLogger, never()).logSessionEnter(any(), any())
+ verify(desktopModeEventLogger, never()).logTaskAdded(any(), any())
+ }
+
+ @Test
+ fun transitOpen_logTaskAddedAndEnterReasonAppFreeformIntent() {
+ val change = createChange(TRANSIT_OPEN, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build()
+
+ callOnTransitionReady(transitionInfo)
+ val sessionId = transitionObserver.getLoggerSessionId()
+
+ assertThat(sessionId).isNotNull()
+ verify(desktopModeEventLogger, times(1))
+ .logSessionEnter(eq(sessionId!!), eq(EnterReason.APP_FREEFORM_INTENT))
+ verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
+ verifyZeroInteractions(desktopModeEventLogger)
+ }
+
+ @Test
+ fun transitEndDragToDesktop_logTaskAddedAndEnterReasonAppHandleDrag() {
+ val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ // task change is finalised when drag ends
+ val transitionInfo =
+ TransitionInfoBuilder(Transitions.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, 0)
+ .addChange(change)
+ .build()
+
+ callOnTransitionReady(transitionInfo)
+ val sessionId = transitionObserver.getLoggerSessionId()
+
+ assertThat(sessionId).isNotNull()
+ verify(desktopModeEventLogger, times(1))
+ .logSessionEnter(eq(sessionId!!), eq(EnterReason.APP_HANDLE_DRAG))
+ verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
+ verifyZeroInteractions(desktopModeEventLogger)
+ }
+
+ @Test
+ fun transitEnterDesktopByButtonTap_logTaskAddedAndEnterReasonButtonTap() {
+ val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ val transitionInfo =
+ TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON, 0)
+ .addChange(change)
+ .build()
+
+ callOnTransitionReady(transitionInfo)
+ val sessionId = transitionObserver.getLoggerSessionId()
+
+ assertThat(sessionId).isNotNull()
+ verify(desktopModeEventLogger, times(1))
+ .logSessionEnter(eq(sessionId!!), eq(EnterReason.APP_HANDLE_MENU_BUTTON))
+ verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
+ verifyZeroInteractions(desktopModeEventLogger)
+ }
+
+ @Test
+ fun transitEnterDesktopFromAppFromOverview_logTaskAddedAndEnterReasonUnknown() {
+ val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ val transitionInfo =
+ TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW, 0)
+ .addChange(change)
+ .build()
+
+ callOnTransitionReady(transitionInfo)
+ val sessionId = transitionObserver.getLoggerSessionId()
+
+ assertThat(sessionId).isNotNull()
+ verify(desktopModeEventLogger, times(1))
+ .logSessionEnter(eq(sessionId!!), eq(EnterReason.APP_FROM_OVERVIEW))
+ verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
+ verifyZeroInteractions(desktopModeEventLogger)
+ }
+
+ @Test
+ fun transitEnterDesktopFromKeyboardShortcut_logTaskAddedAndEnterReasonKeyboardShortcut() {
+ val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ val transitionInfo =
+ TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT, 0)
+ .addChange(change)
+ .build()
+
+ callOnTransitionReady(transitionInfo)
+ val sessionId = transitionObserver.getLoggerSessionId()
+
+ assertThat(sessionId).isNotNull()
+ verify(desktopModeEventLogger, times(1))
+ .logSessionEnter(eq(sessionId!!), eq(EnterReason.KEYBOARD_SHORTCUT_ENTER))
+ verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
+ verifyZeroInteractions(desktopModeEventLogger)
+ }
+
+ @Test
+ fun transitEnterDesktopFromUnknown_logTaskAddedAndEnterReasonUnknown() {
+ val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ val transitionInfo =
+ TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_UNKNOWN, 0).addChange(change).build()
+
+ callOnTransitionReady(transitionInfo)
+ val sessionId = transitionObserver.getLoggerSessionId()
+
+ assertThat(sessionId).isNotNull()
+ verify(desktopModeEventLogger, times(1))
+ .logSessionEnter(eq(sessionId!!), eq(EnterReason.UNKNOWN_ENTER))
+ verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
+ verifyZeroInteractions(desktopModeEventLogger)
+ }
+
+ @Test
+ fun transitWake_logTaskAddedAndEnterReasonScreenOn() {
+ val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ val transitionInfo = TransitionInfoBuilder(TRANSIT_WAKE, 0).addChange(change).build()
+
+ callOnTransitionReady(transitionInfo)
+ val sessionId = transitionObserver.getLoggerSessionId()
+
+ assertThat(sessionId).isNotNull()
+ verify(desktopModeEventLogger, times(1))
+ .logSessionEnter(eq(sessionId!!), eq(EnterReason.SCREEN_ON))
+ verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
+ verifyZeroInteractions(desktopModeEventLogger)
+ }
+
+ @Test
+ fun transitSleep_logTaskAddedAndExitReasonScreenOff_sessionIdNull() {
+ val sessionId = 1
+ // add a freeform task
+ transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ transitionObserver.setLoggerSessionId(sessionId)
+
+ val transitionInfo = TransitionInfoBuilder(TRANSIT_SLEEP).build()
+ callOnTransitionReady(transitionInfo)
+
+ verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
+ verify(desktopModeEventLogger, times(1))
+ .logSessionExit(eq(sessionId), eq(ExitReason.SCREEN_OFF))
+ verifyZeroInteractions(desktopModeEventLogger)
+ assertThat(transitionObserver.getLoggerSessionId()).isNull()
+ }
+
+ @Test
+ fun transitExitDesktopTaskDrag_logTaskRemovedAndExitReasonDragToExit_sessionIdNull() {
+ val sessionId = 1
+ // add a freeform task
+ transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ transitionObserver.setLoggerSessionId(sessionId)
+
+ // window mode changing from FREEFORM to FULLSCREEN
+ val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FULLSCREEN))
+ val transitionInfo =
+ TransitionInfoBuilder(TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG).addChange(change).build()
+ callOnTransitionReady(transitionInfo)
+
+ verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
+ verify(desktopModeEventLogger, times(1))
+ .logSessionExit(eq(sessionId), eq(ExitReason.DRAG_TO_EXIT))
+ verifyZeroInteractions(desktopModeEventLogger)
+ assertThat(transitionObserver.getLoggerSessionId()).isNull()
+ }
+
+ @Test
+ fun transitExitDesktopAppHandleButton_logTaskRemovedAndExitReasonButton_sessionIdNull() {
+ val sessionId = 1
+ // add a freeform task
+ transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ transitionObserver.setLoggerSessionId(sessionId)
+
+ // window mode changing from FREEFORM to FULLSCREEN
+ val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FULLSCREEN))
+ val transitionInfo =
+ TransitionInfoBuilder(TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON)
+ .addChange(change)
+ .build()
+ callOnTransitionReady(transitionInfo)
+
+ verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
+ verify(desktopModeEventLogger, times(1))
+ .logSessionExit(eq(sessionId), eq(ExitReason.APP_HANDLE_MENU_BUTTON_EXIT))
+ verifyZeroInteractions(desktopModeEventLogger)
+ assertThat(transitionObserver.getLoggerSessionId()).isNull()
+ }
+
+ @Test
+ fun transitExitDesktopUsingKeyboard_logTaskRemovedAndExitReasonKeyboard_sessionIdNull() {
+ val sessionId = 1
+ // add a freeform task
+ transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ transitionObserver.setLoggerSessionId(sessionId)
+
+ // window mode changing from FREEFORM to FULLSCREEN
+ val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FULLSCREEN))
+ val transitionInfo =
+ TransitionInfoBuilder(TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT).addChange(change).build()
+ callOnTransitionReady(transitionInfo)
+
+ verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
+ verify(desktopModeEventLogger, times(1))
+ .logSessionExit(eq(sessionId), eq(ExitReason.KEYBOARD_SHORTCUT_EXIT))
+ verifyZeroInteractions(desktopModeEventLogger)
+ assertThat(transitionObserver.getLoggerSessionId()).isNull()
+ }
+
+ @Test
+ fun transitExitDesktopUnknown_logTaskRemovedAndExitReasonUnknown_sessionIdNull() {
+ val sessionId = 1
+ // add a freeform task
+ transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ transitionObserver.setLoggerSessionId(sessionId)
+
+ // window mode changing from FREEFORM to FULLSCREEN
+ val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FULLSCREEN))
+ val transitionInfo =
+ TransitionInfoBuilder(TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN).addChange(change).build()
+ callOnTransitionReady(transitionInfo)
+
+ verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
+ verify(desktopModeEventLogger, times(1))
+ .logSessionExit(eq(sessionId), eq(ExitReason.UNKNOWN_EXIT))
+ verifyZeroInteractions(desktopModeEventLogger)
+ assertThat(transitionObserver.getLoggerSessionId()).isNull()
+ }
+
+ @Test
+ fun transitToFrontWithFlagRecents_logTaskRemovedAndExitReasonOverview_sessionIdNull() {
+ val sessionId = 1
+ // add a freeform task
+ transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ transitionObserver.setLoggerSessionId(sessionId)
+
+ // recents transition
+ val change = createChange(TRANSIT_TO_BACK, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ val transitionInfo =
+ TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS).addChange(change).build()
+ callOnTransitionReady(transitionInfo)
+
+ verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
+ verify(desktopModeEventLogger, times(1))
+ .logSessionExit(eq(sessionId), eq(ExitReason.RETURN_HOME_OR_OVERVIEW))
+ verifyZeroInteractions(desktopModeEventLogger)
+ assertThat(transitionObserver.getLoggerSessionId()).isNull()
+ }
+
+ @Test
+ fun transitClose_logTaskRemovedAndExitReasonTaskFinished_sessionIdNull() {
+ val sessionId = 1
+ // add a freeform task
+ transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ transitionObserver.setLoggerSessionId(sessionId)
+
+ // task closing
+ val change = createChange(TRANSIT_CLOSE, createTaskInfo(1, WINDOWING_MODE_FULLSCREEN))
+ val transitionInfo = TransitionInfoBuilder(TRANSIT_CLOSE).addChange(change).build()
+ callOnTransitionReady(transitionInfo)
+
+ verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
+ verify(desktopModeEventLogger, times(1))
+ .logSessionExit(eq(sessionId), eq(ExitReason.TASK_FINISHED))
+ verifyZeroInteractions(desktopModeEventLogger)
+ assertThat(transitionObserver.getLoggerSessionId()).isNull()
+ }
+
+ @Test
+ fun sessionExitByRecents_cancelledAnimation_sessionRestored() {
+ val sessionId = 1
+ // add a freeform task to an existing session
+ transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ transitionObserver.setLoggerSessionId(sessionId)
+
+ // recents transition sent freeform window to back
+ val change = createChange(TRANSIT_TO_BACK, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ val transitionInfo1 =
+ TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS).addChange(change).build()
+ callOnTransitionReady(transitionInfo1)
+ verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
+ verify(desktopModeEventLogger, times(1))
+ .logSessionExit(eq(sessionId), eq(ExitReason.RETURN_HOME_OR_OVERVIEW))
+ assertThat(transitionObserver.getLoggerSessionId()).isNull()
+
+ val transitionInfo2 = TransitionInfoBuilder(TRANSIT_NONE).build()
+ callOnTransitionReady(transitionInfo2)
+
+ verify(desktopModeEventLogger, times(1)).logSessionEnter(any(), any())
+ verify(desktopModeEventLogger, times(1)).logTaskAdded(any(), any())
+ }
+
+ @Test
+ fun sessionAlreadyStarted_newFreeformTaskAdded_logsTaskAdded() {
+ val sessionId = 1
+ // add an existing freeform task
+ transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ transitionObserver.setLoggerSessionId(sessionId)
+
+ // new freeform task added
+ val change = createChange(TRANSIT_OPEN, createTaskInfo(2, WINDOWING_MODE_FREEFORM))
+ val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build()
+ callOnTransitionReady(transitionInfo)
+
+ verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
+ verify(desktopModeEventLogger, never()).logSessionEnter(any(), any())
+ }
+
+ @Test
+ fun sessionAlreadyStarted_freeformTaskRemoved_logsTaskRemoved() {
+ val sessionId = 1
+ // add two existing freeform tasks
+ transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
+ transitionObserver.addTaskInfosToCachedMap(createTaskInfo(2, WINDOWING_MODE_FREEFORM))
+ transitionObserver.setLoggerSessionId(sessionId)
+
+ // new freeform task added
+ val change = createChange(TRANSIT_CLOSE, createTaskInfo(2, WINDOWING_MODE_FREEFORM))
+ val transitionInfo = TransitionInfoBuilder(TRANSIT_CLOSE, 0).addChange(change).build()
+ callOnTransitionReady(transitionInfo)
+
+ verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
+ verify(desktopModeEventLogger, never()).logSessionExit(any(), any())
+ }
+
+ /** Simulate calling the onTransitionReady() method */
+ private fun callOnTransitionReady(transitionInfo: TransitionInfo) {
+ val transition = mock(IBinder::class.java)
+ val startT = mock(SurfaceControl.Transaction::class.java)
+ val finishT = mock(SurfaceControl.Transaction::class.java)
+
+ transitionObserver.onTransitionReady(transition, transitionInfo, startT, finishT)
+ }
+
+ companion object {
+ fun createTaskInfo(taskId: Int, windowMode: Int): ActivityManager.RunningTaskInfo {
+ val taskInfo = ActivityManager.RunningTaskInfo()
+ taskInfo.taskId = taskId
+ taskInfo.configuration.windowConfiguration.windowingMode = windowMode
+
+ return taskInfo
}
- @Test
- fun testRegistersObserverAtInit() {
- verify(transitions).registerObserver(same(transitionObserver))
+ fun createChange(mode: Int, taskInfo: ActivityManager.RunningTaskInfo): Change {
+ val change =
+ Change(
+ WindowContainerToken(mock(IWindowContainerToken::class.java)),
+ mock(SurfaceControl::class.java))
+ change.mode = mode
+ change.taskInfo = taskInfo
+ return change
}
-
- @Test
- fun transitOpen_notFreeformWindow_doesNotLogTaskAddedOrSessionEnter() {
- val change = createChange(TRANSIT_OPEN, createTaskInfo(1, WINDOWING_MODE_FULLSCREEN))
- val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build()
-
- callOnTransitionReady(transitionInfo)
-
- verify(desktopModeEventLogger, never()).logSessionEnter(any(), any())
- verify(desktopModeEventLogger, never()).logTaskAdded(any(), any())
- }
-
- @Test
- fun transitOpen_logTaskAddedAndEnterReasonAppFreeformIntent() {
- val change = createChange(TRANSIT_OPEN, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
- val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build()
-
- callOnTransitionReady(transitionInfo)
- val sessionId = transitionObserver.getLoggerSessionId()
-
- assertThat(sessionId).isNotNull()
- verify(desktopModeEventLogger, times(1))
- .logSessionEnter(eq(sessionId!!), eq(EnterReason.APP_FREEFORM_INTENT))
- verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
- verifyZeroInteractions(desktopModeEventLogger)
- }
-
- @Test
- fun transitEndDragToDesktop_logTaskAddedAndEnterReasonAppHandleDrag() {
- val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
- // task change is finalised when drag ends
- val transitionInfo =
- TransitionInfoBuilder(Transitions.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, 0)
- .addChange(change)
- .build()
-
- callOnTransitionReady(transitionInfo)
- val sessionId = transitionObserver.getLoggerSessionId()
-
- assertThat(sessionId).isNotNull()
- verify(desktopModeEventLogger, times(1))
- .logSessionEnter(eq(sessionId!!), eq(EnterReason.APP_HANDLE_DRAG))
- verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
- verifyZeroInteractions(desktopModeEventLogger)
- }
-
- @Test
- fun transitEnterDesktopByButtonTap_logTaskAddedAndEnterReasonButtonTap() {
- val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
- val transitionInfo =
- TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON, 0)
- .addChange(change)
- .build()
-
- callOnTransitionReady(transitionInfo)
- val sessionId = transitionObserver.getLoggerSessionId()
-
- assertThat(sessionId).isNotNull()
- verify(desktopModeEventLogger, times(1))
- .logSessionEnter(eq(sessionId!!), eq(EnterReason.APP_HANDLE_MENU_BUTTON))
- verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
- verifyZeroInteractions(desktopModeEventLogger)
- }
-
- @Test
- fun transitEnterDesktopFromAppFromOverview_logTaskAddedAndEnterReasonUnknown() {
- val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
- val transitionInfo =
- TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW, 0)
- .addChange(change)
- .build()
-
- callOnTransitionReady(transitionInfo)
- val sessionId = transitionObserver.getLoggerSessionId()
-
- assertThat(sessionId).isNotNull()
- verify(desktopModeEventLogger, times(1))
- .logSessionEnter(eq(sessionId!!), eq(EnterReason.APP_FROM_OVERVIEW))
- verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
- verifyZeroInteractions(desktopModeEventLogger)
- }
-
- @Test
- fun transitEnterDesktopFromKeyboardShortcut_logTaskAddedAndEnterReasonKeyboardShortcut() {
- val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
- val transitionInfo =
- TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT, 0)
- .addChange(change)
- .build()
-
- callOnTransitionReady(transitionInfo)
- val sessionId = transitionObserver.getLoggerSessionId()
-
- assertThat(sessionId).isNotNull()
- verify(desktopModeEventLogger, times(1))
- .logSessionEnter(eq(sessionId!!), eq(EnterReason.KEYBOARD_SHORTCUT_ENTER))
- verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
- verifyZeroInteractions(desktopModeEventLogger)
- }
-
- @Test
- fun transitEnterDesktopFromUnknown_logTaskAddedAndEnterReasonUnknown() {
- val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
- val transitionInfo =
- TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_UNKNOWN, 0).addChange(change).build()
-
- callOnTransitionReady(transitionInfo)
- val sessionId = transitionObserver.getLoggerSessionId()
-
- assertThat(sessionId).isNotNull()
- verify(desktopModeEventLogger, times(1))
- .logSessionEnter(eq(sessionId!!), eq(EnterReason.UNKNOWN_ENTER))
- verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
- verifyZeroInteractions(desktopModeEventLogger)
- }
-
- @Test
- fun transitWake_logTaskAddedAndEnterReasonScreenOn() {
- val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
- val transitionInfo = TransitionInfoBuilder(TRANSIT_WAKE, 0).addChange(change).build()
-
- callOnTransitionReady(transitionInfo)
- val sessionId = transitionObserver.getLoggerSessionId()
-
- assertThat(sessionId).isNotNull()
- verify(desktopModeEventLogger, times(1))
- .logSessionEnter(eq(sessionId!!), eq(EnterReason.SCREEN_ON))
- verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
- verifyZeroInteractions(desktopModeEventLogger)
- }
-
- @Test
- fun transitSleep_logTaskAddedAndExitReasonScreenOff_sessionIdNull() {
- val sessionId = 1
- // add a freeform task
- transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
- transitionObserver.setLoggerSessionId(sessionId)
-
- val transitionInfo = TransitionInfoBuilder(TRANSIT_SLEEP).build()
- callOnTransitionReady(transitionInfo)
-
- verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
- verify(desktopModeEventLogger, times(1))
- .logSessionExit(eq(sessionId), eq(ExitReason.SCREEN_OFF))
- verifyZeroInteractions(desktopModeEventLogger)
- assertThat(transitionObserver.getLoggerSessionId()).isNull()
- }
-
- @Test
- fun transitExitDesktopTaskDrag_logTaskRemovedAndExitReasonDragToExit_sessionIdNull() {
- val sessionId = 1
- // add a freeform task
- transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
- transitionObserver.setLoggerSessionId(sessionId)
-
- // window mode changing from FREEFORM to FULLSCREEN
- val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FULLSCREEN))
- val transitionInfo =
- TransitionInfoBuilder(TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG).addChange(change).build()
- callOnTransitionReady(transitionInfo)
-
- verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
- verify(desktopModeEventLogger, times(1))
- .logSessionExit(eq(sessionId), eq(ExitReason.DRAG_TO_EXIT))
- verifyZeroInteractions(desktopModeEventLogger)
- assertThat(transitionObserver.getLoggerSessionId()).isNull()
- }
-
- @Test
- fun transitExitDesktopAppHandleButton_logTaskRemovedAndExitReasonButton_sessionIdNull() {
- val sessionId = 1
- // add a freeform task
- transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
- transitionObserver.setLoggerSessionId(sessionId)
-
- // window mode changing from FREEFORM to FULLSCREEN
- val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FULLSCREEN))
- val transitionInfo =
- TransitionInfoBuilder(TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON)
- .addChange(change)
- .build()
- callOnTransitionReady(transitionInfo)
-
- verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
- verify(desktopModeEventLogger, times(1))
- .logSessionExit(eq(sessionId), eq(ExitReason.APP_HANDLE_MENU_BUTTON_EXIT))
- verifyZeroInteractions(desktopModeEventLogger)
- assertThat(transitionObserver.getLoggerSessionId()).isNull()
- }
-
- @Test
- fun transitExitDesktopUsingKeyboard_logTaskRemovedAndExitReasonKeyboard_sessionIdNull() {
- val sessionId = 1
- // add a freeform task
- transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
- transitionObserver.setLoggerSessionId(sessionId)
-
- // window mode changing from FREEFORM to FULLSCREEN
- val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FULLSCREEN))
- val transitionInfo =
- TransitionInfoBuilder(TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT)
- .addChange(change)
- .build()
- callOnTransitionReady(transitionInfo)
-
- verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
- verify(desktopModeEventLogger, times(1))
- .logSessionExit(eq(sessionId), eq(ExitReason.KEYBOARD_SHORTCUT_EXIT))
- verifyZeroInteractions(desktopModeEventLogger)
- assertThat(transitionObserver.getLoggerSessionId()).isNull()
- }
-
- @Test
- fun transitExitDesktopUnknown_logTaskRemovedAndExitReasonUnknown_sessionIdNull() {
- val sessionId = 1
- // add a freeform task
- transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
- transitionObserver.setLoggerSessionId(sessionId)
-
- // window mode changing from FREEFORM to FULLSCREEN
- val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(1, WINDOWING_MODE_FULLSCREEN))
- val transitionInfo =
- TransitionInfoBuilder(TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN).addChange(change).build()
- callOnTransitionReady(transitionInfo)
-
- verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
- verify(desktopModeEventLogger, times(1))
- .logSessionExit(eq(sessionId), eq(ExitReason.UNKNOWN_EXIT))
- verifyZeroInteractions(desktopModeEventLogger)
- assertThat(transitionObserver.getLoggerSessionId()).isNull()
- }
-
- @Test
- fun transitToFrontWithFlagRecents_logTaskRemovedAndExitReasonOverview_sessionIdNull() {
- val sessionId = 1
- // add a freeform task
- transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
- transitionObserver.setLoggerSessionId(sessionId)
-
- // recents transition
- val change = createChange(TRANSIT_TO_BACK, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
- val transitionInfo =
- TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS)
- .addChange(change)
- .build()
- callOnTransitionReady(transitionInfo)
-
- verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
- verify(desktopModeEventLogger, times(1))
- .logSessionExit(eq(sessionId), eq(ExitReason.RETURN_HOME_OR_OVERVIEW))
- verifyZeroInteractions(desktopModeEventLogger)
- assertThat(transitionObserver.getLoggerSessionId()).isNull()
- }
-
- @Test
- fun transitClose_logTaskRemovedAndExitReasonTaskFinished_sessionIdNull() {
- val sessionId = 1
- // add a freeform task
- transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
- transitionObserver.setLoggerSessionId(sessionId)
-
- // task closing
- val change = createChange(TRANSIT_CLOSE, createTaskInfo(1, WINDOWING_MODE_FULLSCREEN))
- val transitionInfo = TransitionInfoBuilder(TRANSIT_CLOSE).addChange(change).build()
- callOnTransitionReady(transitionInfo)
-
- verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
- verify(desktopModeEventLogger, times(1))
- .logSessionExit(eq(sessionId), eq(ExitReason.TASK_FINISHED))
- verifyZeroInteractions(desktopModeEventLogger)
- assertThat(transitionObserver.getLoggerSessionId()).isNull()
- }
-
- @Test
- fun sessionExitByRecents_cancelledAnimation_sessionRestored() {
- val sessionId = 1
- // add a freeform task to an existing session
- transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
- transitionObserver.setLoggerSessionId(sessionId)
-
- // recents transition sent freeform window to back
- val change = createChange(TRANSIT_TO_BACK, createTaskInfo(1, WINDOWING_MODE_FREEFORM))
- val transitionInfo1 =
- TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS)
- .addChange(change)
- .build()
- callOnTransitionReady(transitionInfo1)
- verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
- verify(desktopModeEventLogger, times(1))
- .logSessionExit(eq(sessionId), eq(ExitReason.RETURN_HOME_OR_OVERVIEW))
- assertThat(transitionObserver.getLoggerSessionId()).isNull()
-
- val transitionInfo2 = TransitionInfoBuilder(TRANSIT_NONE).build()
- callOnTransitionReady(transitionInfo2)
-
- verify(desktopModeEventLogger, times(1)).logSessionEnter(any(), any())
- verify(desktopModeEventLogger, times(1)).logTaskAdded(any(), any())
- }
-
- @Test
- fun sessionAlreadyStarted_newFreeformTaskAdded_logsTaskAdded() {
- val sessionId = 1
- // add an existing freeform task
- transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
- transitionObserver.setLoggerSessionId(sessionId)
-
- // new freeform task added
- val change = createChange(TRANSIT_OPEN, createTaskInfo(2, WINDOWING_MODE_FREEFORM))
- val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build()
- callOnTransitionReady(transitionInfo)
-
- verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(sessionId), any())
- verify(desktopModeEventLogger, never()).logSessionEnter(any(), any())
- }
-
- @Test
- fun sessionAlreadyStarted_freeformTaskRemoved_logsTaskRemoved() {
- val sessionId = 1
- // add two existing freeform tasks
- transitionObserver.addTaskInfosToCachedMap(createTaskInfo(1, WINDOWING_MODE_FREEFORM))
- transitionObserver.addTaskInfosToCachedMap(createTaskInfo(2, WINDOWING_MODE_FREEFORM))
- transitionObserver.setLoggerSessionId(sessionId)
-
- // new freeform task added
- val change = createChange(TRANSIT_CLOSE, createTaskInfo(2, WINDOWING_MODE_FREEFORM))
- val transitionInfo = TransitionInfoBuilder(TRANSIT_CLOSE, 0).addChange(change).build()
- callOnTransitionReady(transitionInfo)
-
- verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(sessionId), any())
- verify(desktopModeEventLogger, never()).logSessionExit(any(), any())
- }
-
- /** Simulate calling the onTransitionReady() method */
- private fun callOnTransitionReady(transitionInfo: TransitionInfo) {
- val transition = mock(IBinder::class.java)
- val startT = mock(SurfaceControl.Transaction::class.java)
- val finishT = mock(SurfaceControl.Transaction::class.java)
-
- transitionObserver.onTransitionReady(transition, transitionInfo, startT, finishT)
- }
-
- companion object {
- fun createTaskInfo(taskId: Int, windowMode: Int): ActivityManager.RunningTaskInfo {
- val taskInfo = ActivityManager.RunningTaskInfo()
- taskInfo.taskId = taskId
- taskInfo.configuration.windowConfiguration.windowingMode = windowMode
-
- return taskInfo
- }
-
- fun createChange(mode: Int, taskInfo: ActivityManager.RunningTaskInfo): Change {
- val change =
- Change(
- WindowContainerToken(mock(IWindowContainerToken::class.java)),
- mock(SurfaceControl::class.java)
- )
- change.mode = mode
- change.taskInfo = taskInfo
- return change
- }
- }
+ }
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
index 56c4cea..e291c0e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
@@ -113,6 +113,8 @@
private DisplayInsetsController mDisplayInsetsController;
@Mock
private IRecentTasksListener mRecentTasksListener;
+ @Mock
+ private TaskStackTransitionObserver mTaskStackTransitionObserver;
@Rule
public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
@@ -139,7 +141,8 @@
mDisplayInsetsController, mMainExecutor));
mRecentTasksControllerReal = new RecentTasksController(mContext, mShellInit,
mShellController, mShellCommandHandler, mTaskStackListener, mActivityTaskManager,
- Optional.of(mDesktopModeTaskRepository), mMainExecutor);
+ Optional.of(mDesktopModeTaskRepository), mTaskStackTransitionObserver,
+ mMainExecutor);
mRecentTasksController = spy(mRecentTasksControllerReal);
mShellTaskOrganizer = new ShellTaskOrganizer(mShellInit, mShellCommandHandler,
null /* sizeCompatUI */, Optional.empty(), Optional.of(mRecentTasksController),
@@ -557,6 +560,30 @@
}
@Test
+ @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
+ public void onTaskMovedToFront_TaskStackObserverEnabled_triggersOnTaskMovedToFront()
+ throws Exception {
+ mRecentTasksControllerReal.registerRecentTasksListener(mRecentTasksListener);
+ ActivityManager.RunningTaskInfo taskInfo = makeRunningTaskInfo(/* taskId= */10);
+
+ mRecentTasksControllerReal.onTaskMovedToFrontThroughTransition(taskInfo);
+
+ verify(mRecentTasksListener).onTaskMovedToFront(taskInfo);
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
+ public void onTaskMovedToFront_TaskStackObserverEnabled_doesNotTriggersOnTaskMovedToFront()
+ throws Exception {
+ mRecentTasksControllerReal.registerRecentTasksListener(mRecentTasksListener);
+ ActivityManager.RunningTaskInfo taskInfo = makeRunningTaskInfo(/* taskId= */10);
+
+ mRecentTasksControllerReal.onTaskMovedToFront(taskInfo);
+
+ verify(mRecentTasksListener, never()).onTaskMovedToFront(any());
+ }
+
+ @Test
public void getNullSplitBoundsNonSplitTask() {
SplitBounds sb = mRecentTasksController.getSplitBoundsForTaskId(3);
assertNull("splitBounds should be null for non-split task", sb);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt
new file mode 100644
index 0000000..f959970
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2024 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.wm.shell.recents
+
+import android.app.ActivityManager
+import android.app.WindowConfiguration
+import android.os.IBinder
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import android.testing.AndroidTestingRunner
+import android.view.SurfaceControl
+import android.view.WindowManager
+import android.window.IWindowContainerToken
+import android.window.TransitionInfo
+import android.window.WindowContainerToken
+import androidx.test.filters.SmallTest
+import com.android.window.flags.Flags
+import com.android.wm.shell.TestShellExecutor
+import com.android.wm.shell.common.ShellExecutor
+import com.android.wm.shell.sysui.ShellInit
+import com.android.wm.shell.transition.TransitionInfoBuilder
+import com.android.wm.shell.transition.Transitions
+import com.google.common.truth.Truth.assertThat
+import dagger.Lazy
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.same
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+
+/**
+ * Test class for {@link TaskStackTransitionObserver}
+ *
+ * Usage: atest WMShellUnitTests:TaskStackTransitionObserverTest
+ */
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class TaskStackTransitionObserverTest {
+
+ @JvmField @Rule val setFlagsRule = SetFlagsRule()
+
+ @Mock private lateinit var shellInit: ShellInit
+ @Mock lateinit var testExecutor: ShellExecutor
+ @Mock private lateinit var transitionsLazy: Lazy<Transitions>
+ @Mock private lateinit var transitions: Transitions
+ @Mock private lateinit var mockTransitionBinder: IBinder
+
+ private lateinit var transitionObserver: TaskStackTransitionObserver
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ shellInit = Mockito.spy(ShellInit(testExecutor))
+ whenever(transitionsLazy.get()).thenReturn(transitions)
+ transitionObserver = TaskStackTransitionObserver(transitionsLazy, shellInit)
+ if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+ val initRunnableCaptor = ArgumentCaptor.forClass(Runnable::class.java)
+ verify(shellInit)
+ .addInitCallback(initRunnableCaptor.capture(), same(transitionObserver))
+ initRunnableCaptor.value.run()
+ } else {
+ transitionObserver.onInit()
+ }
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
+ fun testRegistersObserverAtInit() {
+ verify(transitions).registerObserver(same(transitionObserver))
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
+ fun taskCreated_freeformWindow_listenerNotified() {
+ val listener = TestListener()
+ val executor = TestShellExecutor()
+ transitionObserver.addTaskStackTransitionObserverListener(listener, executor)
+ val change =
+ createChange(
+ WindowManager.TRANSIT_OPEN,
+ createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FREEFORM)
+ )
+ val transitionInfo =
+ TransitionInfoBuilder(WindowManager.TRANSIT_OPEN, 0).addChange(change).build()
+
+ callOnTransitionReady(transitionInfo)
+ callOnTransitionFinished()
+ executor.flushAll()
+
+ assertThat(listener.taskInfoToBeNotified.taskId).isEqualTo(change.taskInfo?.taskId)
+ assertThat(listener.taskInfoToBeNotified.windowingMode)
+ .isEqualTo(change.taskInfo?.windowingMode)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
+ fun taskCreated_fullscreenWindow_listenerNotNotified() {
+ val listener = TestListener()
+ val executor = TestShellExecutor()
+ transitionObserver.addTaskStackTransitionObserverListener(listener, executor)
+ val change =
+ createChange(
+ WindowManager.TRANSIT_OPEN,
+ createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FULLSCREEN)
+ )
+ val transitionInfo =
+ TransitionInfoBuilder(WindowManager.TRANSIT_OPEN, 0).addChange(change).build()
+
+ callOnTransitionReady(transitionInfo)
+ callOnTransitionFinished()
+ executor.flushAll()
+
+ assertThat(listener.taskInfoToBeNotified.taskId).isEqualTo(0)
+ assertThat(listener.taskInfoToBeNotified.windowingMode)
+ .isEqualTo(WindowConfiguration.WINDOWING_MODE_UNDEFINED)
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL)
+ fun taskCreated_freeformWindowOnTopOfFreeform_listenerNotified() {
+ val listener = TestListener()
+ val executor = TestShellExecutor()
+ transitionObserver.addTaskStackTransitionObserverListener(listener, executor)
+ val freeformOpenChange =
+ createChange(
+ WindowManager.TRANSIT_OPEN,
+ createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FREEFORM)
+ )
+ val freeformReorderChange =
+ createChange(
+ WindowManager.TRANSIT_TO_BACK,
+ createTaskInfo(2, WindowConfiguration.WINDOWING_MODE_FREEFORM)
+ )
+ val transitionInfo =
+ TransitionInfoBuilder(WindowManager.TRANSIT_OPEN, 0)
+ .addChange(freeformOpenChange)
+ .addChange(freeformReorderChange)
+ .build()
+
+ callOnTransitionReady(transitionInfo)
+ callOnTransitionFinished()
+ executor.flushAll()
+
+ assertThat(listener.taskInfoToBeNotified.taskId)
+ .isEqualTo(freeformOpenChange.taskInfo?.taskId)
+ assertThat(listener.taskInfoToBeNotified.windowingMode)
+ .isEqualTo(freeformOpenChange.taskInfo?.windowingMode)
+ }
+
+ class TestListener : TaskStackTransitionObserver.TaskStackTransitionObserverListener {
+ var taskInfoToBeNotified = ActivityManager.RunningTaskInfo()
+
+ override fun onTaskMovedToFrontThroughTransition(
+ taskInfo: ActivityManager.RunningTaskInfo
+ ) {
+ taskInfoToBeNotified = taskInfo
+ }
+ }
+
+ /** Simulate calling the onTransitionReady() method */
+ private fun callOnTransitionReady(transitionInfo: TransitionInfo) {
+ val startT = Mockito.mock(SurfaceControl.Transaction::class.java)
+ val finishT = Mockito.mock(SurfaceControl.Transaction::class.java)
+
+ transitionObserver.onTransitionReady(mockTransitionBinder, transitionInfo, startT, finishT)
+ }
+
+ /** Simulate calling the onTransitionFinished() method */
+ private fun callOnTransitionFinished() {
+ transitionObserver.onTransitionFinished(mockTransitionBinder, false)
+ }
+
+ companion object {
+ fun createTaskInfo(taskId: Int, windowingMode: Int): ActivityManager.RunningTaskInfo {
+ val taskInfo = ActivityManager.RunningTaskInfo()
+ taskInfo.taskId = taskId
+ taskInfo.configuration.windowConfiguration.windowingMode = windowingMode
+
+ return taskInfo
+ }
+
+ fun createChange(
+ mode: Int,
+ taskInfo: ActivityManager.RunningTaskInfo
+ ): TransitionInfo.Change {
+ val change =
+ TransitionInfo.Change(
+ WindowContainerToken(Mockito.mock(IWindowContainerToken::class.java)),
+ Mockito.mock(SurfaceControl::class.java)
+ )
+ change.mode = mode
+ change.taskInfo = taskInfo
+ return change
+ }
+ }
+}
diff --git a/nfc/api/system-current.txt b/nfc/api/system-current.txt
index 055ccbc..3375e18c 100644
--- a/nfc/api/system-current.txt
+++ b/nfc/api/system-current.txt
@@ -57,7 +57,9 @@
@FlaggedApi("android.nfc.nfc_oem_extension") public final class NfcOemExtension {
method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void clearPreference();
+ method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void maybeTriggerFirmwareUpdate();
method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void registerCallback(@NonNull java.util.concurrent.Executor, @NonNull android.nfc.NfcOemExtension.Callback);
+ method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void synchronizeScreenState();
method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void unregisterCallback(@NonNull android.nfc.NfcOemExtension.Callback);
}
diff --git a/nfc/java/android/nfc/INfcAdapter.aidl b/nfc/java/android/nfc/INfcAdapter.aidl
index 7150b54..fd77820 100644
--- a/nfc/java/android/nfc/INfcAdapter.aidl
+++ b/nfc/java/android/nfc/INfcAdapter.aidl
@@ -110,4 +110,6 @@
void registerOemExtensionCallback(INfcOemExtensionCallback callbacks);
void unregisterOemExtensionCallback(INfcOemExtensionCallback callbacks);
void clearPreference();
+ void setScreenState();
+ void checkFirmware();
}
diff --git a/nfc/java/android/nfc/NfcOemExtension.java b/nfc/java/android/nfc/NfcOemExtension.java
index 1eff58c..f6138a6 100644
--- a/nfc/java/android/nfc/NfcOemExtension.java
+++ b/nfc/java/android/nfc/NfcOemExtension.java
@@ -141,6 +141,34 @@
}
}
+ /**
+ * Get the screen state from system and set it to current screen state.
+ */
+ @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
+ @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
+ public void synchronizeScreenState() {
+ try {
+ NfcAdapter.sService.setScreenState();
+ } catch (RemoteException e) {
+ mAdapter.attemptDeadServiceRecovery(e);
+ }
+ }
+
+ /**
+ * Check if the firmware needs updating.
+ *
+ * <p>If an update is needed, a firmware will be triggered when NFC is disabled.
+ */
+ @FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
+ @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)
+ public void maybeTriggerFirmwareUpdate() {
+ try {
+ NfcAdapter.sService.checkFirmware();
+ } catch (RemoteException e) {
+ mAdapter.attemptDeadServiceRecovery(e);
+ }
+ }
+
private final class NfcOemExtensionCallback extends INfcOemExtensionCallback.Stub {
@Override
public void onTagConnected(boolean connected, Tag tag) throws RemoteException {
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java
index ed964a9..b3e48b2 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java
@@ -209,44 +209,34 @@
CachedBluetoothDevice mainDevice = findMainDevice(cachedDevice);
if (mainDevice != null) {
if (mainDevice.isConnected()) {
- // When main device exists and in connected state, receiving sub device
- // connection. To refresh main device UI
+ // Sub/member device is connected and main device is connected
+ // To refresh main device UI
mainDevice.refresh();
} else {
- // When both Hearing Aid devices are disconnected, receiving sub device
- // connection. To switch content and dispatch to notify UI change
- mBtManager.getEventManager().dispatchDeviceRemoved(mainDevice);
- mainDevice.switchSubDeviceContent();
- mainDevice.refresh();
- // It is necessary to do remove and add for updating the mapping on
- // preference and device
- mBtManager.getEventManager().dispatchDeviceAdded(mainDevice);
+ // Sub/member device is connected and main device is disconnected
+ // To switch content and dispatch to notify UI change
+ switchDeviceContent(mainDevice, cachedDevice);
}
return true;
}
break;
case BluetoothProfile.STATE_DISCONNECTED:
- mainDevice = findMainDevice(cachedDevice);
if (cachedDevice.getUnpairing()) {
return true;
}
+ mainDevice = findMainDevice(cachedDevice);
if (mainDevice != null) {
- // When main device exists, receiving sub device disconnection
+ // Sub/member device is disconnected and main device exists
// To update main device UI
mainDevice.refresh();
return true;
}
- CachedBluetoothDevice subDevice = cachedDevice.getSubDevice();
- if (subDevice != null && subDevice.isConnected()) {
- // Main device is disconnected and sub device is connected
- // To copy data from sub device to main device
- mBtManager.getEventManager().dispatchDeviceRemoved(cachedDevice);
- cachedDevice.switchSubDeviceContent();
- cachedDevice.refresh();
- // It is necessary to do remove and add for updating the mapping on
- // preference and device
- mBtManager.getEventManager().dispatchDeviceAdded(cachedDevice);
-
+ CachedBluetoothDevice connectedSecondaryDevice = getConnectedSecondaryDevice(
+ cachedDevice);
+ if (connectedSecondaryDevice != null) {
+ // Main device is disconnected and sub/member device is connected
+ // To switch content and dispatch to notify UI change
+ switchDeviceContent(cachedDevice, connectedSecondaryDevice);
return true;
}
break;
@@ -254,6 +244,29 @@
return false;
}
+ private void switchDeviceContent(CachedBluetoothDevice mainDevice,
+ CachedBluetoothDevice secondaryDevice) {
+ mBtManager.getEventManager().dispatchDeviceRemoved(mainDevice);
+ if (mainDevice.getSubDevice() != null
+ && mainDevice.getSubDevice().equals(secondaryDevice)) {
+ mainDevice.switchSubDeviceContent();
+ } else {
+ mainDevice.switchMemberDeviceContent(secondaryDevice);
+ }
+ mainDevice.refresh();
+ // It is necessary to do remove and add for updating the mapping on
+ // preference and device
+ mBtManager.getEventManager().dispatchDeviceAdded(mainDevice);
+ }
+
+ private CachedBluetoothDevice getConnectedSecondaryDevice(CachedBluetoothDevice cachedDevice) {
+ if (cachedDevice.getSubDevice() != null && cachedDevice.getSubDevice().isConnected()) {
+ return cachedDevice.getSubDevice();
+ }
+ return cachedDevice.getMemberDevice().stream().filter(
+ CachedBluetoothDevice::isConnected).findAny().orElse(null);
+ }
+
void onActiveDeviceChanged(CachedBluetoothDevice device) {
if (FeatureFlagUtils.isEnabled(mContext, FeatureFlagUtils.SETTINGS_AUDIO_ROUTING)) {
if (device.isActiveDevice(BluetoothProfile.HEARING_AID) || device.isActiveDevice(
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java
index 4188d2e..bf927a1 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java
@@ -681,6 +681,53 @@
verify(mCachedDevice1).refresh();
}
+
+ /**
+ * Test onProfileConnectionStateChangedIfProcessed.
+ * When main device is disconnected, to verify switch() result for member device connected
+ * event
+ */
+ @Test
+ public void onProfileConnectionStateChanged_connect_member_mainDisconnected_switch() {
+ when(mCachedDevice1.isConnected()).thenReturn(false);
+ when(mCachedDevice1.getGroupId()).thenReturn(GROUP_ID_1);
+ when(mCachedDevice2.getGroupId()).thenReturn(GROUP_ID_1);
+ mCachedDeviceManager.mCachedDevices.add(mCachedDevice1);
+ mCachedDevice1.addMemberDevice(mCachedDevice2);
+
+ assertThat(mCachedDevice1.mDevice).isEqualTo(mDevice1);
+ assertThat(mCachedDevice2.mDevice).isEqualTo(mDevice2);
+ assertThat(mHearingAidDeviceManager.onProfileConnectionStateChangedIfProcessed(
+ mCachedDevice2, BluetoothProfile.STATE_CONNECTED)).isTrue();
+
+ assertThat(mCachedDevice1.mDevice).isEqualTo(mDevice2);
+ assertThat(mCachedDevice2.mDevice).isEqualTo(mDevice1);
+ verify(mCachedDevice1).refresh();
+ }
+
+ /**
+ * Test onProfileConnectionStateChangedIfProcessed.
+ * When member device is connected, to verify switch() result for main device disconnected
+ * event
+ */
+ @Test
+ public void onProfileConnectionStateChanged_disconnect_main_subDeviceConnected_switch() {
+ when(mCachedDevice2.isConnected()).thenReturn(true);
+ when(mCachedDevice1.getGroupId()).thenReturn(GROUP_ID_1);
+ when(mCachedDevice2.getGroupId()).thenReturn(GROUP_ID_1);
+ mCachedDeviceManager.mCachedDevices.add(mCachedDevice1);
+ mCachedDevice1.addMemberDevice(mCachedDevice2);
+
+ assertThat(mCachedDevice1.mDevice).isEqualTo(mDevice1);
+ assertThat(mCachedDevice2.mDevice).isEqualTo(mDevice2);
+ assertThat(mHearingAidDeviceManager.onProfileConnectionStateChangedIfProcessed(
+ mCachedDevice1, BluetoothProfile.STATE_DISCONNECTED)).isTrue();
+
+ assertThat(mCachedDevice1.mDevice).isEqualTo(mDevice2);
+ assertThat(mCachedDevice2.mDevice).isEqualTo(mDevice1);
+ verify(mCachedDevice1).refresh();
+ }
+
@Test
public void onActiveDeviceChanged_connected_callSetStrategies() {
when(mHelper.getMatchedHearingDeviceAttributes(mCachedDevice1)).thenReturn(
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java
index 5245456..00fb7a1 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java
@@ -80,6 +80,7 @@
Settings.System.SIP_RECEIVE_CALLS,
Settings.System.POINTER_SPEED,
Settings.System.POINTER_FILL_STYLE,
+ Settings.System.POINTER_SCALE,
Settings.System.VIBRATE_ON,
Settings.System.VIBRATE_WHEN_RINGING,
Settings.System.RINGTONE,
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java
index 2c3be4c..4235bc4 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java
@@ -26,6 +26,8 @@
import static android.provider.settings.validators.SettingsValidators.NON_NEGATIVE_INTEGER_VALIDATOR;
import static android.provider.settings.validators.SettingsValidators.URI_VALIDATOR;
import static android.provider.settings.validators.SettingsValidators.VIBRATION_INTENSITY_VALIDATOR;
+import static android.view.PointerIcon.DEFAULT_POINTER_SCALE;
+import static android.view.PointerIcon.LARGE_POINTER_SCALE;
import static android.view.PointerIcon.POINTER_ICON_VECTOR_STYLE_FILL_BEGIN;
import static android.view.PointerIcon.POINTER_ICON_VECTOR_STYLE_FILL_END;
@@ -211,6 +213,8 @@
VALIDATORS.put(System.POINTER_FILL_STYLE,
new InclusiveIntegerRangeValidator(POINTER_ICON_VECTOR_STYLE_FILL_BEGIN,
POINTER_ICON_VECTOR_STYLE_FILL_END));
+ VALIDATORS.put(System.POINTER_SCALE,
+ new InclusiveFloatRangeValidator(DEFAULT_POINTER_SCALE, LARGE_POINTER_SCALE));
VALIDATORS.put(System.TOUCHPAD_POINTER_SPEED, new InclusiveIntegerRangeValidator(-7, 7));
VALIDATORS.put(System.TOUCHPAD_NATURAL_SCROLLING, BOOLEAN_VALIDATOR);
VALIDATORS.put(System.TOUCHPAD_TAP_TO_CLICK, BOOLEAN_VALIDATOR);
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
index 625b8e4..384cb7e 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java
@@ -2915,6 +2915,9 @@
dumpSetting(s, p,
Settings.System.POINTER_FILL_STYLE,
SystemSettingsProto.Pointer.POINTER_FILL_STYLE);
+ dumpSetting(s, p,
+ Settings.System.POINTER_SCALE,
+ SystemSettingsProto.Pointer.POINTER_SCALE);
p.end(pointerToken);
dumpSetting(s, p,
Settings.System.POINTER_SPEED,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
index a1f8f1b..c63389b 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
@@ -252,7 +252,10 @@
Box(
Modifier.matchParentSize()
.background(colors.primary)
- .animatedRadialGradientBackground(colors.primary, colors.primaryContainer)
+ .animatedRadialGradientBackground(
+ toColor = colors.primary,
+ fromColor = colors.primaryContainer.copy(alpha = 0.6f)
+ )
)
BackgroundTopScrim()
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
index 10c4030..68395b4 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt
@@ -59,6 +59,13 @@
goneToShadeTransition(durationScale = 0.9)
}
from(Scenes.Gone, to = Scenes.QuickSettings) { goneToQuickSettingsTransition() }
+ from(
+ Scenes.Gone,
+ to = Scenes.QuickSettings,
+ key = SlightlyFasterShadeCollapse,
+ ) {
+ goneToQuickSettingsTransition(durationScale = 0.9)
+ }
from(Scenes.Gone, to = Scenes.QuickSettingsShade) { goneToQuickSettingsShadeTransition() }
from(Scenes.Lockscreen, to = Scenes.Bouncer) { lockscreenToBouncerTransition() }
from(Scenes.Lockscreen, to = Scenes.Communal) { lockscreenToCommunalTransition() }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
index ac3e015..b5a10ca 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
@@ -19,7 +19,10 @@
import android.view.ContextThemeWrapper
import android.view.ViewGroup
+import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.interaction.collectIsHoveredAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -32,7 +35,9 @@
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
+import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ColorScheme
+import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
@@ -40,6 +45,7 @@
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer
@@ -58,6 +64,7 @@
import com.android.compose.animation.scene.TransitionState
import com.android.compose.animation.scene.ValueKey
import com.android.compose.animation.scene.animateElementFloatAsState
+import com.android.compose.modifiers.thenIf
import com.android.compose.windowsizeclass.LocalWindowSizeClass
import com.android.settingslib.Utils
import com.android.systemui.battery.BatteryMeterView
@@ -69,6 +76,7 @@
import com.android.systemui.privacy.OngoingPrivacyChip
import com.android.systemui.res.R
import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.shade.ui.composable.ShadeHeader.Colors.onScrimDim
import com.android.systemui.shade.ui.composable.ShadeHeader.Dimensions.CollapsedHeight
import com.android.systemui.shade.ui.composable.ShadeHeader.Values.ClockScale
import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel
@@ -79,7 +87,6 @@
import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernShadeCarrierGroupMobileView
import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.ShadeCarrierGroupMobileIconViewModel
import com.android.systemui.statusbar.policy.Clock
-import kotlin.math.max
object ShadeHeader {
object Elements {
@@ -103,6 +110,8 @@
object Colors {
val ColorScheme.shadeHeaderText: Color
get() = Color.White
+ val ColorScheme.onScrimDim: Color
+ get() = Color.DarkGray
}
object TestTags {
@@ -130,7 +139,7 @@
val horizontalPadding =
max(LocalScreenCornerRadius.current / 2f, Shade.Dimensions.HorizontalPadding)
- val useExpandedFormat by
+ val useExpandedTextFormat by
remember(cutoutLocation) {
derivedStateOf {
cutoutLocation != CutoutLocation.CENTER ||
@@ -138,6 +147,10 @@
}
}
+ val isLargeScreenLayout =
+ LocalWindowSizeClass.current.widthSizeClass == WindowWidthSizeClass.Medium ||
+ LocalWindowSizeClass.current.widthSizeClass == WindowWidthSizeClass.Expanded
+
val isPrivacyChipVisible by viewModel.isPrivacyChipVisible.collectAsStateWithLifecycle()
// This layout assumes it is globally positioned at (0, 0) and is the
@@ -182,22 +195,22 @@
Modifier.element(ShadeHeader.Elements.CollapsedContentEnd)
.padding(horizontal = horizontalPadding)
) {
+ if (isLargeScreenLayout) {
+ ShadeCarrierGroup(
+ viewModel = viewModel,
+ modifier = Modifier.align(Alignment.CenterVertically),
+ )
+ }
SystemIconContainer(
+ viewModel = viewModel,
+ isClickable = isLargeScreenLayout,
modifier = Modifier.align(Alignment.CenterVertically)
) {
- when (LocalWindowSizeClass.current.widthSizeClass) {
- WindowWidthSizeClass.Medium,
- WindowWidthSizeClass.Expanded ->
- ShadeCarrierGroup(
- viewModel = viewModel,
- modifier = Modifier.align(Alignment.CenterVertically),
- )
- }
StatusIcons(
viewModel = viewModel,
createTintedIconManager = createTintedIconManager,
statusBarIconController = statusBarIconController,
- useExpandedFormat = useExpandedFormat,
+ useExpandedFormat = useExpandedTextFormat,
modifier =
Modifier.align(Alignment.CenterVertically)
.padding(end = 6.dp)
@@ -206,7 +219,7 @@
BatteryIcon(
createBatteryMeterViewController =
createBatteryMeterViewController,
- useExpandedFormat = useExpandedFormat,
+ useExpandedFormat = useExpandedTextFormat,
modifier = Modifier.align(Alignment.CenterVertically),
)
}
@@ -322,7 +335,7 @@
modifier = Modifier.widthIn(max = 90.dp).align(Alignment.CenterVertically),
)
Spacer(modifier = Modifier.weight(1f))
- SystemIconContainer {
+ SystemIconContainer(viewModel = viewModel, isClickable = false) {
StatusIcons(
viewModel = viewModel,
createTintedIconManager = createTintedIconManager,
@@ -531,12 +544,30 @@
@Composable
private fun SystemIconContainer(
+ viewModel: ShadeHeaderViewModel,
+ isClickable: Boolean,
modifier: Modifier = Modifier,
content: @Composable RowScope.() -> Unit
) {
- // TODO(b/298524053): add hover state for this container
+ val interactionSource = remember { MutableInteractionSource() }
+ val isHovered by interactionSource.collectIsHoveredAsState()
+
+ val hoverModifier = Modifier
+ .clip(RoundedCornerShape(CollapsedHeight / 4))
+ .background(MaterialTheme.colorScheme.onScrimDim)
+
Row(
- modifier = modifier.height(CollapsedHeight),
+ modifier = modifier
+ .height(CollapsedHeight)
+ .padding(vertical = CollapsedHeight / 4)
+ .thenIf(isClickable) {
+ Modifier.clickable(
+ interactionSource = interactionSource,
+ indication = null,
+ onClick = { viewModel.onSystemIconContainerClicked() },
+ )
+ }
+ .thenIf(isHovered) { hoverModifier },
content = content,
)
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt
index 7fd3a176..114dcf4 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt
@@ -381,18 +381,17 @@
// relayout/redraw for nothing.
fromValue
} else {
- // In the case of bouncing, if the value remains constant during the overscroll, we
- // should use the value of the scene we are bouncing around.
- if (!canOverflow && transition is TransitionState.HasOverscrollProperties) {
- val bouncingScene = transition.bouncingScene
- if (bouncingScene != null) {
- return sharedValue[bouncingScene]
- }
- }
-
+ val overscrollSpec = transition.currentOverscrollSpec
val progress =
- if (canOverflow) transition.progress
- else transition.progress.fastCoerceIn(0f, 1f)
+ when {
+ overscrollSpec == null -> {
+ if (canOverflow) transition.progress
+ else transition.progress.fastCoerceIn(0f, 1f)
+ }
+ overscrollSpec.scene == transition.toScene -> 1f
+ else -> 0f
+ }
+
sharedValue.type.lerp(fromValue, toValue, progress)
}
} else fromValue ?: toValue
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
index 980982a..5611c6e 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
@@ -39,6 +39,8 @@
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.node.DrawModifierNode
import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.node.TraversableNode
+import androidx.compose.ui.node.traverseDescendants
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntSize
@@ -165,7 +167,7 @@
private var currentTransitions: List<TransitionState.Transition>,
private var scene: Scene,
private var key: ElementKey,
-) : Modifier.Node(), DrawModifierNode, ApproachLayoutModifierNode {
+) : Modifier.Node(), DrawModifierNode, ApproachLayoutModifierNode, TraversableNode {
private var _element: Element? = null
private val element: Element
get() = _element!!
@@ -174,6 +176,8 @@
private val sceneState: Element.SceneState
get() = _sceneState!!
+ override val traverseKey: Any = ElementTraverseKey
+
override fun onAttach() {
super.onAttach()
updateElementAndSceneValues()
@@ -289,18 +293,15 @@
val isOtherSceneOverscrolling = overscrollScene != null && overscrollScene != scene.key
val isNotPartOfAnyOngoingTransitions = transitions.isNotEmpty() && transition == null
if (isNotPartOfAnyOngoingTransitions || isOtherSceneOverscrolling) {
- sceneState.lastOffset = Offset.Unspecified
- sceneState.lastScale = Scale.Unspecified
- sceneState.lastAlpha = Element.AlphaUnspecified
+ recursivelyClearPlacementValues()
+ sceneState.lastSize = Element.SizeUnspecified
val placeable = measurable.measure(constraints)
- sceneState.lastSize = placeable.size()
-
return layout(placeable.width, placeable.height) { /* Do not place */ }
}
val placeable =
- measure(layoutImpl, scene, element, transition, sceneState, measurable, constraints)
+ measure(layoutImpl, element, transition, sceneState, measurable, constraints)
sceneState.lastSize = placeable.size()
return layout(placeable.width, placeable.height) { place(transition, placeable) }
}
@@ -315,13 +316,10 @@
// scene when idle.
val coords =
coordinates ?: error("Element ${element.key} does not have any coordinates")
- val targetOffsetInScene = lookaheadScopeCoordinates.localLookaheadPositionOf(coords)
// No need to place the element in this scene if we don't want to draw it anyways.
if (!shouldPlaceElement(layoutImpl, scene.key, element, transition)) {
- sceneState.lastOffset = Offset.Unspecified
- sceneState.lastScale = Scale.Unspecified
- sceneState.lastAlpha = Element.AlphaUnspecified
+ recursivelyClearPlacementValues()
return
}
@@ -329,12 +327,11 @@
val targetOffset =
computeValue(
layoutImpl,
- scene,
+ sceneState,
element,
transition,
sceneValue = { it.targetOffset },
transformation = { it.offset },
- idleValue = targetOffsetInScene,
currentValue = { currentOffset },
isSpecified = { it != Offset.Unspecified },
::lerp,
@@ -395,18 +392,37 @@
return@placeWithLayer
}
- alpha = elementAlpha(layoutImpl, scene, element, transition, sceneState)
+ alpha = elementAlpha(layoutImpl, element, transition, sceneState)
compositingStrategy = CompositingStrategy.ModulateAlpha
}
}
}
}
+ /**
+ * Recursively clear the last placement values on this node and all descendants ElementNodes.
+ * This should be called when this node is not placed anymore, so that we correctly clear values
+ * for the descendants for which approachMeasure() won't be called.
+ */
+ private fun recursivelyClearPlacementValues() {
+ fun Element.SceneState.clearLastPlacementValues() {
+ lastOffset = Offset.Unspecified
+ lastScale = Scale.Unspecified
+ lastAlpha = Element.AlphaUnspecified
+ }
+
+ sceneState.clearLastPlacementValues()
+ traverseDescendants(ElementTraverseKey) { node ->
+ (node as ElementNode).sceneState.clearLastPlacementValues()
+ TraversableNode.Companion.TraverseDescendantsAction.ContinueTraversal
+ }
+ }
+
override fun ContentDrawScope.draw() {
element.wasDrawnInAnyScene = true
val transition = elementTransition(layoutImpl, element, currentTransitions)
- val drawScale = getDrawScale(layoutImpl, scene, element, transition, sceneState)
+ val drawScale = getDrawScale(layoutImpl, element, transition, sceneState)
if (drawScale == Scale.Default) {
drawContent()
} else {
@@ -421,6 +437,8 @@
}
companion object {
+ private val ElementTraverseKey = Any()
+
private fun maybePruneMaps(
layoutImpl: SceneTransitionLayoutImpl,
element: Element,
@@ -494,22 +512,23 @@
// Remove the interruption values to all scenes but the scene(s) where the element will be
// placed, to make sure that interruption deltas are computed only right after this interruption
// is prepared.
- fun maybeCleanPlacementValuesBeforeInterruption(sceneState: Element.SceneState) {
+ fun cleanInterruptionValues(sceneState: Element.SceneState) {
+ sceneState.sizeInterruptionDelta = IntSize.Zero
+ sceneState.offsetInterruptionDelta = Offset.Zero
+ sceneState.alphaInterruptionDelta = 0f
+ sceneState.scaleInterruptionDelta = Scale.Zero
+
if (!shouldPlaceElement(layoutImpl, sceneState.scene, element, transition)) {
sceneState.offsetBeforeInterruption = Offset.Unspecified
sceneState.alphaBeforeInterruption = Element.AlphaUnspecified
sceneState.scaleBeforeInterruption = Scale.Unspecified
-
- sceneState.offsetInterruptionDelta = Offset.Zero
- sceneState.alphaInterruptionDelta = 0f
- sceneState.scaleInterruptionDelta = Scale.Zero
}
}
- previousFromState?.let { maybeCleanPlacementValuesBeforeInterruption(it) }
- previousToState?.let { maybeCleanPlacementValuesBeforeInterruption(it) }
- fromState?.let { maybeCleanPlacementValuesBeforeInterruption(it) }
- toState?.let { maybeCleanPlacementValuesBeforeInterruption(it) }
+ previousFromState?.let { cleanInterruptionValues(it) }
+ previousToState?.let { cleanInterruptionValues(it) }
+ fromState?.let { cleanInterruptionValues(it) }
+ toState?.let { cleanInterruptionValues(it) }
}
/**
@@ -780,7 +799,6 @@
*/
private fun elementAlpha(
layoutImpl: SceneTransitionLayoutImpl,
- scene: Scene,
element: Element,
transition: TransitionState.Transition?,
sceneState: Element.SceneState,
@@ -788,12 +806,11 @@
val alpha =
computeValue(
layoutImpl,
- scene,
+ sceneState,
element,
transition,
sceneValue = { 1f },
transformation = { it.alpha },
- idleValue = 1f,
currentValue = { 1f },
isSpecified = { true },
::lerp,
@@ -841,9 +858,8 @@
)
}
-private fun ApproachMeasureScope.measure(
+private fun measure(
layoutImpl: SceneTransitionLayoutImpl,
- scene: Scene,
element: Element,
transition: TransitionState.Transition?,
sceneState: Element.SceneState,
@@ -858,12 +874,11 @@
val targetSize =
computeValue(
layoutImpl,
- scene,
+ sceneState,
element,
transition,
sceneValue = { it.targetSize },
transformation = { it.size },
- idleValue = lookaheadSize,
currentValue = { measurable.measure(constraints).also { maybePlaceable = it }.size() },
isSpecified = { it != Element.SizeUnspecified },
::lerp,
@@ -909,7 +924,6 @@
private fun ContentDrawScope.getDrawScale(
layoutImpl: SceneTransitionLayoutImpl,
- scene: Scene,
element: Element,
transition: TransitionState.Transition?,
sceneState: Element.SceneState,
@@ -917,12 +931,11 @@
val scale =
computeValue(
layoutImpl,
- scene,
+ sceneState,
element,
transition,
sceneValue = { Scale.Default },
transformation = { it.drawScale },
- idleValue = Scale.Default,
currentValue = { Scale.Default },
isSpecified = { true },
::lerp,
@@ -989,11 +1002,12 @@
* Measurable.
*
* @param layoutImpl the [SceneTransitionLayoutImpl] associated to [element].
- * @param scene the scene containing [element].
+ * @param currentSceneState the scene state of the scene for which we are computing the value. Note
+ * that during interruptions, this could be the state of a scene that is neither
+ * [transition.toScene] nor [transition.fromScene].
* @param element the element being animated.
* @param sceneValue the value being animated.
* @param transformation the transformation associated to the value being animated.
- * @param idleValue the value when idle, i.e. when there is no transition happening.
* @param currentValue the value that would be used if it is not transformed. Note that this is
* different than [idleValue] even if the value is not transformed directly because it could be
* impacted by the transformations on other elements, like a parent that is being translated or
@@ -1003,12 +1017,11 @@
*/
private inline fun <T> computeValue(
layoutImpl: SceneTransitionLayoutImpl,
- scene: Scene,
+ currentSceneState: Element.SceneState,
element: Element,
transition: TransitionState.Transition?,
sceneValue: (Element.SceneState) -> T,
transformation: (ElementTransformations) -> PropertyTransformation<T>?,
- idleValue: T,
currentValue: () -> T,
isSpecified: (T) -> Boolean,
lerp: (T, T, Float) -> T,
@@ -1030,19 +1043,22 @@
if (fromState == null && toState == null) {
// TODO(b/311600838): Throw an exception instead once layers of disposed elements are not
// run anymore.
- return idleValue
+ return sceneValue(currentSceneState)
}
+ val currentScene = currentSceneState.scene
if (transition is TransitionState.HasOverscrollProperties) {
val overscroll = transition.currentOverscrollSpec
- if (overscroll?.scene == scene.key) {
- val elementSpec = overscroll.transformationSpec.transformations(element.key, scene.key)
+ if (overscroll?.scene == currentScene) {
+ val elementSpec =
+ overscroll.transformationSpec.transformations(element.key, currentScene)
val propertySpec = transformation(elementSpec) ?: return currentValue()
- val overscrollState = checkNotNull(if (scene.key == toScene) toState else fromState)
+ val overscrollState = checkNotNull(if (currentScene == toScene) toState else fromState)
+ val idleValue = sceneValue(overscrollState)
val targetValue =
propertySpec.transform(
layoutImpl,
- scene,
+ currentScene,
element,
overscrollState,
transition,
@@ -1086,24 +1102,30 @@
return if (start == end) start else lerp(start, end, transition.progress)
}
- val transformation =
- transformation(transition.transformationSpec.transformations(element.key, scene.key))
- // If there is no transformation explicitly associated to this element value, let's use
- // the value given by the system (like the current position and size given by the layout
- // pass).
- ?: return currentValue()
-
// Get the transformed value, i.e. the target value at the beginning (for entering elements) or
// end (for leaving elements) of the transition.
val sceneState =
checkNotNull(
when {
- isSharedElement && scene.key == fromScene -> fromState
+ isSharedElement && currentScene == fromScene -> fromState
isSharedElement -> toState
else -> fromState ?: toState
}
)
+ // The scene for which we compute the transformation. Note that this is not necessarily
+ // [currentScene] because [currentScene] could be a different scene than the transition
+ // fromScene or toScene during interruptions.
+ val scene = sceneState.scene
+
+ val transformation =
+ transformation(transition.transformationSpec.transformations(element.key, scene))
+ // If there is no transformation explicitly associated to this element value, let's use
+ // the value given by the system (like the current position and size given by the layout
+ // pass).
+ ?: return currentValue()
+
+ val idleValue = sceneValue(sceneState)
val targetValue =
transformation.transform(
layoutImpl,
@@ -1125,7 +1147,7 @@
val rangeProgress = transformation.range?.progress(progress) ?: progress
// Interpolate between the value at rest and the value before entering/after leaving.
- val isEntering = scene.key == toScene
+ val isEntering = scene == toScene
return if (isEntering) {
lerp(targetValue, idleValue, rangeProgress)
} else {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
index f32720c..7ea8cbd 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
@@ -293,7 +293,15 @@
width = fromSize.width
height = fromSize.height
} else {
- val size = lerp(fromSize, toSize, transition.progress)
+ val overscrollSpec = transition.currentOverscrollSpec
+ val progress =
+ when {
+ overscrollSpec == null -> transition.progress
+ overscrollSpec.scene == transition.toScene -> 1f
+ else -> 0f
+ }
+
+ val size = lerp(fromSize, toSize, progress)
width = size.width.coerceAtLeast(0)
height = size.height.coerceAtLeast(0)
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
index 6a178c8..a8df6f4 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
@@ -768,7 +768,7 @@
/** A [MutableSceneTransitionLayoutState] that holds the value for the current scene. */
internal class MutableSceneTransitionLayoutStateImpl(
initialScene: SceneKey,
- override var transitions: SceneTransitions,
+ override var transitions: SceneTransitions = transitions {},
private val canChangeScene: (SceneKey) -> Boolean = { true },
stateLinks: List<StateLink> = emptyList(),
enableInterruptions: Boolean = DEFAULT_INTERRUPTIONS_ENABLED,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt
index b54afae..73ee451 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt
@@ -20,7 +20,6 @@
import com.android.compose.animation.scene.Element
import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.ElementMatcher
-import com.android.compose.animation.scene.Scene
import com.android.compose.animation.scene.SceneKey
import com.android.compose.animation.scene.SceneTransitionLayoutImpl
import com.android.compose.animation.scene.TransitionState
@@ -34,7 +33,7 @@
) : PropertyTransformation<IntSize> {
override fun transform(
layoutImpl: SceneTransitionLayoutImpl,
- scene: Scene,
+ scene: SceneKey,
element: Element,
sceneState: Element.SceneState,
transition: TransitionState.Transition,
@@ -60,7 +59,7 @@
// This simple implementation assumes that the size of [element] is the same as the size of
// the [anchor] in [scene], so simply transform to the size of the anchor in the other
// scene.
- return if (scene.key == transition.fromScene) {
+ return if (scene == transition.fromScene) {
anchorSizeIn(transition.toScene)
} else {
anchorSizeIn(transition.fromScene)
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt
index 2bab4f8..70dca4c 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt
@@ -21,7 +21,6 @@
import com.android.compose.animation.scene.Element
import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.ElementMatcher
-import com.android.compose.animation.scene.Scene
import com.android.compose.animation.scene.SceneKey
import com.android.compose.animation.scene.SceneTransitionLayoutImpl
import com.android.compose.animation.scene.TransitionState
@@ -33,7 +32,7 @@
) : PropertyTransformation<Offset> {
override fun transform(
layoutImpl: SceneTransitionLayoutImpl,
- scene: Scene,
+ scene: SceneKey,
element: Element,
sceneState: Element.SceneState,
transition: TransitionState.Transition,
@@ -61,7 +60,7 @@
anchorOffsetIn(transition.toScene) ?: throwException(transition.toScene)
val offset = anchorToOffset - anchorFromOffset
- return if (scene.key == transition.toScene) {
+ return if (scene == transition.toScene) {
Offset(
value.x - offset.x,
value.y - offset.y,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt
index 6704a3b..98c2dd3 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt
@@ -20,7 +20,7 @@
import com.android.compose.animation.scene.Element
import com.android.compose.animation.scene.ElementMatcher
import com.android.compose.animation.scene.Scale
-import com.android.compose.animation.scene.Scene
+import com.android.compose.animation.scene.SceneKey
import com.android.compose.animation.scene.SceneTransitionLayoutImpl
import com.android.compose.animation.scene.TransitionState
@@ -37,7 +37,7 @@
override fun transform(
layoutImpl: SceneTransitionLayoutImpl,
- scene: Scene,
+ scene: SceneKey,
element: Element,
sceneState: Element.SceneState,
transition: TransitionState.Transition,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt
index 191a8fb..aa8dc38 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt
@@ -20,7 +20,7 @@
import com.android.compose.animation.scene.Edge
import com.android.compose.animation.scene.Element
import com.android.compose.animation.scene.ElementMatcher
-import com.android.compose.animation.scene.Scene
+import com.android.compose.animation.scene.SceneKey
import com.android.compose.animation.scene.SceneTransitionLayoutImpl
import com.android.compose.animation.scene.TransitionState
@@ -32,13 +32,13 @@
) : PropertyTransformation<Offset> {
override fun transform(
layoutImpl: SceneTransitionLayoutImpl,
- scene: Scene,
+ scene: SceneKey,
element: Element,
sceneState: Element.SceneState,
transition: TransitionState.Transition,
value: Offset
): Offset {
- val sceneSize = scene.targetSize
+ val sceneSize = layoutImpl.scene(scene).targetSize
val elementSize = sceneState.targetSize
if (elementSize == Element.SizeUnspecified) {
return value
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt
index 41f626e..ada814e 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt
@@ -18,7 +18,7 @@
import com.android.compose.animation.scene.Element
import com.android.compose.animation.scene.ElementMatcher
-import com.android.compose.animation.scene.Scene
+import com.android.compose.animation.scene.SceneKey
import com.android.compose.animation.scene.SceneTransitionLayoutImpl
import com.android.compose.animation.scene.TransitionState
@@ -28,7 +28,7 @@
) : PropertyTransformation<Float> {
override fun transform(
layoutImpl: SceneTransitionLayoutImpl,
- scene: Scene,
+ scene: SceneKey,
element: Element,
sceneState: Element.SceneState,
transition: TransitionState.Transition,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt
index f5207dc..dca8f85 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt
@@ -19,7 +19,7 @@
import androidx.compose.ui.unit.IntSize
import com.android.compose.animation.scene.Element
import com.android.compose.animation.scene.ElementMatcher
-import com.android.compose.animation.scene.Scene
+import com.android.compose.animation.scene.SceneKey
import com.android.compose.animation.scene.SceneTransitionLayoutImpl
import com.android.compose.animation.scene.TransitionState
import kotlin.math.roundToInt
@@ -35,7 +35,7 @@
) : PropertyTransformation<IntSize> {
override fun transform(
layoutImpl: SceneTransitionLayoutImpl,
- scene: Scene,
+ scene: SceneKey,
element: Element,
sceneState: Element.SceneState,
transition: TransitionState.Transition,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt
index 603f7ba..7be9ce1 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt
@@ -21,7 +21,7 @@
import androidx.compose.ui.util.fastCoerceIn
import com.android.compose.animation.scene.Element
import com.android.compose.animation.scene.ElementMatcher
-import com.android.compose.animation.scene.Scene
+import com.android.compose.animation.scene.SceneKey
import com.android.compose.animation.scene.SceneTransitionLayoutImpl
import com.android.compose.animation.scene.TransitionState
@@ -61,7 +61,7 @@
// to these internal classes.
fun transform(
layoutImpl: SceneTransitionLayoutImpl,
- scene: Scene,
+ scene: SceneKey,
element: Element,
sceneState: Element.SceneState,
transition: TransitionState.Transition,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt
index 849c9d7..f066511 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt
@@ -22,7 +22,7 @@
import com.android.compose.animation.scene.Element
import com.android.compose.animation.scene.ElementMatcher
import com.android.compose.animation.scene.OverscrollScope
-import com.android.compose.animation.scene.Scene
+import com.android.compose.animation.scene.SceneKey
import com.android.compose.animation.scene.SceneTransitionLayoutImpl
import com.android.compose.animation.scene.TransitionState
@@ -33,7 +33,7 @@
) : PropertyTransformation<Offset> {
override fun transform(
layoutImpl: SceneTransitionLayoutImpl,
- scene: Scene,
+ scene: SceneKey,
element: Element,
sceneState: Element.SceneState,
transition: TransitionState.Transition,
@@ -55,7 +55,7 @@
) : PropertyTransformation<Offset> {
override fun transform(
layoutImpl: SceneTransitionLayoutImpl,
- scene: Scene,
+ scene: SceneKey,
element: Element,
sceneState: Element.SceneState,
transition: TransitionState.Transition,
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt
index 6e8b208..a7889e2 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/AnimatedSharedAsStateTest.kt
@@ -18,10 +18,13 @@
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
+import androidx.compose.foundation.gestures.Orientation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -443,4 +446,56 @@
assertThat(lastValues[bar]?.get(SceneC)).isWithin(0.001f).of(7f)
assertThat(lastValues[bar]?.get(SceneD)).isWithin(0.001f).of(7f)
}
+
+ @Test
+ fun animatedValueDoesNotOverscrollWhenOverscrollIsSpecified() {
+ val state =
+ rule.runOnUiThread {
+ MutableSceneTransitionLayoutStateImpl(
+ SceneA,
+ transitions { overscroll(SceneB, Orientation.Horizontal) }
+ )
+ }
+
+ val key = ValueKey("foo")
+ val lastValues = mutableMapOf<SceneKey, Float>()
+
+ @Composable
+ fun SceneScope.animateFloat(value: Float, key: ValueKey) {
+ val animatedValue = animateSceneFloatAsState(value, key)
+ LaunchedEffect(animatedValue) {
+ snapshotFlow { animatedValue.value }.collect { lastValues[sceneKey] = it }
+ }
+ }
+
+ rule.setContent {
+ SceneTransitionLayout(state) {
+ scene(SceneA) { animateFloat(0f, key) }
+ scene(SceneB) { animateFloat(100f, key) }
+ }
+ }
+
+ // Overscroll on A at -100%: value should be interpolated given that there is no overscroll
+ // defined for scene A.
+ var progress by mutableStateOf(-1f)
+ rule.runOnIdle {
+ state.startTransition(transition(from = SceneA, to = SceneB, progress = { progress }))
+ }
+ rule.waitForIdle()
+ assertThat(lastValues[SceneA]).isWithin(0.001f).of(-100f)
+ assertThat(lastValues[SceneB]).isWithin(0.001f).of(-100f)
+
+ // Middle of the transition.
+ progress = 0.5f
+ rule.waitForIdle()
+ assertThat(lastValues[SceneA]).isWithin(0.001f).of(50f)
+ assertThat(lastValues[SceneB]).isWithin(0.001f).of(50f)
+
+ // Overscroll on B at 200%: value should not be interpolated given that there is an
+ // overscroll defined for scene B.
+ progress = 2f
+ rule.waitForIdle()
+ assertThat(lastValues[SceneA]).isWithin(0.001f).of(100f)
+ assertThat(lastValues[SceneB]).isWithin(0.001f).of(100f)
+ }
}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
index 41cacb4..a18da73 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
@@ -47,10 +47,12 @@
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.layout.approachLayout
import androidx.compose.ui.platform.LocalViewConfiguration
+import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsNotDisplayed
import androidx.compose.ui.test.assertPositionInRootIsEqualTo
import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.hasTestTag
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
@@ -1719,4 +1721,220 @@
rule.onNode(isElement(TestElements.Foo, SceneB)).assertIsNotDisplayed()
rule.onNode(isElement(TestElements.Foo, SceneC)).assertPositionInRootIsEqualTo(40.dp, 40.dp)
}
+
+ @Test
+ fun lastPlacementValuesAreClearedOnNestedElements() {
+ val state = rule.runOnIdle { MutableSceneTransitionLayoutStateImpl(SceneA) }
+
+ @Composable
+ fun SceneScope.NestedFooBar() {
+ Box(Modifier.element(TestElements.Foo)) {
+ Box(Modifier.element(TestElements.Bar).size(10.dp))
+ }
+ }
+
+ lateinit var layoutImpl: SceneTransitionLayoutImpl
+ rule.setContent {
+ SceneTransitionLayoutForTesting(state, onLayoutImpl = { layoutImpl = it }) {
+ scene(SceneA) { NestedFooBar() }
+ scene(SceneB) { NestedFooBar() }
+ }
+ }
+
+ // Idle on A: composed and placed only in B.
+ rule.onNode(isElement(TestElements.Foo, SceneA)).assertIsDisplayed()
+ rule.onNode(isElement(TestElements.Bar, SceneA)).assertIsDisplayed()
+ rule.onNode(isElement(TestElements.Foo, SceneB)).assertDoesNotExist()
+ rule.onNode(isElement(TestElements.Bar, SceneB)).assertDoesNotExist()
+
+ assertThat(layoutImpl.elements).containsKey(TestElements.Foo)
+ assertThat(layoutImpl.elements).containsKey(TestElements.Bar)
+ val foo = layoutImpl.elements.getValue(TestElements.Foo)
+ val bar = layoutImpl.elements.getValue(TestElements.Bar)
+
+ assertThat(foo.sceneStates).containsKey(SceneA)
+ assertThat(bar.sceneStates).containsKey(SceneA)
+ assertThat(foo.sceneStates).doesNotContainKey(SceneB)
+ assertThat(bar.sceneStates).doesNotContainKey(SceneB)
+
+ val fooInA = foo.sceneStates.getValue(SceneA)
+ val barInA = bar.sceneStates.getValue(SceneA)
+ assertThat(fooInA.lastOffset).isNotEqualTo(Offset.Unspecified)
+ assertThat(fooInA.lastAlpha).isNotEqualTo(Element.AlphaUnspecified)
+ assertThat(fooInA.lastScale).isNotEqualTo(Scale.Unspecified)
+
+ assertThat(barInA.lastOffset).isNotEqualTo(Offset.Unspecified)
+ assertThat(barInA.lastAlpha).isNotEqualTo(Element.AlphaUnspecified)
+ assertThat(barInA.lastScale).isNotEqualTo(Scale.Unspecified)
+
+ // A => B: composed in both and placed only in B.
+ rule.runOnUiThread { state.startTransition(transition(from = SceneA, to = SceneB)) }
+ rule.onNode(isElement(TestElements.Foo, SceneA)).assertExists().assertIsNotDisplayed()
+ rule.onNode(isElement(TestElements.Bar, SceneA)).assertExists().assertIsNotDisplayed()
+ rule.onNode(isElement(TestElements.Foo, SceneB)).assertIsDisplayed()
+ rule.onNode(isElement(TestElements.Bar, SceneB)).assertIsDisplayed()
+
+ assertThat(foo.sceneStates).containsKey(SceneB)
+ assertThat(bar.sceneStates).containsKey(SceneB)
+
+ val fooInB = foo.sceneStates.getValue(SceneB)
+ val barInB = bar.sceneStates.getValue(SceneB)
+ assertThat(fooInA.lastOffset).isEqualTo(Offset.Unspecified)
+ assertThat(fooInA.lastAlpha).isEqualTo(Element.AlphaUnspecified)
+ assertThat(fooInA.lastScale).isEqualTo(Scale.Unspecified)
+ assertThat(fooInB.lastOffset).isNotEqualTo(Offset.Unspecified)
+ assertThat(fooInB.lastAlpha).isNotEqualTo(Element.AlphaUnspecified)
+ assertThat(fooInB.lastScale).isNotEqualTo(Scale.Unspecified)
+
+ assertThat(barInA.lastOffset).isEqualTo(Offset.Unspecified)
+ assertThat(barInA.lastAlpha).isEqualTo(Element.AlphaUnspecified)
+ assertThat(barInA.lastScale).isEqualTo(Scale.Unspecified)
+ assertThat(barInB.lastOffset).isNotEqualTo(Offset.Unspecified)
+ assertThat(barInB.lastAlpha).isNotEqualTo(Element.AlphaUnspecified)
+ assertThat(barInB.lastScale).isNotEqualTo(Scale.Unspecified)
+ }
+
+ @Test
+ fun currentTransitionSceneIsUsedToComputeElementValues() = runTest {
+ val state =
+ rule.runOnIdle {
+ MutableSceneTransitionLayoutStateImpl(
+ SceneA,
+ transitions {
+ from(SceneB, to = SceneC) {
+ scaleSize(TestElements.Foo, width = 2f, height = 3f)
+ }
+ }
+ )
+ }
+
+ @Composable
+ fun SceneScope.Foo() {
+ Box(Modifier.testTag("fooParentIn${sceneKey.debugName}")) {
+ Box(Modifier.element(TestElements.Foo).size(20.dp))
+ }
+ }
+
+ rule.setContent {
+ SceneTransitionLayout(state, Modifier.size(200.dp)) {
+ scene(SceneA) { Foo() }
+ scene(SceneB) {}
+ scene(SceneC) { Foo() }
+ }
+ }
+
+ // We have 2 transitions:
+ // - A => B at 100%
+ // - B => C at 0%
+ // So Foo should have a size of (40dp, 60dp) in both A and C given that it is scaling its
+ // size in B => C.
+ rule.runOnUiThread {
+ state.startTransition(
+ transition(from = SceneA, to = SceneB, progress = { 1f }, onFinish = neverFinish())
+ )
+ state.startTransition(transition(from = SceneB, to = SceneC, progress = { 0f }))
+ }
+
+ rule.onNode(hasTestTag("fooParentInSceneA")).assertSizeIsEqualTo(40.dp, 60.dp)
+ rule.onNode(hasTestTag("fooParentInSceneC")).assertSizeIsEqualTo(40.dp, 60.dp)
+ }
+
+ @Test
+ fun interruptionDeltasAreProperlyCleaned() = runTest {
+ val state = rule.runOnIdle { MutableSceneTransitionLayoutStateImpl(SceneA) }
+
+ @Composable
+ fun SceneScope.Foo(offset: Dp) {
+ Box(Modifier.fillMaxSize()) {
+ Box(Modifier.offset(offset, offset).element(TestElements.Foo).size(20.dp))
+ }
+ }
+
+ rule.setContent {
+ SceneTransitionLayout(state, Modifier.size(200.dp)) {
+ scene(SceneA) { Foo(offset = 0.dp) }
+ scene(SceneB) { Foo(offset = 20.dp) }
+ scene(SceneC) { Foo(offset = 40.dp) }
+ }
+ }
+
+ // Start A => B at 50%.
+ val aToB =
+ transition(from = SceneA, to = SceneB, progress = { 0.5f }, onFinish = neverFinish())
+ rule.runOnUiThread { state.startTransition(aToB) }
+ rule.onNode(isElement(TestElements.Foo, SceneB)).assertPositionInRootIsEqualTo(10.dp, 10.dp)
+
+ // Start B => C at 0%. This will compute an interruption delta of (-10dp, -10dp) so that the
+ // position of Foo is unchanged and converges to (20dp, 20dp).
+ var interruptionProgress by mutableStateOf(1f)
+ val bToC =
+ transition(
+ from = SceneB,
+ to = SceneC,
+ progress = { 0f },
+ interruptionProgress = { interruptionProgress },
+ onFinish = neverFinish(),
+ )
+ rule.runOnUiThread { state.startTransition(bToC) }
+ rule.onNode(isElement(TestElements.Foo, SceneC)).assertPositionInRootIsEqualTo(10.dp, 10.dp)
+
+ // Finish the interruption and leave the transition progress at 0f. We should be at the same
+ // state as in B.
+ interruptionProgress = 0f
+ rule.onNode(isElement(TestElements.Foo, SceneC)).assertPositionInRootIsEqualTo(20.dp, 20.dp)
+
+ // Finish both transitions but directly start a new one B => A with interruption progress
+ // 100%. We should be at (20dp, 20dp), unless the interruption deltas have not been
+ // correctly cleaned.
+ rule.runOnUiThread {
+ state.finishTransition(aToB, idleScene = SceneB)
+ state.finishTransition(bToC, idleScene = SceneB)
+ state.startTransition(
+ transition(
+ from = SceneB,
+ to = SceneA,
+ progress = { 0f },
+ interruptionProgress = { 1f },
+ )
+ )
+ }
+ rule.onNode(isElement(TestElements.Foo, SceneB)).assertPositionInRootIsEqualTo(20.dp, 20.dp)
+ }
+
+ @Test
+ fun lastSizeIsUnspecifiedWhenOverscrollingOtherScene() = runTest {
+ val state =
+ rule.runOnIdle {
+ MutableSceneTransitionLayoutStateImpl(
+ SceneA,
+ transitions { overscroll(SceneA, Orientation.Horizontal) }
+ )
+ }
+
+ @Composable
+ fun SceneScope.Foo() {
+ Box(Modifier.element(TestElements.Foo).size(10.dp))
+ }
+
+ lateinit var layoutImpl: SceneTransitionLayoutImpl
+ rule.setContent {
+ SceneTransitionLayoutForTesting(state, onLayoutImpl = { layoutImpl = it }) {
+ scene(SceneA) { Foo() }
+ scene(SceneB) { Foo() }
+ }
+ }
+
+ // Overscroll A => B on A.
+ rule.runOnUiThread {
+ state.startTransition(
+ transition(from = SceneA, to = SceneB, progress = { -1f }, onFinish = neverFinish())
+ )
+ }
+ rule.waitForIdle()
+
+ assertThat(
+ layoutImpl.elements.getValue(TestElements.Foo).sceneStates.getValue(SceneB).lastSize
+ )
+ .isEqualTo(Element.SizeUnspecified)
+ }
}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
index 08532bd..a8dd572 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
@@ -21,6 +21,7 @@
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
+import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
@@ -333,6 +334,42 @@
}
@Test
+ fun layoutSizeDoesNotOverscrollWhenOverscrollIsSpecified() {
+ val state =
+ rule.runOnUiThread {
+ MutableSceneTransitionLayoutStateImpl(
+ SceneA,
+ transitions { overscroll(SceneB, Orientation.Horizontal) }
+ )
+ }
+
+ val layoutTag = "layout"
+ rule.setContent {
+ SceneTransitionLayout(state, Modifier.testTag(layoutTag)) {
+ scene(SceneA) { Box(Modifier.size(50.dp)) }
+ scene(SceneB) { Box(Modifier.size(70.dp)) }
+ }
+ }
+
+ // Overscroll on A at -100%: size should be interpolated given that there is no overscroll
+ // defined for scene A.
+ var progress by mutableStateOf(-1f)
+ rule.runOnIdle {
+ state.startTransition(transition(from = SceneA, to = SceneB, progress = { progress }))
+ }
+ rule.onNodeWithTag(layoutTag).assertSizeIsEqualTo(30.dp)
+
+ // Middle of the transition.
+ progress = 0.5f
+ rule.onNodeWithTag(layoutTag).assertSizeIsEqualTo(60.dp)
+
+ // Overscroll on B at 200%: size should not be interpolated given that there is an
+ // overscroll defined for scene B.
+ progress = 2f
+ rule.onNodeWithTag(layoutTag).assertSizeIsEqualTo(70.dp)
+ }
+
+ @Test
fun multipleTransitionsWillComposeMultipleScenes() {
val duration = 10 * 16L
diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestMatchers.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestMatchers.kt
index e743c78..6d063a0 100644
--- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestMatchers.kt
+++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestMatchers.kt
@@ -17,7 +17,7 @@
package com.android.compose.animation.scene
import androidx.compose.ui.test.SemanticsMatcher
-import androidx.compose.ui.test.hasParent
+import androidx.compose.ui.test.hasAnyAncestor
import androidx.compose.ui.test.hasTestTag
/** A [SemanticsMatcher] that matches [element], optionally restricted to scene [scene]. */
@@ -25,6 +25,6 @@
return if (scene == null) {
hasTestTag(element.testTag)
} else {
- hasTestTag(element.testTag) and hasParent(hasTestTag(scene.testTag))
+ hasTestTag(element.testTag) and hasAnyAncestor(hasTestTag(scene.testTag))
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractorTest.kt
index cfc6b33..a12b6f8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractorTest.kt
@@ -32,8 +32,11 @@
package com.android.systemui.keyguard.domain.interactor
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
@@ -76,6 +79,7 @@
}
@Test
+ @DisableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
fun transitionToGone_keyguardOccluded_biometricAuthenticated() =
testScope.runTest {
transitionRepository.sendTransitionSteps(
@@ -96,6 +100,25 @@
}
@Test
+ @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+ fun transitionToGone_keyguardOccludedThenAltBouncer_authed_wmStateRefactor() =
+ testScope.runTest {
+ transitionRepository.sendTransitionSteps(
+ from = KeyguardState.OCCLUDED,
+ to = KeyguardState.ALTERNATE_BOUNCER,
+ testScope
+ )
+ reset(transitionRepository)
+
+ // Authentication results in calling startDismissKeyguardTransition.
+ kosmos.keyguardTransitionInteractor.startDismissKeyguardTransition()
+ runCurrent()
+
+ assertThat(transitionRepository)
+ .startedTransition(from = KeyguardState.ALTERNATE_BOUNCER, to = KeyguardState.GONE)
+ }
+
+ @Test
fun noTransition_keyguardNotOccluded_biometricAuthenticated() =
testScope.runTest {
transitionRepository.sendTransitionSteps(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt
index 6c5001a..6eb9862 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt
@@ -53,6 +53,7 @@
import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor
import com.android.systemui.testKosmos
import junit.framework.Assert.assertEquals
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -299,6 +300,7 @@
fun testTransitionToOccluded_onWake() =
testScope.runTest {
kosmos.fakeKeyguardRepository.setKeyguardOccluded(true)
+ kosmos.keyguardOcclusionInteractor.setWmNotifiedShowWhenLockedActivityOnTop(true)
powerInteractor.setAwakeForTest()
advanceTimeBy(100) // account for debouncing
@@ -312,6 +314,7 @@
testScope.runTest {
kosmos.fakeKeyguardRepository.setKeyguardShowing(false)
kosmos.fakeKeyguardRepository.setKeyguardDismissible(true)
+ kosmos.keyguardTransitionInteractor.startDismissKeyguardTransition()
powerInteractor.setAwakeForTest()
advanceTimeBy(100) // account for debouncing
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt
index addbdb6..7906a82 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt
@@ -27,6 +27,7 @@
import com.android.systemui.bouncer.data.repository.keyguardBouncerRepository
import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.coroutines.collectValues
import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.keyguard.data.repository.fakeCommandQueue
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
@@ -191,6 +192,7 @@
fun dismissAlpha() =
testScope.runTest {
val dismissAlpha by collectLastValue(underTest.dismissAlpha)
+ assertThat(dismissAlpha).isEqualTo(1f)
keyguardTransitionRepository.sendTransitionSteps(
from = KeyguardState.AOD,
@@ -202,9 +204,9 @@
// User begins to swipe up
shadeRepository.setLegacyShadeExpansion(0.99f)
- // When not dismissable, no alpha value (null) should emit
+ // When not dismissable, the last alpha value should still be present
repository.setKeyguardDismissible(false)
- assertThat(dismissAlpha).isNull()
+ assertThat(dismissAlpha).isEqualTo(1f)
repository.setKeyguardDismissible(true)
shadeRepository.setLegacyShadeExpansion(0.98f)
@@ -212,9 +214,11 @@
}
@Test
- fun dismissAlpha_whenShadeIsExpandedEmitsNull() =
+ fun dismissAlpha_whenShadeResetsEmitsOne() =
testScope.runTest {
- val dismissAlpha by collectLastValue(underTest.dismissAlpha)
+ val dismissAlpha by collectValues(underTest.dismissAlpha)
+ assertThat(dismissAlpha[0]).isEqualTo(1f)
+ assertThat(dismissAlpha.size).isEqualTo(1)
keyguardTransitionRepository.sendTransitionSteps(
from = KeyguardState.AOD,
@@ -222,14 +226,50 @@
testScope,
)
- repository.setStatusBarState(StatusBarState.SHADE_LOCKED)
- shadeRepository.setQsExpansion(1f)
+ // User begins to swipe up
+ repository.setStatusBarState(StatusBarState.KEYGUARD)
+ repository.setKeyguardDismissible(true)
+ shadeRepository.setLegacyShadeExpansion(0.98f)
- repository.setKeyguardDismissible(false)
- assertThat(dismissAlpha).isNull()
+ assertThat(dismissAlpha[1]).isGreaterThan(0.5f)
+ assertThat(dismissAlpha[1]).isLessThan(1f)
+ assertThat(dismissAlpha.size).isEqualTo(2)
+
+ // Now reset the shade
+ shadeRepository.setLegacyShadeExpansion(1f)
+ assertThat(dismissAlpha[2]).isEqualTo(1f)
+ assertThat(dismissAlpha.size).isEqualTo(3)
+ }
+
+ @Test
+ fun dismissAlpha_doesNotEmitWhileTransitioning() =
+ testScope.runTest {
+ val dismissAlpha by collectLastValue(underTest.dismissAlpha)
+ assertThat(dismissAlpha).isEqualTo(1f)
+
+ keyguardTransitionRepository.sendTransitionSteps(
+ listOf(
+ TransitionStep(
+ from = KeyguardState.AOD,
+ to = KeyguardState.GONE,
+ value = 0f,
+ transitionState = TransitionState.STARTED,
+ ),
+ TransitionStep(
+ from = KeyguardState.AOD,
+ to = KeyguardState.GONE,
+ value = 0.1f,
+ transitionState = TransitionState.RUNNING,
+ ),
+ ),
+ testScope,
+ )
repository.setKeyguardDismissible(true)
- assertThat(dismissAlpha).isNull()
+ shadeRepository.setLegacyShadeExpansion(0.98f)
+
+ // Should still be one
+ assertThat(dismissAlpha).isEqualTo(1f)
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryTest.kt
index 73e6506..bc0512a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryTest.kt
@@ -152,7 +152,6 @@
underTest.setRecommendation(mediaRecommendation.copy(isActive = false))
assertThat(smartspaceMediaData).isNotEqualTo(mediaRecommendation)
- assertThat(smartspaceMediaData?.isActive).isFalse()
assertThat(underTest.isRecommendationActive()).isFalse()
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
index ac66e66..e40c8ee 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-@file:OptIn(ExperimentalCoroutinesApi::class, ExperimentalCoroutinesApi::class)
+@file:OptIn(ExperimentalCoroutinesApi::class)
package com.android.systemui.scene.domain.startable
@@ -395,6 +395,7 @@
)
assertThat(currentSceneKey).isEqualTo(Scenes.Gone)
underTest.start()
+ runCurrent()
kosmos.fakePowerRepository.updateWakefulness(
rawState = WakefulnessState.STARTING_TO_SLEEP,
@@ -1285,6 +1286,42 @@
}
@Test
+ fun switchToGone_whenSurfaceBehindLockscreenVisibleMidTransition() =
+ testScope.runTest {
+ val currentScene by collectLastValue(sceneInteractor.currentScene)
+ val transitionStateFlow =
+ prepareState(
+ authenticationMethod = AuthenticationMethodModel.None,
+ )
+ underTest.start()
+ assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+ // Swipe to Gone, more than halfway
+ transitionStateFlow.value =
+ ObservableTransitionState.Transition(
+ fromScene = Scenes.Lockscreen,
+ toScene = Scenes.Gone,
+ currentScene = flowOf(Scenes.Gone),
+ progress = flowOf(0.51f),
+ isInitiatedByUserInput = true,
+ isUserInputOngoing = flowOf(true),
+ )
+ runCurrent()
+ // Lift finger
+ transitionStateFlow.value =
+ ObservableTransitionState.Transition(
+ fromScene = Scenes.Lockscreen,
+ toScene = Scenes.Gone,
+ currentScene = flowOf(Scenes.Gone),
+ progress = flowOf(0.51f),
+ isInitiatedByUserInput = true,
+ isUserInputOngoing = flowOf(false),
+ )
+ runCurrent()
+
+ assertThat(currentScene).isEqualTo(Scenes.Gone)
+ }
+
+ @Test
fun switchToGone_extendUnlock() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt
index f89f18a..3ded8a3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt
@@ -6,15 +6,27 @@
import android.telephony.SubscriptionManager.PROFILE_CLASS_UNSET
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.compose.animation.scene.SceneKey
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
+import com.android.systemui.flags.EnableSceneContainer
+import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
import com.android.systemui.kosmos.testScope
import com.android.systemui.plugins.activityStarter
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.fakeMobileIconsInteractor
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.argThat
import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
@@ -24,12 +36,16 @@
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
+@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
+@EnableSceneContainer
class ShadeHeaderViewModelTest : SysuiTestCase() {
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
private val mobileIconsInteractor = kosmos.fakeMobileIconsInteractor
+ private val sceneInteractor = kosmos.sceneInteractor
+ private val deviceEntryInteractor = kosmos.deviceEntryInteractor
private val underTest: ShadeHeaderViewModel = kosmos.shadeHeaderViewModel
@@ -77,6 +93,30 @@
)
}
+ @Test
+ fun onSystemIconContainerClicked_locked_collapsesShadeToLockscreen() =
+ testScope.runTest {
+ setDeviceEntered(false)
+ setScene(Scenes.Shade)
+
+ underTest.onSystemIconContainerClicked()
+ runCurrent()
+
+ assertThat(sceneInteractor.currentScene.value).isEqualTo(Scenes.Lockscreen)
+ }
+
+ @Test
+ fun onSystemIconContainerClicked_unlocked_collapsesShadeToGone() =
+ testScope.runTest {
+ setDeviceEntered(true)
+ setScene(Scenes.Shade)
+
+ underTest.onSystemIconContainerClicked()
+ runCurrent()
+
+ assertThat(sceneInteractor.currentScene.value).isEqualTo(Scenes.Gone)
+ }
+
companion object {
private val SUB_1 =
SubscriptionModel(
@@ -93,6 +133,32 @@
profileClass = PROFILE_CLASS_UNSET,
)
}
+
+ private fun setScene(key: SceneKey) {
+ sceneInteractor.changeScene(key, "test")
+ sceneInteractor.setTransitionState(
+ MutableStateFlow<ObservableTransitionState>(ObservableTransitionState.Idle(key))
+ )
+ testScope.runCurrent()
+ }
+
+ private fun TestScope.setDeviceEntered(isEntered: Boolean) {
+ if (isEntered) {
+ // Unlock the device marking the device has entered.
+ kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+ SuccessFingerprintAuthenticationStatus(0, true)
+ )
+ runCurrent()
+ }
+ setScene(
+ if (isEntered) {
+ Scenes.Gone
+ } else {
+ Scenes.Lockscreen
+ }
+ )
+ assertThat(deviceEntryInteractor.isDeviceEntered.value).isEqualTo(isEntered)
+ }
}
private class IntentMatcherAction(private val action: String) : ArgumentMatcher<Intent> {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
index c35c165..497484f90 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
@@ -42,6 +42,7 @@
import com.android.systemui.keyguard.shared.model.BurnInModel
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
+import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
import com.android.systemui.keyguard.shared.model.StatusBarState
import com.android.systemui.keyguard.shared.model.TransitionState
@@ -842,6 +843,30 @@
}
@Test
+ @DisableSceneContainer
+ fun updateBounds_fromGone_withoutTransitions() =
+ testScope.runTest {
+ // Start step is already at 1.0
+ val runningStep = TransitionStep(GONE, AOD, 1.0f, TransitionState.RUNNING)
+ val finishStep = TransitionStep(GONE, AOD, 1.0f, TransitionState.FINISHED)
+
+ val bounds by collectLastValue(underTest.bounds)
+ val top = 123f
+ val bottom = 456f
+
+ kosmos.fakeKeyguardTransitionRepository.sendTransitionStep(runningStep)
+ runCurrent()
+ kosmos.fakeKeyguardTransitionRepository.sendTransitionStep(finishStep)
+ runCurrent()
+ keyguardRootViewModel.onNotificationContainerBoundsChanged(top, bottom)
+ runCurrent()
+
+ assertThat(bounds).isEqualTo(
+ NotificationContainerBounds(top = top, bottom = bottom)
+ )
+ }
+
+ @Test
fun alphaOnFullQsExpansion() =
testScope.runTest {
val viewState = ViewStateAccessor()
diff --git a/packages/SystemUI/res/layout/super_notification_shade.xml b/packages/SystemUI/res/layout/super_notification_shade.xml
index 221b791..fbb07be 100644
--- a/packages/SystemUI/res/layout/super_notification_shade.xml
+++ b/packages/SystemUI/res/layout/super_notification_shade.xml
@@ -62,7 +62,9 @@
<com.android.systemui.keyguard.ui.view.KeyguardRootView
android:id="@id/keyguard_root_view"
android:layout_width="match_parent"
- android:layout_height="match_parent" />
+ android:layout_height="match_parent"
+ android:clipChildren="false"
+ />
<!-- Shared container for the notification stack. Can be positioned by either
the keyguard_root_view or notification_panel -->
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/animation/UnfoldConstantTranslateAnimator.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/animation/UnfoldConstantTranslateAnimator.kt
index 7a8c82c..4fd5456 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/animation/UnfoldConstantTranslateAnimator.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/animation/UnfoldConstantTranslateAnimator.kt
@@ -37,10 +37,17 @@
private lateinit var rootView: ViewGroup
private var translationMax = 0f
+ /**
+ * Initializes the animator, it is allowed to call this method multiple times, for example
+ * to update the rootView or maximum translation
+ */
fun init(rootView: ViewGroup, translationMax: Float) {
+ if (!::rootView.isInitialized) {
+ progressProvider.addCallback(this)
+ }
+
this.rootView = rootView
this.translationMax = translationMax
- progressProvider.addCallback(this)
}
override fun onTransitionStarted() {
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUnfoldTransition.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardUnfoldTransition.kt
index 7170be61..19d918f 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUnfoldTransition.kt
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUnfoldTransition.kt
@@ -17,16 +17,21 @@
package com.android.keyguard
import android.content.Context
-import android.view.ViewGroup
-import com.android.systemui.res.R
+import android.view.View
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
+import com.android.systemui.keyguard.ui.view.KeyguardRootView
import com.android.systemui.plugins.statusbar.StatusBarStateController
-import com.android.systemui.statusbar.StatusBarState.KEYGUARD
+import com.android.systemui.res.R
+import com.android.systemui.shared.R as sharedR
+import com.android.systemui.shade.NotificationShadeWindowView
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
import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator.ViewIdToTranslate
+import com.android.systemui.statusbar.StatusBarState.KEYGUARD
import com.android.systemui.unfold.SysUIUnfoldScope
-import com.android.systemui.unfold.util.NaturalRotationUnfoldProgressProvider
+import com.android.systemui.unfold.UnfoldTransitionProgressProvider
+import com.android.systemui.unfold.dagger.NaturalRotation
import javax.inject.Inject
/**
@@ -38,8 +43,10 @@
@Inject
constructor(
private val context: Context,
+ private val keyguardRootView: KeyguardRootView,
+ private val shadeWindowView: NotificationShadeWindowView,
statusBarStateController: StatusBarStateController,
- unfoldProgressProvider: NaturalRotationUnfoldProgressProvider,
+ @NaturalRotation unfoldProgressProvider: UnfoldTransitionProgressProvider,
) {
/** Certain views only need to move if they are not currently centered */
@@ -50,27 +57,94 @@
private val filterKeyguard: () -> Boolean = { statusBarStateController.getState() == KEYGUARD }
private val translateAnimator by lazy {
+ val smartSpaceViews = if (MigrateClocksToBlueprint.isEnabled) {
+ // Use scrollX instead of translationX as translation is already set by [AodBurnInLayer]
+ val scrollXTranslation = { view: View, translation: Float ->
+ view.scrollX = -translation.toInt()
+ }
+
+ setOf(
+ ViewIdToTranslate(
+ viewId = sharedR.id.date_smartspace_view,
+ direction = START,
+ shouldBeAnimated = filterKeyguard,
+ translateFunc = scrollXTranslation,
+ ),
+ ViewIdToTranslate(
+ viewId = sharedR.id.bc_smartspace_view,
+ direction = START,
+ shouldBeAnimated = filterKeyguard,
+ translateFunc = scrollXTranslation,
+ ),
+ ViewIdToTranslate(
+ viewId = sharedR.id.weather_smartspace_view,
+ direction = START,
+ shouldBeAnimated = filterKeyguard,
+ translateFunc = scrollXTranslation,
+ )
+ )
+ } else {
+ setOf(ViewIdToTranslate(
+ viewId = R.id.keyguard_status_area,
+ direction = START,
+ shouldBeAnimated = filterKeyguard,
+ translateFunc = { view, value ->
+ (view as? KeyguardStatusAreaView)?.translateXFromUnfold = value
+ }
+ ))
+ }
+
UnfoldConstantTranslateAnimator(
viewsIdToTranslate =
setOf(
- ViewIdToTranslate(R.id.keyguard_status_area, START, filterKeyguard,
- { view, value ->
- (view as? KeyguardStatusAreaView)?.translateXFromUnfold = value
- }),
ViewIdToTranslate(
- R.id.lockscreen_clock_view_large, START, filterKeyguardAndSplitShadeOnly),
- ViewIdToTranslate(R.id.lockscreen_clock_view, START, filterKeyguard),
+ viewId = R.id.lockscreen_clock_view_large,
+ direction = START,
+ shouldBeAnimated = filterKeyguardAndSplitShadeOnly
+ ),
ViewIdToTranslate(
- R.id.notification_stack_scroller, END, filterKeyguardAndSplitShadeOnly),
- ViewIdToTranslate(R.id.start_button, START, filterKeyguard),
- ViewIdToTranslate(R.id.end_button, END, filterKeyguard)),
- progressProvider = unfoldProgressProvider)
+ viewId = R.id.lockscreen_clock_view,
+ direction = START,
+ shouldBeAnimated = filterKeyguard
+ ),
+ ViewIdToTranslate(
+ viewId = R.id.notification_stack_scroller,
+ direction = END,
+ shouldBeAnimated = filterKeyguardAndSplitShadeOnly
+ )
+ ) + smartSpaceViews,
+ progressProvider = unfoldProgressProvider
+ )
}
- /** Relies on the [parent] to locate views to translate. */
- fun setup(parent: ViewGroup) {
+ private val shortcutButtonsAnimator by lazy {
+ UnfoldConstantTranslateAnimator(
+ viewsIdToTranslate =
+ setOf(
+ ViewIdToTranslate(
+ viewId = R.id.start_button,
+ direction = START,
+ shouldBeAnimated = filterKeyguard
+ ),
+ ViewIdToTranslate(
+ viewId = R.id.end_button,
+ direction = END,
+ shouldBeAnimated = filterKeyguard
+ )
+ ),
+ progressProvider = unfoldProgressProvider
+ )
+ }
+
+ /** Initializes the keyguard fold/unfold transition */
+ fun setup() {
val translationMax =
context.resources.getDimensionPixelSize(R.dimen.keyguard_unfold_translation_x).toFloat()
- translateAnimator.init(parent, translationMax)
+
+ translateAnimator.init(shadeWindowView, translationMax)
+
+ // Use keyguard root view as there is another instance of start/end buttons with the same ID
+ // outside of the keyguard root view
+ shortcutButtonsAnimator.init(keyguardRootView, translationMax)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
index 9d110e6..10d6881 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
@@ -29,6 +29,7 @@
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.util.kotlin.Quad
+import com.android.systemui.utils.coroutines.flow.mapLatestConflated
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -37,6 +38,7 @@
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
@@ -96,7 +98,16 @@
.filter { currentScene ->
currentScene == Scenes.Gone || currentScene == Scenes.Lockscreen
}
- .map { it == Scenes.Gone }
+ .mapLatestConflated { scene ->
+ if (scene == Scenes.Gone) {
+ // Make sure device unlock status is definitely unlocked before we consider the
+ // device "entered".
+ deviceUnlockedInteractor.deviceUnlockStatus.first { it.isUnlocked }
+ true
+ } else {
+ false
+ }
+ }
.stateIn(
scope = applicationScope,
started = SharingStarted.Eagerly,
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 48660f2..a595eeb 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
@@ -50,6 +50,7 @@
import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.notification.NotificationUtils.interpolate
import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor
+import com.android.systemui.util.kotlin.Utils.Companion.sample as sampleCombine
import com.android.systemui.util.kotlin.Utils.Companion.sampleFilter
import com.android.systemui.util.kotlin.pairwise
import com.android.systemui.util.kotlin.sample
@@ -77,7 +78,7 @@
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
-import com.android.systemui.util.kotlin.Utils.Companion.sample as sampleCombine
+import kotlinx.coroutines.flow.transform
/**
* Encapsulates business-logic related to the keyguard but not to a more specific part within it.
@@ -111,7 +112,7 @@
keyguardTransitionInteractor.transitionState.map { step ->
val startingProgress = lastChangeStep.value
val progress = step.value
- if (step.to == AOD && progress >= startingProgress) {
+ if (step.to == AOD && progress >= startingProgress && startingProgress < 1f) {
val adjustedProgress =
((progress - startingProgress) / (1F - startingProgress)).coerceIn(
0F,
@@ -327,28 +328,35 @@
* This uses legacyShadeExpansion to process swipe up events. In the future, the touch input
* signal should be sent directly to transitions.
*/
- val dismissAlpha: Flow<Float?> =
+ val dismissAlpha: Flow<Float> =
shadeRepository.legacyShadeExpansion
- .filter { it < 1f }
.sampleCombine(
statusBarState,
keyguardTransitionInteractor.currentKeyguardState,
+ keyguardTransitionInteractor.transitionState,
isKeyguardDismissible,
)
- .map {
- (legacyShadeExpansion, statusBarState, currentKeyguardState, isKeyguardDismissible)
- ->
+ .filter { (_, _, _, step, _) -> !step.transitionState.isTransitioning() }
+ .transform {
+ (
+ legacyShadeExpansion,
+ statusBarState,
+ currentKeyguardState,
+ step,
+ isKeyguardDismissible) ->
if (
statusBarState == StatusBarState.KEYGUARD &&
isKeyguardDismissible &&
- currentKeyguardState == LOCKSCREEN
+ currentKeyguardState == LOCKSCREEN &&
+ legacyShadeExpansion != 1f
) {
- MathUtils.constrainedMap(0f, 1f, 0.95f, 1f, legacyShadeExpansion)
- } else {
- null
+ emit(MathUtils.constrainedMap(0f, 1f, 0.95f, 1f, legacyShadeExpansion))
+ } else if (legacyShadeExpansion == 0f || legacyShadeExpansion == 1f) {
+ // Resets alpha state
+ emit(1f)
}
}
- .onStart { emit(null) }
+ .onStart { emit(1f) }
.distinctUntilChanged()
val keyguardTranslationY: Flow<Float> =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
index 5027524..aefff7d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
@@ -66,7 +66,6 @@
import kotlinx.coroutines.flow.combineTransform
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onStart
@@ -236,7 +235,7 @@
// value emitted by any of them. Do not add flows that cannot make this guarantee.
merge(
alphaOnShadeExpansion,
- keyguardInteractor.dismissAlpha.filterNotNull(),
+ keyguardInteractor.dismissAlpha,
alternateBouncerToGoneTransitionViewModel.lockscreenAlpha(viewState),
aodToGoneTransitionViewModel.lockscreenAlpha(viewState),
aodToLockscreenTransitionViewModel.lockscreenAlpha(viewState),
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaFilterRepository.kt b/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaFilterRepository.kt
index 220d326..6a91d1b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaFilterRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaFilterRepository.kt
@@ -218,7 +218,7 @@
mediaFromRecPackageName = null
_currentMedia.value = sortedMap.values.toList()
}
- } else if (sortedMap.size > sortedMedia.size && it.active) {
+ } else if (sortedMap.size > _currentMedia.value.size && it.active) {
_currentMedia.value = sortedMap.values.toList()
} else {
// When loading an update for an existing media control.
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
index 41cd2c4..99c95b5 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
@@ -24,6 +24,7 @@
import static com.android.systemui.classifier.Classifier.BACK_GESTURE;
import static com.android.systemui.navigationbar.gestural.Utilities.isTrackpadScroll;
import static com.android.systemui.navigationbar.gestural.Utilities.isTrackpadThreeFingerSwipe;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_TOUCHPAD_GESTURES_DISABLED;
import static java.util.stream.Collectors.joining;
@@ -1021,8 +1022,10 @@
if (mIsTrackpadThreeFingerSwipe) {
// Trackpad back gestures don't have zones, so we don't need to check if the down
// event is within insets.
- mAllowGesture = isBackAllowedCommon && isValidTrackpadBackGesture(
- true /* isTrackpadEvent */);
+ boolean trackpadGesturesEnabled =
+ (mSysUiFlags & SYSUI_STATE_TOUCHPAD_GESTURES_DISABLED) == 0;
+ mAllowGesture = isBackAllowedCommon && trackpadGesturesEnabled
+ && isValidTrackpadBackGesture(true /* isTrackpadEvent */);
} else {
mAllowGesture = isBackAllowedCommon && !mUsingThreeButtonNav && isWithinInsets
&& isWithinTouchRegion((int) ev.getX(), (int) ev.getY())
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
index 0304e73..1e689bd 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
@@ -39,6 +39,7 @@
import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor
import com.android.systemui.deviceentry.shared.model.DeviceUnlockSource
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.WindowManagerLockscreenVisibilityInteractor
import com.android.systemui.model.SceneContainerPlugin
import com.android.systemui.model.SysUiState
import com.android.systemui.model.updateFlags
@@ -81,6 +82,7 @@
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.filterNot
+import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
@@ -120,6 +122,7 @@
private val uiEventLogger: UiEventLogger,
private val sceneBackInteractor: SceneBackInteractor,
private val shadeSessionStorage: SessionStorage,
+ private val windowMgrLockscreenVisInteractor: WindowManagerLockscreenVisibilityInteractor,
) : CoreStartable {
private val centralSurfaces: CentralSurfaces?
get() = centralSurfacesOptLazy.get().getOrNull()
@@ -227,6 +230,25 @@
handleDeviceUnlockStatus()
handlePowerState()
handleShadeTouchability()
+ handleSurfaceBehindKeyguardVisibility()
+ }
+
+ private fun handleSurfaceBehindKeyguardVisibility() {
+ applicationScope.launch {
+ sceneInteractor.currentScene.collectLatest { currentScene ->
+ if (currentScene == Scenes.Lockscreen) {
+ // Wait for surface to become visible
+ windowMgrLockscreenVisInteractor.surfaceBehindVisibility.first { it }
+ // Make sure the device is actually unlocked before force-changing the scene
+ deviceUnlockedInteractor.deviceUnlockStatus.first { it.isUnlocked }
+ // Override the current transition, if any, by forcing the scene to Gone
+ sceneInteractor.changeScene(
+ toScene = Scenes.Gone,
+ loggingReason = "surface behind keyguard is visible",
+ )
+ }
+ }
+ }
}
private fun handleBouncerImeVisibility() {
@@ -329,8 +351,7 @@
Scenes.Gone to "device was unlocked in Bouncer scene"
} else {
val prevScene = previousScene.value
- (prevScene
- ?: Scenes.Gone) to
+ (prevScene ?: Scenes.Gone) to
"device was unlocked in Bouncer scene, from sceneKey=$prevScene"
}
isOnLockscreen ->
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelUnfoldAnimationController.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelUnfoldAnimationController.kt
index 14659e7..f463cb5 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelUnfoldAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelUnfoldAnimationController.kt
@@ -47,6 +47,7 @@
viewsIdToTranslate =
setOf(
ViewIdToTranslate(R.id.quick_settings_panel, START, filterShade),
+ ViewIdToTranslate(R.id.qs_footer_actions, START, filterShade),
ViewIdToTranslate(R.id.notification_stack_scroller, END, filterShade)),
progressProvider = progressProvider)
}
@@ -55,9 +56,8 @@
UnfoldConstantTranslateAnimator(
viewsIdToTranslate =
setOf(
- ViewIdToTranslate(R.id.statusIcons, END, filterShade),
+ ViewIdToTranslate(R.id.shade_header_system_icons, END, filterShade),
ViewIdToTranslate(R.id.privacy_container, END, filterShade),
- ViewIdToTranslate(R.id.batteryRemainingIcon, END, filterShade),
ViewIdToTranslate(R.id.carrier_group, END, filterShade),
ViewIdToTranslate(R.id.clock, START, filterShade),
ViewIdToTranslate(R.id.date, START, filterShade)
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 3826b50..262befc 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -537,8 +537,6 @@
private final KeyguardMediaController mKeyguardMediaController;
private final Optional<KeyguardUnfoldTransition> mKeyguardUnfoldTransition;
- private final Optional<NotificationPanelUnfoldAnimationController>
- mNotificationPanelUnfoldAnimationController;
/** The drag distance required to fully expand the split shade. */
private int mSplitShadeFullTransitionDistance;
@@ -964,8 +962,6 @@
mKeyguardUnfoldTransition = unfoldComponent.map(
SysUIUnfoldComponent::getKeyguardUnfoldTransition);
- mNotificationPanelUnfoldAnimationController = unfoldComponent.map(
- SysUIUnfoldComponent::getNotificationPanelUnfoldAnimationController);
updateUserSwitcherFlags();
mKeyguardBottomAreaViewModel = keyguardBottomAreaViewModel;
@@ -1131,9 +1127,6 @@
mShadeHeaderController.init();
mShadeHeaderController.setShadeCollapseAction(
() -> collapse(/* delayed= */ false , /* speedUpFactor= */ 1.0f));
- mKeyguardUnfoldTransition.ifPresent(u -> u.setup(mView));
- mNotificationPanelUnfoldAnimationController.ifPresent(controller ->
- controller.setup(mNotificationContainerParent));
// Dreaming->Lockscreen
collectFlow(
@@ -1511,9 +1504,6 @@
if (!KeyguardBottomAreaRefactor.isEnabled()) {
setKeyguardBottomAreaVisibility(mBarState, false);
}
-
- mKeyguardUnfoldTransition.ifPresent(u -> u.setup(mView));
- mNotificationPanelUnfoldAnimationController.ifPresent(u -> u.setup(mView));
}
private void attachSplitShadeMediaPlayerContainer(FrameLayout container) {
@@ -1797,6 +1787,7 @@
private void updateKeyguardStatusViewAlignment(boolean animate) {
boolean shouldBeCentered = shouldKeyguardStatusViewBeCentered();
+ mKeyguardUnfoldTransition.ifPresent(t -> t.setStatusViewCentered(shouldBeCentered));
if (MigrateClocksToBlueprint.isEnabled()) {
mKeyguardInteractor.setClockShouldBeCentered(shouldBeCentered);
return;
@@ -1804,7 +1795,6 @@
ConstraintLayout layout = mNotificationContainerParent;
mKeyguardStatusViewController.updateAlignment(
layout, mSplitShadeEnabled, shouldBeCentered, animate);
- mKeyguardUnfoldTransition.ifPresent(t -> t.setStatusViewCentered(shouldBeCentered));
}
private boolean shouldKeyguardStatusViewBeCentered() {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
index 1df085b..e41f99b 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
@@ -32,6 +32,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.keyguard.AuthKeyguardMessageArea;
+import com.android.keyguard.KeyguardUnfoldTransition;
import com.android.keyguard.LockIconViewController;
import com.android.systemui.Dumpable;
import com.android.systemui.animation.ActivityTransitionAnimator;
@@ -72,6 +73,7 @@
import com.android.systemui.statusbar.phone.PhoneStatusBarViewController;
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
import com.android.systemui.statusbar.window.StatusBarWindowStateController;
+import com.android.systemui.unfold.SysUIUnfoldComponent;
import com.android.systemui.unfold.UnfoldTransitionProgressProvider;
import com.android.systemui.util.time.SystemClock;
@@ -174,6 +176,7 @@
DozeScrimController dozeScrimController,
NotificationShadeWindowController controller,
Optional<UnfoldTransitionProgressProvider> unfoldTransitionProgressProvider,
+ Optional<SysUIUnfoldComponent> unfoldComponent,
KeyguardUnlockAnimationController keyguardUnlockAnimationController,
NotificationInsetsController notificationInsetsController,
AmbientState ambientState,
@@ -234,6 +237,14 @@
notificationLaunchAnimationInteractor.isLaunchAnimationRunning(),
this::setExpandAnimationRunning);
+ var keyguardUnfoldTransition = unfoldComponent.map(
+ SysUIUnfoldComponent::getKeyguardUnfoldTransition);
+ var notificationPanelUnfoldAnimationController = unfoldComponent.map(
+ SysUIUnfoldComponent::getNotificationPanelUnfoldAnimationController);
+
+ keyguardUnfoldTransition.ifPresent(KeyguardUnfoldTransition::setup);
+ notificationPanelUnfoldAnimationController.ifPresent(u -> u.setup(mView));
+
mClock = clock;
if (featureFlagsClassic.isEnabled(Flags.SPLIT_SHADE_SUBPIXEL_OPTIMIZATION)) {
unfoldTransitionProgressProvider.ifPresent(
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt
index 6c76061..b2e0cd0 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt
@@ -30,6 +30,9 @@
import com.android.systemui.privacy.OngoingPrivacyChip
import com.android.systemui.privacy.PrivacyItem
import com.android.systemui.res.R
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.model.SceneFamilies
+import com.android.systemui.scene.shared.model.TransitionKeys
import com.android.systemui.shade.domain.interactor.PrivacyChipInteractor
import com.android.systemui.shade.domain.interactor.ShadeHeaderClockInteractor
import com.android.systemui.shade.domain.interactor.ShadeInteractor
@@ -57,6 +60,7 @@
@Application private val applicationScope: CoroutineScope,
context: Context,
private val activityStarter: ActivityStarter,
+ private val sceneInteractor: SceneInteractor,
shadeInteractor: ShadeInteractor,
mobileIconsInteractor: MobileIconsInteractor,
val mobileIconsViewModel: MobileIconsViewModel,
@@ -139,6 +143,15 @@
clockInteractor.launchClockActivity()
}
+ /** Notifies that the system icons container was clicked. */
+ fun onSystemIconContainerClicked() {
+ sceneInteractor.changeScene(
+ SceneFamilies.Home,
+ "ShadeHeaderViewModel.onSystemIconContainerClicked",
+ TransitionKeys.SlightlyFasterShadeCollapse,
+ )
+ }
+
/** Notifies that the shadeCarrierGroup was clicked. */
fun onShadeCarrierGroupClicked() {
activityStarter.postStartActivityDismissingKeyguard(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java
index d1fabb1..9394249 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java
@@ -358,11 +358,7 @@
* @param whenMillis
*/
public void setNotificationWhen(long whenMillis) {
- if (mNotificationHeader == null) {
- return;
- }
-
- final View timeView = mNotificationHeader.findViewById(com.android.internal.R.id.time);
+ final View timeView = mView.findViewById(com.android.internal.R.id.time);
if (timeView instanceof DateTimeView) {
((DateTimeView) timeView).setTime(whenMillis);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
index 6a8c43a..b13630f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
@@ -53,6 +53,7 @@
import com.android.systemui.keyguard.ui.viewmodel.GoneToAodTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.GoneToDozingTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.GoneToDreamingTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.GoneToLockscreenTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.LockscreenToDreamingTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.LockscreenToGlanceableHubTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.LockscreenToGoneTransitionViewModel
@@ -120,6 +121,7 @@
private val goneToAodTransitionViewModel: GoneToAodTransitionViewModel,
private val goneToDozingTransitionViewModel: GoneToDozingTransitionViewModel,
private val goneToDreamingTransitionViewModel: GoneToDreamingTransitionViewModel,
+ private val goneToLockscreenTransitionViewModel: GoneToLockscreenTransitionViewModel,
private val lockscreenToDreamingTransitionViewModel: LockscreenToDreamingTransitionViewModel,
private val lockscreenToGlanceableHubTransitionViewModel:
LockscreenToGlanceableHubTransitionViewModel,
@@ -472,6 +474,9 @@
// All transition view models are mututally exclusive, and safe to merge
val alphaTransitions =
merge(
+ keyguardInteractor.dismissAlpha.dumpWhileCollecting(
+ "keyguardInteractor.dismissAlpha"
+ ),
alternateBouncerToGoneTransitionViewModel.notificationAlpha(viewState),
aodToGoneTransitionViewModel.notificationAlpha(viewState),
aodToLockscreenTransitionViewModel.notificationAlpha,
@@ -482,6 +487,7 @@
goneToAodTransitionViewModel.notificationAlpha,
goneToDreamingTransitionViewModel.lockscreenAlpha,
goneToDozingTransitionViewModel.lockscreenAlpha,
+ goneToLockscreenTransitionViewModel.lockscreenAlpha,
lockscreenToDreamingTransitionViewModel.lockscreenAlpha,
lockscreenToGoneTransitionViewModel.notificationAlpha(viewState),
lockscreenToOccludedTransitionViewModel.lockscreenAlpha,
@@ -498,24 +504,12 @@
// These remaining cases handle alpha changes within an existing state, such as
// shade expansion or swipe to dismiss
combineTransform(
- isOnLockscreenWithoutShade,
isTransitioningToHiddenKeyguard,
- shadeCollapseFadeIn,
alphaForShadeAndQsExpansion,
- keyguardInteractor.dismissAlpha.dumpWhileCollecting(
- "keyguardInteractor.keyguardAlpha"
- ),
) {
- isOnLockscreenWithoutShade,
isTransitioningToHiddenKeyguard,
- shadeCollapseFadeIn,
- alphaForShadeAndQsExpansion,
- dismissAlpha ->
- if (isOnLockscreenWithoutShade) {
- if (!shadeCollapseFadeIn && dismissAlpha != null) {
- emit(dismissAlpha)
- }
- } else if (!isTransitioningToHiddenKeyguard) {
+ alphaForShadeAndQsExpansion ->
+ if (!isTransitioningToHiddenKeyguard) {
emit(alphaForShadeAndQsExpansion)
}
},
diff --git a/packages/SystemUI/src/com/android/systemui/unfold/SysUIUnfoldModule.kt b/packages/SystemUI/src/com/android/systemui/unfold/SysUIUnfoldModule.kt
index a27989d..291903d 100644
--- a/packages/SystemUI/src/com/android/systemui/unfold/SysUIUnfoldModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/unfold/SysUIUnfoldModule.kt
@@ -21,6 +21,7 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.shade.NotificationPanelUnfoldAnimationController
import com.android.systemui.statusbar.phone.StatusBarMoveFromCenterAnimationController
+import com.android.systemui.unfold.dagger.NaturalRotation
import com.android.systemui.unfold.dagger.UnfoldBg
import com.android.systemui.unfold.util.NaturalRotationUnfoldProgressProvider
import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider
@@ -108,6 +109,13 @@
abstract fun bindsFoldLightRevealOverlayAnimation(
anim: FoldLightRevealOverlayAnimation
): FullscreenLightRevealAnimation
+
+ @Binds
+ @NaturalRotation
+ @SysUIUnfoldScope
+ abstract fun bindNaturalRotationUnfoldProgressProvider(
+ provider: NaturalRotationUnfoldProgressProvider
+ ): UnfoldTransitionProgressProvider
}
@SysUIUnfoldScope
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUnfoldTransitionTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUnfoldTransitionTest.kt
index 3afca59..336183d 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUnfoldTransitionTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUnfoldTransitionTest.kt
@@ -18,26 +18,25 @@
import android.testing.AndroidTestingRunner
import android.view.View
-import android.view.ViewGroup
import androidx.test.filters.SmallTest
-import com.android.systemui.res.R
import com.android.systemui.SysuiTestCase
+import com.android.systemui.keyguard.ui.view.KeyguardRootView
+import com.android.systemui.kosmos.Kosmos
import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.res.R
+import com.android.systemui.shade.NotificationShadeWindowView
import com.android.systemui.statusbar.StatusBarState.KEYGUARD
import com.android.systemui.statusbar.StatusBarState.SHADE
+import com.android.systemui.unfold.FakeUnfoldTransitionProvider
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.android.systemui.unfold.fakeUnfoldTransitionProgressProvider
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
+import org.mockito.kotlin.whenever
/**
* Translates items away/towards the hinge when the device is opened/closed. This is controlled by
@@ -47,11 +46,16 @@
@RunWith(AndroidTestingRunner::class)
class KeyguardUnfoldTransitionTest : SysuiTestCase() {
- @Mock private lateinit var progressProvider: NaturalRotationUnfoldProgressProvider
+ private val kosmos = Kosmos()
- @Captor private lateinit var progressListenerCaptor: ArgumentCaptor<TransitionProgressListener>
+ private val progressProvider: FakeUnfoldTransitionProvider =
+ kosmos.fakeUnfoldTransitionProgressProvider
- @Mock private lateinit var parent: ViewGroup
+ @Mock
+ private lateinit var keyguardRootView: KeyguardRootView
+
+ @Mock
+ private lateinit var notificationShadeWindowView: NotificationShadeWindowView
@Mock private lateinit var statusBarStateController: StatusBarStateController
@@ -66,13 +70,15 @@
xTranslationMax =
context.resources.getDimensionPixelSize(R.dimen.keyguard_unfold_translation_x).toFloat()
- underTest = KeyguardUnfoldTransition(context, statusBarStateController, progressProvider)
+ underTest = KeyguardUnfoldTransition(
+ context, keyguardRootView, notificationShadeWindowView,
+ statusBarStateController, progressProvider
+ )
- underTest.setup(parent)
+ underTest.setup()
underTest.statusViewCentered = false
- verify(progressProvider).addCallback(capture(progressListenerCaptor))
- progressListener = progressListenerCaptor.value
+ progressListener = progressProvider
}
@Test
@@ -81,7 +87,9 @@
underTest.statusViewCentered = true
val view = View(context)
- whenever(parent.findViewById<View>(R.id.lockscreen_clock_view_large)).thenReturn(view)
+ whenever(keyguardRootView.findViewById<View>(R.id.lockscreen_clock_view_large)).thenReturn(
+ view
+ )
progressListener.onTransitionStarted()
assertThat(view.translationX).isZero()
@@ -101,7 +109,9 @@
whenever(statusBarStateController.getState()).thenReturn(SHADE)
val view = View(context)
- whenever(parent.findViewById<View>(R.id.lockscreen_clock_view_large)).thenReturn(view)
+ whenever(keyguardRootView.findViewById<View>(R.id.lockscreen_clock_view_large)).thenReturn(
+ view
+ )
progressListener.onTransitionStarted()
assertThat(view.translationX).isZero()
@@ -121,7 +131,10 @@
whenever(statusBarStateController.getState()).thenReturn(KEYGUARD)
val view = View(context)
- whenever(parent.findViewById<View>(R.id.lockscreen_clock_view_large)).thenReturn(view)
+ whenever(
+ notificationShadeWindowView
+ .findViewById<View>(R.id.lockscreen_clock_view_large)
+ ).thenReturn(view)
progressListener.onTransitionStarted()
assertThat(view.translationX).isZero()
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 e02fb29..89e0971 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
@@ -30,7 +30,6 @@
import com.android.systemui.communal.domain.interactor.communalInteractor
import com.android.systemui.communal.domain.interactor.setCommunalAvailable
import com.android.systemui.communal.shared.model.CommunalScenes
-import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
import com.android.systemui.dock.fakeDockManager
import com.android.systemui.flags.BrokenWithSceneContainer
import com.android.systemui.flags.DisableSceneContainer
@@ -608,31 +607,6 @@
/** This handles security method NONE and screen off with lock timeout */
@Test
- fun dozingToGoneWithKeyguardNotShowing() =
- testScope.runTest {
- // GIVEN a prior transition has run to DOZING
- runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.DOZING)
- runCurrent()
-
- // WHEN the device wakes up without a keyguard
- keyguardRepository.setKeyguardShowing(false)
- keyguardRepository.setKeyguardDismissible(true)
- kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(false)
- powerInteractor.setAwakeForTest()
- advanceTimeBy(60L)
-
- assertThat(transitionRepository)
- .startedTransition(
- to = KeyguardState.GONE,
- from = KeyguardState.DOZING,
- animatorAssertion = { it.isNotNull() }
- )
-
- coroutineContext.cancelChildren()
- }
-
- /** This handles security method NONE and screen off with lock timeout */
- @Test
@DisableSceneContainer
fun dreamingToGoneWithKeyguardNotShowing() =
testScope.runTest {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
index 586adbd..74a2999 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
@@ -70,6 +70,7 @@
import com.android.systemui.statusbar.phone.PhoneStatusBarViewController
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
import com.android.systemui.statusbar.window.StatusBarWindowStateController
+import com.android.systemui.unfold.SysUIUnfoldComponent
import com.android.systemui.unfold.UnfoldTransitionProgressProvider
import com.android.systemui.user.domain.interactor.SelectedUserInteractor
import com.android.systemui.util.mockito.any
@@ -85,6 +86,7 @@
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.Answers
import org.mockito.ArgumentCaptor
import org.mockito.Mock
import org.mockito.Mockito.anyFloat
@@ -132,6 +134,8 @@
private lateinit var mLockscreenHostedDreamGestureListener: LockscreenHostedDreamGestureListener
@Mock private lateinit var notificationInsetsController: NotificationInsetsController
@Mock private lateinit var mGlanceableHubContainerController: GlanceableHubContainerController
+ @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+ private lateinit var sysUiUnfoldComponent: SysUIUnfoldComponent
@Mock lateinit var keyguardBouncerComponentFactory: KeyguardBouncerComponent.Factory
@Mock lateinit var keyguardBouncerComponent: KeyguardBouncerComponent
@Mock lateinit var keyguardSecurityContainerController: KeyguardSecurityContainerController
@@ -209,6 +213,7 @@
dozeScrimController,
notificationShadeWindowController,
unfoldTransitionProgressProvider,
+ Optional.of(sysUiUnfoldComponent),
keyguardUnlockAnimationController,
notificationInsetsController,
ambientState,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
index e83a46b..31bd12f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt
@@ -58,6 +58,7 @@
import com.android.systemui.statusbar.phone.DozeServiceHost
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
import com.android.systemui.statusbar.window.StatusBarWindowStateController
+import com.android.systemui.unfold.SysUIUnfoldComponent
import com.android.systemui.unfold.UnfoldTransitionProgressProvider
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.mock
@@ -112,6 +113,7 @@
@Mock private lateinit var shadeLogger: ShadeLogger
@Mock private lateinit var dumpManager: DumpManager
@Mock private lateinit var pulsingGestureListener: PulsingGestureListener
+ @Mock private lateinit var sysUiUnfoldComponent: SysUIUnfoldComponent
@Mock
private lateinit var mLockscreenHostedDreamGestureListener: LockscreenHostedDreamGestureListener
@Mock private lateinit var keyguardBouncerComponentFactory: KeyguardBouncerComponent.Factory
@@ -181,6 +183,7 @@
dozeScrimController,
notificationShadeWindowController,
unfoldTransitionProgressProvider,
+ Optional.of(sysUiUnfoldComponent),
keyguardUnlockAnimationController,
notificationInsetsController,
ambientState,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/animation/UnfoldConstantTranslateAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/animation/UnfoldConstantTranslateAnimatorTest.kt
index a05a23b..293dc04 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/animation/UnfoldConstantTranslateAnimatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/animation/UnfoldConstantTranslateAnimatorTest.kt
@@ -27,6 +27,9 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
+import org.mockito.Mockito.anyFloat
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` as whenever
import org.mockito.MockitoAnnotations
@@ -78,6 +81,22 @@
}
@Test
+ fun initMultipleTimes_onTransition_translationIsSetOnlyOnce() {
+ animator.init(parent, MAX_TRANSLATION)
+ animator.init(parent, MAX_TRANSLATION)
+ animator.init(parent, MAX_TRANSLATION)
+
+ // GIVEN one view with a matching id
+ val view = spy(View(context))
+ whenever(parent.findViewById<View>(START_VIEW_ID)).thenReturn(view)
+ progressProvider.onTransitionStarted()
+
+ // WHEN the transition progresses, translation is updated once
+ progressProvider.onTransitionProgress(.5f)
+ verify(view).translationX = anyFloat()
+ }
+
+ @Test
fun onTransition_oneMovesStartWithRTL() {
// GIVEN one view with a matching id
val view = View(context)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorFactory.kt
index 02842cc..b5ca964 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorFactory.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorFactory.kt
@@ -24,6 +24,7 @@
import com.android.systemui.keyguard.data.repository.FakeCommandQueue
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.power.domain.interactor.PowerInteractor
import com.android.systemui.power.domain.interactor.PowerInteractorFactory
import com.android.systemui.scene.domain.interactor.SceneInteractor
@@ -34,6 +35,7 @@
import com.android.systemui.util.mockito.whenever
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.TestScope
/**
@@ -60,9 +62,11 @@
): WithDependencies {
// Mock these until they are replaced by kosmos
val currentKeyguardStateFlow = MutableSharedFlow<KeyguardState>()
+ val transitionStateFlow = MutableStateFlow(TransitionStep())
val keyguardTransitionInteractor =
mock<KeyguardTransitionInteractor>().also {
whenever(it.currentKeyguardState).thenReturn(currentKeyguardStateFlow)
+ whenever(it.transitionState).thenReturn(transitionStateFlow)
}
val configurationDimensionFlow = MutableSharedFlow<ConfigurationBasedDimensions>()
configurationDimensionFlow.tryEmit(
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneContainerStartableKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneContainerStartableKosmos.kt
index d82286f..cf18c0e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneContainerStartableKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneContainerStartableKosmos.kt
@@ -26,6 +26,7 @@
import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.windowManagerLockscreenVisibilityInteractor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
import com.android.systemui.kosmos.testScope
@@ -67,5 +68,6 @@
uiEventLogger = uiEventLogger,
sceneBackInteractor = sceneBackInteractor,
shadeSessionStorage = shadeSessionStorage,
+ windowMgrLockscreenVisInteractor = windowManagerLockscreenVisibilityInteractor,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt
index 8d653f7..0e21698 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt
@@ -21,6 +21,7 @@
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.plugins.activityStarter
+import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.shade.domain.interactor.privacyChipInteractor
import com.android.systemui.shade.domain.interactor.shadeHeaderClockInteractor
import com.android.systemui.shade.domain.interactor.shadeInteractor
@@ -33,6 +34,7 @@
applicationScope = applicationCoroutineScope,
context = applicationContext,
activityStarter = activityStarter,
+ sceneInteractor = sceneInteractor,
shadeInteractor = shadeInteractor,
mobileIconsInteractor = mobileIconsInteractor,
mobileIconsViewModel = mobileIconsViewModel,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
index d00eedf..299486f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
@@ -31,6 +31,7 @@
import com.android.systemui.keyguard.ui.viewmodel.goneToAodTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.goneToDozingTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.goneToDreamingTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.goneToLockscreenTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.lockscreenToDreamingTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.lockscreenToGlanceableHubTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.lockscreenToGoneTransitionViewModel
@@ -70,6 +71,7 @@
goneToAodTransitionViewModel = goneToAodTransitionViewModel,
goneToDozingTransitionViewModel = goneToDozingTransitionViewModel,
goneToDreamingTransitionViewModel = goneToDreamingTransitionViewModel,
+ goneToLockscreenTransitionViewModel = goneToLockscreenTransitionViewModel,
glanceableHubToLockscreenTransitionViewModel = glanceableHubToLockscreenTransitionViewModel,
lockscreenToDreamingTransitionViewModel = lockscreenToDreamingTransitionViewModel,
lockscreenToGlanceableHubTransitionViewModel = lockscreenToGlanceableHubTransitionViewModel,
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/dagger/NaturalRotation.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/dagger/NaturalRotation.kt
new file mode 100644
index 0000000..be02487
--- /dev/null
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/dagger/NaturalRotation.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2024 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.unfold.dagger
+
+import javax.inject.Qualifier
+
+/** Qualifier annotation for a progress provider that emits animation events only when
+ * in natural rotation */
+@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class NaturalRotation
diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
index 2607ed3..89c888c 100644
--- a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
+++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java
@@ -18,6 +18,7 @@
import static android.Manifest.permission.MANAGE_CONTENT_CAPTURE;
import static android.content.Context.CONTENT_CAPTURE_MANAGER_SERVICE;
+import static android.service.contentcapture.ContentCaptureService.ASSIST_CONTENT_ACTIVITY_START_KEY;
import static android.service.contentcapture.ContentCaptureService.setClientState;
import static android.view.contentcapture.ContentCaptureHelper.toList;
import static android.view.contentcapture.ContentCaptureManager.DEVICE_CONFIG_PROPERTY_CONTENT_PROTECTION_ALLOWLIST_DELAY_MS;
@@ -28,6 +29,7 @@
import static android.view.contentcapture.ContentCaptureManager.DEVICE_CONFIG_PROPERTY_CONTENT_PROTECTION_OPTIONAL_GROUPS_THRESHOLD;
import static android.view.contentcapture.ContentCaptureManager.DEVICE_CONFIG_PROPERTY_CONTENT_PROTECTION_REQUIRED_GROUPS_CONFIG;
import static android.view.contentcapture.ContentCaptureManager.DEVICE_CONFIG_PROPERTY_ENABLE_CONTENT_PROTECTION_RECEIVER;
+import static android.view.contentcapture.ContentCaptureManager.DEVICE_CONFIG_ENABLE_ACTIVITY_START_ASSIST_CONTENT;
import static android.view.contentcapture.ContentCaptureManager.RESULT_CODE_FALSE;
import static android.view.contentcapture.ContentCaptureManager.RESULT_CODE_OK;
import static android.view.contentcapture.ContentCaptureManager.RESULT_CODE_SECURITY_EXCEPTION;
@@ -44,6 +46,7 @@
import static com.android.internal.util.FrameworkStatsLog.CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__DATA_SHARE_WRITE_FINISHED;
import static com.android.internal.util.FrameworkStatsLog.CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__REJECT_DATA_SHARE_REQUEST;
import static com.android.internal.util.SyncResultReceiver.bundleFor;
+import static com.android.server.wm.ActivityTaskManagerInternal.ASSIST_KEY_CONTENT;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -52,10 +55,12 @@
import android.app.ActivityThread;
import android.app.admin.DevicePolicyCache;
import android.app.assist.ActivityId;
+import android.app.assist.AssistContent;
import android.content.ComponentName;
import android.content.ContentCaptureOptions;
import android.content.ContentResolver;
import android.content.Context;
+import android.content.Intent;
import android.content.pm.ActivityPresentationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
@@ -184,6 +189,9 @@
@Nullable
private boolean mDisabledByDeviceConfig;
+ @GuardedBy("mLock")
+ private boolean activityStartAssistDataEnabled;
+
// Device-config settings that are cached and passed back to apps
@GuardedBy("mLock")
int mDevCfgLoggingLevel;
@@ -451,6 +459,8 @@
case DEVICE_CONFIG_PROPERTY_CONTENT_PROTECTION_AUTO_DISCONNECT_TIMEOUT:
setFineTuneParamsFromDeviceConfig();
return;
+ case DEVICE_CONFIG_ENABLE_ACTIVITY_START_ASSIST_CONTENT:
+ setActivityStartAssistDataEnabled();
default:
Slog.i(TAG, "Ignoring change on " + key);
}
@@ -639,6 +649,15 @@
final String enabled = DeviceConfig.getProperty(DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
ContentCaptureManager.DEVICE_CONFIG_PROPERTY_SERVICE_EXPLICITLY_ENABLED);
setDisabledByDeviceConfig(enabled);
+ setActivityStartAssistDataEnabled();
+ }
+
+ private void setActivityStartAssistDataEnabled() {
+ synchronized (mLock) {
+ this.activityStartAssistDataEnabled = DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
+ DEVICE_CONFIG_ENABLE_ACTIVITY_START_ASSIST_CONTENT, false);
+ }
}
private void setDisabledByDeviceConfig(@Nullable String explicitlyEnabled) {
@@ -908,6 +927,9 @@
pw.print(prefix2);
pw.print("contentProtectionAutoDisconnectTimeoutMs: ");
pw.println(mDevCfgContentProtectionAutoDisconnectTimeoutMs);
+ pw.print(prefix2);
+ pw.print("activityStartAssistDataEnabled: ");
+ pw.println(activityStartAssistDataEnabled);
pw.print(prefix);
pw.println("Global Options:");
mGlobalContentCaptureOptions.dump(prefix2, pw);
@@ -1327,6 +1349,33 @@
}
@Override
+ @SuppressWarnings("GuardedBy")
+ public boolean sendActivityStartAssistData(@UserIdInt int userId,
+ @NonNull IBinder activityToken,
+ @NonNull Intent intentData) {
+ synchronized (mLock) {
+ if (!activityStartAssistDataEnabled) {
+ return false;
+ }
+ Intent intent = new Intent(intentData);
+ intent.setFlags(intent.getFlags() & ~(Intent.FLAG_GRANT_WRITE_URI_PERMISSION
+ | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION));
+ Bundle assistContentExtras = new Bundle();
+ assistContentExtras.putBoolean(ASSIST_CONTENT_ACTIVITY_START_KEY, true);
+ AssistContent assistContent = new AssistContent(assistContentExtras);
+ assistContent.setDefaultIntent(intent);
+
+ final Bundle activityAssistData = new Bundle();
+ activityAssistData.putParcelable(ASSIST_KEY_CONTENT, assistContent);
+ final ContentCapturePerUserService service = peekServiceForUserLocked(userId);
+ if (service != null) {
+ return service.sendActivityAssistDataLocked(activityToken, activityAssistData);
+ }
+ }
+ return false;
+ }
+
+ @Override
public boolean sendActivityAssistData(@UserIdInt int userId, @NonNull IBinder activityToken,
@NonNull Bundle data) {
synchronized (mLock) {
diff --git a/services/core/java/com/android/server/contentcapture/ContentCaptureManagerInternal.java b/services/core/java/com/android/server/contentcapture/ContentCaptureManagerInternal.java
index 0812fd9..de7341d 100644
--- a/services/core/java/com/android/server/contentcapture/ContentCaptureManagerInternal.java
+++ b/services/core/java/com/android/server/contentcapture/ContentCaptureManagerInternal.java
@@ -21,6 +21,7 @@
import android.app.assist.ActivityId;
import android.content.ComponentName;
import android.content.ContentCaptureOptions;
+import android.content.Intent;
import android.os.Bundle;
import android.os.IBinder;
import android.service.contentcapture.ActivityEvent.ActivityEventType;
@@ -40,6 +41,14 @@
public abstract boolean isContentCaptureServiceForUser(int uid, @UserIdInt int userId);
/**
+ * Notifies the intelligence service of new intent data associated with an activity start event.
+ *
+ * @return {@code false} if there was no service set for the given user
+ */
+ public abstract boolean sendActivityStartAssistData(@UserIdInt int userId,
+ @NonNull IBinder activityToken, @NonNull Intent intentData);
+
+ /**
* Notifies the intelligence service of new assist data for the given activity.
*
* @return {@code false} if there was no service set for the given user
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 5fd0253..2d5f38e 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -3406,6 +3406,31 @@
}
}
+ boolean requestDisplayPower(int displayId, boolean on) {
+ synchronized (mSyncRoot) {
+ final var display = mLogicalDisplayMapper.getDisplayLocked(displayId);
+ if (display == null) {
+ Slog.w(TAG, "requestDisplayPower: Cannot find a display with displayId="
+ + displayId);
+ return false;
+ }
+ final BrightnessPair brightnessPair = mDisplayBrightnesses.get(displayId);
+ var runnable = display.getPrimaryDisplayDeviceLocked().requestDisplayStateLocked(
+ on ? Display.STATE_ON : Display.STATE_OFF,
+ on ? brightnessPair.brightness : PowerManager.BRIGHTNESS_OFF_FLOAT,
+ brightnessPair.sdrBrightness,
+ display.getDisplayOffloadSessionLocked());
+ if (runnable == null) {
+ Slog.w(TAG, "requestDisplayPower: Cannot update the power state to ON=" + on
+ + " for a display with displayId=" + displayId + ", runnable is null");
+ return false;
+ }
+ runnable.run();
+ Slog.i(TAG, "requestDisplayPower(displayId=" + displayId + ", on=" + on + ")");
+ }
+ return true;
+ }
+
/**
* This is the object that everything in the display manager locks on.
* We make it an inner class within the {@link DisplayManagerService} to so that it is
@@ -4629,6 +4654,12 @@
DisplayManagerService.this.enableConnectedDisplay(displayId, false);
}
+ @EnforcePermission(MANAGE_DISPLAYS)
+ public boolean requestDisplayPower(int displayId, boolean on) {
+ requestDisplayPower_enforcePermission();
+ return DisplayManagerService.this.requestDisplayPower(displayId, on);
+ }
+
@EnforcePermission(RESTRICT_DISPLAY_MODES)
@Override // Binder call
public void requestDisplayModes(IBinder token, int displayId, @Nullable int[] modeIds) {
diff --git a/services/core/java/com/android/server/display/DisplayManagerShellCommand.java b/services/core/java/com/android/server/display/DisplayManagerShellCommand.java
index 8c39d7d..d973b71 100644
--- a/services/core/java/com/android/server/display/DisplayManagerShellCommand.java
+++ b/services/core/java/com/android/server/display/DisplayManagerShellCommand.java
@@ -106,6 +106,10 @@
return setDisplayEnabled(true);
case "disable-display":
return setDisplayEnabled(false);
+ case "power-on":
+ return requestDisplayPower(true);
+ case "power-off":
+ return requestDisplayPower(false);
default:
return handleDefaultCommands(cmd);
}
@@ -592,4 +596,21 @@
mService.enableConnectedDisplay(displayId, enable);
return 0;
}
+
+ private int requestDisplayPower(boolean enable) {
+ final String displayIdText = getNextArg();
+ if (displayIdText == null) {
+ getErrPrintWriter().println("Error: no displayId specified");
+ return 1;
+ }
+ final int displayId;
+ try {
+ displayId = Integer.parseInt(displayIdText);
+ } catch (NumberFormatException e) {
+ getErrPrintWriter().println("Error: invalid displayId: '" + displayIdText + "'");
+ return 1;
+ }
+ mService.requestDisplayPower(displayId, enable);
+ return 0;
+ }
}
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 0bd40d1..e5dbce9 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -3352,6 +3352,10 @@
mPointerIconCache.setPointerFillStyle(fillStyle);
}
+ void setPointerScale(float scale) {
+ mPointerIconCache.setPointerScale(scale);
+ }
+
interface KeyboardBacklightControllerInterface {
default void incrementKeyboardBacklight(int deviceId) {}
default void decrementKeyboardBacklight(int deviceId) {}
diff --git a/services/core/java/com/android/server/input/InputSettingsObserver.java b/services/core/java/com/android/server/input/InputSettingsObserver.java
index 9585b49..593b091 100644
--- a/services/core/java/com/android/server/input/InputSettingsObserver.java
+++ b/services/core/java/com/android/server/input/InputSettingsObserver.java
@@ -16,6 +16,7 @@
package com.android.server.input;
+import static android.view.PointerIcon.DEFAULT_POINTER_SCALE;
import static android.view.PointerIcon.POINTER_ICON_VECTOR_STYLE_FILL_BLACK;
import static android.view.flags.Flags.enableVectorCursorA11ySettings;
@@ -101,7 +102,9 @@
Map.entry(Settings.Secure.getUriFor(Settings.Secure.STYLUS_POINTER_ICON_ENABLED),
(reason) -> updateStylusPointerIconEnabled()),
Map.entry(Settings.System.getUriFor(Settings.System.POINTER_FILL_STYLE),
- (reason) -> updatePointerFillStyleFromSettings()));
+ (reason) -> updatePointerFillStyleFromSettings()),
+ Map.entry(Settings.System.getUriFor(Settings.System.POINTER_SCALE),
+ (reason) -> updatePointerScaleFromSettings()));
}
/**
@@ -277,4 +280,14 @@
UserHandle.USER_CURRENT);
mService.setPointerFillStyle(pointerFillStyle);
}
+
+ private void updatePointerScaleFromSettings() {
+ if (!enableVectorCursorA11ySettings()) {
+ return;
+ }
+ final float pointerScale = Settings.System.getFloatForUser(mContext.getContentResolver(),
+ Settings.System.POINTER_SCALE, DEFAULT_POINTER_SCALE,
+ UserHandle.USER_CURRENT);
+ mService.setPointerScale(pointerScale);
+ }
}
diff --git a/services/core/java/com/android/server/input/PointerIconCache.java b/services/core/java/com/android/server/input/PointerIconCache.java
index 936e17f..44622d8 100644
--- a/services/core/java/com/android/server/input/PointerIconCache.java
+++ b/services/core/java/com/android/server/input/PointerIconCache.java
@@ -16,6 +16,7 @@
package com.android.server.input;
+import static android.view.PointerIcon.DEFAULT_POINTER_SCALE;
import static android.view.PointerIcon.POINTER_ICON_VECTOR_STYLE_FILL_BLACK;
import android.annotation.NonNull;
@@ -63,6 +64,8 @@
@GuardedBy("mLoadedPointerIconsByDisplayAndType")
private @PointerIcon.PointerIconVectorStyleFill int mPointerIconFillStyle =
POINTER_ICON_VECTOR_STYLE_FILL_BLACK;
+ @GuardedBy("mLoadedPointerIconsByDisplayAndType")
+ private float mPointerIconScale = DEFAULT_POINTER_SCALE;
private final DisplayManager.DisplayListener mDisplayListener =
new DisplayManager.DisplayListener() {
@@ -117,6 +120,11 @@
mUiThreadHandler.post(() -> handleSetPointerFillStyle(fillStyle));
}
+ /** Set the scale for vector pointer icons. */
+ public void setPointerScale(float scale) {
+ mUiThreadHandler.post(() -> handleSetPointerScale(scale));
+ }
+
/**
* Get a loaded system pointer icon. This will fetch the icon from the cache, or load it if
* it isn't already cached.
@@ -137,7 +145,7 @@
theme.applyStyle(PointerIcon.vectorFillStyleToResource(mPointerIconFillStyle),
/* force= */ true);
icon = PointerIcon.getLoadedSystemIcon(new ContextThemeWrapper(context, theme),
- type, mUseLargePointerIcons);
+ type, mUseLargePointerIcons, mPointerIconScale);
iconsByType.put(type, icon);
}
return Objects.requireNonNull(icon);
@@ -215,6 +223,19 @@
mNative.reloadPointerIcons();
}
+ @android.annotation.UiThread
+ private void handleSetPointerScale(float scale) {
+ synchronized (mLoadedPointerIconsByDisplayAndType) {
+ if (mPointerIconScale == scale) {
+ return;
+ }
+ mPointerIconScale = scale;
+ // Clear all cached icons on all displays.
+ mLoadedPointerIconsByDisplayAndType.clear();
+ }
+ mNative.reloadPointerIcons();
+ }
+
// Updates the cached display density for the given displayId, and returns true if
// the cached density changed.
@GuardedBy("mLoadedPointerIconsByDisplayAndType")
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index d236d7a..1a0ffb7 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -5241,7 +5241,8 @@
return;
}
- final InputMethodSettings settings = InputMethodSettingsRepository.get(mCurrentUserId);
+ final int userId = mCurrentUserId;
+ final InputMethodSettings settings = InputMethodSettingsRepository.get(userId);
boolean reenableMinimumNonAuxSystemImes = false;
// TODO: The following code should find better place to live.
@@ -5304,18 +5305,18 @@
updateDefaultVoiceImeIfNeededLocked();
// TODO: Instantiate mSwitchingController for each user.
- if (settings.getUserId() == mSwitchingController.getUserId()) {
+ if (userId == mSwitchingController.getUserId()) {
mSwitchingController.resetCircularListLocked(settings.getMethodMap());
} else {
mSwitchingController = InputMethodSubtypeSwitchingController.createInstanceLocked(
mContext, settings.getMethodMap(), mCurrentUserId);
}
// TODO: Instantiate mHardwareKeyboardShortcutController for each user.
- if (settings.getUserId() == mHardwareKeyboardShortcutController.getUserId()) {
+ if (userId == mHardwareKeyboardShortcutController.getUserId()) {
mHardwareKeyboardShortcutController.reset(settings.getMethodMap());
} else {
mHardwareKeyboardShortcutController = new HardwareKeyboardShortcutController(
- settings.getMethodMap(), settings.getUserId());
+ settings.getMethodMap(), userId);
}
sendOnNavButtonFlagsChangedLocked();
@@ -5323,7 +5324,7 @@
// Notify InputMethodListListeners of the new installed InputMethods.
final List<InputMethodInfo> inputMethodList = settings.getMethodList();
mHandler.obtainMessage(MSG_DISPATCH_ON_INPUT_METHOD_LIST_UPDATED,
- settings.getUserId(), 0 /* unused */, inputMethodList).sendToTarget();
+ userId, 0 /* unused */, inputMethodList).sendToTarget();
}
@GuardedBy("ImfLock.class")
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
index cd1d799..ea71953 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
@@ -165,6 +165,7 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
@@ -368,18 +369,32 @@
return false;
}
+ int userId = UserHandle.getUserId(uid);
+
+ boolean isInSetup =
+ getSecureInt(Settings.Secure.USER_SETUP_COMPLETE, userId)
+ .map(setupState -> setupState == 0)
+ .orElse(false);
+ if (isInSetup) {
+ return true;
+ }
+
+ boolean isInDeferredSetup =
+ getSecureInt(Settings.Secure.USER_SETUP_PERSONALIZATION_STATE, userId)
+ .map(state ->
+ state == Settings.Secure.USER_SETUP_PERSONALIZATION_STARTED)
+ .orElse(false);
+ return isInDeferredSetup;
+ }
+
+ private Optional<Integer> getSecureInt(String settingName, int userId) {
try {
- int userId = UserHandle.getUserId(uid);
- boolean isInSetup = Settings.Secure.getIntForUser(mContext.getContentResolver(),
- Settings.Secure.USER_SETUP_COMPLETE, userId) == 0;
- boolean isInDeferredSetup = Settings.Secure.getIntForUser(
- mContext.getContentResolver(),
- Settings.Secure.USER_SETUP_PERSONALIZATION_STATE, userId)
- == Settings.Secure.USER_SETUP_PERSONALIZATION_STARTED;
- return isInSetup || isInDeferredSetup;
+ return Optional.of(
+ Settings.Secure.getIntForUser(
+ mContext.getContentResolver(), settingName, userId));
} catch (Settings.SettingNotFoundException e) {
- Slog.w(LOG_TAG, "Failed to check if the user is in restore: " + e);
- return false;
+ Slog.i(LOG_TAG, "Setting " + settingName + " not found", e);
+ return Optional.empty();
}
}
diff --git a/services/core/java/com/android/server/power/Notifier.java b/services/core/java/com/android/server/power/Notifier.java
index 11b9e77..80c262a 100644
--- a/services/core/java/com/android/server/power/Notifier.java
+++ b/services/core/java/com/android/server/power/Notifier.java
@@ -17,6 +17,7 @@
package com.android.server.power;
import android.annotation.Nullable;
+import android.annotation.SuppressLint;
import android.annotation.UserIdInt;
import android.app.ActivityManagerInternal;
import android.app.AppOpsManager;
@@ -197,9 +198,10 @@
FaceDownDetector faceDownDetector, ScreenUndimDetector screenUndimDetector,
Executor backgroundExecutor, PowerManagerFlags powerManagerFlags, Injector injector) {
mContext = context;
+ mInjector = (injector == null) ? new RealInjector() : injector;
mFlags = powerManagerFlags;
mBatteryStats = batteryStats;
- mAppOps = mContext.getSystemService(AppOpsManager.class);
+ mAppOps = mInjector.getAppOpsManager(context);
mSuspendBlocker = suspendBlocker;
mPolicy = policy;
mFaceDownDetector = faceDownDetector;
@@ -230,7 +232,6 @@
mShowWirelessChargingAnimationConfig = context.getResources().getBoolean(
com.android.internal.R.bool.config_showBuiltinWirelessChargingAnim);
- mInjector = (injector == null) ? new RealInjector() : injector;
mWakeLockLog = mInjector.getWakeLockLog(context);
// Initialize interactive state for battery stats.
try {
@@ -264,6 +265,7 @@
/**
* Called when a wake lock is acquired.
*/
+ @SuppressLint("AndroidFrameworkRequiresPermission")
public void onWakeLockAcquired(int flags, String tag, String packageName,
int ownerUid, int ownerPid, WorkSource workSource, String historyTag,
IWakeLockCallback callback) {
@@ -273,27 +275,28 @@
+ ", ownerUid=" + ownerUid + ", ownerPid=" + ownerPid
+ ", workSource=" + workSource);
}
- notifyWakeLockListener(callback, tag, true, ownerUid, flags);
- final int monitorType = getBatteryStatsWakeLockMonitorType(flags);
- if (monitorType >= 0) {
- try {
- final boolean unimportantForLogging = ownerUid == Process.SYSTEM_UID
- && (flags & PowerManager.UNIMPORTANT_FOR_LOGGING) != 0;
- if (workSource != null) {
- mBatteryStats.noteStartWakelockFromSource(workSource, ownerPid, tag,
- historyTag, monitorType, unimportantForLogging);
- } else {
- mBatteryStats.noteStartWakelock(ownerUid, ownerPid, tag, historyTag,
- monitorType, unimportantForLogging);
- // XXX need to deal with disabled operations.
- mAppOps.startOpNoThrow(AppOpsManager.OP_WAKE_LOCK, ownerUid, packageName);
- }
- } catch (RemoteException ex) {
- // Ignore
- }
- }
-
+ notifyWakeLockListener(callback, tag, true, ownerUid, ownerPid, flags, workSource,
+ packageName, historyTag);
if (!mFlags.improveWakelockLatency()) {
+ final int monitorType = getBatteryStatsWakeLockMonitorType(flags);
+ if (monitorType >= 0) {
+ try {
+ final boolean unimportantForLogging = ownerUid == Process.SYSTEM_UID
+ && (flags & PowerManager.UNIMPORTANT_FOR_LOGGING) != 0;
+ if (workSource != null) {
+ mBatteryStats.noteStartWakelockFromSource(workSource, ownerPid, tag,
+ historyTag, monitorType, unimportantForLogging);
+ } else {
+ mBatteryStats.noteStartWakelock(ownerUid, ownerPid, tag, historyTag,
+ monitorType, unimportantForLogging);
+ // XXX need to deal with disabled operations.
+ mAppOps.startOpNoThrow(AppOpsManager.OP_WAKE_LOCK, ownerUid, packageName,
+ false, null, null);
+ }
+ } catch (RemoteException ex) {
+ // Ignore
+ }
+ }
mWakeLockLog.onWakeLockAcquired(tag, ownerUid, flags, /*eventTime=*/ -1);
}
mWakefulnessSessionObserver.onWakeLockAcquired(flags);
@@ -404,6 +407,7 @@
/**
* Called when a wake lock is released.
*/
+ @SuppressLint("AndroidFrameworkRequiresPermission")
public void onWakeLockReleased(int flags, String tag, String packageName,
int ownerUid, int ownerPid, WorkSource workSource, String historyTag,
IWakeLockCallback callback, int releaseReason) {
@@ -413,23 +417,24 @@
+ ", ownerUid=" + ownerUid + ", ownerPid=" + ownerPid
+ ", workSource=" + workSource);
}
- notifyWakeLockListener(callback, tag, false, ownerUid, flags);
- final int monitorType = getBatteryStatsWakeLockMonitorType(flags);
- if (monitorType >= 0) {
- try {
- if (workSource != null) {
- mBatteryStats.noteStopWakelockFromSource(workSource, ownerPid, tag,
- historyTag, monitorType);
- } else {
- mBatteryStats.noteStopWakelock(ownerUid, ownerPid, tag,
- historyTag, monitorType);
- mAppOps.finishOp(AppOpsManager.OP_WAKE_LOCK, ownerUid, packageName);
- }
- } catch (RemoteException ex) {
- // Ignore
- }
- }
+ notifyWakeLockListener(callback, tag, false, ownerUid, ownerPid, flags, workSource,
+ packageName, historyTag);
if (!mFlags.improveWakelockLatency()) {
+ final int monitorType = getBatteryStatsWakeLockMonitorType(flags);
+ if (monitorType >= 0) {
+ try {
+ if (workSource != null) {
+ mBatteryStats.noteStopWakelockFromSource(workSource, ownerPid, tag,
+ historyTag, monitorType);
+ } else {
+ mBatteryStats.noteStopWakelock(ownerUid, ownerPid, tag,
+ historyTag, monitorType);
+ mAppOps.finishOp(AppOpsManager.OP_WAKE_LOCK, ownerUid, packageName, null);
+ }
+ } catch (RemoteException ex) {
+ // Ignore
+ }
+ }
mWakeLockLog.onWakeLockReleased(tag, ownerUid, /*eventTime=*/ -1);
}
mWakefulnessSessionObserver.onWakeLockReleased(flags, releaseReason);
@@ -1049,24 +1054,75 @@
}
private void notifyWakeLockListener(IWakeLockCallback callback, String tag, boolean isEnabled,
- int ownerUid, int flags) {
- if (callback != null) {
- long currentTime = mInjector.currentTimeMillis();
- mHandler.post(() -> {
- try {
- if (mFlags.improveWakelockLatency()) {
+ int ownerUid, int ownerPid, int flags, WorkSource workSource, String packageName,
+ String historyTag) {
+ if (mFlags.improveWakelockLatency()) {
+ if (callback != null) {
+ long currentTime = mInjector.currentTimeMillis();
+ mHandler.post(() -> {
+ try {
if (isEnabled) {
- mWakeLockLog.onWakeLockAcquired(tag, ownerUid, flags, currentTime);
+ notifyWakelockAcquisition(tag, ownerUid, ownerPid, flags,
+ workSource, packageName, historyTag, currentTime);
} else {
- mWakeLockLog.onWakeLockReleased(tag, ownerUid, currentTime);
+ notifyWakelockRelease(tag, ownerUid, ownerPid, flags,
+ workSource, packageName, historyTag, currentTime);
}
+ callback.onStateChanged(isEnabled);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Wakelock.mCallback [" + tag + "] is already dead.", e);
}
- callback.onStateChanged(isEnabled);
- } catch (RemoteException e) {
- Slog.e(TAG, "Wakelock.mCallback [" + tag + "] is already dead.", e);
- }
- });
+ });
+ }
}
+
+ }
+
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ private void notifyWakelockAcquisition(String tag, int ownerUid, int ownerPid, int flags,
+ WorkSource workSource, String packageName, String historyTag, long currentTime) {
+ final int monitorType = getBatteryStatsWakeLockMonitorType(flags);
+ if (monitorType >= 0) {
+ try {
+ final boolean unimportantForLogging = ownerUid == Process.SYSTEM_UID
+ && (flags & PowerManager.UNIMPORTANT_FOR_LOGGING) != 0;
+ if (workSource != null) {
+ mBatteryStats.noteStartWakelockFromSource(workSource, ownerPid, tag,
+ historyTag, monitorType, unimportantForLogging);
+ } else {
+ mBatteryStats.noteStartWakelock(ownerUid, ownerPid, tag, historyTag,
+ monitorType, unimportantForLogging);
+ // XXX need to deal with disabled operations.
+ mAppOps.startOpNoThrow(
+ AppOpsManager.OP_WAKE_LOCK, ownerUid, packageName,
+ false, null, null);
+ }
+ } catch (RemoteException ex) {
+ // Do Nothing
+ }
+ }
+ mWakeLockLog.onWakeLockAcquired(tag, ownerUid, flags, currentTime);
+ }
+
+ @SuppressLint("AndroidFrameworkRequiresPermission")
+ private void notifyWakelockRelease(String tag, int ownerUid, int ownerPid, int flags,
+ WorkSource workSource, String packageName, String historyTag, long currentTime) {
+ final int monitorType = getBatteryStatsWakeLockMonitorType(flags);
+ if (monitorType >= 0) {
+ try {
+ if (workSource != null) {
+ mBatteryStats.noteStopWakelockFromSource(workSource, ownerPid, tag,
+ historyTag, monitorType);
+ } else {
+ mBatteryStats.noteStopWakelock(ownerUid, ownerPid, tag,
+ historyTag, monitorType);
+ mAppOps.finishOp(AppOpsManager.OP_WAKE_LOCK, ownerUid, packageName, null);
+ }
+ } catch (RemoteException ex) {
+ // Ignore
+ }
+ }
+ mWakeLockLog.onWakeLockReleased(tag, ownerUid, currentTime);
}
private final class NotifierHandler extends Handler {
@@ -1114,6 +1170,11 @@
* Gets the WakeLockLog object
*/
WakeLockLog getWakeLockLog(Context context);
+
+ /**
+ * Gets the AppOpsManager system service
+ */
+ AppOpsManager getAppOpsManager(Context context);
}
static class RealInjector implements Injector {
@@ -1126,5 +1187,10 @@
public WakeLockLog getWakeLockLog(Context context) {
return new WakeLockLog(context);
}
+
+ @Override
+ public AppOpsManager getAppOpsManager(Context context) {
+ return context.getSystemService(AppOpsManager.class);
+ }
}
}
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 78a6816..15f4c8c 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -156,6 +156,7 @@
import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_SIZE_COMPAT_MODE;
import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__NOT_LETTERBOXED;
import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__NOT_VISIBLE;
+import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_ANIM;
import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER;
import static com.android.server.wm.ActivityRecord.State.DESTROYED;
@@ -6032,14 +6033,7 @@
true /* activityChange */, true /* updateOomAdj */,
true /* addPendingTopUid */);
}
- final ContentCaptureManagerInternal contentCaptureService =
- LocalServices.getService(ContentCaptureManagerInternal.class);
- if (contentCaptureService != null) {
- contentCaptureService.notifyActivityEvent(mUserId, mActivityComponent,
- ActivityEvent.TYPE_ACTIVITY_STARTED,
- new ActivityId(getTask() != null ? getTask().mTaskId : INVALID_TASK_ID,
- shareableActivityToken));
- }
+ mAtmService.mH.post(this::notifyActivityStartedToContentCaptureService);
break;
case PAUSED:
mAtmService.updateBatteryStats(this, false);
@@ -6071,6 +6065,22 @@
}
}
+ private void notifyActivityStartedToContentCaptureService() {
+ final ContentCaptureManagerInternal contentCaptureService =
+ LocalServices.getService(ContentCaptureManagerInternal.class);
+ if (contentCaptureService != null) {
+ // For ACTIVITY_STARTED content capture is directly invoked to avoid persisting
+ // to UsageStats.
+ contentCaptureService.notifyActivityEvent(mUserId, mActivityComponent,
+ ActivityEvent.TYPE_ACTIVITY_STARTED,
+ new ActivityId(getTask() != null ? getTask().mTaskId : INVALID_TASK_ID,
+ shareableActivityToken));
+
+ contentCaptureService.sendActivityStartAssistData(mUserId,
+ shareableActivityToken, intent);
+ }
+ }
+
State getState() {
return mState;
}
diff --git a/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt b/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt
index 6499556..78dbc60 100644
--- a/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt
+++ b/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt
@@ -533,8 +533,7 @@
val packageState =
packageManagerLocal.withFilteredSnapshot(Binder.getCallingUid(), userId).use {
it.getPackageState(packageName)
- }
- ?: return PackageManager.PERMISSION_DENIED
+ } ?: return PackageManager.PERMISSION_DENIED
val isPermissionGranted =
service.getState { isPermissionGranted(packageState, userId, permissionName, deviceId) }
@@ -1164,8 +1163,7 @@
val packageState =
packageManagerLocal.withFilteredSnapshot(Binder.getCallingUid(), userId).use {
it.getPackageState(packageName)
- }
- ?: return false
+ } ?: return false
service.getState {
if (isPermissionGranted(packageState, userId, permissionName, deviceId)) {
@@ -1216,8 +1214,7 @@
val packageState =
packageManagerLocal.withFilteredSnapshot(callingUid, userId).use {
it.getPackageState(packageName)
- }
- ?: return false
+ } ?: return false
val appId = packageState.appId
if (UserHandle.getAppId(callingUid) != appId) {
return false
@@ -1546,8 +1543,7 @@
val packageState =
packageManagerLocal.withFilteredSnapshot(callingUid, userId).use {
it.getPackageState(packageName)
- }
- ?: return null
+ } ?: return null
val androidPackage = packageState.androidPackage ?: return null
val isCallerPrivileged =
@@ -1710,8 +1706,7 @@
PackageManager.FLAG_PERMISSION_WHITELIST_INSTALLER,
userId
)
- ?.let { ArraySet(permissionNames).apply { this += it }.toList() }
- ?: permissionNames
+ ?.let { ArraySet(permissionNames).apply { this += it }.toList() } ?: permissionNames
setAllowlistedRestrictedPermissionsUnchecked(
androidPackage,
@@ -2753,27 +2748,25 @@
) {
return false
}
- return try {
- val contentResolver = context.contentResolver
- val userId = UserHandle.getUserId(uid)
- val isInSetup =
- Settings.Secure.getIntForUser(
- contentResolver,
- Settings.Secure.USER_SETUP_COMPLETE,
- userId
- ) == 0
- val isInDeferredSetup =
- Settings.Secure.getIntForUser(
- contentResolver,
- Settings.Secure.USER_SETUP_PERSONALIZATION_STATE,
- userId
- ) == Settings.Secure.USER_SETUP_PERSONALIZATION_STARTED
- isInSetup || isInDeferredSetup
- } catch (e: Settings.SettingNotFoundException) {
- Slog.w(LOG_TAG, "Failed to check if the user is in restore: $e")
- false
- }
+
+ val userId = UserHandle.getUserId(uid)
+
+ val isInSetup = getSecureInt(Settings.Secure.USER_SETUP_COMPLETE, userId) == 0
+ if (isInSetup) return true
+
+ val isInDeferredSetup =
+ getSecureInt(Settings.Secure.USER_SETUP_PERSONALIZATION_STATE, userId) ==
+ Settings.Secure.USER_SETUP_PERSONALIZATION_STARTED
+ return isInDeferredSetup
}
+
+ private fun getSecureInt(settingName: String, userId: Int): Int? =
+ try {
+ Settings.Secure.getIntForUser(context.contentResolver, settingName, userId)
+ } catch (e: Settings.SettingNotFoundException) {
+ Slog.i(LOG_TAG, "Setting $settingName not found", e)
+ null
+ }
}
private class OnPermissionsChangeListeners(looper: Looper) : Handler(looper) {
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
index d0eb83a..211ab03 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
@@ -2569,6 +2569,63 @@
}
@Test
+ public void testPowerOnAndOffInternalDisplay() {
+ manageDisplaysPermission(/* granted= */ true);
+ DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+ DisplayManagerService.BinderService bs = displayManager.new BinderService();
+ LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper();
+ FakeDisplayManagerCallback callback = new FakeDisplayManagerCallback();
+ bs.registerCallbackWithEventMask(callback, STANDARD_AND_CONNECTION_DISPLAY_EVENTS);
+
+ callback.expectsEvent(EVENT_DISPLAY_ADDED);
+ FakeDisplayDevice displayDevice =
+ createFakeDisplayDevice(displayManager, new float[]{60f}, Display.TYPE_INTERNAL);
+ callback.waitForExpectedEvent();
+
+ LogicalDisplay display =
+ logicalDisplayMapper.getDisplayLocked(displayDevice, /* includeDisabled= */ true);
+
+ assertThat(displayDevice.getDisplayDeviceInfoLocked().committedState)
+ .isEqualTo(Display.STATE_ON);
+
+ assertThat(displayManager.requestDisplayPower(display.getDisplayIdLocked(), false))
+ .isTrue();
+
+ assertThat(displayDevice.getDisplayDeviceInfoLocked().committedState)
+ .isEqualTo(Display.STATE_OFF);
+
+ assertThat(displayManager.requestDisplayPower(display.getDisplayIdLocked(), true))
+ .isTrue();
+
+ assertThat(displayDevice.getDisplayDeviceInfoLocked().committedState)
+ .isEqualTo(Display.STATE_ON);
+ }
+
+ @Test
+ public void testPowerOnAndOffInternalDisplay_withoutPermission_shouldThrowException() {
+ DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+ DisplayManagerService.BinderService bs = displayManager.new BinderService();
+ LogicalDisplayMapper logicalDisplayMapper = displayManager.getLogicalDisplayMapper();
+ FakeDisplayManagerCallback callback = new FakeDisplayManagerCallback();
+ bs.registerCallbackWithEventMask(callback, STANDARD_AND_CONNECTION_DISPLAY_EVENTS);
+
+ callback.expectsEvent(EVENT_DISPLAY_ADDED);
+ FakeDisplayDevice displayDevice =
+ createFakeDisplayDevice(displayManager, new float[]{60f}, Display.TYPE_INTERNAL);
+ callback.waitForExpectedEvent();
+
+ LogicalDisplay display =
+ logicalDisplayMapper.getDisplayLocked(displayDevice, /* includeDisabled= */ true);
+ var displayId = display.getDisplayIdLocked();
+
+ assertThat(displayDevice.getDisplayDeviceInfoLocked().committedState)
+ .isEqualTo(Display.STATE_ON);
+
+ assertThrows(SecurityException.class, () -> bs.requestDisplayPower(displayId, true));
+ assertThrows(SecurityException.class, () -> bs.requestDisplayPower(displayId, false));
+ }
+
+ @Test
public void testEnableExternalDisplay_withDisplayManagement_shouldSignalDisplayAdded() {
when(mMockFlags.isConnectedDisplayManagementEnabled()).thenReturn(true);
manageDisplaysPermission(/* granted= */ true);
@@ -3529,6 +3586,7 @@
public void setDisplayDeviceInfo(DisplayDeviceInfo displayDeviceInfo) {
mDisplayDeviceInfo = displayDeviceInfo;
+ mDisplayDeviceInfo.committedState = Display.STATE_ON;
}
@Override
@@ -3558,5 +3616,14 @@
public Display.Mode getUserPreferredDisplayModeLocked() {
return mPreferredMode;
}
+
+ @Override
+ public Runnable requestDisplayStateLocked(
+ final int state,
+ final float brightnessState,
+ final float sdrBrightnessState,
+ @Nullable DisplayOffloadSessionImpl displayOffloadSession) {
+ return () -> mDisplayDeviceInfo.committedState = state;
+ }
}
}
diff --git a/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java b/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java
index 4460c6a..ce2bb95 100644
--- a/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java
+++ b/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java
@@ -31,6 +31,7 @@
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
+import android.app.AppOpsManager;
import android.content.Context;
import android.content.res.Resources;
import android.hardware.SensorManager;
@@ -41,7 +42,6 @@
import android.os.Looper;
import android.os.PowerManager;
import android.os.RemoteException;
-import android.os.ServiceManager;
import android.os.VibrationAttributes;
import android.os.Vibrator;
import android.os.test.TestLooper;
@@ -82,8 +82,12 @@
@Mock private StatusBarManagerInternal mStatusBarManagerInternal;
@Mock private WakeLockLog mWakeLockLog;
+ @Mock private IBatteryStats mBatteryStats;
+
@Mock private PowerManagerFlags mPowerManagerFlags;
+ @Mock private AppOpsManager mAppOpsManager;
+
private PowerManagerService mService;
private Context mContextSpy;
private Resources mResourcesSpy;
@@ -230,7 +234,7 @@
public void testOnWakeLockListener_RemoteException_NoRethrow() {
when(mPowerManagerFlags.improveWakelockLatency()).thenReturn(true);
createNotifier();
-
+ clearInvocations(mWakeLockLog, mBatteryStats, mAppOpsManager);
IWakeLockCallback exceptingCallback = new IWakeLockCallback.Stub() {
@Override public void onStateChanged(boolean enabled) throws RemoteException {
throw new RemoteException("Just testing");
@@ -245,6 +249,7 @@
verifyZeroInteractions(mWakeLockLog);
mTestLooper.dispatchAll();
verify(mWakeLockLog).onWakeLockReleased("wakelockTag", uid, 1);
+
mNotifier.onWakeLockAcquired(PowerManager.PARTIAL_WAKE_LOCK, "wakelockTag",
"my.package.name", uid, pid, /* workSource= */ null, /* historyTag= */ null,
exceptingCallback);
@@ -277,6 +282,115 @@
verify(mWakeLockLog).onWakeLockReleased("wakelockTag", uid, -1);
}
+
+ @Test
+ public void testOnWakeLockListener_FullWakeLock_ProcessesOnHandler() throws RemoteException {
+ when(mPowerManagerFlags.improveWakelockLatency()).thenReturn(true);
+ createNotifier();
+
+ IWakeLockCallback exceptingCallback = new IWakeLockCallback.Stub() {
+ @Override public void onStateChanged(boolean enabled) throws RemoteException {
+ throw new RemoteException("Just testing");
+ }
+ };
+ clearInvocations(mWakeLockLog, mBatteryStats, mAppOpsManager);
+
+ final int uid = 1234;
+ final int pid = 5678;
+
+ // Release the wakelock
+ mNotifier.onWakeLockReleased(PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "wakelockTag",
+ "my.package.name", uid, pid, /* workSource= */ null, /* historyTag= */ null,
+ exceptingCallback);
+
+ // No interaction because we expect that to happen in async
+ verifyZeroInteractions(mWakeLockLog, mBatteryStats, mAppOpsManager);
+
+ // Progressing the looper, and validating all the interactions
+ mTestLooper.dispatchAll();
+ verify(mWakeLockLog).onWakeLockReleased("wakelockTag", uid, 1);
+ verify(mBatteryStats).noteStopWakelock(uid, pid, "wakelockTag", /* historyTag= */ null,
+ BatteryStats.WAKE_TYPE_FULL);
+ verify(mAppOpsManager).finishOp(AppOpsManager.OP_WAKE_LOCK, uid,
+ "my.package.name", null);
+
+ clearInvocations(mWakeLockLog, mBatteryStats, mAppOpsManager);
+
+ // Acquire the wakelock
+ mNotifier.onWakeLockAcquired(PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "wakelockTag",
+ "my.package.name", uid, pid, /* workSource= */ null, /* historyTag= */ null,
+ exceptingCallback);
+
+ // No interaction because we expect that to happen in async
+ verifyNoMoreInteractions(mWakeLockLog, mBatteryStats, mAppOpsManager);
+
+ // Progressing the looper, and validating all the interactions
+ mTestLooper.dispatchAll();
+ verify(mWakeLockLog).onWakeLockAcquired("wakelockTag", uid,
+ PowerManager.SCREEN_BRIGHT_WAKE_LOCK, 1);
+ verify(mBatteryStats).noteStartWakelock(uid, pid, "wakelockTag", /* historyTag= */ null,
+ BatteryStats.WAKE_TYPE_FULL, false);
+ verify(mAppOpsManager).startOpNoThrow(AppOpsManager.OP_WAKE_LOCK, uid,
+ "my.package.name", false, null, null);
+
+ // Test with improveWakelockLatency flag false, hence the wakelock log will run on the same
+ // thread
+ clearInvocations(mWakeLockLog, mBatteryStats, mAppOpsManager);
+ when(mPowerManagerFlags.improveWakelockLatency()).thenReturn(false);
+
+ mNotifier.onWakeLockAcquired(PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "wakelockTag",
+ "my.package.name", uid, pid, /* workSource= */ null, /* historyTag= */ null,
+ exceptingCallback);
+ verify(mWakeLockLog).onWakeLockAcquired("wakelockTag", uid,
+ PowerManager.SCREEN_BRIGHT_WAKE_LOCK, -1);
+
+ mNotifier.onWakeLockReleased(PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "wakelockTag",
+ "my.package.name", uid, pid, /* workSource= */ null, /* historyTag= */ null,
+ exceptingCallback);
+ verify(mWakeLockLog).onWakeLockReleased("wakelockTag", uid, -1);
+ }
+
+ @Test
+ public void testOnWakeLockListener_FullWakeLock_ProcessesInSync() throws RemoteException {
+ createNotifier();
+
+ IWakeLockCallback exceptingCallback = new IWakeLockCallback.Stub() {
+ @Override public void onStateChanged(boolean enabled) throws RemoteException {
+ throw new RemoteException("Just testing");
+ }
+ };
+ clearInvocations(mWakeLockLog, mBatteryStats, mAppOpsManager);
+
+ final int uid = 1234;
+ final int pid = 5678;
+
+ // Release the wakelock
+ mNotifier.onWakeLockReleased(PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "wakelockTag",
+ "my.package.name", uid, pid, /* workSource= */ null, /* historyTag= */ null,
+ exceptingCallback);
+
+ verify(mWakeLockLog).onWakeLockReleased("wakelockTag", uid, -1);
+ verify(mBatteryStats).noteStopWakelock(uid, pid, "wakelockTag", /* historyTag= */ null,
+ BatteryStats.WAKE_TYPE_FULL);
+ verify(mAppOpsManager).finishOp(AppOpsManager.OP_WAKE_LOCK, uid,
+ "my.package.name", null);
+
+ clearInvocations(mWakeLockLog, mBatteryStats, mAppOpsManager);
+
+ // Acquire the wakelock
+ mNotifier.onWakeLockAcquired(PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "wakelockTag",
+ "my.package.name", uid, pid, /* workSource= */ null, /* historyTag= */ null,
+ exceptingCallback);
+
+ mTestLooper.dispatchAll();
+ verify(mWakeLockLog).onWakeLockAcquired("wakelockTag", uid,
+ PowerManager.SCREEN_BRIGHT_WAKE_LOCK, -1);
+ verify(mBatteryStats).noteStartWakelock(uid, pid, "wakelockTag", /* historyTag= */ null,
+ BatteryStats.WAKE_TYPE_FULL, false);
+ verify(mAppOpsManager).startOpNoThrow(AppOpsManager.OP_WAKE_LOCK, uid,
+ "my.package.name", false, null, null);
+ }
+
private final PowerManagerService.Injector mInjector = new PowerManagerService.Injector() {
@Override
Notifier createNotifier(Looper looper, Context context, IBatteryStats batteryStats,
@@ -365,13 +479,17 @@
public WakeLockLog getWakeLockLog(Context context) {
return mWakeLockLog;
}
+
+ @Override
+ public AppOpsManager getAppOpsManager(Context context) {
+ return mAppOpsManager;
+ }
};
mNotifier = new Notifier(
mTestLooper.getLooper(),
mContextSpy,
- IBatteryStats.Stub.asInterface(ServiceManager.getService(
- BatteryStats.SERVICE_NAME)),
+ mBatteryStats,
mInjector.createSuspendBlocker(mService, "testBlocker"),
null,
null,
diff --git a/tests/Input/assets/testPointerScale.png b/tests/Input/assets/testPointerScale.png
new file mode 100644
index 0000000..54d37c2
--- /dev/null
+++ b/tests/Input/assets/testPointerScale.png
Binary files differ
diff --git a/tests/Input/src/com/android/test/input/PointerIconLoadingTest.kt b/tests/Input/src/com/android/test/input/PointerIconLoadingTest.kt
index dac4253..d196b85 100644
--- a/tests/Input/src/com/android/test/input/PointerIconLoadingTest.kt
+++ b/tests/Input/src/com/android/test/input/PointerIconLoadingTest.kt
@@ -93,7 +93,29 @@
PointerIcon.getLoadedSystemIcon(
ContextThemeWrapper(context, theme),
PointerIcon.TYPE_ARROW,
- /* useLargeIcons= */ false)
+ /* useLargeIcons= */ false,
+ /* pointerScale= */ 1f)
+
+ pointerIcon.getBitmap().assertAgainstGolden(
+ screenshotRule,
+ testName.methodName,
+ exactScreenshotMatcher
+ )
+ }
+
+ @Test
+ fun testPointerScale() {
+ assumeTrue(enableVectorCursors())
+ assumeTrue(enableVectorCursorA11ySettings())
+
+ val pointerScale = 2f
+
+ val pointerIcon =
+ PointerIcon.getLoadedSystemIcon(
+ context,
+ PointerIcon.TYPE_ARROW,
+ /* useLargeIcons= */ false,
+ pointerScale)
pointerIcon.getBitmap().assertAgainstGolden(
screenshotRule,