diff options
208 files changed, 3972 insertions, 1628 deletions
diff --git a/apct-tests/perftests/inputmethod/AndroidManifest.xml b/apct-tests/perftests/inputmethod/AndroidManifest.xml index 3eea418fe5c7..5dd6ccccfb1c 100644 --- a/apct-tests/perftests/inputmethod/AndroidManifest.xml +++ b/apct-tests/perftests/inputmethod/AndroidManifest.xml @@ -22,7 +22,8 @@ <application> <uses-library android:name="android.test.runner" /> <activity android:name="android.perftests.utils.PerfTestActivity" - android:exported="true"> + android:theme="@android:style/Theme.DeviceDefault.NoActionBar" + android:exported="true"> <intent-filter> <action android:name="com.android.perftests.core.PERFTEST" /> </intent-filter> diff --git a/apct-tests/perftests/utils/src/android/perftests/utils/PerfTestActivity.java b/apct-tests/perftests/utils/src/android/perftests/utils/PerfTestActivity.java index f3bea17b2f0d..0c2ee8cb238a 100644 --- a/apct-tests/perftests/utils/src/android/perftests/utils/PerfTestActivity.java +++ b/apct-tests/perftests/utils/src/android/perftests/utils/PerfTestActivity.java @@ -19,7 +19,9 @@ package android.perftests.utils; import android.app.Activity; import android.content.Context; import android.content.Intent; +import android.graphics.Insets; import android.os.Bundle; +import android.view.WindowInsets; import android.view.WindowManager; import android.widget.EditText; import android.widget.LinearLayout; @@ -42,6 +44,11 @@ public class PerfTestActivity extends Activity { if (getIntent().getBooleanExtra(INTENT_EXTRA_ADD_EDIT_TEXT, false)) { final LinearLayout layout = new LinearLayout(this); layout.setOrientation(LinearLayout.VERTICAL); + layout.setOnApplyWindowInsetsListener((v, w) -> { + final Insets insets = w.getSystemWindowInsets(); + v.setPadding(insets.left, insets.top, insets.right, insets.bottom); + return WindowInsets.CONSUMED; + }); final EditText editText = new EditText(this); editText.setId(ID_EDITOR); diff --git a/apex/jobscheduler/service/aconfig/device_idle.aconfig b/apex/jobscheduler/service/aconfig/device_idle.aconfig index e8c99b12828f..c4d0d1850a18 100644 --- a/apex/jobscheduler/service/aconfig/device_idle.aconfig +++ b/apex/jobscheduler/service/aconfig/device_idle.aconfig @@ -2,6 +2,16 @@ package: "com.android.server.deviceidle" container: "system" flag { + name: "remove_idle_location" + namespace: "location" + description: "Remove DeviceIdleController usage of location" + bug: "332770178" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "disable_wakelocks_in_light_idle" namespace: "backstage_power" description: "Disable wakelocks for background apps while Light Device Idle is active" diff --git a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java index 4832ea624bd7..11fa7b75182f 100644 --- a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java +++ b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java @@ -109,6 +109,7 @@ import com.android.modules.expresslog.Counter; import com.android.server.am.BatteryStatsService; import com.android.server.deviceidle.ConstraintController; import com.android.server.deviceidle.DeviceIdleConstraintTracker; +import com.android.server.deviceidle.Flags; import com.android.server.deviceidle.IDeviceIdleConstraint; import com.android.server.deviceidle.TvConstraintController; import com.android.server.net.NetworkPolicyManagerInternal; @@ -2558,7 +2559,7 @@ public class DeviceIdleController extends SystemService } boolean isLocationPrefetchEnabled() { - return mContext.getResources().getBoolean( + return !Flags.removeIdleLocation() && mContext.getResources().getBoolean( com.android.internal.R.bool.config_autoPowerModePrefetchLocation); } diff --git a/api/StubLibraries.bp b/api/StubLibraries.bp index 1b1bc6b9afdb..f56a95011dee 100644 --- a/api/StubLibraries.bp +++ b/api/StubLibraries.bp @@ -54,34 +54,34 @@ non_updatable_exportable_droidstubs { baseline_file: ":non-updatable-lint-baseline.txt", }, }, - dists: [ - { - targets: ["sdk"], - dir: "apistubs/android/public/api", - dest: "android-non-updatable.txt", - }, - { - targets: ["sdk"], - dir: "apistubs/android/public/api", - dest: "android-non-updatable-removed.txt", - }, - ], soong_config_variables: { release_hidden_api_exportable_stubs: { dists: [ { + targets: ["sdk"], + dir: "apistubs/android/public/api", + dest: "android-non-updatable.txt", tag: ".exportable.api.txt", }, { + targets: ["sdk"], + dir: "apistubs/android/public/api", + dest: "android-non-updatable-removed.txt", tag: ".exportable.removed-api.txt", }, ], conditions_default: { dists: [ { + targets: ["sdk"], + dir: "apistubs/android/public/api", + dest: "android-non-updatable.txt", tag: ".api.txt", }, { + targets: ["sdk"], + dir: "apistubs/android/public/api", + dest: "android-non-updatable-removed.txt", tag: ".removed-api.txt", }, ], @@ -134,34 +134,34 @@ non_updatable_exportable_droidstubs { baseline_file: ":non-updatable-system-lint-baseline.txt", }, }, - dists: [ - { - targets: ["sdk"], - dir: "apistubs/android/system/api", - dest: "android-non-updatable.txt", - }, - { - targets: ["sdk"], - dir: "apistubs/android/system/api", - dest: "android-non-updatable-removed.txt", - }, - ], soong_config_variables: { release_hidden_api_exportable_stubs: { dists: [ { + targets: ["sdk"], + dir: "apistubs/android/system/api", + dest: "android-non-updatable.txt", tag: ".exportable.api.txt", }, { + targets: ["sdk"], + dir: "apistubs/android/system/api", + dest: "android-non-updatable-removed.txt", tag: ".exportable.removed-api.txt", }, ], conditions_default: { dists: [ { + targets: ["sdk"], + dir: "apistubs/android/system/api", + dest: "android-non-updatable.txt", tag: ".api.txt", }, { + targets: ["sdk"], + dir: "apistubs/android/system/api", + dest: "android-non-updatable-removed.txt", tag: ".removed-api.txt", }, ], @@ -189,56 +189,58 @@ non_updatable_exportable_droidstubs { baseline_file: ":non-updatable-test-lint-baseline.txt", }, }, - dists: [ - { - targets: ["sdk"], - dir: "apistubs/android/test/api", - dest: "android.txt", - }, - { - targets: ["sdk"], - dir: "apistubs/android/test/api", - dest: "removed.txt", - }, - { - targets: ["sdk"], - dir: "apistubs/android/test/api", - dest: "android-non-updatable.txt", - }, - { - targets: ["sdk"], - dir: "apistubs/android/test/api", - dest: "android-non-updatable-removed.txt", - }, - ], soong_config_variables: { release_hidden_api_exportable_stubs: { dists: [ { + targets: ["sdk"], + dir: "apistubs/android/test/api", + dest: "android.txt", tag: ".exportable.api.txt", }, { + targets: ["sdk"], + dir: "apistubs/android/test/api", + dest: "removed.txt", tag: ".exportable.removed-api.txt", }, { + targets: ["sdk"], + dir: "apistubs/android/test/api", + dest: "android-non-updatable.txt", tag: ".exportable.api.txt", }, { + targets: ["sdk"], + dir: "apistubs/android/test/api", + dest: "android-non-updatable-removed.txt", tag: ".exportable.removed-api.txt", }, ], conditions_default: { dists: [ { + targets: ["sdk"], + dir: "apistubs/android/test/api", + dest: "android.txt", tag: ".api.txt", }, { + targets: ["sdk"], + dir: "apistubs/android/test/api", + dest: "removed.txt", tag: ".removed-api.txt", }, { + targets: ["sdk"], + dir: "apistubs/android/test/api", + dest: "android-non-updatable.txt", tag: ".api.txt", }, { + targets: ["sdk"], + dir: "apistubs/android/test/api", + dest: "android-non-updatable-removed.txt", tag: ".removed-api.txt", }, ], @@ -271,34 +273,34 @@ non_updatable_exportable_droidstubs { baseline_file: ":non-updatable-module-lib-lint-baseline.txt", }, }, - dists: [ - { - targets: ["sdk"], - dir: "apistubs/android/module-lib/api", - dest: "android-non-updatable.txt", - }, - { - targets: ["sdk"], - dir: "apistubs/android/module-lib/api", - dest: "android-non-updatable-removed.txt", - }, - ], soong_config_variables: { release_hidden_api_exportable_stubs: { dists: [ { + targets: ["sdk"], + dir: "apistubs/android/module-lib/api", + dest: "android-non-updatable.txt", tag: ".exportable.api.txt", }, { + targets: ["sdk"], + dir: "apistubs/android/module-lib/api", + dest: "android-non-updatable-removed.txt", tag: ".exportable.removed-api.txt", }, ], conditions_default: { dists: [ { + targets: ["sdk"], + dir: "apistubs/android/module-lib/api", + dest: "android-non-updatable.txt", tag: ".api.txt", }, { + targets: ["sdk"], + dir: "apistubs/android/module-lib/api", + dest: "android-non-updatable-removed.txt", tag: ".removed-api.txt", }, ], diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index f64418587918..eaa23b9db166 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -3727,12 +3727,6 @@ public final class ActivityThread extends ClientTransactionHandler return mActivities.get(token); } - @Nullable - @Override - public Context getWindowContext(@NonNull IBinder clientToken) { - return WindowTokenClientController.getInstance().getWindowContext(clientToken); - } - @VisibleForTesting(visibility = PACKAGE) public Configuration getConfiguration() { return mConfigurationController.getConfiguration(); diff --git a/core/java/android/app/ClientTransactionHandler.java b/core/java/android/app/ClientTransactionHandler.java index 01153c9e7efc..f0c319673ade 100644 --- a/core/java/android/app/ClientTransactionHandler.java +++ b/core/java/android/app/ClientTransactionHandler.java @@ -23,7 +23,6 @@ import android.app.servertransaction.ClientTransaction; import android.app.servertransaction.DestroyActivityItem; import android.app.servertransaction.PendingTransactionActions; import android.app.servertransaction.TransactionExecutor; -import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.res.Configuration; @@ -32,7 +31,6 @@ import android.util.MergedConfiguration; import android.view.SurfaceControl; import android.window.ActivityWindowInfo; import android.window.SplashScreenView.SplashScreenViewParcelable; -import android.window.WindowContext; import android.window.WindowContextInfo; import com.android.internal.annotations.VisibleForTesting; @@ -90,10 +88,6 @@ public abstract class ClientTransactionHandler { /** Get activity instance for the token. */ public abstract Activity getActivity(IBinder token); - /** Gets the {@link WindowContext} instance for the token. */ - @Nullable - public abstract Context getWindowContext(@NonNull IBinder clientToken); - // Prepare phase related logic and handlers. Methods that inform about about pending changes or // do other internal bookkeeping. diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index d9e0413e5ad5..dbde7d20f0d8 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -2655,8 +2655,16 @@ public class Notification implements Parcelable if (mAllowlistToken == null) { mAllowlistToken = processAllowlistToken; } - // Propagate this token to all pending intents that are unmarshalled from the parcel. - parcel.setClassCookie(PendingIntent.class, mAllowlistToken); + if (Flags.secureAllowlistToken()) { + // Propagate this token to all pending intents that are unmarshalled from the parcel, + // or keep the one we're already propagating, if that's the case. + if (!parcel.hasClassCookie(PendingIntent.class)) { + parcel.setClassCookie(PendingIntent.class, mAllowlistToken); + } + } else { + // Propagate this token to all pending intents that are unmarshalled from the parcel. + parcel.setClassCookie(PendingIntent.class, mAllowlistToken); + } when = parcel.readLong(); creationTime = parcel.readLong(); @@ -3210,9 +3218,29 @@ public class Notification implements Parcelable PendingIntent.addOnMarshaledListener(addedListener); } try { - // IMPORTANT: Add marshaling code in writeToParcelImpl as we - // want to intercept all pending events written to the parcel. - writeToParcelImpl(parcel, flags); + if (Flags.secureAllowlistToken()) { + boolean mustClearCookie = false; + if (!parcel.hasClassCookie(Notification.class)) { + // This is the "root" notification, and not an "inner" notification (including + // publicVersion or anything else that might be embedded in extras). + parcel.setClassCookie(Notification.class, this); + mustClearCookie = true; + } + try { + // IMPORTANT: Add marshaling code in writeToParcelImpl as we + // want to intercept all pending events written to the parcel. + writeToParcelImpl(parcel, flags); + } finally { + if (mustClearCookie) { + parcel.removeClassCookie(Notification.class, this); + } + } + } else { + // IMPORTANT: Add marshaling code in writeToParcelImpl as we + // want to intercept all pending events written to the parcel. + writeToParcelImpl(parcel, flags); + } + synchronized (this) { // Must be written last! parcel.writeArraySet(allPendingIntents); @@ -3227,7 +3255,19 @@ public class Notification implements Parcelable private void writeToParcelImpl(Parcel parcel, int flags) { parcel.writeInt(1); - parcel.writeStrongBinder(mAllowlistToken); + if (Flags.secureAllowlistToken()) { + Notification rootNotification = (Notification) parcel.getClassCookie( + Notification.class); + if (rootNotification != null && rootNotification != this) { + // Always use the same token as the root notification + parcel.writeStrongBinder(rootNotification.mAllowlistToken); + } else { + parcel.writeStrongBinder(mAllowlistToken); + } + } else { + parcel.writeStrongBinder(mAllowlistToken); + } + parcel.writeLong(when); parcel.writeLong(creationTime); if (mSmallIcon == null && icon != 0) { @@ -3620,18 +3660,23 @@ public class Notification implements Parcelable * Sets the token used for background operations for the pending intents associated with this * notification. * - * This token is automatically set during deserialization for you, you usually won't need to - * call this unless you want to change the existing token, if any. + * Note: Should <em>only</em> be invoked by NotificationManagerService, since this is normally + * populated by unparceling (and also used there). Any other usage is suspect. * * @hide */ - public void clearAllowlistToken() { - mAllowlistToken = null; + public void overrideAllowlistToken(IBinder token) { + mAllowlistToken = token; if (publicVersion != null) { - publicVersion.clearAllowlistToken(); + publicVersion.overrideAllowlistToken(token); } } + /** @hide */ + public IBinder getAllowlistToken() { + return mAllowlistToken; + } + /** * @hide */ diff --git a/core/java/android/app/notification.aconfig b/core/java/android/app/notification.aconfig index 1f6ac2efd64f..250953e61137 100644 --- a/core/java/android/app/notification.aconfig +++ b/core/java/android/app/notification.aconfig @@ -74,6 +74,16 @@ flag { } flag { + name: "secure_allowlist_token" + namespace: "systemui" + description: "Prevents allowlist_token from leaking out and foreign tokens from being accepted" + bug: "328254922" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "update_ranking_time" namespace: "systemui" description: "Updates notification sorting criteria to highlight new content while maintaining stability" diff --git a/core/java/android/app/servertransaction/ActivityConfigurationChangeItem.java b/core/java/android/app/servertransaction/ActivityConfigurationChangeItem.java index 631772556879..11d7ff86ce3d 100644 --- a/core/java/android/app/servertransaction/ActivityConfigurationChangeItem.java +++ b/core/java/android/app/servertransaction/ActivityConfigurationChangeItem.java @@ -23,7 +23,6 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityThread.ActivityClientRecord; import android.app.ClientTransactionHandler; -import android.content.Context; import android.content.res.CompatibilityInfo; import android.content.res.Configuration; import android.os.IBinder; @@ -60,12 +59,6 @@ public class ActivityConfigurationChangeItem extends ActivityTransactionItem { Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER); } - @Nullable - @Override - public Context getContextToUpdate(@NonNull ClientTransactionHandler client) { - return client.getActivity(getActivityToken()); - } - // ObjectPoolItem implementation private ActivityConfigurationChangeItem() {} diff --git a/core/java/android/app/servertransaction/ActivityRelaunchItem.java b/core/java/android/app/servertransaction/ActivityRelaunchItem.java index 6da871a74383..45bf235de2cd 100644 --- a/core/java/android/app/servertransaction/ActivityRelaunchItem.java +++ b/core/java/android/app/servertransaction/ActivityRelaunchItem.java @@ -23,7 +23,6 @@ import android.annotation.Nullable; import android.app.ActivityThread.ActivityClientRecord; import android.app.ClientTransactionHandler; import android.app.ResultInfo; -import android.content.Context; import android.content.res.CompatibilityInfo; import android.os.IBinder; import android.os.Parcel; @@ -88,12 +87,6 @@ public class ActivityRelaunchItem extends ActivityTransactionItem { client.reportRelaunch(r); } - @Nullable - @Override - public Context getContextToUpdate(@NonNull ClientTransactionHandler client) { - return client.getActivity(getActivityToken()); - } - // ObjectPoolItem implementation private ActivityRelaunchItem() {} diff --git a/core/java/android/app/servertransaction/ClientTransactionItem.java b/core/java/android/app/servertransaction/ClientTransactionItem.java index 6e7e93009993..99ebe1b975a4 100644 --- a/core/java/android/app/servertransaction/ClientTransactionItem.java +++ b/core/java/android/app/servertransaction/ClientTransactionItem.java @@ -24,7 +24,6 @@ import static com.android.internal.annotations.VisibleForTesting.Visibility.PACK import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ClientTransactionHandler; -import android.content.Context; import android.os.IBinder; import android.os.Parcelable; @@ -53,16 +52,6 @@ public abstract class ClientTransactionItem implements BaseClientRequest, Parcel return true; } - // TODO(b/260873529): cleanup - /** - * If this {@link ClientTransactionItem} is updating configuration, returns the {@link Context} - * it is updating; otherwise, returns {@code null}. - */ - @Nullable - public Context getContextToUpdate(@NonNull ClientTransactionHandler client) { - return null; - } - /** * Returns the activity token if this transaction item is activity-targeting. Otherwise, * returns {@code null}. diff --git a/core/java/android/app/servertransaction/ConfigurationChangeItem.java b/core/java/android/app/servertransaction/ConfigurationChangeItem.java index 0e327a7627d1..22da706cc7f4 100644 --- a/core/java/android/app/servertransaction/ConfigurationChangeItem.java +++ b/core/java/android/app/servertransaction/ConfigurationChangeItem.java @@ -18,9 +18,7 @@ package android.app.servertransaction; import android.annotation.NonNull; import android.annotation.Nullable; -import android.app.ActivityThread; import android.app.ClientTransactionHandler; -import android.content.Context; import android.content.res.CompatibilityInfo; import android.content.res.Configuration; import android.os.Parcel; @@ -48,12 +46,6 @@ public class ConfigurationChangeItem extends ClientTransactionItem { client.handleConfigurationChanged(mConfiguration, mDeviceId); } - @Nullable - @Override - public Context getContextToUpdate(@NonNull ClientTransactionHandler client) { - return ActivityThread.currentApplication(); - } - // ObjectPoolItem implementation private ConfigurationChangeItem() {} diff --git a/core/java/android/app/servertransaction/LaunchActivityItem.java b/core/java/android/app/servertransaction/LaunchActivityItem.java index f02cb212276b..7dcbebaeba0b 100644 --- a/core/java/android/app/servertransaction/LaunchActivityItem.java +++ b/core/java/android/app/servertransaction/LaunchActivityItem.java @@ -24,14 +24,12 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityClient; import android.app.ActivityOptions.SceneTransitionInfo; -import android.app.ActivityThread; import android.app.ActivityThread.ActivityClientRecord; import android.app.ClientTransactionHandler; import android.app.IActivityClientController; import android.app.ProfilerInfo; import android.app.ResultInfo; import android.compat.annotation.UnsupportedAppUsage; -import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.res.CompatibilityInfo; @@ -121,13 +119,6 @@ public class LaunchActivityItem extends ClientTransactionItem { client.countLaunchingActivities(-1); } - @Nullable - @Override - public Context getContextToUpdate(@NonNull ClientTransactionHandler client) { - // LaunchActivityItem may update the global config with #mCurConfig. - return ActivityThread.currentApplication(); - } - // ObjectPoolItem implementation private LaunchActivityItem() {} diff --git a/core/java/android/app/servertransaction/MoveToDisplayItem.java b/core/java/android/app/servertransaction/MoveToDisplayItem.java index 0702c4594075..8706edd26406 100644 --- a/core/java/android/app/servertransaction/MoveToDisplayItem.java +++ b/core/java/android/app/servertransaction/MoveToDisplayItem.java @@ -22,7 +22,6 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityThread.ActivityClientRecord; import android.app.ClientTransactionHandler; -import android.content.Context; import android.content.res.CompatibilityInfo; import android.content.res.Configuration; import android.os.IBinder; @@ -59,12 +58,6 @@ public class MoveToDisplayItem extends ActivityTransactionItem { Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER); } - @Nullable - @Override - public Context getContextToUpdate(@NonNull ClientTransactionHandler client) { - return client.getActivity(getActivityToken()); - } - // ObjectPoolItem implementation private MoveToDisplayItem() {} diff --git a/core/java/android/app/servertransaction/WindowContextInfoChangeItem.java b/core/java/android/app/servertransaction/WindowContextInfoChangeItem.java index cbad92ff3f38..f6a72915e639 100644 --- a/core/java/android/app/servertransaction/WindowContextInfoChangeItem.java +++ b/core/java/android/app/servertransaction/WindowContextInfoChangeItem.java @@ -21,7 +21,6 @@ import static java.util.Objects.requireNonNull; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ClientTransactionHandler; -import android.content.Context; import android.content.res.Configuration; import android.os.IBinder; import android.os.Parcel; @@ -46,12 +45,6 @@ public class WindowContextInfoChangeItem extends ClientTransactionItem { client.handleWindowContextInfoChanged(mClientToken, mInfo); } - @Nullable - @Override - public Context getContextToUpdate(@NonNull ClientTransactionHandler client) { - return client.getWindowContext(mClientToken); - } - // ObjectPoolItem implementation private WindowContextInfoChangeItem() {} diff --git a/core/java/android/app/servertransaction/WindowStateResizeItem.java b/core/java/android/app/servertransaction/WindowStateResizeItem.java index 1817c5eefb14..da99096f022a 100644 --- a/core/java/android/app/servertransaction/WindowStateResizeItem.java +++ b/core/java/android/app/servertransaction/WindowStateResizeItem.java @@ -22,10 +22,7 @@ import static java.util.Objects.requireNonNull; import android.annotation.NonNull; import android.annotation.Nullable; -import android.app.ActivityThread; import android.app.ClientTransactionHandler; -import android.content.Context; -import android.os.IBinder; import android.os.Parcel; import android.os.RemoteException; import android.os.Trace; @@ -59,10 +56,6 @@ public class WindowStateResizeItem extends ClientTransactionItem { /** {@code null} if this is not an Activity window. */ @Nullable - private IBinder mActivityToken; - - /** {@code null} if this is not an Activity window. */ - @Nullable private ActivityWindowInfo mActivityWindowInfo; @Override @@ -86,14 +79,6 @@ public class WindowStateResizeItem extends ClientTransactionItem { Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER); } - @Nullable - @Override - public Context getContextToUpdate(@NonNull ClientTransactionHandler client) { - // TODO(b/260873529): dispatch for mActivityToken as well. - // WindowStateResizeItem may update the global config with #mConfiguration. - return ActivityThread.currentApplication(); - } - // ObjectPoolItem implementation private WindowStateResizeItem() {} @@ -103,8 +88,7 @@ public class WindowStateResizeItem extends ClientTransactionItem { @NonNull ClientWindowFrames frames, boolean reportDraw, @NonNull MergedConfiguration configuration, @NonNull InsetsState insetsState, boolean forceLayout, boolean alwaysConsumeSystemBars, int displayId, int syncSeqId, - boolean dragResizing, @Nullable IBinder activityToken, - @Nullable ActivityWindowInfo activityWindowInfo) { + boolean dragResizing, @Nullable ActivityWindowInfo activityWindowInfo) { WindowStateResizeItem instance = ObjectPool.obtain(WindowStateResizeItem.class); if (instance == null) { @@ -120,7 +104,6 @@ public class WindowStateResizeItem extends ClientTransactionItem { instance.mDisplayId = displayId; instance.mSyncSeqId = syncSeqId; instance.mDragResizing = dragResizing; - instance.mActivityToken = activityToken; instance.mActivityWindowInfo = activityWindowInfo != null ? new ActivityWindowInfo(activityWindowInfo) : null; @@ -140,7 +123,6 @@ public class WindowStateResizeItem extends ClientTransactionItem { mDisplayId = INVALID_DISPLAY; mSyncSeqId = -1; mDragResizing = false; - mActivityToken = null; mActivityWindowInfo = null; ObjectPool.recycle(this); } @@ -160,7 +142,6 @@ public class WindowStateResizeItem extends ClientTransactionItem { dest.writeInt(mDisplayId); dest.writeInt(mSyncSeqId); dest.writeBoolean(mDragResizing); - dest.writeStrongBinder(mActivityToken); dest.writeTypedObject(mActivityWindowInfo, flags); } @@ -176,7 +157,6 @@ public class WindowStateResizeItem extends ClientTransactionItem { mDisplayId = in.readInt(); mSyncSeqId = in.readInt(); mDragResizing = in.readBoolean(); - mActivityToken = in.readStrongBinder(); mActivityWindowInfo = in.readTypedObject(ActivityWindowInfo.CREATOR); } @@ -209,7 +189,6 @@ public class WindowStateResizeItem extends ClientTransactionItem { && mDisplayId == other.mDisplayId && mSyncSeqId == other.mSyncSeqId && mDragResizing == other.mDragResizing - && Objects.equals(mActivityToken, other.mActivityToken) && Objects.equals(mActivityWindowInfo, other.mActivityWindowInfo); } @@ -226,7 +205,6 @@ public class WindowStateResizeItem extends ClientTransactionItem { result = 31 * result + mDisplayId; result = 31 * result + mSyncSeqId; result = 31 * result + (mDragResizing ? 1 : 0); - result = 31 * result + Objects.hashCode(mActivityToken); result = 31 * result + Objects.hashCode(mActivityWindowInfo); return result; } @@ -236,7 +214,6 @@ public class WindowStateResizeItem extends ClientTransactionItem { return "WindowStateResizeItem{window=" + mWindow + ", reportDrawn=" + mReportDraw + ", configuration=" + mConfiguration - + ", activityToken=" + mActivityToken + ", activityWindowInfo=" + mActivityWindowInfo + "}"; } diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java index 35a3a5f32648..136c45d1695f 100644 --- a/core/java/android/os/Parcel.java +++ b/core/java/android/os/Parcel.java @@ -863,6 +863,28 @@ public final class Parcel { } /** @hide */ + public void removeClassCookie(Class clz, Object expectedCookie) { + if (mClassCookies != null) { + Object removedCookie = mClassCookies.remove(clz); + if (removedCookie != expectedCookie) { + Log.wtf(TAG, "Expected to remove " + expectedCookie + " (with key=" + clz + + ") but instead removed " + removedCookie); + } + } else { + Log.wtf(TAG, "Expected to remove " + expectedCookie + " (with key=" + clz + + ") but no cookies were present"); + } + } + + /** + * Whether {@link #setClassCookie} has been called with the specified {@code clz}. + * @hide + */ + public boolean hasClassCookie(Class clz) { + return mClassCookies != null && mClassCookies.containsKey(clz); + } + + /** @hide */ public final void adoptClassCookies(Parcel from) { mClassCookies = from.mClassCookies; } diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig index ec3c9789f655..23ece310b926 100644 --- a/core/java/android/permission/flags.aconfig +++ b/core/java/android/permission/flags.aconfig @@ -155,14 +155,3 @@ flag { description: "Use runtime permission state to determine appop state" bug: "266164193" } - -flag { - name: "ignore_apex_permissions" - is_fixed_read_only: true - namespace: "permissions" - description: "Ignore APEX pacakges for permissions on V+" - bug: "301320911" - metadata { - purpose: PURPOSE_BUGFIX - } -} diff --git a/core/java/android/util/PackageUtils.java b/core/java/android/util/PackageUtils.java index ea7efc79de87..c1ed19fef032 100644 --- a/core/java/android/util/PackageUtils.java +++ b/core/java/android/util/PackageUtils.java @@ -203,9 +203,8 @@ public final class PackageUtils { } File f = new File(filePath); - try { - DigestInputStream digestInputStream = new DigestInputStream(new FileInputStream(f), - messageDigest); + try (DigestInputStream digestInputStream = new DigestInputStream(new FileInputStream(f), + messageDigest)) { while (digestInputStream.read(fileBuffer) != -1); } catch (IOException e) { e.printStackTrace(); diff --git a/core/java/android/view/ImeBackAnimationController.java b/core/java/android/view/ImeBackAnimationController.java index d14e858d9fa1..665fac18be99 100644 --- a/core/java/android/view/ImeBackAnimationController.java +++ b/core/java/android/view/ImeBackAnimationController.java @@ -28,6 +28,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.graphics.Insets; import android.util.Log; +import android.view.animation.BackGestureInterpolator; import android.view.animation.Interpolator; import android.view.animation.PathInterpolator; import android.window.BackEvent; @@ -44,7 +45,7 @@ public class ImeBackAnimationController implements OnBackAnimationCallback { private static final int POST_COMMIT_DURATION_MS = 200; private static final int POST_COMMIT_CANCEL_DURATION_MS = 50; private static final float PEEK_FRACTION = 0.1f; - private static final Interpolator STANDARD_DECELERATE = new PathInterpolator(0f, 0f, 0f, 1f); + private static final Interpolator BACK_GESTURE = new BackGestureInterpolator(); private static final Interpolator EMPHASIZED_DECELERATE = new PathInterpolator( 0.05f, 0.7f, 0.1f, 1f); private static final Interpolator STANDARD_ACCELERATE = new PathInterpolator(0.3f, 0f, 1f, 1f); @@ -140,7 +141,7 @@ public class ImeBackAnimationController implements OnBackAnimationCallback { float hiddenY = mWindowInsetsAnimationController.getHiddenStateInsets().bottom; float shownY = mWindowInsetsAnimationController.getShownStateInsets().bottom; float imeHeight = shownY - hiddenY; - float interpolatedProgress = STANDARD_DECELERATE.getInterpolation(progress); + float interpolatedProgress = BACK_GESTURE.getInterpolation(progress); int newY = (int) (imeHeight - interpolatedProgress * (imeHeight * PEEK_FRACTION)); mWindowInsetsAnimationController.setInsetsAndAlpha(Insets.of(0, 0, 0, newY), 1f, progress); diff --git a/core/java/android/view/animation/BackGestureInterpolator.java b/core/java/android/view/animation/BackGestureInterpolator.java new file mode 100644 index 000000000000..c1595db98998 --- /dev/null +++ b/core/java/android/view/animation/BackGestureInterpolator.java @@ -0,0 +1,26 @@ +/* + * 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 android.view.animation; +/** + * Decelerating interpolator with a very slight acceleration phase at the beginning. + * @hide + */ +public class BackGestureInterpolator extends PathInterpolator { + public BackGestureInterpolator() { + super(0.1f, 0.1f, 0f, 1f); + } +} diff --git a/core/java/android/window/RemoteTransitionStub.java b/core/java/android/window/RemoteTransitionStub.java new file mode 100644 index 000000000000..c9932ab31469 --- /dev/null +++ b/core/java/android/window/RemoteTransitionStub.java @@ -0,0 +1,37 @@ +/* + * 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 android.window; + +import android.os.IBinder; +import android.os.RemoteException; +import android.view.SurfaceControl; + +/** + * Utility base implementation of {@link IRemoteTransition} that users can extend to avoid stubbing. + * + * @hide + */ +public abstract class RemoteTransitionStub extends IRemoteTransition.Stub { + @Override + public void mergeAnimation(IBinder transition, TransitionInfo info, + SurfaceControl.Transaction t, IBinder mergeTarget, + IRemoteTransitionFinishedCallback finishCallback) throws RemoteException {} + + @Override + public void onTransitionConsumed(IBinder transition, boolean aborted) + throws RemoteException {} +} diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java index 5227724e705e..994e73288e93 100644 --- a/core/java/android/window/TransitionInfo.java +++ b/core/java/android/window/TransitionInfo.java @@ -538,6 +538,11 @@ public final class TransitionInfo implements Parcelable { // If the change has no parent (it is root), then it is independent if (change.getParent() == null) return true; + if (change.getLastParent() != null && !change.getLastParent().equals(change.getParent())) { + // If the change has been reparented, then it's independent. + return true; + } + // non-visibility changes will just be folded into the parent change, so they aren't // independent either. if (change.getMode() == TRANSIT_CHANGE) return false; diff --git a/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java b/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java index 8c7b360a3fcd..97ce96ec30f6 100644 --- a/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java +++ b/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java @@ -1054,8 +1054,7 @@ public class ParsingPackageUtils { // An Apex package shouldn't have permission declarations final boolean isApex = (flags & PARSE_APEX) != 0; - if (android.permission.flags.Flags.ignoreApexPermissions() - && isApex && !pkg.getPermissions().isEmpty()) { + if (isApex && !pkg.getPermissions().isEmpty()) { return input.error( INSTALL_PARSE_FAILED_MANIFEST_MALFORMED, pkg.getPackageName() diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl index f5b1a47e917e..f931a762871c 100644 --- a/core/java/com/android/internal/statusbar/IStatusBar.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl @@ -28,6 +28,7 @@ import android.media.INearbyMediaDevicesProvider; import android.media.MediaRoute2Info; import android.os.Bundle; import android.os.ParcelFileDescriptor; +import android.os.UserHandle; import android.view.KeyEvent; import android.service.notification.StatusBarNotification; @@ -384,9 +385,11 @@ oneway interface IStatusBar /** * Shows the media output switcher dialog. * - * @param packageName of the session for which the output switcher is shown. + * @param targetPackageName The package name for which to show the output switcher. + * @param targetUserHandle The UserHandle on which the package for which to show the output + * switcher is running. */ - void showMediaOutputSwitcher(String packageName); + void showMediaOutputSwitcher(String targetPackageName, in UserHandle targetUserHandle); /** Enters desktop mode from the current focused app. * diff --git a/core/res/res/values-mcc404/config.xml b/core/res/res/values-mcc404/config.xml index 4cadef7893d3..0cb1029626b1 100644 --- a/core/res/res/values-mcc404/config.xml +++ b/core/res/res/values-mcc404/config.xml @@ -18,8 +18,6 @@ --> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> - <!-- Whether camera shutter sound is forced or not (country specific). --> - <bool name="config_camera_sound_forced">true</bool> <!-- Show area update info settings in CellBroadcastReceiver and information in SIM status in Settings app --> <bool name="config_showAreaUpdateInfoSettings">true</bool> </resources> diff --git a/core/res/res/values-mcc405/config.xml b/core/res/res/values-mcc405/config.xml index 4cadef7893d3..0cb1029626b1 100644 --- a/core/res/res/values-mcc405/config.xml +++ b/core/res/res/values-mcc405/config.xml @@ -18,8 +18,6 @@ --> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> - <!-- Whether camera shutter sound is forced or not (country specific). --> - <bool name="config_camera_sound_forced">true</bool> <!-- Show area update info settings in CellBroadcastReceiver and information in SIM status in Settings app --> <bool name="config_showAreaUpdateInfoSettings">true</bool> </resources> diff --git a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java index 2ce7a7d3d70d..a0aff6e7b9a0 100644 --- a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java +++ b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java @@ -16,7 +16,6 @@ package android.app.servertransaction; -import static android.content.Context.DEVICE_ID_DEFAULT; import static android.view.Display.DEFAULT_DISPLAY; import static org.junit.Assert.assertEquals; @@ -29,9 +28,6 @@ import static org.mockito.Mockito.verify; import android.app.Activity; import android.app.ActivityThread; import android.app.ClientTransactionHandler; -import android.content.Context; -import android.content.Intent; -import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.os.IBinder; import android.os.RemoteException; @@ -107,35 +103,6 @@ public class ClientTransactionItemTest { } @Test - public void testActivityConfigurationChangeItem_getContextToUpdate() { - final ActivityConfigurationChangeItem item = ActivityConfigurationChangeItem - .obtain(mActivityToken, mConfiguration, mActivityWindowInfo); - final Context context = item.getContextToUpdate(mHandler); - - assertEquals(mActivity, context); - } - - @Test - public void testActivityRelaunchItem_getContextToUpdate() { - final ActivityRelaunchItem item = ActivityRelaunchItem - .obtain(mActivityToken, null /* pendingResults */, null /* pendingNewIntents */, - 0 /* configChange */, mMergedConfiguration, false /* preserveWindow */, - mActivityWindowInfo); - final Context context = item.getContextToUpdate(mHandler); - - assertEquals(mActivity, context); - } - - @Test - public void testConfigurationChangeItem_getContextToUpdate() { - final ConfigurationChangeItem item = ConfigurationChangeItem - .obtain(mConfiguration, DEVICE_ID_DEFAULT); - final Context context = item.getContextToUpdate(mHandler); - - assertEquals(ActivityThread.currentApplication(), context); - } - - @Test public void testDestroyActivityItem_preExecute() { final DestroyActivityItem item = DestroyActivityItem .obtain(mActivityToken, false /* finished */); @@ -166,26 +133,6 @@ public class ClientTransactionItemTest { } @Test - public void testLaunchActivityItem_getContextToUpdate() { - final LaunchActivityItem item = new TestUtils.LaunchActivityItemBuilder( - mActivityToken, new Intent(), new ActivityInfo()) - .build(); - - final Context context = item.getContextToUpdate(mHandler); - - assertEquals(ActivityThread.currentApplication(), context); - } - - @Test - public void testMoveToDisplayItem_getContextToUpdate() { - final MoveToDisplayItem item = MoveToDisplayItem - .obtain(mActivityToken, DEFAULT_DISPLAY, mConfiguration, mActivityWindowInfo); - final Context context = item.getContextToUpdate(mHandler); - - assertEquals(mActivity, context); - } - - @Test public void testWindowContextInfoChangeItem_execute() { final WindowContextInfoChangeItem item = WindowContextInfoChangeItem .obtain(mWindowClientToken, mConfiguration, DEFAULT_DISPLAY); @@ -196,17 +143,6 @@ public class ClientTransactionItemTest { } @Test - public void testWindowContextInfoChangeItem_getContextToUpdate() { - doReturn(mWindowContext).when(mHandler).getWindowContext(mWindowClientToken); - - final WindowContextInfoChangeItem item = WindowContextInfoChangeItem - .obtain(mWindowClientToken, mConfiguration, DEFAULT_DISPLAY); - final Context context = item.getContextToUpdate(mHandler); - - assertEquals(mWindowContext, context); - } - - @Test public void testWindowContextWindowRemovalItem_execute() { final WindowContextWindowRemovalItem item = WindowContextWindowRemovalItem.obtain( mWindowClientToken); @@ -220,7 +156,7 @@ public class ClientTransactionItemTest { final WindowStateResizeItem item = WindowStateResizeItem.obtain(mWindow, mFrames, true /* reportDraw */, mMergedConfiguration, mInsetsState, true /* forceLayout */, true /* alwaysConsumeSystemBars */, 123 /* displayId */, 321 /* syncSeqId */, - true /* dragResizing */, mActivityToken, mActivityWindowInfo); + true /* dragResizing */, mActivityWindowInfo); item.execute(mHandler, mPendingActions); verify(mWindow).resized(mFrames, @@ -228,16 +164,4 @@ public class ClientTransactionItemTest { true /* alwaysConsumeSystemBars */, 123 /* displayId */, 321 /* syncSeqId */, true /* dragResizing */, mActivityWindowInfo); } - - @Test - public void testWindowStateResizeItem_getContextToUpdate() { - final WindowStateResizeItem item = WindowStateResizeItem.obtain(mWindow, mFrames, - true /* reportDraw */, mMergedConfiguration, mInsetsState, true /* forceLayout */, - true /* alwaysConsumeSystemBars */, 123 /* displayId */, 321 /* syncSeqId */, - true /* dragResizing */, mActivityToken, mActivityWindowInfo); - final Context context = item.getContextToUpdate(mHandler); - - assertEquals(ActivityThread.currentApplication(), context); - } - } diff --git a/core/tests/coretests/src/android/os/ParcelTest.java b/core/tests/coretests/src/android/os/ParcelTest.java index 442394e3428a..0697c96052f6 100644 --- a/core/tests/coretests/src/android/os/ParcelTest.java +++ b/core/tests/coretests/src/android/os/ParcelTest.java @@ -16,6 +16,8 @@ package android.os; +import static com.google.common.truth.Truth.assertThat; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThrows; @@ -24,6 +26,7 @@ import static org.junit.Assert.assertTrue; import android.platform.test.annotations.IgnoreUnderRavenwood; import android.platform.test.annotations.Presubmit; import android.platform.test.ravenwood.RavenwoodRule; +import android.util.Log; import androidx.test.runner.AndroidJUnit4; @@ -31,6 +34,8 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import java.util.ArrayList; + @Presubmit @RunWith(AndroidJUnit4.class) public class ParcelTest { @@ -349,6 +354,50 @@ public class ParcelTest { } @Test + public void testClassCookies() { + Parcel p = Parcel.obtain(); + assertThat(p.hasClassCookie(ParcelTest.class)).isFalse(); + + p.setClassCookie(ParcelTest.class, "string_cookie"); + assertThat(p.hasClassCookie(ParcelTest.class)).isTrue(); + assertThat(p.getClassCookie(ParcelTest.class)).isEqualTo("string_cookie"); + + p.removeClassCookie(ParcelTest.class, "string_cookie"); + assertThat(p.hasClassCookie(ParcelTest.class)).isFalse(); + assertThat(p.getClassCookie(ParcelTest.class)).isEqualTo(null); + + p.setClassCookie(ParcelTest.class, "to_be_discarded_cookie"); + p.recycle(); + assertThat(p.getClassCookie(ParcelTest.class)).isNull(); + } + + @Test + public void testClassCookies_removeUnexpected() { + Parcel p = Parcel.obtain(); + + assertLogsWtf(() -> p.removeClassCookie(ParcelTest.class, "not_present")); + + p.setClassCookie(ParcelTest.class, "value"); + + assertLogsWtf(() -> p.removeClassCookie(ParcelTest.class, "different")); + assertThat(p.getClassCookie(ParcelTest.class)).isNull(); // still removed + + p.recycle(); + } + + private static void assertLogsWtf(Runnable test) { + ArrayList<Log.TerribleFailure> wtfs = new ArrayList<>(); + Log.TerribleFailureHandler oldHandler = Log.setWtfHandler( + (tag, what, system) -> wtfs.add(what)); + try { + test.run(); + } finally { + Log.setWtfHandler(oldHandler); + } + assertThat(wtfs).hasSize(1); + } + + @Test @IgnoreUnderRavenwood(blockedBy = Parcel.class) public void testHasBinders_AfterWritingBinderToParcel() { Binder binder = new Binder(); @@ -360,7 +409,6 @@ public class ParcelTest { assertTrue(pA.hasBinders()); } - @Test @IgnoreUnderRavenwood(blockedBy = Parcel.class) public void testHasBindersInRange_AfterWritingBinderToParcel() { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java index 19963675ff86..ce0bf8b29374 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/Interpolators.java @@ -17,6 +17,7 @@ package com.android.wm.shell.animation; import android.graphics.Path; +import android.view.animation.BackGestureInterpolator; import android.view.animation.Interpolator; import android.view.animation.LinearInterpolator; import android.view.animation.PathInterpolator; @@ -95,6 +96,15 @@ public class Interpolators { public static final PathInterpolator DIM_INTERPOLATOR = new PathInterpolator(.23f, .87f, .52f, -0.11f); + /** + * Use this interpolator for animating progress values coming from the back callback to get + * the predictive-back-typical decelerate motion. + * + * This interpolator is similar to {@link Interpolators#STANDARD_DECELERATE} but has a slight + * acceleration phase at the start. + */ + public static final Interpolator BACK_GESTURE = new BackGestureInterpolator(); + // Create the default emphasized interpolator private static PathInterpolator createEmphasizedInterpolator() { Path path = new Path(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt index 112ed0941122..7cb56605cc12 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt @@ -87,7 +87,7 @@ class CrossActivityBackAnimation @Inject constructor( private val enteringStartOffset = context.resources.getDimension(R.dimen.cross_activity_back_entering_start_offset) - private val gestureInterpolator = Interpolators.STANDARD_DECELERATE + private val gestureInterpolator = Interpolators.BACK_GESTURE private val postCommitInterpolator = Interpolators.FAST_OUT_SLOW_IN private val verticalMoveInterpolator: Interpolator = DecelerateInterpolator() diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java index c34f30df33a2..ee898a73a291 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java @@ -93,7 +93,7 @@ public class CrossTaskBackAnimation extends ShellBackAnimation { private final PointF mInitialTouchPos = new PointF(); private final Interpolator mPostAnimationInterpolator = Interpolators.EMPHASIZED; - private final Interpolator mProgressInterpolator = Interpolators.STANDARD_DECELERATE; + private final Interpolator mProgressInterpolator = Interpolators.BACK_GESTURE; private final Interpolator mVerticalMoveInterpolator = new DecelerateInterpolator(); private final Matrix mTransformMatrix = new Matrix(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/ShellBackAnimationRegistry.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/ShellBackAnimationRegistry.java index 00daddc13346..7a6032c60cce 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/ShellBackAnimationRegistry.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/ShellBackAnimationRegistry.java @@ -28,7 +28,7 @@ public class ShellBackAnimationRegistry { private static final String TAG = "ShellBackPreview"; private final SparseArray<BackAnimationRunner> mAnimationDefinition = new SparseArray<>(); - private final ShellBackAnimation mDefaultCrossActivityAnimation; + private ShellBackAnimation mDefaultCrossActivityAnimation; private final ShellBackAnimation mCustomizeActivityAnimation; private final ShellBackAnimation mCrossTaskAnimation; @@ -67,10 +67,18 @@ public class ShellBackAnimationRegistry { void registerAnimation( @BackNavigationInfo.BackTargetType int type, @NonNull BackAnimationRunner runner) { mAnimationDefinition.set(type, runner); + // Only happen in test + if (BackNavigationInfo.TYPE_CROSS_ACTIVITY == type) { + mDefaultCrossActivityAnimation = null; + } } void unregisterAnimation(@BackNavigationInfo.BackTargetType int type) { mAnimationDefinition.remove(type); + // Only happen in test + if (BackNavigationInfo.TYPE_CROSS_ACTIVITY == type) { + mDefaultCrossActivityAnimation = null; + } } /** @@ -129,9 +137,15 @@ public class ShellBackAnimationRegistry { } void onConfigurationChanged(Configuration newConfig) { - mCustomizeActivityAnimation.onConfigurationChanged(newConfig); - mDefaultCrossActivityAnimation.onConfigurationChanged(newConfig); - mCrossTaskAnimation.onConfigurationChanged(newConfig); + if (mCustomizeActivityAnimation != null) { + mCustomizeActivityAnimation.onConfigurationChanged(newConfig); + } + if (mDefaultCrossActivityAnimation != null) { + mDefaultCrossActivityAnimation.onConfigurationChanged(newConfig); + } + if (mCrossTaskAnimation != null) { + mCrossTaskAnimation.onConfigurationChanged(newConfig); + } } BackAnimationRunner getAnimationRunnerAndInit(BackNavigationInfo backNavigationInfo) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MoveToDesktopAnimator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MoveToDesktopAnimator.kt index 987aadfbdef2..74499c7e429e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MoveToDesktopAnimator.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MoveToDesktopAnimator.kt @@ -33,6 +33,7 @@ class MoveToDesktopAnimator @JvmOverloads constructor( get() = dragToDesktopAnimator.animatedValue as Float * startBounds.width() val scale: Float get() = dragToDesktopAnimator.animatedValue as Float + private val mostRecentInput = PointF() private val dragToDesktopAnimator: ValueAnimator = ValueAnimator.ofFloat(1f, DRAG_FREEFORM_SCALE) .setDuration(ANIMATION_DURATION.toLong()) @@ -40,9 +41,13 @@ class MoveToDesktopAnimator @JvmOverloads constructor( val t = SurfaceControl.Transaction() val cornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context) addUpdateListener { + setTaskPosition(mostRecentInput.x, mostRecentInput.y) t.setScale(taskSurface, scale, scale) - .setCornerRadius(taskSurface, cornerRadius) - .apply() + .setCornerRadius(taskSurface, cornerRadius) + .setScale(taskSurface, scale, scale) + .setCornerRadius(taskSurface, cornerRadius) + .setPosition(taskSurface, position.x, position.y) + .apply() } } @@ -78,19 +83,28 @@ class MoveToDesktopAnimator @JvmOverloads constructor( // allow dragging beyond its stage across any region of the display. Because of that, the // rawX/Y are more true to where the gesture is on screen and where the surface should be // positioned. - position.x = ev.rawX - animatedTaskWidth / 2 - position.y = ev.rawY + mostRecentInput.set(ev.rawX, ev.rawY) - if (!allowSurfaceChangesOnMove) { + // If animator is running, allow it to set scale and position at the same time. + if (!allowSurfaceChangesOnMove || dragToDesktopAnimator.isRunning) { return } - + setTaskPosition(ev.rawX, ev.rawY) val t = transactionFactory() t.setPosition(taskSurface, position.x, position.y) t.apply() } /** + * Calculates the top left corner of task from input coordinates. + * Top left will be needed for the resulting surface control transaction. + */ + private fun setTaskPosition(x: Float, y: Float) { + position.x = x - animatedTaskWidth / 2 + position.y = y + } + + /** * Cancels the animation, intended to be used when another animator will take over. */ fun cancelAnimator() { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java index e9da25813510..2366917a0158 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java @@ -83,6 +83,7 @@ import android.window.IRemoteTransition; import android.window.IRemoteTransitionFinishedCallback; import android.window.IWindowContainerToken; import android.window.RemoteTransition; +import android.window.RemoteTransitionStub; import android.window.TransitionFilter; import android.window.TransitionInfo; import android.window.TransitionRequestInfo; @@ -280,7 +281,7 @@ public class ShellTransitionTests extends ShellTestCase { final boolean[] remoteCalled = new boolean[]{false}; final WindowContainerTransaction remoteFinishWCT = new WindowContainerTransaction(); - IRemoteTransition testRemote = new IRemoteTransition.Stub() { + IRemoteTransition testRemote = new RemoteTransitionStub() { @Override public void startAnimation(IBinder token, TransitionInfo info, SurfaceControl.Transaction t, @@ -288,16 +289,6 @@ public class ShellTransitionTests extends ShellTestCase { remoteCalled[0] = true; finishCallback.onTransitionFinished(remoteFinishWCT, null /* sct */); } - - @Override - public void mergeAnimation(IBinder token, TransitionInfo info, - SurfaceControl.Transaction t, IBinder mergeTarget, - IRemoteTransitionFinishedCallback finishCallback) throws RemoteException { - } - - @Override - public void onTransitionConsumed(IBinder iBinder, boolean b) throws RemoteException { - } }; IBinder transitToken = new Binder(); transitions.requestStartTransition(transitToken, @@ -450,7 +441,7 @@ public class ShellTransitionTests extends ShellTestCase { transitions.replaceDefaultHandlerForTest(mDefaultHandler); final boolean[] remoteCalled = new boolean[]{false}; - IRemoteTransition testRemote = new IRemoteTransition.Stub() { + IRemoteTransition testRemote = new RemoteTransitionStub() { @Override public void startAnimation(IBinder token, TransitionInfo info, SurfaceControl.Transaction t, @@ -458,16 +449,6 @@ public class ShellTransitionTests extends ShellTestCase { remoteCalled[0] = true; finishCallback.onTransitionFinished(null /* wct */, null /* sct */); } - - @Override - public void mergeAnimation(IBinder token, TransitionInfo info, - SurfaceControl.Transaction t, IBinder mergeTarget, - IRemoteTransitionFinishedCallback finishCallback) throws RemoteException { - } - - @Override - public void onTransitionConsumed(IBinder iBinder, boolean b) throws RemoteException { - } }; TransitionFilter filter = new TransitionFilter(); @@ -500,7 +481,7 @@ public class ShellTransitionTests extends ShellTestCase { final boolean[] remoteCalled = new boolean[]{false}; final WindowContainerTransaction remoteFinishWCT = new WindowContainerTransaction(); - IRemoteTransition testRemote = new IRemoteTransition.Stub() { + IRemoteTransition testRemote = new RemoteTransitionStub() { @Override public void startAnimation(IBinder token, TransitionInfo info, SurfaceControl.Transaction t, @@ -508,16 +489,6 @@ public class ShellTransitionTests extends ShellTestCase { remoteCalled[0] = true; finishCallback.onTransitionFinished(remoteFinishWCT, null /* sct */); } - - @Override - public void mergeAnimation(IBinder token, TransitionInfo info, - SurfaceControl.Transaction t, IBinder mergeTarget, - IRemoteTransitionFinishedCallback finishCallback) throws RemoteException { - } - - @Override - public void onTransitionConsumed(IBinder iBinder, boolean b) throws RemoteException { - } }; final int transitType = TRANSIT_FIRST_CUSTOM + 1; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/TestRemoteTransition.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/TestRemoteTransition.java index 87330d2dc877..184e8955d08c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/TestRemoteTransition.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/TestRemoteTransition.java @@ -20,6 +20,7 @@ import android.os.RemoteException; import android.view.SurfaceControl; import android.window.IRemoteTransition; import android.window.IRemoteTransitionFinishedCallback; +import android.window.RemoteTransitionStub; import android.window.TransitionInfo; import android.window.WindowContainerTransaction; @@ -29,7 +30,7 @@ import android.window.WindowContainerTransaction; * {@link #startAnimation(IBinder, TransitionInfo, SurfaceControl.Transaction, * IRemoteTransitionFinishedCallback)} being called. */ -public class TestRemoteTransition extends IRemoteTransition.Stub { +public class TestRemoteTransition extends RemoteTransitionStub { private boolean mCalled = false; private boolean mConsumed = false; final WindowContainerTransaction mRemoteFinishWCT = new WindowContainerTransaction(); @@ -44,12 +45,6 @@ public class TestRemoteTransition extends IRemoteTransition.Stub { } @Override - public void mergeAnimation(IBinder transition, TransitionInfo info, - SurfaceControl.Transaction t, IBinder mergeTarget, - IRemoteTransitionFinishedCallback finishCallback) throws RemoteException { - } - - @Override public void onTransitionConsumed(IBinder iBinder, boolean b) throws RemoteException { mConsumed = true; } diff --git a/libs/hostgraphics/ANativeWindow.cpp b/libs/hostgraphics/ANativeWindow.cpp new file mode 100644 index 000000000000..fcfaf0235293 --- /dev/null +++ b/libs/hostgraphics/ANativeWindow.cpp @@ -0,0 +1,106 @@ +/* + * 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. + */ + +#include <system/window.h> + +static int32_t query(ANativeWindow* window, int what) { + int value; + int res = window->query(window, what, &value); + return res < 0 ? res : value; +} + +static int64_t query64(ANativeWindow* window, int what) { + int64_t value; + int res = window->perform(window, what, &value); + return res < 0 ? res : value; +} + +int ANativeWindow_setCancelBufferInterceptor(ANativeWindow* window, + ANativeWindow_cancelBufferInterceptor interceptor, + void* data) { + return window->perform(window, NATIVE_WINDOW_SET_CANCEL_INTERCEPTOR, interceptor, data); +} + +int ANativeWindow_setDequeueBufferInterceptor(ANativeWindow* window, + ANativeWindow_dequeueBufferInterceptor interceptor, + void* data) { + return window->perform(window, NATIVE_WINDOW_SET_DEQUEUE_INTERCEPTOR, interceptor, data); +} + +int ANativeWindow_setQueueBufferInterceptor(ANativeWindow* window, + ANativeWindow_queueBufferInterceptor interceptor, + void* data) { + return window->perform(window, NATIVE_WINDOW_SET_QUEUE_INTERCEPTOR, interceptor, data); +} + +int ANativeWindow_setPerformInterceptor(ANativeWindow* window, + ANativeWindow_performInterceptor interceptor, void* data) { + return window->perform(window, NATIVE_WINDOW_SET_PERFORM_INTERCEPTOR, interceptor, data); +} + +int ANativeWindow_dequeueBuffer(ANativeWindow* window, ANativeWindowBuffer** buffer, int* fenceFd) { + return window->dequeueBuffer(window, buffer, fenceFd); +} + +int ANativeWindow_cancelBuffer(ANativeWindow* window, ANativeWindowBuffer* buffer, int fenceFd) { + return window->cancelBuffer(window, buffer, fenceFd); +} + +int ANativeWindow_setDequeueTimeout(ANativeWindow* window, int64_t timeout) { + return window->perform(window, NATIVE_WINDOW_SET_DEQUEUE_TIMEOUT, timeout); +} + +// extern "C", so that it can be used outside libhostgraphics (in host hwui/.../CanvasContext.cpp) +extern "C" void ANativeWindow_tryAllocateBuffers(ANativeWindow* window) { + if (!window || !query(window, NATIVE_WINDOW_IS_VALID)) { + return; + } + window->perform(window, NATIVE_WINDOW_ALLOCATE_BUFFERS); +} + +int64_t ANativeWindow_getLastDequeueStartTime(ANativeWindow* window) { + return query64(window, NATIVE_WINDOW_GET_LAST_DEQUEUE_START); +} + +int64_t ANativeWindow_getLastDequeueDuration(ANativeWindow* window) { + return query64(window, NATIVE_WINDOW_GET_LAST_DEQUEUE_DURATION); +} + +int64_t ANativeWindow_getLastQueueDuration(ANativeWindow* window) { + return query64(window, NATIVE_WINDOW_GET_LAST_QUEUE_DURATION); +} + +int32_t ANativeWindow_getWidth(ANativeWindow* window) { + return query(window, NATIVE_WINDOW_WIDTH); +} + +int32_t ANativeWindow_getHeight(ANativeWindow* window) { + return query(window, NATIVE_WINDOW_HEIGHT); +} + +int32_t ANativeWindow_getFormat(ANativeWindow* window) { + return query(window, NATIVE_WINDOW_FORMAT); +} + +void ANativeWindow_acquire(ANativeWindow* window) { + // incStrong/decStrong token must be the same, doesn't matter what it is + window->incStrong((void*)ANativeWindow_acquire); +} + +void ANativeWindow_release(ANativeWindow* window) { + // incStrong/decStrong token must be the same, doesn't matter what it is + window->decStrong((void*)ANativeWindow_acquire); +} diff --git a/libs/hostgraphics/Android.bp b/libs/hostgraphics/Android.bp index 4407af68de99..09232b64616d 100644 --- a/libs/hostgraphics/Android.bp +++ b/libs/hostgraphics/Android.bp @@ -17,26 +17,18 @@ cc_library_host_static { static_libs: [ "libbase", "libmath", + "libui-types", "libutils", ], srcs: [ - ":libui_host_common", "ADisplay.cpp", + "ANativeWindow.cpp", "Fence.cpp", "HostBufferQueue.cpp", "PublicFormat.cpp", ], - include_dirs: [ - // Here we override all the headers automatically included with frameworks/native/include. - // When frameworks/native/include will be removed from the list of automatic includes. - // We will have to copy necessary headers with a pre-build step (generated headers). - ".", - "frameworks/native/libs/arect/include", - "frameworks/native/libs/ui/include_private", - ], - header_libs: [ "libnativebase_headers", "libnativedisplay_headers", diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp index 4706699039a6..7439fbc1149c 100644 --- a/libs/hwui/Android.bp +++ b/libs/hwui/Android.bp @@ -96,6 +96,7 @@ cc_defaults { ], cflags: [ "-Wno-unused-variable", + "-D__INTRODUCED_IN(n)=", ], }, }, @@ -539,7 +540,10 @@ cc_defaults { "pipeline/skia/ReorderBarrierDrawables.cpp", "pipeline/skia/TransformCanvas.cpp", "renderstate/RenderState.cpp", + "renderthread/CanvasContext.cpp", + "renderthread/DrawFrameTask.cpp", "renderthread/Frame.cpp", + "renderthread/RenderProxy.cpp", "renderthread/RenderTask.cpp", "renderthread/TimeLord.cpp", "hwui/AnimatedImageDrawable.cpp", @@ -589,6 +593,7 @@ cc_defaults { "SkiaCanvas.cpp", "SkiaInterpolator.cpp", "Tonemapper.cpp", + "TreeInfo.cpp", "VectorDrawable.cpp", ], @@ -617,14 +622,11 @@ cc_defaults { "pipeline/skia/VkFunctorDrawable.cpp", "pipeline/skia/VkInteropFunctorDrawable.cpp", "renderthread/CacheManager.cpp", - "renderthread/CanvasContext.cpp", - "renderthread/DrawFrameTask.cpp", "renderthread/EglManager.cpp", "renderthread/ReliableSurface.cpp", "renderthread/RenderEffectCapabilityQuery.cpp", "renderthread/VulkanManager.cpp", "renderthread/VulkanSurface.cpp", - "renderthread/RenderProxy.cpp", "renderthread/RenderThread.cpp", "renderthread/HintSessionWrapper.cpp", "service/GraphicsStatsService.cpp", @@ -636,7 +638,6 @@ cc_defaults { "Layer.cpp", "ProfileDataContainer.cpp", "Readback.cpp", - "TreeInfo.cpp", "WebViewFunctorManager.cpp", "protos/graphicsstats.proto", ], @@ -654,6 +655,8 @@ cc_defaults { srcs: [ "platform/host/renderthread/CacheManager.cpp", + "platform/host/renderthread/HintSessionWrapper.cpp", + "platform/host/renderthread/ReliableSurface.cpp", "platform/host/renderthread/RenderThread.cpp", "platform/host/ProfileDataContainer.cpp", "platform/host/Readback.cpp", diff --git a/libs/hwui/RenderNode.cpp b/libs/hwui/RenderNode.cpp index f526a280b113..589abb4d87f4 100644 --- a/libs/hwui/RenderNode.cpp +++ b/libs/hwui/RenderNode.cpp @@ -16,18 +16,6 @@ #include "RenderNode.h" -#include "DamageAccumulator.h" -#include "Debug.h" -#include "Properties.h" -#include "TreeInfo.h" -#include "VectorDrawable.h" -#include "private/hwui/WebViewFunctor.h" -#ifdef __ANDROID__ -#include "renderthread/CanvasContext.h" -#else -#include "DamageAccumulator.h" -#include "pipeline/skia/SkiaDisplayList.h" -#endif #include <SkPathOps.h> #include <gui/TraceUtils.h> #include <ui/FatVector.h> @@ -37,6 +25,14 @@ #include <sstream> #include <string> +#include "DamageAccumulator.h" +#include "Debug.h" +#include "Properties.h" +#include "TreeInfo.h" +#include "VectorDrawable.h" +#include "private/hwui/WebViewFunctor.h" +#include "renderthread/CanvasContext.h" + #ifdef __ANDROID__ #include "include/gpu/ganesh/SkImageGanesh.h" #endif @@ -186,7 +182,6 @@ void RenderNode::prepareLayer(TreeInfo& info, uint32_t dirtyMask) { } void RenderNode::pushLayerUpdate(TreeInfo& info) { -#ifdef __ANDROID__ // Layoutlib does not support CanvasContext and Layers LayerType layerType = properties().effectiveLayerType(); // If we are not a layer OR we cannot be rendered (eg, view was detached) // we need to destroy any Layers we may have had previously @@ -218,7 +213,6 @@ void RenderNode::pushLayerUpdate(TreeInfo& info) { // That might be us, so tell CanvasContext that this layer is in the // tree and should not be destroyed. info.canvasContext.markLayerInUse(this); -#endif } /** diff --git a/libs/hwui/RootRenderNode.cpp b/libs/hwui/RootRenderNode.cpp index ddbbf58b3071..5174e27ae587 100644 --- a/libs/hwui/RootRenderNode.cpp +++ b/libs/hwui/RootRenderNode.cpp @@ -18,11 +18,12 @@ #ifdef __ANDROID__ // Layoutlib does not support Looper (windows) #include <utils/Looper.h> +#else +#include "utils/MessageHandler.h" #endif namespace android::uirenderer { -#ifdef __ANDROID__ // Layoutlib does not support Looper class FinishAndInvokeListener : public MessageHandler { public: explicit FinishAndInvokeListener(PropertyValuesAnimatorSet* anim) : mAnimator(anim) { @@ -237,9 +238,13 @@ void RootRenderNode::detachVectorDrawableAnimator(PropertyValuesAnimatorSet* ani // user events, in which case the already posted listener's id will become stale, and // the onFinished callback will then be ignored. sp<FinishAndInvokeListener> message = new FinishAndInvokeListener(anim); +#ifdef __ANDROID__ // Layoutlib does not support Looper auto looper = Looper::getForThread(); LOG_ALWAYS_FATAL_IF(looper == nullptr, "Not on a looper thread?"); looper->sendMessageDelayed(ms2ns(remainingTimeInMs), message, 0); +#else + message->handleMessage(0); +#endif anim->clearOneShotListener(); } } @@ -285,22 +290,5 @@ private: AnimationContext* ContextFactoryImpl::createAnimationContext(renderthread::TimeLord& clock) { return new AnimationContextBridge(clock, mRootNode); } -#else - -void RootRenderNode::prepareTree(TreeInfo& info) { - info.errorHandler = mErrorHandler.get(); - info.updateWindowPositions = true; - RenderNode::prepareTree(info); - info.updateWindowPositions = false; - info.errorHandler = nullptr; -} - -void RootRenderNode::attachAnimatingNode(RenderNode* animatingNode) { } - -void RootRenderNode::destroy() { } - -void RootRenderNode::addVectorDrawableAnimator(PropertyValuesAnimatorSet* anim) { } - -#endif } // namespace android::uirenderer diff --git a/libs/hwui/RootRenderNode.h b/libs/hwui/RootRenderNode.h index 1d3f5a8a51e0..7a5cda7041ed 100644 --- a/libs/hwui/RootRenderNode.h +++ b/libs/hwui/RootRenderNode.h @@ -74,7 +74,6 @@ private: void detachVectorDrawableAnimator(PropertyValuesAnimatorSet* anim); }; -#ifdef __ANDROID__ // Layoutlib does not support Animations class ContextFactoryImpl : public IContextFactory { public: explicit ContextFactoryImpl(RootRenderNode* rootNode) : mRootNode(rootNode) {} @@ -84,6 +83,5 @@ public: private: RootRenderNode* mRootNode; }; -#endif } // namespace android::uirenderer diff --git a/libs/hwui/WebViewFunctorManager.cpp b/libs/hwui/WebViewFunctorManager.cpp index efa9b1174a3a..9d16ee86739e 100644 --- a/libs/hwui/WebViewFunctorManager.cpp +++ b/libs/hwui/WebViewFunctorManager.cpp @@ -87,7 +87,7 @@ void WebViewFunctor_release(int functor) { WebViewFunctorManager::instance().releaseFunctor(functor); } -void WebViewFunctor_reportRenderingThreads(int functor, const int32_t* thread_ids, size_t size) { +void WebViewFunctor_reportRenderingThreads(int functor, const pid_t* thread_ids, size_t size) { WebViewFunctorManager::instance().reportRenderingThreads(functor, thread_ids, size); } @@ -265,8 +265,8 @@ void WebViewFunctor::reparentSurfaceControl(ASurfaceControl* parent) { funcs.transactionDeleteFunc(transaction); } -void WebViewFunctor::reportRenderingThreads(const int32_t* thread_ids, size_t size) { - mRenderingThreads = std::vector<int32_t>(thread_ids, thread_ids + size); +void WebViewFunctor::reportRenderingThreads(const pid_t* thread_ids, size_t size) { + mRenderingThreads = std::vector<pid_t>(thread_ids, thread_ids + size); } WebViewFunctorManager& WebViewFunctorManager::instance() { @@ -355,7 +355,7 @@ void WebViewFunctorManager::destroyFunctor(int functor) { } } -void WebViewFunctorManager::reportRenderingThreads(int functor, const int32_t* thread_ids, +void WebViewFunctorManager::reportRenderingThreads(int functor, const pid_t* thread_ids, size_t size) { std::lock_guard _lock{mLock}; for (auto& iter : mFunctors) { @@ -366,8 +366,8 @@ void WebViewFunctorManager::reportRenderingThreads(int functor, const int32_t* t } } -std::vector<int32_t> WebViewFunctorManager::getRenderingThreadsForActiveFunctors() { - std::vector<int32_t> renderingThreads; +std::vector<pid_t> WebViewFunctorManager::getRenderingThreadsForActiveFunctors() { + std::vector<pid_t> renderingThreads; std::lock_guard _lock{mLock}; for (const auto& iter : mActiveFunctors) { const auto& functorThreads = iter->getRenderingThreads(); diff --git a/libs/hwui/WebViewFunctorManager.h b/libs/hwui/WebViewFunctorManager.h index 2d77dd8d09bc..ec17640f9b5e 100644 --- a/libs/hwui/WebViewFunctorManager.h +++ b/libs/hwui/WebViewFunctorManager.h @@ -17,13 +17,11 @@ #pragma once #include <private/hwui/WebViewFunctor.h> -#ifdef __ANDROID__ // Layoutlib does not support render thread #include <renderthread/RenderProxy.h> -#endif - #include <utils/LightRefBase.h> #include <utils/Log.h> #include <utils/StrongPointer.h> + #include <mutex> #include <vector> @@ -38,11 +36,7 @@ public: class Handle : public LightRefBase<Handle> { public: - ~Handle() { -#ifdef __ANDROID__ // Layoutlib does not support render thread - renderthread::RenderProxy::destroyFunctor(id()); -#endif - } + ~Handle() { renderthread::RenderProxy::destroyFunctor(id()); } int id() const { return mReference.id(); } @@ -60,7 +54,7 @@ public: void onRemovedFromTree() { mReference.onRemovedFromTree(); } - const std::vector<int32_t>& getRenderingThreads() const { + const std::vector<pid_t>& getRenderingThreads() const { return mReference.getRenderingThreads(); } @@ -85,8 +79,8 @@ public: ASurfaceControl* getSurfaceControl(); void mergeTransaction(ASurfaceTransaction* transaction); - void reportRenderingThreads(const int32_t* thread_ids, size_t size); - const std::vector<int32_t>& getRenderingThreads() const { return mRenderingThreads; } + void reportRenderingThreads(const pid_t* thread_ids, size_t size); + const std::vector<pid_t>& getRenderingThreads() const { return mRenderingThreads; } sp<Handle> createHandle() { LOG_ALWAYS_FATAL_IF(mCreatedHandle); @@ -107,7 +101,7 @@ private: bool mCreatedHandle = false; int32_t mParentSurfaceControlGenerationId = 0; ASurfaceControl* mSurfaceControl = nullptr; - std::vector<int32_t> mRenderingThreads; + std::vector<pid_t> mRenderingThreads; }; class WebViewFunctorManager { @@ -118,8 +112,8 @@ public: void releaseFunctor(int functor); void onContextDestroyed(); void destroyFunctor(int functor); - void reportRenderingThreads(int functor, const int32_t* thread_ids, size_t size); - std::vector<int32_t> getRenderingThreadsForActiveFunctors(); + void reportRenderingThreads(int functor, const pid_t* thread_ids, size_t size); + std::vector<pid_t> getRenderingThreadsForActiveFunctors(); sp<WebViewFunctor::Handle> handleFor(int functor); diff --git a/libs/hwui/apex/LayoutlibLoader.cpp b/libs/hwui/apex/LayoutlibLoader.cpp index 770822a049b7..fd9915a54bb5 100644 --- a/libs/hwui/apex/LayoutlibLoader.cpp +++ b/libs/hwui/apex/LayoutlibLoader.cpp @@ -164,8 +164,10 @@ static vector<string> parseCsv(JNIEnv* env, jstring csvJString) { } // namespace android using namespace android; +using namespace android::uirenderer; void init_android_graphics() { + Properties::overrideRenderPipelineType(RenderPipelineType::SkiaCpu); SkGraphics::Init(); } diff --git a/libs/hwui/jni/AnimatedImageDrawable.cpp b/libs/hwui/jni/AnimatedImageDrawable.cpp index 0f80c55d0ed0..b01e38d014a9 100644 --- a/libs/hwui/jni/AnimatedImageDrawable.cpp +++ b/libs/hwui/jni/AnimatedImageDrawable.cpp @@ -27,6 +27,8 @@ #include <hwui/ImageDecoder.h> #ifdef __ANDROID__ #include <utils/Looper.h> +#else +#include "utils/MessageHandler.h" #endif #include "ColorFilter.h" @@ -182,23 +184,6 @@ static void AnimatedImageDrawable_nSetRepeatCount(JNIEnv* env, jobject /*clazz*/ drawable->setRepetitionCount(loopCount); } -#ifndef __ANDROID__ -struct Message { - Message(int w) {} -}; - -class MessageHandler : public virtual RefBase { -protected: - virtual ~MessageHandler() override {} - -public: - /** - * Handles a message. - */ - virtual void handleMessage(const Message& message) = 0; -}; -#endif - class InvokeListener : public MessageHandler { public: InvokeListener(JNIEnv* env, jobject javaObject) { diff --git a/libs/hwui/jni/Bitmap.cpp b/libs/hwui/jni/Bitmap.cpp index 9e21f860ce21..d4157008ca46 100644 --- a/libs/hwui/jni/Bitmap.cpp +++ b/libs/hwui/jni/Bitmap.cpp @@ -1,8 +1,14 @@ // #define LOG_NDEBUG 0 #include "Bitmap.h" +#include <android-base/unique_fd.h> #include <hwui/Bitmap.h> #include <hwui/Paint.h> +#include <inttypes.h> +#include <renderthread/RenderProxy.h> +#include <string.h> + +#include <memory> #include "CreateJavaOutputStreamAdaptor.h" #include "Gainmap.h" @@ -24,16 +30,6 @@ #include "SkTypes.h" #include "android_nio_utils.h" -#ifdef __ANDROID__ // Layoutlib does not support graphic buffer, parcel or render thread -#include <android-base/unique_fd.h> -#include <renderthread/RenderProxy.h> -#endif - -#include <inttypes.h> -#include <string.h> - -#include <memory> - #define DEBUG_PARCEL 0 static jclass gBitmap_class; @@ -1105,11 +1101,9 @@ static jboolean Bitmap_sameAs(JNIEnv* env, jobject, jlong bm0Handle, jlong bm1Ha } static void Bitmap_prepareToDraw(JNIEnv* env, jobject, jlong bitmapPtr) { -#ifdef __ANDROID__ // Layoutlib does not support render thread LocalScopedBitmap bitmapHandle(bitmapPtr); if (!bitmapHandle.valid()) return; android::uirenderer::renderthread::RenderProxy::prepareToDraw(bitmapHandle->bitmap()); -#endif } static jint Bitmap_getAllocationByteCount(JNIEnv* env, jobject, jlong bitmapPtr) { diff --git a/libs/hwui/jni/android_graphics_DisplayListCanvas.cpp b/libs/hwui/jni/android_graphics_DisplayListCanvas.cpp index 426644ee6a4e..948362c30a31 100644 --- a/libs/hwui/jni/android_graphics_DisplayListCanvas.cpp +++ b/libs/hwui/jni/android_graphics_DisplayListCanvas.cpp @@ -16,22 +16,19 @@ #include "GraphicsJNI.h" -#ifdef __ANDROID__ // Layoutlib does not support Looper and device properties +#ifdef __ANDROID__ // Layoutlib does not support Looper #include <utils/Looper.h> #endif -#include <SkRegion.h> -#include <SkRuntimeEffect.h> - +#include <CanvasProperty.h> #include <Rect.h> #include <RenderNode.h> -#include <CanvasProperty.h> +#include <SkRegion.h> +#include <SkRuntimeEffect.h> #include <hwui/Canvas.h> #include <hwui/Paint.h> #include <minikin/Layout.h> -#ifdef __ANDROID__ // Layoutlib does not support RenderThread #include <renderthread/RenderProxy.h> -#endif namespace android { @@ -85,11 +82,7 @@ static void android_view_DisplayListCanvas_resetDisplayListCanvas(CRITICAL_JNI_P } static jint android_view_DisplayListCanvas_getMaxTextureSize(JNIEnv*, jobject) { -#ifdef __ANDROID__ // Layoutlib does not support RenderProxy (RenderThread) return android::uirenderer::renderthread::RenderProxy::maxTextureSize(); -#else - return 4096; -#endif } static void android_view_DisplayListCanvas_enableZ(CRITICAL_JNI_PARAMS_COMMA jlong canvasPtr, diff --git a/libs/hwui/jni/android_graphics_RenderNode.cpp b/libs/hwui/jni/android_graphics_RenderNode.cpp index a7d64231da80..6e03bbd0fa16 100644 --- a/libs/hwui/jni/android_graphics_RenderNode.cpp +++ b/libs/hwui/jni/android_graphics_RenderNode.cpp @@ -15,19 +15,17 @@ */ #define ATRACE_TAG ATRACE_TAG_VIEW -#include "GraphicsJNI.h" - #include <Animator.h> #include <DamageAccumulator.h> #include <Matrix.h> #include <RenderNode.h> -#ifdef __ANDROID__ // Layoutlib does not support CanvasContext -#include <renderthread/CanvasContext.h> -#endif #include <TreeInfo.h> #include <effects/StretchEffect.h> #include <gui/TraceUtils.h> #include <hwui/Paint.h> +#include <renderthread/CanvasContext.h> + +#include "GraphicsJNI.h" namespace android { @@ -640,7 +638,6 @@ static void android_view_RenderNode_requestPositionUpdates(JNIEnv* env, jobject, ATRACE_NAME("Update SurfaceView position"); -#ifdef __ANDROID__ // Layoutlib does not support CanvasContext JNIEnv* env = jnienv(); // Update the new position synchronously. We cannot defer this to // a worker pool to process asynchronously because the UI thread @@ -669,7 +666,6 @@ static void android_view_RenderNode_requestPositionUpdates(JNIEnv* env, jobject, env->DeleteGlobalRef(mListener); mListener = nullptr; } -#endif } virtual void onPositionLost(RenderNode& node, const TreeInfo* info) override { @@ -682,7 +678,6 @@ static void android_view_RenderNode_requestPositionUpdates(JNIEnv* env, jobject, ATRACE_NAME("SurfaceView position lost"); JNIEnv* env = jnienv(); -#ifdef __ANDROID__ // Layoutlib does not support CanvasContext // Update the lost position synchronously. We cannot defer this to // a worker pool to process asynchronously because the UI thread // may be unblocked by the time a worker thread can process this, @@ -698,7 +693,6 @@ static void android_view_RenderNode_requestPositionUpdates(JNIEnv* env, jobject, env->DeleteGlobalRef(mListener); mListener = nullptr; } -#endif } private: @@ -750,7 +744,6 @@ static void android_view_RenderNode_requestPositionUpdates(JNIEnv* env, jobject, StretchEffectBehavior::Shader) { JNIEnv* env = jnienv(); -#ifdef __ANDROID__ // Layoutlib does not support CanvasContext SkVector stretchDirection = effect->getStretchDirection(); jboolean keepListening = env->CallStaticBooleanMethod( gPositionListener.clazz, gPositionListener.callApplyStretch, mListener, @@ -762,7 +755,6 @@ static void android_view_RenderNode_requestPositionUpdates(JNIEnv* env, jobject, env->DeleteGlobalRef(mListener); mListener = nullptr; } -#endif } } diff --git a/libs/hwui/pipeline/skia/SkiaDisplayList.cpp b/libs/hwui/pipeline/skia/SkiaDisplayList.cpp index e0216b680064..36dc933aa7b0 100644 --- a/libs/hwui/pipeline/skia/SkiaDisplayList.cpp +++ b/libs/hwui/pipeline/skia/SkiaDisplayList.cpp @@ -15,22 +15,18 @@ */ #include "SkiaDisplayList.h" -#include "FunctorDrawable.h" +#include <SkImagePriv.h> +#include <SkPathOps.h> + +// clang-format off +#include "FunctorDrawable.h" // Must be included before DumpOpsCanvas.h #include "DumpOpsCanvas.h" -#ifdef __ANDROID__ // Layoutlib does not support SkiaPipeline +// clang-format on #include "SkiaPipeline.h" -#else -#include "DamageAccumulator.h" -#endif #include "TreeInfo.h" #include "VectorDrawable.h" -#ifdef __ANDROID__ #include "renderthread/CanvasContext.h" -#endif - -#include <SkImagePriv.h> -#include <SkPathOps.h> namespace android { namespace uirenderer { @@ -101,7 +97,6 @@ bool SkiaDisplayList::prepareListAndChildren( // If the prepare tree is triggered by the UI thread and no previous call to // pinImages has failed then we must pin all mutable images in the GPU cache // until the next UI thread draw. -#ifdef __ANDROID__ // Layoutlib does not support CanvasContext if (info.prepareTextures && !info.canvasContext.pinImages(mMutableImages)) { // In the event that pinning failed we prevent future pinImage calls for the // remainder of this tree traversal and also unpin any currently pinned images @@ -110,11 +105,11 @@ bool SkiaDisplayList::prepareListAndChildren( info.canvasContext.unpinImages(); } +#ifdef __ANDROID__ auto grContext = info.canvasContext.getGrContext(); for (const auto& bufferData : mMeshBufferData) { bufferData->updateBuffers(grContext); } - #endif bool hasBackwardProjectedNodesHere = false; diff --git a/libs/hwui/platform/host/WebViewFunctorManager.cpp b/libs/hwui/platform/host/WebViewFunctorManager.cpp index 1d16655bf73c..4ba206b41b39 100644 --- a/libs/hwui/platform/host/WebViewFunctorManager.cpp +++ b/libs/hwui/platform/host/WebViewFunctorManager.cpp @@ -50,6 +50,8 @@ ASurfaceControl* WebViewFunctor::getSurfaceControl() { void WebViewFunctor::mergeTransaction(ASurfaceTransaction* transaction) {} +void WebViewFunctor::reportRenderingThreads(const pid_t* thread_ids, size_t size) {} + void WebViewFunctor::reparentSurfaceControl(ASurfaceControl* parent) {} WebViewFunctorManager& WebViewFunctorManager::instance() { @@ -68,6 +70,13 @@ void WebViewFunctorManager::onContextDestroyed() {} void WebViewFunctorManager::destroyFunctor(int functor) {} +void WebViewFunctorManager::reportRenderingThreads(int functor, const pid_t* thread_ids, + size_t size) {} + +std::vector<pid_t> WebViewFunctorManager::getRenderingThreadsForActiveFunctors() { + return {}; +} + sp<WebViewFunctor::Handle> WebViewFunctorManager::handleFor(int functor) { return nullptr; } diff --git a/libs/hwui/platform/host/renderthread/HintSessionWrapper.cpp b/libs/hwui/platform/host/renderthread/HintSessionWrapper.cpp new file mode 100644 index 000000000000..b1b1d5830834 --- /dev/null +++ b/libs/hwui/platform/host/renderthread/HintSessionWrapper.cpp @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "renderthread/HintSessionWrapper.h" + +namespace android { +namespace uirenderer { +namespace renderthread { + +void HintSessionWrapper::HintSessionBinding::init() {} + +HintSessionWrapper::HintSessionWrapper(pid_t uiThreadId, pid_t renderThreadId) + : mUiThreadId(uiThreadId) + , mRenderThreadId(renderThreadId) + , mBinding(std::make_shared<HintSessionBinding>()) {} + +HintSessionWrapper::~HintSessionWrapper() {} + +void HintSessionWrapper::destroy() {} + +bool HintSessionWrapper::init() { + return false; +} + +void HintSessionWrapper::updateTargetWorkDuration(long targetWorkDurationNanos) {} + +void HintSessionWrapper::reportActualWorkDuration(long actualDurationNanos) {} + +void HintSessionWrapper::sendLoadResetHint() {} + +void HintSessionWrapper::sendLoadIncreaseHint() {} + +bool HintSessionWrapper::alive() { + return false; +} + +nsecs_t HintSessionWrapper::getLastUpdate() { + return -1; +} + +void HintSessionWrapper::delayedDestroy(RenderThread& rt, nsecs_t delay, + std::shared_ptr<HintSessionWrapper> wrapperPtr) {} + +void HintSessionWrapper::setActiveFunctorThreads(std::vector<pid_t> threadIds) {} + +} /* namespace renderthread */ +} /* namespace uirenderer */ +} /* namespace android */ diff --git a/libs/hwui/platform/host/renderthread/ReliableSurface.cpp b/libs/hwui/platform/host/renderthread/ReliableSurface.cpp new file mode 100644 index 000000000000..2deaaf3b909c --- /dev/null +++ b/libs/hwui/platform/host/renderthread/ReliableSurface.cpp @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2018 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. + */ + +#include "renderthread/ReliableSurface.h" + +#include <log/log_main.h> +#include <system/window.h> + +namespace android::uirenderer::renderthread { + +ReliableSurface::ReliableSurface(ANativeWindow* window) : mWindow(window) { + LOG_ALWAYS_FATAL_IF(!mWindow, "Error, unable to wrap a nullptr"); + ANativeWindow_acquire(mWindow); +} + +ReliableSurface::~ReliableSurface() { + ANativeWindow_release(mWindow); +} + +void ReliableSurface::init() {} + +int ReliableSurface::reserveNext() { + return OK; +} + +}; // namespace android::uirenderer::renderthread diff --git a/libs/hwui/platform/host/utils/MessageHandler.h b/libs/hwui/platform/host/utils/MessageHandler.h new file mode 100644 index 000000000000..51ee48e0c6d2 --- /dev/null +++ b/libs/hwui/platform/host/utils/MessageHandler.h @@ -0,0 +1,34 @@ +/* + * 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. + */ + +#pragma once + +#include <utils/RefBase.h> + +struct Message { + Message(int w) {} +}; + +class MessageHandler : public virtual android::RefBase { +protected: + virtual ~MessageHandler() override {} + +public: + /** + * Handles a message. + */ + virtual void handleMessage(const Message& message) = 0; +}; diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp index 22de2f29792d..66e089627a7b 100644 --- a/libs/hwui/renderthread/CanvasContext.cpp +++ b/libs/hwui/renderthread/CanvasContext.cpp @@ -35,6 +35,7 @@ #include "Properties.h" #include "RenderThread.h" #include "hwui/Canvas.h" +#include "pipeline/skia/SkiaCpuPipeline.h" #include "pipeline/skia/SkiaGpuPipeline.h" #include "pipeline/skia/SkiaOpenGLPipeline.h" #include "pipeline/skia/SkiaVulkanPipeline.h" @@ -72,7 +73,7 @@ CanvasContext* ScopedActiveContext::sActiveContext = nullptr; CanvasContext* CanvasContext::create(RenderThread& thread, bool translucent, RenderNode* rootRenderNode, IContextFactory* contextFactory, - int32_t uiThreadId, int32_t renderThreadId) { + pid_t uiThreadId, pid_t renderThreadId) { auto renderType = Properties::getRenderPipelineType(); switch (renderType) { @@ -84,6 +85,12 @@ CanvasContext* CanvasContext::create(RenderThread& thread, bool translucent, return new CanvasContext(thread, translucent, rootRenderNode, contextFactory, std::make_unique<skiapipeline::SkiaVulkanPipeline>(thread), uiThreadId, renderThreadId); +#ifndef __ANDROID__ + case RenderPipelineType::SkiaCpu: + return new CanvasContext(thread, translucent, rootRenderNode, contextFactory, + std::make_unique<skiapipeline::SkiaCpuPipeline>(thread), + uiThreadId, renderThreadId); +#endif default: LOG_ALWAYS_FATAL("canvas context type %d not supported", (int32_t)renderType); break; @@ -182,6 +189,7 @@ static void setBufferCount(ANativeWindow* window) { } void CanvasContext::setHardwareBuffer(AHardwareBuffer* buffer) { +#ifdef __ANDROID__ if (mHardwareBuffer) { AHardwareBuffer_release(mHardwareBuffer); mHardwareBuffer = nullptr; @@ -192,6 +200,7 @@ void CanvasContext::setHardwareBuffer(AHardwareBuffer* buffer) { mHardwareBuffer = buffer; } mRenderPipeline->setHardwareBuffer(mHardwareBuffer); +#endif } void CanvasContext::setSurface(ANativeWindow* window, bool enableTimeout) { @@ -561,6 +570,7 @@ Frame CanvasContext::getFrame() { } void CanvasContext::draw(bool solelyTextureViewUpdates) { +#ifdef __ANDROID__ if (auto grContext = getGrContext()) { if (grContext->abandoned()) { if (grContext->isDeviceLost()) { @@ -571,6 +581,7 @@ void CanvasContext::draw(bool solelyTextureViewUpdates) { return; } } +#endif SkRect dirty; mDamageAccumulator.finish(&dirty); @@ -594,11 +605,13 @@ void CanvasContext::draw(bool solelyTextureViewUpdates) { if (skippedFrameReason) { mCurrentFrameInfo->setSkippedFrameReason(*skippedFrameReason); +#ifdef __ANDROID__ if (auto grContext = getGrContext()) { // Submit to ensure that any texture uploads complete and Skia can // free its staging buffers. grContext->flushAndSubmit(); } +#endif // Notify the callbacks, even if there's nothing to draw so they aren't waiting // indefinitely diff --git a/libs/hwui/renderthread/DrawFrameTask.cpp b/libs/hwui/renderthread/DrawFrameTask.cpp index 1b333bfccbf1..826d00e1f32f 100644 --- a/libs/hwui/renderthread/DrawFrameTask.cpp +++ b/libs/hwui/renderthread/DrawFrameTask.cpp @@ -140,12 +140,14 @@ void DrawFrameTask::run() { if (CC_LIKELY(canDrawThisFrame)) { context->draw(solelyTextureViewUpdates); } else { +#ifdef __ANDROID__ // Do a flush in case syncFrameState performed any texture uploads. Since we skipped // the draw() call, those uploads (or deletes) will end up sitting in the queue. // Do them now if (GrDirectContext* grContext = mRenderThread->getGrContext()) { grContext->flushAndSubmit(); } +#endif // wait on fences so tasks don't overlap next frame context->waitOnFences(); } @@ -176,11 +178,13 @@ bool DrawFrameTask::syncFrameState(TreeInfo& info) { bool canDraw = mContext->makeCurrent(); mContext->unpinImages(); +#ifdef __ANDROID__ for (size_t i = 0; i < mLayers.size(); i++) { if (mLayers[i]) { mLayers[i]->apply(); } } +#endif mLayers.clear(); mContext->setContentDrawBounds(mContentDrawBounds); diff --git a/libs/hwui/renderthread/ReliableSurface.h b/libs/hwui/renderthread/ReliableSurface.h index 595964741049..d6a4d50d3327 100644 --- a/libs/hwui/renderthread/ReliableSurface.h +++ b/libs/hwui/renderthread/ReliableSurface.h @@ -21,7 +21,9 @@ #include <apex/window.h> #include <utils/Errors.h> #include <utils/Macros.h> +#ifdef __ANDROID__ #include <utils/NdkUtils.h> +#endif #include <utils/StrongPointer.h> #include <memory> @@ -62,9 +64,11 @@ private: mutable std::mutex mMutex; uint64_t mUsage = AHARDWAREBUFFER_USAGE_GPU_FRAMEBUFFER; +#ifdef __ANDROID__ AHardwareBuffer_Format mFormat = AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM; UniqueAHardwareBuffer mScratchBuffer; ANativeWindowBuffer* mReservedBuffer = nullptr; +#endif base::unique_fd mReservedFenceFd; bool mHasDequeuedBuffer = false; int mBufferQueueState = OK; diff --git a/libs/hwui/renderthread/RenderProxy.cpp b/libs/hwui/renderthread/RenderProxy.cpp index eab36050896f..715153b5083d 100644 --- a/libs/hwui/renderthread/RenderProxy.cpp +++ b/libs/hwui/renderthread/RenderProxy.cpp @@ -42,7 +42,11 @@ namespace renderthread { RenderProxy::RenderProxy(bool translucent, RenderNode* rootRenderNode, IContextFactory* contextFactory) : mRenderThread(RenderThread::getInstance()), mContext(nullptr) { +#ifdef __ANDROID__ pid_t uiThreadId = pthread_gettid_np(pthread_self()); +#else + pid_t uiThreadId = 0; +#endif pid_t renderThreadId = getRenderThreadTid(); mContext = mRenderThread.queue().runSync([=, this]() -> CanvasContext* { CanvasContext* context = CanvasContext::create(mRenderThread, translucent, rootRenderNode, @@ -90,6 +94,7 @@ void RenderProxy::setName(const char* name) { } void RenderProxy::setHardwareBuffer(AHardwareBuffer* buffer) { +#ifdef __ANDROID__ if (buffer) { AHardwareBuffer_acquire(buffer); } @@ -99,6 +104,7 @@ void RenderProxy::setHardwareBuffer(AHardwareBuffer* buffer) { AHardwareBuffer_release(hardwareBuffer); } }); +#endif } void RenderProxy::setSurface(ANativeWindow* window, bool enableTimeout) { @@ -216,7 +222,9 @@ void RenderProxy::cancelLayerUpdate(DeferredLayerUpdater* layer) { } void RenderProxy::detachSurfaceTexture(DeferredLayerUpdater* layer) { +#ifdef __ANDROID__ return mRenderThread.queue().runSync([&]() { layer->detachSurfaceTexture(); }); +#endif } void RenderProxy::destroyHardwareResources() { @@ -324,11 +332,13 @@ void RenderProxy::dumpGraphicsMemory(int fd, bool includeProfileData, bool reset } }); } +#ifdef __ANDROID__ if (!Properties::isolatedProcess) { std::string grallocInfo; GraphicBufferAllocator::getInstance().dump(grallocInfo); dprintf(fd, "%s\n", grallocInfo.c_str()); } +#endif } void RenderProxy::getMemoryUsage(size_t* cpuUsage, size_t* gpuUsage) { @@ -352,7 +362,11 @@ void RenderProxy::rotateProcessStatsBuffer() { } int RenderProxy::getRenderThreadTid() { +#ifdef __ANDROID__ return mRenderThread.getTid(); +#else + return 0; +#endif } void RenderProxy::addRenderNode(RenderNode* node, bool placeFront) { @@ -461,7 +475,7 @@ void RenderProxy::prepareToDraw(Bitmap& bitmap) { int RenderProxy::copyHWBitmapInto(Bitmap* hwBitmap, SkBitmap* bitmap) { ATRACE_NAME("HardwareBitmap readback"); RenderThread& thread = RenderThread::getInstance(); - if (gettid() == thread.getTid()) { + if (RenderThread::isCurrent()) { // TODO: fix everything that hits this. We should never be triggering a readback ourselves. return (int)thread.readback().copyHWBitmapInto(hwBitmap, bitmap); } else { @@ -472,7 +486,7 @@ int RenderProxy::copyHWBitmapInto(Bitmap* hwBitmap, SkBitmap* bitmap) { int RenderProxy::copyImageInto(const sk_sp<SkImage>& image, SkBitmap* bitmap) { RenderThread& thread = RenderThread::getInstance(); - if (gettid() == thread.getTid()) { + if (RenderThread::isCurrent()) { // TODO: fix everything that hits this. We should never be triggering a readback ourselves. return (int)thread.readback().copyImageInto(image, bitmap); } else { diff --git a/media/java/android/media/session/ISessionManager.aidl b/media/java/android/media/session/ISessionManager.aidl index 207ccbee0b50..871e9ab87299 100644 --- a/media/java/android/media/session/ISessionManager.aidl +++ b/media/java/android/media/session/ISessionManager.aidl @@ -80,4 +80,7 @@ interface ISessionManager { boolean hasCustomMediaSessionPolicyProvider(String componentName); int getSessionPolicies(in MediaSession.Token token); void setSessionPolicies(in MediaSession.Token token, int policies); + + // For testing of temporarily engaged sessions. + void expireTempEngagedSessions(); } diff --git a/packages/SettingsLib/Spa/gradle/libs.versions.toml b/packages/SettingsLib/Spa/gradle/libs.versions.toml index 0ee9d595d875..85ad160f6d66 100644 --- a/packages/SettingsLib/Spa/gradle/libs.versions.toml +++ b/packages/SettingsLib/Spa/gradle/libs.versions.toml @@ -15,7 +15,7 @@ # [versions] -agp = "8.3.1" +agp = "8.3.2" compose-compiler = "1.5.11" dexmaker-mockito = "2.28.3" jvm = "17" diff --git a/packages/SettingsLib/Spa/gradle/wrapper/gradle-8.6-bin.zip b/packages/SettingsLib/Spa/gradle/wrapper/gradle-8.7-bin.zip Binary files differindex 5c9634782bbe..7a9ac5afe013 100644 --- a/packages/SettingsLib/Spa/gradle/wrapper/gradle-8.6-bin.zip +++ b/packages/SettingsLib/Spa/gradle/wrapper/gradle-8.7-bin.zip diff --git a/packages/SettingsLib/Spa/gradle/wrapper/gradle-wrapper.properties b/packages/SettingsLib/Spa/gradle/wrapper/gradle-wrapper.properties index 50ff9dff549b..182095e76e76 100644 --- a/packages/SettingsLib/Spa/gradle/wrapper/gradle-wrapper.properties +++ b/packages/SettingsLib/Spa/gradle/wrapper/gradle-wrapper.properties @@ -16,6 +16,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=gradle-8.6-bin.zip +distributionUrl=gradle-8.7-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/packages/SettingsLib/Spa/spa/build.gradle.kts b/packages/SettingsLib/Spa/spa/build.gradle.kts index 2f2ac2467a6c..6344501ce789 100644 --- a/packages/SettingsLib/Spa/spa/build.gradle.kts +++ b/packages/SettingsLib/Spa/spa/build.gradle.kts @@ -65,7 +65,7 @@ dependencies { api("androidx.lifecycle:lifecycle-runtime-compose") api("androidx.navigation:navigation-compose:2.8.0-alpha05") api("com.github.PhilJay:MPAndroidChart:v3.1.0-alpha") - api("com.google.android.material:material:1.7.0-alpha03") + api("com.google.android.material:material:1.11.0") debugApi("androidx.compose.ui:ui-tooling:$jetpackComposeVersion") implementation("com.airbnb.android:lottie-compose:5.2.0") diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml index 7e6b004be9b8..4640de304ed8 100644 --- a/packages/SettingsLib/res/values/strings.xml +++ b/packages/SettingsLib/res/values/strings.xml @@ -1148,6 +1148,16 @@ <!-- [CHAR_LIMIT=80] Label for battery charging future pause --> <string name="power_charging_future_paused"><xliff:g id="level">%1$s</xliff:g> - Charging</string> + <!-- [CHAR_LIMIT=40] Label for battery level when fast charging with duration. --> + <string name="power_fast_charging_duration_v2"><xliff:g id="level">%1$s</xliff:g> - <xliff:g id="status">%2$s</xliff:g> - Full by <xliff:g id="time">%3$s</xliff:g></string> + <!-- [CHAR_LIMIT=40] Label for battery level when non-fast charging with duration. --> + <string name="power_charging_duration_v2"><xliff:g id="level">%1$s</xliff:g> - Fully charged by <xliff:g id="time">%2$s</xliff:g></string> + + <!-- [CHAR_LIMIT=40] Label for estimated remaining duration of battery charging. --> + <string name="power_remaining_charging_duration_only_v2">Fully charged by <xliff:g id="time">%1$s</xliff:g></string> + <!-- [CHAR_LIMIT=40] Label for estimated remaining duration of battery charging. --> + <string name="power_remaining_fast_charging_duration_only_v2">Full by <xliff:g id="time">%1$s</xliff:g></string> + <!-- Battery Info screen. Value for a status item. Used for diagnostic info screens, precise translation isn't needed --> <string name="battery_info_status_unknown">Unknown</string> <!-- [CHAR_LIMIT=20] Battery use screen. Battery status shown in chart label when charging from an unknown source. --> @@ -1171,6 +1181,11 @@ <!-- [CHAR_LIMIT=None] Battery Info screen. Value for a status item. A state which device charging on hold --> <string name="battery_info_status_charging_on_hold">Charging on hold</string> + <!-- [CHAR_LIMIT=20] Battery use screen. Battery status shown in chart label when charging isn't fast. --> + <string name="battery_info_status_charging_v2">Charging</string> + <!-- [CHAR_LIMIT=20] Battery use screen. Battery status shown in chart label when charging speed is fast. --> + <string name="battery_info_status_charging_fast_v2">Fast charging</string> + <!-- Summary for settings preference disabled by administrator [CHAR LIMIT=50] --> <string name="disabled_by_admin_summary_text">Controlled by admin</string> diff --git a/packages/SettingsLib/src/com/android/settingslib/Utils.java b/packages/SettingsLib/src/com/android/settingslib/Utils.java index e95a506376fd..563f02d95f3c 100644 --- a/packages/SettingsLib/src/com/android/settingslib/Utils.java +++ b/packages/SettingsLib/src/com/android/settingslib/Utils.java @@ -65,6 +65,7 @@ import com.android.launcher3.icons.IconFactory; import com.android.launcher3.util.UserIconInfo; import com.android.settingslib.drawable.UserIconDrawable; import com.android.settingslib.fuelgauge.BatteryStatus; +import com.android.settingslib.fuelgauge.BatteryUtils; import com.android.settingslib.utils.BuildCompatUtils; import java.util.List; @@ -246,25 +247,23 @@ public class Utils { } else { if (status == BatteryManager.BATTERY_STATUS_CHARGING) { if (compactStatus) { - statusString = res.getString(R.string.battery_info_status_charging); + statusString = getRegularChargingStatusString(res); } else if (batteryStatus.isPluggedInWired()) { switch (batteryStatus.getChargingSpeed(context)) { case BatteryStatus.CHARGING_FAST: - statusString = - res.getString(R.string.battery_info_status_charging_fast); + statusString = getFastChargingStatusString(res); break; case BatteryStatus.CHARGING_SLOWLY: - statusString = - res.getString(R.string.battery_info_status_charging_slow); + statusString = getSlowChargingStatusString(res); break; default: - statusString = res.getString(R.string.battery_info_status_charging); + statusString = getRegularChargingStatusString(res); break; } } else if (batteryStatus.isPluggedInDock()) { - statusString = res.getString(R.string.battery_info_status_charging_dock); + statusString = getDockChargingStatusString(res); } else { - statusString = res.getString(R.string.battery_info_status_charging_wireless); + statusString = getWirelessChargingStatusString(res); } } else if (status == BatteryManager.BATTERY_STATUS_DISCHARGING) { statusString = res.getString(R.string.battery_info_status_discharging); @@ -276,6 +275,41 @@ public class Utils { return statusString; } + private static String getFastChargingStatusString(Resources res) { + return res.getString( + BatteryUtils.isChargingStringV2Enabled() + ? R.string.battery_info_status_charging_fast_v2 + : R.string.battery_info_status_charging_fast); + } + + private static String getSlowChargingStatusString(Resources res) { + return res.getString( + BatteryUtils.isChargingStringV2Enabled() + ? R.string.battery_info_status_charging_v2 + : R.string.battery_info_status_charging_slow); + } + + private static String getRegularChargingStatusString(Resources res) { + return res.getString( + BatteryUtils.isChargingStringV2Enabled() + ? R.string.battery_info_status_charging_v2 + : R.string.battery_info_status_charging); + } + + private static String getWirelessChargingStatusString(Resources res) { + return res.getString( + BatteryUtils.isChargingStringV2Enabled() + ? R.string.battery_info_status_charging_v2 + : R.string.battery_info_status_charging_wireless); + } + + private static String getDockChargingStatusString(Resources res) { + return res.getString( + BatteryUtils.isChargingStringV2Enabled() + ? R.string.battery_info_status_charging_v2 + : R.string.battery_info_status_charging_dock); + } + public static ColorStateList getColorAccent(Context context) { return getColorAttr(context, android.R.attr.colorAccent); } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java index 0996d52b0e30..e926b1684348 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java @@ -57,6 +57,7 @@ public class BluetoothEventManager { private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); private final LocalBluetoothAdapter mLocalAdapter; + private final LocalBluetoothManager mBtManager; private final CachedBluetoothDeviceManager mDeviceManager; private final IntentFilter mAdapterIntentFilter, mProfileIntentFilter; private final Map<String, Handler> mHandlerMap; @@ -80,10 +81,15 @@ public class BluetoothEventManager { * userHandle passed in is {@code null}, we register event receiver for the * {@code context.getUser()} handle. */ - BluetoothEventManager(LocalBluetoothAdapter adapter, - CachedBluetoothDeviceManager deviceManager, Context context, - android.os.Handler handler, @Nullable UserHandle userHandle) { + BluetoothEventManager( + LocalBluetoothAdapter adapter, + LocalBluetoothManager btManager, + CachedBluetoothDeviceManager deviceManager, + Context context, + android.os.Handler handler, + @Nullable UserHandle userHandle) { mLocalAdapter = adapter; + mBtManager = btManager; mDeviceManager = deviceManager; mAdapterIntentFilter = new IntentFilter(); mProfileIntentFilter = new IntentFilter(); @@ -210,11 +216,27 @@ public class BluetoothEventManager { } } - void dispatchProfileConnectionStateChanged(@NonNull CachedBluetoothDevice device, int state, - int bluetoothProfile) { + void dispatchProfileConnectionStateChanged( + @NonNull CachedBluetoothDevice device, int state, int bluetoothProfile) { for (BluetoothCallback callback : mCallbacks) { callback.onProfileConnectionStateChanged(device, state, bluetoothProfile); } + + // Trigger updateFallbackActiveDeviceIfNeeded when ASSISTANT profile disconnected when + // audio sharing is enabled. + if (bluetoothProfile == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT + && state == BluetoothAdapter.STATE_DISCONNECTED + && BluetoothUtils.isAudioSharingEnabled()) { + LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager(); + if (profileManager != null + && profileManager.getLeAudioBroadcastProfile() != null + && profileManager.getLeAudioBroadcastProfile().isProfileReady() + && profileManager.getLeAudioBroadcastAssistantProfile() != null + && profileManager.getLeAudioBroadcastAssistantProfile().isProfileReady()) { + Log.d(TAG, "updateFallbackActiveDeviceIfNeeded, ASSISTANT profile disconnected"); + profileManager.getLeAudioBroadcastProfile().updateFallbackActiveDeviceIfNeeded(); + } + } } private void dispatchConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state) { @@ -536,7 +558,6 @@ public class BluetoothEventManager { default: Log.w(TAG, "ActiveDeviceChangedHandler: unknown action " + action); return; - } dispatchAclStateChanged(activeDevice, state); } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothManager.java index 53c6075ccff4..c4300d214c0c 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothManager.java @@ -21,11 +21,11 @@ import android.os.Handler; import android.os.UserHandle; import android.util.Log; -import java.lang.ref.WeakReference; - import androidx.annotation.Nullable; import androidx.annotation.RequiresPermission; +import java.lang.ref.WeakReference; + /** * LocalBluetoothManager provides a simplified interface on top of a subset of * the Bluetooth API. Note that {@link #getInstance} will return null @@ -111,10 +111,17 @@ public class LocalBluetoothManager { mContext = context.getApplicationContext(); mLocalAdapter = adapter; mCachedDeviceManager = new CachedBluetoothDeviceManager(mContext, this); - mEventManager = new BluetoothEventManager(mLocalAdapter, mCachedDeviceManager, mContext, - handler, userHandle); - mProfileManager = new LocalBluetoothProfileManager(mContext, - mLocalAdapter, mCachedDeviceManager, mEventManager); + mEventManager = + new BluetoothEventManager( + mLocalAdapter, + this, + mCachedDeviceManager, + mContext, + handler, + userHandle); + mProfileManager = + new LocalBluetoothProfileManager( + mContext, mLocalAdapter, mCachedDeviceManager, mEventManager); mProfileManager.updateLocalProfiles(); mEventManager.readPairedDevices(); diff --git a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatteryUtils.java b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatteryUtils.java index 92db50878a70..327e470e7d22 100644 --- a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatteryUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/BatteryUtils.java @@ -21,11 +21,14 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.provider.Settings; +import android.os.SystemProperties; import android.os.UserManager; +import android.provider.Settings; import android.util.ArraySet; import android.view.accessibility.AccessibilityManager; +import androidx.annotation.VisibleForTesting; + import java.util.List; public final class BatteryUtils { @@ -33,6 +36,9 @@ public final class BatteryUtils { /** The key to get the time to full from Settings.Global */ public static final String GLOBAL_TIME_TO_FULL_MILLIS = "time_to_full_millis"; + /** The system property key to check whether the charging string v2 is enabled or not. */ + public static final String PROPERTY_CHARGING_STRING_V2_KEY = "charging_string.apply_v2"; + /** Gets the latest sticky battery intent from the Android system. */ public static Intent getBatteryIntent(Context context) { return context.registerReceiver( @@ -75,4 +81,25 @@ public final class BatteryUtils { final UserManager userManager = context.getSystemService(UserManager.class); return userManager.isManagedProfile() && !userManager.isSystemUser(); } + + private static Boolean sChargingStringV2Enabled = null; + + /** Returns {@code true} if the charging string v2 is enabled. */ + public static boolean isChargingStringV2Enabled() { + if (sChargingStringV2Enabled == null) { + sChargingStringV2Enabled = + SystemProperties.getBoolean(PROPERTY_CHARGING_STRING_V2_KEY, false); + } + return sChargingStringV2Enabled; + } + + + /** Used to override the system property to enable or reset for charging string V2. */ + @VisibleForTesting + public static void setChargingStringV2Enabled(Boolean enabled) { + SystemProperties.set( + BatteryUtils.PROPERTY_CHARGING_STRING_V2_KEY, + enabled == null ? "" : String.valueOf(enabled)); + BatteryUtils.sChargingStringV2Enabled = enabled; + } } diff --git a/packages/SettingsLib/src/com/android/settingslib/utils/PowerUtil.java b/packages/SettingsLib/src/com/android/settingslib/utils/PowerUtil.java index 22726549ce05..5ed59996bee3 100644 --- a/packages/SettingsLib/src/com/android/settingslib/utils/PowerUtil.java +++ b/packages/SettingsLib/src/com/android/settingslib/utils/PowerUtil.java @@ -33,7 +33,7 @@ import java.util.Date; import java.util.Locale; import java.util.concurrent.TimeUnit; -/** Utility class for keeping power related strings consistent**/ +/** Utility class for keeping power related strings consistent. **/ public class PowerUtil { private static final long SEVEN_MINUTES_MILLIS = TimeUnit.MINUTES.toMillis(7); @@ -221,4 +221,19 @@ public class PowerUtil { return time - remainder + multiple; } } + + /** Gets the rounded target time string in a short format. */ + public static String getTargetTimeShortString( + Context context, long targetTimeOffsetMs, long currentTimeMs) { + final long roundedTimeOfDayMs = + roundTimeToNearestThreshold( + currentTimeMs + targetTimeOffsetMs, FIFTEEN_MINUTES_MILLIS); + + // convert the time to a properly formatted string. + String skeleton = android.text.format.DateFormat.getTimeFormatString(context); + DateFormat fmt = DateFormat.getInstanceForSkeleton(skeleton); + Date date = Date.from(Instant.ofEpochMilli(roundedTimeOfDayMs)); + return fmt.format(date); + } } + diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/bluetooth/BluetoothEventManagerIntegTest.java b/packages/SettingsLib/tests/integ/src/com/android/settingslib/bluetooth/BluetoothEventManagerIntegTest.java index 50f5b9d81000..69f6305fa1b2 100644 --- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/bluetooth/BluetoothEventManagerIntegTest.java +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/bluetooth/BluetoothEventManagerIntegTest.java @@ -20,6 +20,8 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; +import static java.util.concurrent.TimeUnit.SECONDS; + import android.app.ActivityManager; import android.content.Context; import android.content.Intent; @@ -37,13 +39,11 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import static java.util.concurrent.TimeUnit.SECONDS; - import java.util.concurrent.CountDownLatch; /** - * Test that verifies that BluetoothEventManager can receive broadcasts for non-current - * users for all bluetooth events. + * Test that verifies that BluetoothEventManager can receive broadcasts for non-current users for + * all bluetooth events. * * <p>Creation and deletion of users takes a long time, so marking this as a LargeTest. */ @@ -64,9 +64,14 @@ public class BluetoothEventManagerIntegTest { mContext = InstrumentationRegistry.getTargetContext(); mUserManager = UserManager.get(mContext); - mBluetoothEventManager = new BluetoothEventManager( - mock(LocalBluetoothAdapter.class), mock(CachedBluetoothDeviceManager.class), - mContext, /* handler= */ null, UserHandle.ALL); + mBluetoothEventManager = + new BluetoothEventManager( + mock(LocalBluetoothAdapter.class), + mock(LocalBluetoothManager.class), + mock(CachedBluetoothDeviceManager.class), + mContext, + /* handler= */ null, + UserHandle.ALL); // Create and start another user in the background. mOtherUser = mUserManager.createUser("TestUser", /* flags= */ 0); diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java index 48bbf4ea6a65..b1489be943e6 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothEventManagerTest.java @@ -22,6 +22,7 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -29,35 +30,47 @@ import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHeadset; import android.bluetooth.BluetoothProfile; +import android.bluetooth.BluetoothStatusCodes; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.UserHandle; +import android.platform.test.flag.junit.SetFlagsRule; import android.telephony.TelephonyManager; import com.android.settingslib.R; +import com.android.settingslib.flags.Flags; +import com.android.settingslib.testutils.shadow.ShadowBluetoothAdapter; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; +import org.robolectric.shadow.api.Shadow; import java.util.ArrayList; import java.util.Collections; import java.util.List; @RunWith(RobolectricTestRunner.class) +@Config(shadows = {ShadowBluetoothAdapter.class}) public class BluetoothEventManagerTest { + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); private static final String DEVICE_NAME = "test_device_name"; @Mock private LocalBluetoothAdapter mLocalAdapter; @Mock + private LocalBluetoothManager mBtManager; + @Mock private CachedBluetoothDeviceManager mCachedDeviceManager; @Mock private BluetoothCallback mBluetoothCallback; @@ -96,8 +109,15 @@ public class BluetoothEventManagerTest { MockitoAnnotations.initMocks(this); mContext = RuntimeEnvironment.application; - mBluetoothEventManager = new BluetoothEventManager(mLocalAdapter, - mCachedDeviceManager, mContext, /* handler= */ null, /* userHandle= */ null); + mBluetoothEventManager = + new BluetoothEventManager( + mLocalAdapter, + mBtManager, + mCachedDeviceManager, + mContext, + /* handler= */ null, + /* userHandle= */ null); + when(mBtManager.getProfileManager()).thenReturn(mLocalProfileManager); when(mCachedDeviceManager.findDevice(mBluetoothDevice)).thenReturn(mCachedBluetoothDevice); when(mHfpProfile.isProfileReady()).thenReturn(true); when(mA2dpProfile.isProfileReady()).thenReturn(true); @@ -113,8 +133,13 @@ public class BluetoothEventManagerTest { public void ifUserHandleIsNull_registerReceiverIsCalled() { Context mockContext = mock(Context.class); BluetoothEventManager eventManager = - new BluetoothEventManager(mLocalAdapter, mCachedDeviceManager, mockContext, - /* handler= */ null, /* userHandle= */ null); + new BluetoothEventManager( + mLocalAdapter, + mBtManager, + mCachedDeviceManager, + mockContext, + /* handler= */ null, + /* userHandle= */ null); verify(mockContext).registerReceiver(any(BroadcastReceiver.class), any(IntentFilter.class), eq(null), eq(null), eq(Context.RECEIVER_EXPORTED)); @@ -124,8 +149,13 @@ public class BluetoothEventManagerTest { public void ifUserHandleSpecified_registerReceiverAsUserIsCalled() { Context mockContext = mock(Context.class); BluetoothEventManager eventManager = - new BluetoothEventManager(mLocalAdapter, mCachedDeviceManager, mockContext, - /* handler= */ null, UserHandle.ALL); + new BluetoothEventManager( + mLocalAdapter, + mBtManager, + mCachedDeviceManager, + mockContext, + /* handler= */ null, + UserHandle.ALL); verify(mockContext).registerReceiverAsUser(any(BroadcastReceiver.class), eq(UserHandle.ALL), any(IntentFilter.class), eq(null), eq(null), eq(Context.RECEIVER_EXPORTED)); @@ -172,6 +202,160 @@ public class BluetoothEventManagerTest { BluetoothProfile.STATE_CONNECTED, BluetoothProfile.A2DP); } + /** + * dispatchProfileConnectionStateChanged should not call {@link + * LocalBluetoothLeBroadcast}#updateFallbackActiveDeviceIfNeeded when audio sharing flag is off. + */ + @Test + public void dispatchProfileConnectionStateChanged_flagOff_noUpdateFallbackDevice() { + ShadowBluetoothAdapter shadowBluetoothAdapter = + Shadow.extract(BluetoothAdapter.getDefaultAdapter()); + shadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); + shadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); + mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + LocalBluetoothLeBroadcast broadcast = mock(LocalBluetoothLeBroadcast.class); + when(broadcast.isProfileReady()).thenReturn(true); + LocalBluetoothLeBroadcastAssistant assistant = + mock(LocalBluetoothLeBroadcastAssistant.class); + when(assistant.isProfileReady()).thenReturn(true); + LocalBluetoothProfileManager profileManager = mock(LocalBluetoothProfileManager.class); + when(profileManager.getLeAudioBroadcastProfile()).thenReturn(broadcast); + when(profileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(assistant); + when(mBtManager.getProfileManager()).thenReturn(profileManager); + mBluetoothEventManager.dispatchProfileConnectionStateChanged( + mCachedBluetoothDevice, + BluetoothProfile.STATE_DISCONNECTED, + BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT); + + verify(broadcast, times(0)).updateFallbackActiveDeviceIfNeeded(); + } + + /** + * dispatchProfileConnectionStateChanged should not call {@link + * LocalBluetoothLeBroadcast}#updateFallbackActiveDeviceIfNeeded when the device does not + * support audio sharing. + */ + @Test + public void dispatchProfileConnectionStateChanged_notSupport_noUpdateFallbackDevice() { + ShadowBluetoothAdapter shadowBluetoothAdapter = + Shadow.extract(BluetoothAdapter.getDefaultAdapter()); + shadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported( + BluetoothStatusCodes.FEATURE_NOT_SUPPORTED); + shadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + LocalBluetoothLeBroadcast broadcast = mock(LocalBluetoothLeBroadcast.class); + when(broadcast.isProfileReady()).thenReturn(true); + LocalBluetoothLeBroadcastAssistant assistant = + mock(LocalBluetoothLeBroadcastAssistant.class); + when(assistant.isProfileReady()).thenReturn(true); + LocalBluetoothProfileManager profileManager = mock(LocalBluetoothProfileManager.class); + when(profileManager.getLeAudioBroadcastProfile()).thenReturn(broadcast); + when(profileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(assistant); + when(mBtManager.getProfileManager()).thenReturn(profileManager); + mBluetoothEventManager.dispatchProfileConnectionStateChanged( + mCachedBluetoothDevice, + BluetoothProfile.STATE_DISCONNECTED, + BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT); + + verify(broadcast, times(0)).updateFallbackActiveDeviceIfNeeded(); + } + + /** + * dispatchProfileConnectionStateChanged should not call {@link + * LocalBluetoothLeBroadcast}#updateFallbackActiveDeviceIfNeeded when audio sharing profile is + * not ready. + */ + @Test + public void dispatchProfileConnectionStateChanged_profileNotReady_noUpdateFallbackDevice() { + ShadowBluetoothAdapter shadowBluetoothAdapter = + Shadow.extract(BluetoothAdapter.getDefaultAdapter()); + shadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); + shadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + LocalBluetoothLeBroadcast broadcast = mock(LocalBluetoothLeBroadcast.class); + when(broadcast.isProfileReady()).thenReturn(false); + LocalBluetoothLeBroadcastAssistant assistant = + mock(LocalBluetoothLeBroadcastAssistant.class); + when(assistant.isProfileReady()).thenReturn(true); + LocalBluetoothProfileManager profileManager = mock(LocalBluetoothProfileManager.class); + when(profileManager.getLeAudioBroadcastProfile()).thenReturn(broadcast); + when(profileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(assistant); + when(mBtManager.getProfileManager()).thenReturn(profileManager); + mBluetoothEventManager.dispatchProfileConnectionStateChanged( + mCachedBluetoothDevice, + BluetoothProfile.STATE_DISCONNECTED, + BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT); + + verify(broadcast, times(0)).updateFallbackActiveDeviceIfNeeded(); + } + + /** + * dispatchProfileConnectionStateChanged should not call {@link + * LocalBluetoothLeBroadcast}#updateFallbackActiveDeviceIfNeeded when triggered for profile + * other than LE_AUDIO_BROADCAST_ASSISTANT or state other than STATE_DISCONNECTED. + */ + @Test + public void dispatchProfileConnectionStateChanged_notAssistantProfile_noUpdateFallbackDevice() { + ShadowBluetoothAdapter shadowBluetoothAdapter = + Shadow.extract(BluetoothAdapter.getDefaultAdapter()); + shadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); + shadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + LocalBluetoothLeBroadcast broadcast = mock(LocalBluetoothLeBroadcast.class); + when(broadcast.isProfileReady()).thenReturn(true); + LocalBluetoothLeBroadcastAssistant assistant = + mock(LocalBluetoothLeBroadcastAssistant.class); + when(assistant.isProfileReady()).thenReturn(true); + LocalBluetoothProfileManager profileManager = mock(LocalBluetoothProfileManager.class); + when(profileManager.getLeAudioBroadcastProfile()).thenReturn(broadcast); + when(profileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(assistant); + when(mBtManager.getProfileManager()).thenReturn(profileManager); + mBluetoothEventManager.dispatchProfileConnectionStateChanged( + mCachedBluetoothDevice, + BluetoothProfile.STATE_DISCONNECTED, + BluetoothProfile.LE_AUDIO); + + verify(broadcast, times(0)).updateFallbackActiveDeviceIfNeeded(); + } + + /** + * dispatchProfileConnectionStateChanged should call {@link + * LocalBluetoothLeBroadcast}#updateFallbackActiveDeviceIfNeeded when assistant profile is + * disconnected and audio sharing is enabled. + */ + @Test + public void dispatchProfileConnectionStateChanged_audioSharing_updateFallbackDevice() { + ShadowBluetoothAdapter shadowBluetoothAdapter = + Shadow.extract(BluetoothAdapter.getDefaultAdapter()); + shadowBluetoothAdapter.setIsLeAudioBroadcastSourceSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); + shadowBluetoothAdapter.setIsLeAudioBroadcastAssistantSupported( + BluetoothStatusCodes.FEATURE_SUPPORTED); + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); + LocalBluetoothLeBroadcast broadcast = mock(LocalBluetoothLeBroadcast.class); + when(broadcast.isProfileReady()).thenReturn(true); + LocalBluetoothLeBroadcastAssistant assistant = + mock(LocalBluetoothLeBroadcastAssistant.class); + when(assistant.isProfileReady()).thenReturn(true); + LocalBluetoothProfileManager profileManager = mock(LocalBluetoothProfileManager.class); + when(profileManager.getLeAudioBroadcastProfile()).thenReturn(broadcast); + when(profileManager.getLeAudioBroadcastAssistantProfile()).thenReturn(assistant); + when(mBtManager.getProfileManager()).thenReturn(profileManager); + mBluetoothEventManager.dispatchProfileConnectionStateChanged( + mCachedBluetoothDevice, + BluetoothProfile.STATE_DISCONNECTED, + BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT); + + verify(broadcast).updateFallbackActiveDeviceIfNeeded(); + } + @Test public void dispatchAclConnectionStateChanged_aclDisconnected_shouldDispatchCallback() { mBluetoothEventManager.registerCallback(mBluetoothCallback); diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManagerTest.java index 4f8fa2fdb96e..cef083584744 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManagerTest.java @@ -56,7 +56,8 @@ import java.util.List; @Config(shadows = {ShadowBluetoothAdapter.class}) public class LocalBluetoothProfileManagerTest { private final static long HISYNCID = 10; - + @Mock + private LocalBluetoothManager mBtManager; @Mock private CachedBluetoothDeviceManager mDeviceManager; @Mock @@ -77,13 +78,21 @@ public class LocalBluetoothProfileManagerTest { MockitoAnnotations.initMocks(this); mContext = spy(RuntimeEnvironment.application); mLocalBluetoothAdapter = LocalBluetoothAdapter.getInstance(); - mEventManager = spy(new BluetoothEventManager(mLocalBluetoothAdapter, mDeviceManager, - mContext, /* handler= */ null, /* userHandle= */ null)); + mEventManager = + spy( + new BluetoothEventManager( + mLocalBluetoothAdapter, + mBtManager, + mDeviceManager, + mContext, + /* handler= */ null, + /* userHandle= */ null)); mShadowBluetoothAdapter = Shadow.extract(BluetoothAdapter.getDefaultAdapter()); when(mDeviceManager.findDevice(mDevice)).thenReturn(mCachedBluetoothDevice); when(mCachedBluetoothDevice.getDevice()).thenReturn(mDevice); - mProfileManager = new LocalBluetoothProfileManager(mContext, mLocalBluetoothAdapter, - mDeviceManager, mEventManager); + mProfileManager = + new LocalBluetoothProfileManager( + mContext, mLocalBluetoothAdapter, mDeviceManager, mEventManager); } /** diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/utils/PowerUtilTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/utils/PowerUtilTest.java index 2e7905f2e1e4..cbc382b6b920 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/utils/PowerUtilTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/utils/PowerUtilTest.java @@ -20,30 +20,24 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.spy; +import android.app.AlarmManager; import android.content.Context; +import androidx.test.core.app.ApplicationProvider; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; -import org.robolectric.RuntimeEnvironment; import java.time.Duration; +import java.time.Instant; +import java.util.Locale; import java.util.regex.Pattern; @RunWith(RobolectricTestRunner.class) public class PowerUtilTest { - private static final String TEST_BATTERY_LEVEL_10 = "10%"; - private static final long TEN_SEC_MILLIS = Duration.ofSeconds(10).toMillis(); - private static final long SEVENTEEN_MIN_MILLIS = Duration.ofMinutes(17).toMillis(); - private static final long FIVE_MINUTES_MILLIS = Duration.ofMinutes(5).toMillis(); - private static final long TEN_MINUTES_MILLIS = Duration.ofMinutes(10).toMillis(); - private static final long THREE_DAYS_MILLIS = Duration.ofDays(3).toMillis(); - private static final long TEN_HOURS_MILLIS = Duration.ofHours(10).toMillis(); - private static final long THIRTY_HOURS_MILLIS = Duration.ofHours(30).toMillis(); - private static final String NORMAL_CASE_EXPECTED_PREFIX = "Should last until about"; - private static final String ENHANCED_SUFFIX = " based on your usage"; private static final String BATTERY_RUN_OUT_PREFIX = "Battery may run out by"; // matches a time (ex: '1:15 PM', '2 AM', '23:00') private static final String TIME_OF_DAY_REGEX = " (\\d)+:?(\\d)* ((AM)*)|((PM)*)"; @@ -55,29 +49,31 @@ public class PowerUtilTest { @Before public void setup() { MockitoAnnotations.initMocks(this); - mContext = spy(RuntimeEnvironment.application); + mContext = spy(ApplicationProvider.getApplicationContext()); } @Test public void getBatteryTipStringFormatted_moreThanOneDay_usesCorrectString() { - String info = PowerUtil.getBatteryTipStringFormatted(mContext, - THREE_DAYS_MILLIS); + var threeDayMillis = Duration.ofDays(3).toMillis(); + + String batteryTipString = PowerUtil.getBatteryTipStringFormatted(mContext, threeDayMillis); - assertThat(info).isEqualTo("More than 3 days left"); + assertThat(batteryTipString).isEqualTo("More than 3 days left"); } @Test public void getBatteryTipStringFormatted_lessThanOneDay_usesCorrectString() { - String info = PowerUtil.getBatteryTipStringFormatted(mContext, - SEVENTEEN_MIN_MILLIS); + var drainTimeMs = Duration.ofMinutes(17).toMillis(); + + String batteryTipString = PowerUtil.getBatteryTipStringFormatted(mContext, drainTimeMs); // ex: Battery may run out by 1:15 PM - assertThat(info).containsMatch(Pattern.compile( - BATTERY_RUN_OUT_PREFIX + TIME_OF_DAY_REGEX)); + assertThat(batteryTipString) + .containsMatch(Pattern.compile(BATTERY_RUN_OUT_PREFIX + TIME_OF_DAY_REGEX)); } @Test - public void testRoundToNearestThreshold_roundsCorrectly() { + public void roundTimeToNearestThreshold_roundsCorrectly() { // test some pretty normal values assertThat(PowerUtil.roundTimeToNearestThreshold(1200, 1000)).isEqualTo(1000); assertThat(PowerUtil.roundTimeToNearestThreshold(800, 1000)).isEqualTo(1000); @@ -89,4 +85,17 @@ public class PowerUtilTest { assertThat(PowerUtil.roundTimeToNearestThreshold(-120, 100)).isEqualTo(100); assertThat(PowerUtil.roundTimeToNearestThreshold(-200, -75)).isEqualTo(225); } + + @Test + public void getTargetTimeShortString_returnsTimeShortString() { + mContext.getSystemService(AlarmManager.class).setTimeZone("UTC"); + mContext.getResources().getConfiguration().setLocale(Locale.US); + var currentTimeMs = Instant.parse("2024-06-06T15:00:00Z").toEpochMilli(); + var remainingTimeMs = Duration.ofMinutes(30).toMillis(); + + var actualTimeString = + PowerUtil.getTargetTimeShortString(mContext, remainingTimeMs, currentTimeMs); + + assertThat(actualTimeString).isEqualTo("3:30 PM"); + } } diff --git a/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowBluetoothAdapter.java b/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowBluetoothAdapter.java index c7e96bcdb856..00e47729a796 100644 --- a/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowBluetoothAdapter.java +++ b/packages/SettingsLib/tests/robotests/testutils/com/android/settingslib/testutils/shadow/ShadowBluetoothAdapter.java @@ -38,6 +38,8 @@ public class ShadowBluetoothAdapter extends org.robolectric.shadows.ShadowBlueto private List<BluetoothDevice> mMostRecentlyConnectedDevices; private BluetoothProfile.ServiceListener mServiceListener; private ParcelUuid[] mParcelUuids; + private int mIsLeAudioBroadcastSourceSupported; + private int mIsLeAudioBroadcastAssistantSupported; @Implementation protected boolean getProfileProxy(Context context, BluetoothProfile.ServiceListener listener, @@ -97,4 +99,22 @@ public class ShadowBluetoothAdapter extends org.robolectric.shadows.ShadowBlueto public void setUuids(ParcelUuid[] uuids) { mParcelUuids = uuids; } + + @Implementation + protected int isLeAudioBroadcastSourceSupported() { + return mIsLeAudioBroadcastSourceSupported; + } + + public void setIsLeAudioBroadcastSourceSupported(int isSupported) { + mIsLeAudioBroadcastSourceSupported = isSupported; + } + + @Implementation + protected int isLeAudioBroadcastAssistantSupported() { + return mIsLeAudioBroadcastAssistantSupported; + } + + public void setIsLeAudioBroadcastAssistantSupported(int isSupported) { + mIsLeAudioBroadcastAssistantSupported = isSupported; + } } diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteAnimationRunnerCompat.java b/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteAnimationRunnerCompat.java index e20425d4b98c..94f884673fbd 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteAnimationRunnerCompat.java +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteAnimationRunnerCompat.java @@ -36,6 +36,7 @@ import android.view.WindowManager; import android.view.WindowManager.TransitionOldType; import android.window.IRemoteTransition; import android.window.IRemoteTransitionFinishedCallback; +import android.window.RemoteTransitionStub; import android.window.TransitionInfo; import com.android.wm.shell.shared.CounterRotator; @@ -69,8 +70,8 @@ public abstract class RemoteAnimationRunnerCompat extends IRemoteAnimationRunner } /** Wraps a remote animation runner in a remote-transition. */ - public static IRemoteTransition.Stub wrap(IRemoteAnimationRunner runner) { - return new IRemoteTransition.Stub() { + public static RemoteTransitionStub wrap(IRemoteAnimationRunner runner) { + return new RemoteTransitionStub() { final ArrayMap<IBinder, Runnable> mFinishRunnables = new ArrayMap<>(); @Override @@ -233,11 +234,6 @@ public abstract class RemoteAnimationRunnerCompat extends IRemoteAnimationRunner runner.onAnimationCancelled(); finishRunnable.run(); } - - @Override - public void onTransitionConsumed(IBinder iBinder, boolean aborted) - throws RemoteException { - } }; } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt index 3ec5508c81b3..d59f1f5bbe25 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt @@ -22,11 +22,8 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import com.android.compose.animation.scene.Back import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.SceneScope -import com.android.compose.animation.scene.Swipe -import com.android.compose.animation.scene.SwipeDirection import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.systemui.bouncer.ui.BouncerDialogFactory @@ -35,9 +32,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.ui.composable.ComposableScene import javax.inject.Inject -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow object Bouncer { object Elements { @@ -57,13 +52,7 @@ constructor( override val key = Scenes.Bouncer override val destinationScenes: StateFlow<Map<UserAction, UserActionResult>> = - MutableStateFlow( - mapOf( - Back to UserActionResult(Scenes.Lockscreen), - Swipe(SwipeDirection.Down) to UserActionResult(Scenes.Lockscreen), - ) - ) - .asStateFlow() + viewModel.destinationScenes @Composable override fun SceneScope.Content( diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncPopup.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncPopup.kt index 2af042aac5b4..e1ee01e78566 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncPopup.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncPopup.kt @@ -28,11 +28,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.slice.Slice +import com.android.internal.logging.UiEventLogger import com.android.systemui.animation.Expandable import com.android.systemui.res.R import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.volume.panel.component.anc.ui.viewmodel.AncViewModel import com.android.systemui.volume.panel.component.popup.ui.composable.VolumePanelPopup +import com.android.systemui.volume.panel.ui.VolumePanelUiEvent import javax.inject.Inject /** ANC popup up displaying ANC control [Slice]. */ @@ -41,10 +43,12 @@ class AncPopup constructor( private val volumePanelPopup: VolumePanelPopup, private val viewModel: AncViewModel, + private val uiEventLogger: UiEventLogger, ) { /** Shows a popup with the [expandable] animation. */ fun show(expandable: Expandable?) { + uiEventLogger.log(VolumePanelUiEvent.VOLUME_PANEL_ANC_POPUP_SHOWN) volumePanelPopup.show(expandable, { Title() }, { Content(it) }) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/spatialaudio/ui/composable/SpatialAudioPopup.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/spatialaudio/ui/composable/SpatialAudioPopup.kt index eed54dab6faf..9a98bdeec8f1 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/spatialaudio/ui/composable/SpatialAudioPopup.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/spatialaudio/ui/composable/SpatialAudioPopup.kt @@ -26,6 +26,7 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign +import com.android.internal.logging.UiEventLogger import com.android.systemui.animation.Expandable import com.android.systemui.common.ui.compose.Icon import com.android.systemui.common.ui.compose.toColor @@ -34,6 +35,7 @@ import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.volume.panel.component.popup.ui.composable.VolumePanelPopup import com.android.systemui.volume.panel.component.selector.ui.composable.VolumePanelRadioButtonBar import com.android.systemui.volume.panel.component.spatial.ui.viewmodel.SpatialAudioViewModel +import com.android.systemui.volume.panel.ui.VolumePanelUiEvent import javax.inject.Inject class SpatialAudioPopup @@ -41,10 +43,17 @@ class SpatialAudioPopup constructor( private val viewModel: SpatialAudioViewModel, private val volumePanelPopup: VolumePanelPopup, + private val uiEventLogger: UiEventLogger, ) { /** Shows a popup with the [expandable] animation. */ fun show(expandable: Expandable) { + uiEventLogger.logWithPosition( + VolumePanelUiEvent.VOLUME_PANEL_SPATIAL_AUDIO_POP_UP_SHOWN, + 0, + null, + viewModel.spatialAudioButtons.value.indexOfFirst { it.button.isChecked } + ) volumePanelPopup.show(expandable, { Title() }, { Content(it) }) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/ColumnVolumeSliders.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/ColumnVolumeSliders.kt index f89669c8456c..a54d005c990a 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/ColumnVolumeSliders.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/ColumnVolumeSliders.kt @@ -85,6 +85,7 @@ fun ColumnVolumeSliders( onValueChange = { newValue: Float -> sliderViewModel.onValueChanged(sliderState, newValue) }, + onValueChangeFinished = { sliderViewModel.onValueChangeFinished() }, onIconTapped = { sliderViewModel.toggleMuted(sliderState) }, sliderColors = sliderColors, ) @@ -131,6 +132,7 @@ fun ColumnVolumeSliders( onValueChange = { newValue: Float -> sliderViewModel.onValueChanged(sliderState, newValue) }, + onValueChangeFinished = { sliderViewModel.onValueChangeFinished() }, onIconTapped = { sliderViewModel.toggleMuted(sliderState) }, sliderColors = sliderColors, ) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/GridVolumeSliders.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/GridVolumeSliders.kt index b284c691ef0e..bb17499f021f 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/GridVolumeSliders.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/GridVolumeSliders.kt @@ -46,6 +46,7 @@ fun GridVolumeSliders( onValueChange = { newValue: Float -> sliderViewModel.onValueChanged(sliderState, newValue) }, + onValueChangeFinished = { sliderViewModel.onValueChangeFinished() }, onIconTapped = { sliderViewModel.toggleMuted(sliderState) }, sliderColors = sliderColors, ) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt index 28cd37ea9327..228d29259038 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt @@ -51,6 +51,7 @@ import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.Sl fun VolumeSlider( state: SliderState, onValueChange: (newValue: Float) -> Unit, + onValueChangeFinished: (() -> Unit)? = null, onIconTapped: () -> Unit, modifier: Modifier = Modifier, sliderColors: PlatformSliderColors, @@ -85,6 +86,7 @@ fun VolumeSlider( value = value, valueRange = state.valueRange, onValueChange = onValueChange, + onValueChangeFinished = onValueChangeFinished, enabled = state.isEnabled, icon = { state.icon?.let { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt index 3afca96e07a0..0db0e0767767 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt @@ -18,6 +18,10 @@ package com.android.systemui.bouncer.ui.viewmodel import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.compose.animation.scene.Back +import com.android.compose.animation.scene.Swipe +import com.android.compose.animation.scene.SwipeDirection +import com.android.compose.animation.scene.UserActionResult import com.android.systemui.SysuiTestCase import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository @@ -34,7 +38,10 @@ import com.android.systemui.flags.Flags import com.android.systemui.flags.fakeFeatureFlagsClassic import com.android.systemui.kosmos.testScope import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags +import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.scene.shared.model.fakeSceneDataSource import com.android.systemui.testKosmos +import com.android.systemui.truth.containsEntriesExactly import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -193,6 +200,23 @@ class BouncerViewModelTest : SysuiTestCase() { assertThat(isFoldSplitRequired).isTrue() } + @Test + fun destinationScenes() = + testScope.runTest { + val destinationScenes by collectLastValue(underTest.destinationScenes) + kosmos.fakeSceneDataSource.changeScene(Scenes.QuickSettings) + runCurrent() + + kosmos.fakeSceneDataSource.changeScene(Scenes.Bouncer) + runCurrent() + + assertThat(destinationScenes) + .containsEntriesExactly( + Back to UserActionResult(Scenes.QuickSettings), + Swipe(SwipeDirection.Down) to UserActionResult(Scenes.QuickSettings), + ) + } + private fun authMethodsToTest(): List<AuthenticationMethodModel> { return listOf(None, Pin, Password, Pattern, Sim) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorTest.kt index 70582daf8e9f..c88e432d15d2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorTest.kt @@ -191,7 +191,7 @@ class KeyguardClockInteractorTest : SysuiTestCase() { val value by collectLastValue(underTest.clockShouldBeCentered) kosmos.shadeRepository.setShadeMode(ShadeMode.Split) kosmos.activeNotificationListRepository.setActiveNotifs(1) - kosmos.headsUpNotificationRepository.headsUpAnimatingAway.value = true + kosmos.headsUpNotificationRepository.isHeadsUpAnimatingAway.value = true kosmos.keyguardRepository.setIsDozing(true) assertThat(value).isEqualTo(false) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt index 15c9cf73d51d..412292554e73 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt @@ -55,28 +55,29 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() { val testScope = kosmos.testScope @Test - fun transitionCollectorsReceivesOnlyAppropriateEvents() = runTest { - val lockscreenToAodSteps by collectValues(underTest.lockscreenToAodTransition) - val aodToLockscreenSteps by collectValues(underTest.aodToLockscreenTransition) - - val steps = mutableListOf<TransitionStep>() - steps.add(TransitionStep(AOD, GONE, 0f, STARTED)) - steps.add(TransitionStep(AOD, GONE, 1f, FINISHED)) - steps.add(TransitionStep(AOD, LOCKSCREEN, 0f, STARTED)) - steps.add(TransitionStep(AOD, LOCKSCREEN, 0.5f, RUNNING)) - steps.add(TransitionStep(AOD, LOCKSCREEN, 1f, FINISHED)) - steps.add(TransitionStep(LOCKSCREEN, AOD, 0f, STARTED)) - steps.add(TransitionStep(LOCKSCREEN, AOD, 0.1f, RUNNING)) - steps.add(TransitionStep(LOCKSCREEN, AOD, 0.2f, RUNNING)) + fun transitionCollectorsReceivesOnlyAppropriateEvents() = + testScope.runTest { + val lockscreenToAodSteps by collectValues(underTest.transition(LOCKSCREEN, AOD)) + val aodToLockscreenSteps by collectValues(underTest.transition(AOD, LOCKSCREEN)) - steps.forEach { - repository.sendTransitionStep(it) - runCurrent() - } + val steps = mutableListOf<TransitionStep>() + steps.add(TransitionStep(AOD, GONE, 0f, STARTED)) + steps.add(TransitionStep(AOD, GONE, 1f, FINISHED)) + steps.add(TransitionStep(AOD, LOCKSCREEN, 0f, STARTED)) + steps.add(TransitionStep(AOD, LOCKSCREEN, 0.5f, RUNNING)) + steps.add(TransitionStep(AOD, LOCKSCREEN, 1f, FINISHED)) + steps.add(TransitionStep(LOCKSCREEN, AOD, 0f, STARTED)) + steps.add(TransitionStep(LOCKSCREEN, AOD, 0.1f, RUNNING)) + steps.add(TransitionStep(LOCKSCREEN, AOD, 0.2f, RUNNING)) - assertThat(aodToLockscreenSteps).isEqualTo(steps.subList(2, 5)) - assertThat(lockscreenToAodSteps).isEqualTo(steps.subList(5, 8)) - } + steps.forEach { + repository.sendTransitionStep(it) + runCurrent() + } + + assertThat(aodToLockscreenSteps).isEqualTo(steps.subList(2, 5)) + assertThat(lockscreenToAodSteps).isEqualTo(steps.subList(5, 8)) + } @Test fun dozeAmountTransitionTest_AodToFromLockscreen() = @@ -187,59 +188,60 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() { } @Test - fun finishedKeyguardTransitionStepTests() = runTest { - val finishedSteps by collectValues(underTest.finishedKeyguardTransitionStep) + fun finishedKeyguardTransitionStepTests() = + testScope.runTest { + val finishedSteps by collectValues(underTest.finishedKeyguardTransitionStep) + val steps = mutableListOf<TransitionStep>() - val steps = mutableListOf<TransitionStep>() + steps.add(TransitionStep(LOCKSCREEN, AOD, 0f, STARTED)) + steps.add(TransitionStep(LOCKSCREEN, AOD, 0.9f, RUNNING)) + steps.add(TransitionStep(LOCKSCREEN, AOD, 1f, FINISHED)) + steps.add(TransitionStep(AOD, LOCKSCREEN, 0f, STARTED)) + steps.add(TransitionStep(AOD, LOCKSCREEN, 0.5f, RUNNING)) + steps.add(TransitionStep(AOD, LOCKSCREEN, 1f, FINISHED)) + steps.add(TransitionStep(AOD, GONE, 1f, STARTED)) - steps.add(TransitionStep(LOCKSCREEN, AOD, 0f, STARTED)) - steps.add(TransitionStep(LOCKSCREEN, AOD, 0.9f, RUNNING)) - steps.add(TransitionStep(LOCKSCREEN, AOD, 1f, FINISHED)) - steps.add(TransitionStep(AOD, LOCKSCREEN, 0f, STARTED)) - steps.add(TransitionStep(AOD, LOCKSCREEN, 0.5f, RUNNING)) - steps.add(TransitionStep(AOD, LOCKSCREEN, 1f, FINISHED)) - steps.add(TransitionStep(AOD, GONE, 1f, STARTED)) + steps.forEach { + repository.sendTransitionStep(it) + runCurrent() + } - steps.forEach { - repository.sendTransitionStep(it) - runCurrent() + // Ignore the default state. + assertThat(finishedSteps.subList(1, finishedSteps.size)) + .isEqualTo(listOf(steps[2], steps[5])) } - // Ignore the default state. - assertThat(finishedSteps.subList(1, finishedSteps.size)) - .isEqualTo(listOf(steps[2], steps[5])) - } - @Test - fun startedKeyguardTransitionStepTests() = runTest { - val startedSteps by collectValues(underTest.startedKeyguardTransitionStep) + fun startedKeyguardTransitionStepTests() = + testScope.runTest { + val startedSteps by collectValues(underTest.startedKeyguardTransitionStep) - val steps = mutableListOf<TransitionStep>() + val steps = mutableListOf<TransitionStep>() - steps.add(TransitionStep(AOD, LOCKSCREEN, 0f, STARTED)) - steps.add(TransitionStep(AOD, LOCKSCREEN, 0.5f, RUNNING)) - steps.add(TransitionStep(AOD, LOCKSCREEN, 1f, FINISHED)) - steps.add(TransitionStep(LOCKSCREEN, AOD, 0f, STARTED)) - steps.add(TransitionStep(LOCKSCREEN, AOD, 0.9f, RUNNING)) - steps.add(TransitionStep(LOCKSCREEN, AOD, 1f, FINISHED)) - steps.add(TransitionStep(AOD, GONE, 1f, STARTED)) + steps.add(TransitionStep(AOD, LOCKSCREEN, 0f, STARTED)) + steps.add(TransitionStep(AOD, LOCKSCREEN, 0.5f, RUNNING)) + steps.add(TransitionStep(AOD, LOCKSCREEN, 1f, FINISHED)) + steps.add(TransitionStep(LOCKSCREEN, AOD, 0f, STARTED)) + steps.add(TransitionStep(LOCKSCREEN, AOD, 0.9f, RUNNING)) + steps.add(TransitionStep(LOCKSCREEN, AOD, 1f, FINISHED)) + steps.add(TransitionStep(AOD, GONE, 1f, STARTED)) - steps.forEach { - repository.sendTransitionStep(it) - runCurrent() - } + steps.forEach { + repository.sendTransitionStep(it) + runCurrent() + } - assertThat(startedSteps) - .isEqualTo( - listOf( - // The initial transition will also get sent when collect started - TransitionStep(OFF, LOCKSCREEN, 0f, STARTED), - steps[0], - steps[3], - steps[6] + assertThat(startedSteps) + .isEqualTo( + listOf( + // The initial transition will also get sent when collect started + TransitionStep(OFF, LOCKSCREEN, 0f, STARTED), + steps[0], + steps[3], + steps[6] + ) ) - ) - } + } @Test fun transitionValue() = diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt index f0607f4b70e1..0ac7ff5232a3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.keyguard.ui +import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.app.animation.Interpolators.EMPHASIZED_ACCELERATE import com.android.systemui.SysuiTestCase @@ -35,10 +36,9 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.junit.runners.JUnit4 @SmallTest -@RunWith(JUnit4::class) +@RunWith(AndroidJUnit4::class) class KeyguardTransitionAnimationFlowTest : SysuiTestCase() { val kosmos = testKosmos() val testScope = kosmos.testScope diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt index dddf6485d0f4..4c16a339d696 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt @@ -49,9 +49,7 @@ class OccludedToLockscreenTransitionViewModelTest : SysuiTestCase() { val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository val fingerprintPropertyRepository = kosmos.fingerprintPropertyRepository val configurationRepository = kosmos.fakeConfigurationRepository - val underTest by lazy { - kosmos.occludedToLockscreenTransitionViewModel - } + val underTest by lazy { kosmos.occludedToLockscreenTransitionViewModel } @Test fun lockscreenFadeIn() = @@ -164,25 +162,6 @@ class OccludedToLockscreenTransitionViewModelTest : SysuiTestCase() { values.forEach { assertThat(it).isEqualTo(1f) } } - @Test - fun deviceEntryBackgroundView_noUdfpsEnrolled_noUpdates() = - testScope.runTest { - fingerprintPropertyRepository.supportsRearFps() - biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true) - val values by collectValues(underTest.deviceEntryBackgroundViewAlpha) - - keyguardTransitionRepository.sendTransitionStep(step(0f, TransitionState.STARTED)) - keyguardTransitionRepository.sendTransitionStep(step(0.1f)) - keyguardTransitionRepository.sendTransitionStep(step(0.3f)) - keyguardTransitionRepository.sendTransitionStep(step(0.4f)) - keyguardTransitionRepository.sendTransitionStep(step(0.5f)) - keyguardTransitionRepository.sendTransitionStep(step(0.6f)) - keyguardTransitionRepository.sendTransitionStep(step(0.8f)) - keyguardTransitionRepository.sendTransitionStep(step(1f)) - - assertThat(values).isEmpty() // no updates - } - private fun step( value: Float, state: TransitionState = TransitionState.RUNNING 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 956ef661d467..33eb90acdcb3 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 @@ -26,7 +26,9 @@ import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope import com.android.systemui.media.controls.MediaTestHelper import com.android.systemui.media.controls.shared.model.MediaData +import com.android.systemui.media.controls.shared.model.MediaDataLoadingModel import com.android.systemui.media.controls.shared.model.SmartspaceMediaData +import com.android.systemui.media.controls.shared.model.SmartspaceMediaLoadingModel import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest @@ -144,6 +146,37 @@ class MediaFilterRepositoryTest : SysuiTestCase() { assertThat(smartspaceMediaData?.isActive).isFalse() } + @Test + fun addMediaDataLoadingState() = + testScope.runTest { + val mediaDataLoadedStates by collectLastValue(underTest.mediaDataLoadedStates) + val instanceId = InstanceId.fakeInstanceId(123) + val mediaLoadedStates = mutableListOf(MediaDataLoadingModel.Loaded(instanceId)) + + underTest.addMediaDataLoadingState(MediaDataLoadingModel.Loaded(instanceId)) + + assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStates) + + mediaLoadedStates.remove(MediaDataLoadingModel.Loaded(instanceId)) + + underTest.addMediaDataLoadingState(MediaDataLoadingModel.Removed(instanceId)) + + assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStates) + } + + @Test + fun setRecommendationsLoadingState() = + testScope.runTest { + val recommendationsLoadingState by + collectLastValue(underTest.recommendationsLoadingState) + val recommendationsLoadingModel = + SmartspaceMediaLoadingModel.Loaded(KEY_MEDIA_SMARTSPACE) + + underTest.setRecommedationsLoadingState(recommendationsLoadingModel) + + assertThat(recommendationsLoadingState).isEqualTo(recommendationsLoadingModel) + } + companion object { private const val KEY = "KEY" private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID" diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaCarouselInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaCarouselInteractorTest.kt index d9d84f2d2aac..a0a1eb3a6ca6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaCarouselInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaCarouselInteractorTest.kt @@ -20,6 +20,7 @@ import android.R import android.graphics.drawable.Icon import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.internal.logging.InstanceId import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.Flags @@ -28,13 +29,20 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.media.controls.MediaTestHelper import com.android.systemui.media.controls.data.repository.MediaFilterRepository import com.android.systemui.media.controls.data.repository.mediaFilterRepository +import com.android.systemui.media.controls.domain.pipeline.MediaDataFilterImpl import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor import com.android.systemui.media.controls.domain.pipeline.interactor.mediaCarouselInteractor +import com.android.systemui.media.controls.domain.pipeline.mediaDataFilter import com.android.systemui.media.controls.shared.model.MediaData +import com.android.systemui.media.controls.shared.model.MediaDataLoadingModel import com.android.systemui.media.controls.shared.model.SmartspaceMediaData +import com.android.systemui.media.controls.shared.model.SmartspaceMediaLoadingModel +import com.android.systemui.statusbar.notificationLockscreenUserManager import com.android.systemui.testKosmos +import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -45,9 +53,17 @@ class MediaCarouselInteractorTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope + private val mediaDataFilter: MediaDataFilterImpl = kosmos.mediaDataFilter + private val notificationLockscreenUserManager = kosmos.notificationLockscreenUserManager private val mediaFilterRepository: MediaFilterRepository = kosmos.mediaFilterRepository + private val underTest: MediaCarouselInteractor = kosmos.mediaCarouselInteractor + @Before + fun setUp() { + underTest.start() + } + @Test fun addUserMediaEntry_activeThenInactivate() = testScope.runTest { @@ -56,7 +72,7 @@ class MediaCarouselInteractorTest : SysuiTestCase() { val hasActiveMedia by collectLastValue(underTest.hasActiveMedia) val hasAnyMedia by collectLastValue(underTest.hasAnyMedia) - val userMedia = MediaData().copy(active = true) + val userMedia = MediaData(active = true) mediaFilterRepository.addSelectedUserMediaEntry(userMedia) @@ -79,7 +95,7 @@ class MediaCarouselInteractorTest : SysuiTestCase() { val hasActiveMedia by collectLastValue(underTest.hasActiveMedia) val hasAnyMedia by collectLastValue(underTest.hasAnyMedia) - val userMedia = MediaData().copy(active = false) + val userMedia = MediaData(active = false) val instanceId = userMedia.instanceId mediaFilterRepository.addSelectedUserMediaEntry(userMedia) @@ -112,7 +128,7 @@ class MediaCarouselInteractorTest : SysuiTestCase() { isActive = true, recommendations = MediaTestHelper.getValidRecommendationList(icon), ) - val userMedia = MediaData().copy(active = false) + val userMedia = MediaData(active = false) mediaFilterRepository.setRecommendation(userMediaRecommendation) @@ -199,7 +215,80 @@ class MediaCarouselInteractorTest : SysuiTestCase() { fun hasActiveMediaOrRecommendation_nothingSet_returnsFalse() = testScope.runTest { assertThat(underTest.hasActiveMediaOrRecommendation.value).isFalse() } + @Test + fun onMediaDataUpdated_updatesLoadingState() = + testScope.runTest { + whenever(notificationLockscreenUserManager.isCurrentProfile(USER_ID)).thenReturn(true) + whenever(notificationLockscreenUserManager.isProfileAvailable(USER_ID)).thenReturn(true) + val mediaDataLoadedStates by collectLastValue(underTest.mediaDataLoadedStates) + val instanceId = InstanceId.fakeInstanceId(123) + val mediaLoadedStates: MutableList<MediaDataLoadingModel> = mutableListOf() + + mediaLoadedStates.add(MediaDataLoadingModel.Loaded(instanceId)) + mediaDataFilter.onMediaDataLoaded( + KEY, + KEY, + MediaData(userId = USER_ID, instanceId = instanceId) + ) + + assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStates) + + val newInstanceId = InstanceId.fakeInstanceId(321) + + mediaLoadedStates.add(MediaDataLoadingModel.Loaded(newInstanceId)) + mediaDataFilter.onMediaDataLoaded( + KEY_2, + KEY_2, + MediaData(userId = USER_ID, instanceId = newInstanceId) + ) + + assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStates) + + mediaLoadedStates.remove(MediaDataLoadingModel.Loaded(instanceId)) + + mediaDataFilter.onMediaDataRemoved(KEY) + + assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStates) + + mediaLoadedStates.remove(MediaDataLoadingModel.Loaded(newInstanceId)) + + mediaDataFilter.onMediaDataRemoved(KEY_2) + + assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStates) + } + + @Test + fun onMediaRecommendationsUpdated_updatesLoadingState() = + testScope.runTest { + whenever(notificationLockscreenUserManager.isCurrentProfile(USER_ID)).thenReturn(true) + whenever(notificationLockscreenUserManager.isProfileAvailable(USER_ID)).thenReturn(true) + val recommendationsLoadingState by + collectLastValue(underTest.recommendationsLoadingState) + val icon = Icon.createWithResource(context, R.drawable.ic_media_play) + val mediaRecommendations = + SmartspaceMediaData( + targetId = KEY_MEDIA_SMARTSPACE, + isActive = true, + recommendations = MediaTestHelper.getValidRecommendationList(icon), + ) + var recommendationsLoadingModel: SmartspaceMediaLoadingModel = + SmartspaceMediaLoadingModel.Loaded(KEY_MEDIA_SMARTSPACE, isPrioritized = true) + + mediaDataFilter.onSmartspaceMediaDataLoaded(KEY_MEDIA_SMARTSPACE, mediaRecommendations) + + assertThat(recommendationsLoadingState).isEqualTo(recommendationsLoadingModel) + + recommendationsLoadingModel = SmartspaceMediaLoadingModel.Removed(KEY_MEDIA_SMARTSPACE) + + mediaDataFilter.onSmartspaceMediaDataRemoved(KEY_MEDIA_SMARTSPACE) + + assertThat(recommendationsLoadingState).isEqualTo(recommendationsLoadingModel) + } + companion object { + private const val KEY = "key" + private const val KEY_2 = "key2" + private const val USER_ID = 0 private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID" } } 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 ae2fefd7b957..61adcd2e2c25 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 @@ -290,6 +290,38 @@ class SceneContainerStartableTest : SysuiTestCase() { } @Test + fun switchFromBouncerToQuickSettingsWhenDeviceUnlocked() = + testScope.runTest { + val currentSceneKey by collectLastValue(sceneInteractor.currentScene) + + val transitionState = + prepareState( + authenticationMethod = AuthenticationMethodModel.Pin, + isDeviceUnlocked = false, + initialSceneKey = Scenes.Lockscreen, + ) + assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) + underTest.start() + runCurrent() + + sceneInteractor.changeScene(Scenes.QuickSettings, "switching to qs for test") + transitionState.value = ObservableTransitionState.Idle(Scenes.QuickSettings) + runCurrent() + assertThat(currentSceneKey).isEqualTo(Scenes.QuickSettings) + + sceneInteractor.changeScene(Scenes.Bouncer, "switching to bouncer for test") + transitionState.value = ObservableTransitionState.Idle(Scenes.Bouncer) + runCurrent() + assertThat(currentSceneKey).isEqualTo(Scenes.Bouncer) + + kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( + SuccessFingerprintAuthenticationStatus(0, true) + ) + + assertThat(currentSceneKey).isEqualTo(Scenes.QuickSettings) + } + + @Test fun switchFromLockscreenToGoneWhenDeviceUnlocksWithBypassOn() = testScope.runTest { val currentSceneKey by collectLastValue(sceneInteractor.currentScene) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt index 757a6c9e5ac6..5b33ecbb28be 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt @@ -641,7 +641,6 @@ class ShadeInteractorImplTest : SysuiTestCase() { ) ) val isShadeTouchable by collectLastValue(underTest.isShadeTouchable) - runCurrent() assertThat(isShadeTouchable).isFalse() } @@ -668,13 +667,17 @@ class ShadeInteractorImplTest : SysuiTestCase() { ) ) val isShadeTouchable by collectLastValue(underTest.isShadeTouchable) - runCurrent() assertThat(isShadeTouchable).isTrue() } @Test fun isShadeTouchable_isFalse_whenStartingToSleepAndNotControlScreenOff() = testScope.runTest { + whenever(dozeParameters.shouldControlScreenOff()).thenReturn(false) + val isShadeTouchable by collectLastValue(underTest.isShadeTouchable) + // Assert the default condition is true + assertThat(isShadeTouchable).isTrue() + powerRepository.updateWakefulness( rawState = WakefulnessState.STARTING_TO_SLEEP, lastWakeReason = WakeSleepReason.POWER_BUTTON, @@ -688,15 +691,17 @@ class ShadeInteractorImplTest : SysuiTestCase() { transitionState = TransitionState.STARTED, ) ) - whenever(dozeParameters.shouldControlScreenOff()).thenReturn(false) - val isShadeTouchable by collectLastValue(underTest.isShadeTouchable) - runCurrent() assertThat(isShadeTouchable).isFalse() } @Test fun isShadeTouchable_isTrue_whenStartingToSleepAndControlScreenOff() = testScope.runTest { + whenever(dozeParameters.shouldControlScreenOff()).thenReturn(true) + val isShadeTouchable by collectLastValue(underTest.isShadeTouchable) + // Assert the default condition is true + assertThat(isShadeTouchable).isTrue() + powerRepository.updateWakefulness( rawState = WakefulnessState.STARTING_TO_SLEEP, lastWakeReason = WakeSleepReason.POWER_BUTTON, @@ -710,9 +715,6 @@ class ShadeInteractorImplTest : SysuiTestCase() { transitionState = TransitionState.STARTED, ) ) - whenever(dozeParameters.shouldControlScreenOff()).thenReturn(true) - val isShadeTouchable by collectLastValue(underTest.isShadeTouchable) - runCurrent() assertThat(isShadeTouchable).isTrue() } @@ -730,7 +732,6 @@ class ShadeInteractorImplTest : SysuiTestCase() { ) ) val isShadeTouchable by collectLastValue(underTest.isShadeTouchable) - runCurrent() assertThat(isShadeTouchable).isTrue() } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/TestableHeadsUpManager.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/TestableHeadsUpManager.java index 3c9dc6345d31..69207ba07e6e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/TestableHeadsUpManager.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/TestableHeadsUpManager.java @@ -89,7 +89,7 @@ class TestableHeadsUpManager extends BaseHeadsUpManager { } @Override - public boolean isHeadsUpGoingAway() { + public boolean isHeadsUpAnimatingAwayValue() { throw new UnsupportedOperationException(); } @@ -115,7 +115,7 @@ class TestableHeadsUpManager extends BaseHeadsUpManager { } @Override - public void setHeadsUpGoingAway(boolean headsUpGoingAway) { + public void setHeadsUpAnimatingAway(boolean headsUpAnimatingAway) { throw new UnsupportedOperationException(); } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/startable/AudioModeLoggerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/startable/AudioModeLoggerStartableTest.kt new file mode 100644 index 000000000000..8bb36724d1d8 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/startable/AudioModeLoggerStartableTest.kt @@ -0,0 +1,93 @@ +/* + * 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.volume.domain.startable + +import android.media.AudioManager +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.internal.logging.uiEventLogger +import com.android.internal.logging.uiEventLoggerFake +import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.android.systemui.volume.audioModeInteractor +import com.android.systemui.volume.audioRepository +import com.android.systemui.volume.panel.ui.VolumePanelUiEvent +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidJUnit4::class) +@SmallTest +class AudioModeLoggerStartableTest : SysuiTestCase() { + @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule() + + private val kosmos = testKosmos() + + private lateinit var underTest: AudioModeLoggerStartable + + @Before + fun setUp() { + with(kosmos) { + underTest = + AudioModeLoggerStartable( + applicationCoroutineScope, + uiEventLogger, + audioModeInteractor + ) + } + } + + @Test + fun audioMode_inCall() { + with(kosmos) { + testScope.runTest { + audioRepository.setMode(AudioManager.MODE_IN_CALL) + + underTest.start() + runCurrent() + + assertThat(uiEventLoggerFake.eventId(0)) + .isEqualTo(VolumePanelUiEvent.VOLUME_PANEL_AUDIO_MODE_CHANGE_TO_CALLING.id) + } + } + } + + @Test + fun audioMode_notInCall() { + with(kosmos) { + testScope.runTest { + audioRepository.setMode(AudioManager.MODE_NORMAL) + + underTest.start() + runCurrent() + + assertThat(uiEventLoggerFake.eventId(0)) + .isEqualTo(VolumePanelUiEvent.VOLUME_PANEL_AUDIO_MODE_CHANGE_TO_NORMAL.id) + } + } + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModelTest.kt index 2cc1ad335535..27a813fb149e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModelTest.kt @@ -21,6 +21,8 @@ import android.content.Intent import android.provider.Settings import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.internal.logging.uiEventLogger +import com.android.internal.logging.uiEventLoggerFake import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope @@ -29,6 +31,7 @@ import com.android.systemui.plugins.activityStarter import com.android.systemui.testKosmos import com.android.systemui.util.mockito.capture import com.android.systemui.util.mockito.eq +import com.android.systemui.volume.panel.ui.VolumePanelUiEvent import com.android.systemui.volume.panel.volumePanelViewModel import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -58,7 +61,10 @@ class BottomBarViewModelTest : SysuiTestCase() { private lateinit var underTest: BottomBarViewModel private fun initUnderTest() { - underTest = with(kosmos) { BottomBarViewModel(activityStarter, volumePanelViewModel) } + underTest = + with(kosmos) { + BottomBarViewModel(activityStarter, volumePanelViewModel, uiEventLogger) + } } @Test @@ -96,6 +102,8 @@ class BottomBarViewModelTest : SysuiTestCase() { /* userHandle = */ eq(null), ) assertThat(intentCaptor.value.action).isEqualTo(Settings.ACTION_SOUND_SETTINGS) + assertThat(uiEventLoggerFake.eventId(0)) + .isEqualTo(VolumePanelUiEvent.VOLUME_PANEL_SOUND_SETTINGS_CLICKED.id) activityStartedCaptor.value.onActivityStarted(ActivityManager.START_SUCCESS) val volumePanelState by collectLastValue(volumePanelViewModel.volumePanelState) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/captioning/ui/viewmodel/CaptioningViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/captioning/ui/viewmodel/CaptioningViewModelTest.kt index 610195f5e87e..fdeded8422d6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/captioning/ui/viewmodel/CaptioningViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/captioning/ui/viewmodel/CaptioningViewModelTest.kt @@ -18,6 +18,7 @@ package com.android.systemui.volume.panel.component.captioning.ui.viewmodel import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.internal.logging.uiEventLogger import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope @@ -45,7 +46,12 @@ class CaptioningViewModelTest : SysuiTestCase() { fun setup() { underTest = with(kosmos) { - CaptioningViewModel(context, captioningInteractor, testScope.backgroundScope) + CaptioningViewModel( + context, + captioningInteractor, + testScope.backgroundScope, + uiEventLogger, + ) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/MediaOutputAvailabilityCriteriaTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/MediaOutputAvailabilityCriteriaTest.kt index ec55c75d4ae5..da0a2295143b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/MediaOutputAvailabilityCriteriaTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/MediaOutputAvailabilityCriteriaTest.kt @@ -46,7 +46,10 @@ class MediaOutputAvailabilityCriteriaTest : SysuiTestCase() { @Before fun setup() { - underTest = MediaOutputAvailabilityCriteria(kosmos.audioModeInteractor) + underTest = + MediaOutputAvailabilityCriteria( + kosmos.audioModeInteractor, + ) } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModelTest.kt index 462f36d73138..30524d93dc02 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModelTest.kt @@ -22,6 +22,7 @@ import android.media.session.PlaybackState import android.testing.TestableLooper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.internal.logging.uiEventLogger import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope @@ -64,6 +65,7 @@ class MediaOutputViewModelTest : SysuiTestCase() { mediaOutputActionsInteractor, mediaDeviceSessionInteractor, mediaOutputInteractor, + uiEventLogger, ) with(context.orCreateTestableResources) { diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index af327d2b7791..26fa2b136ed4 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -613,7 +613,7 @@ <dimen name="volume_panel_slice_vertical_padding">8dp</dimen> <dimen name="volume_panel_slice_horizontal_padding">24dp</dimen> - <dimen name="volume_panel_corner_radius">52dp</dimen> + <dimen name="volume_panel_corner_radius">28dp</dimen> <!-- Size of each item in the ringer selector drawer. --> <dimen name="volume_ringer_drawer_item_size">42dp</dimen> diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt index 3b8a268cd5ea..460779c73cda 100644 --- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt +++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt @@ -15,6 +15,7 @@ */ package com.android.keyguard +import android.os.Trace import android.content.BroadcastReceiver import android.content.Context import android.content.Intent @@ -430,6 +431,7 @@ constructor( if (MigrateClocksToBlueprint.isEnabled) { listenForDozeAmountTransition(this) listenForAnyStateToAodTransition(this) + listenForAnyStateToLockscreenTransition(this) } else { listenForDozeAmount(this) } @@ -520,8 +522,12 @@ constructor( private fun handleDoze(doze: Float) { dozeAmount = doze clock?.run { + Trace.beginSection("$TAG#smallClock.animations.doze") smallClock.animations.doze(dozeAmount) + Trace.endSection() + Trace.beginSection("$TAG#largeClock.animations.doze") largeClock.animations.doze(dozeAmount) + Trace.endSection() } smallTimeListener?.update(doze < DOZE_TICKRATE_THRESHOLD) largeTimeListener?.update(doze < DOZE_TICKRATE_THRESHOLD) @@ -536,10 +542,10 @@ constructor( internal fun listenForDozeAmountTransition(scope: CoroutineScope): Job { return scope.launch { merge( - keyguardTransitionInteractor.aodToLockscreenTransition.map { step -> + keyguardTransitionInteractor.transition(AOD, LOCKSCREEN).map { step -> step.copy(value = 1f - step.value) }, - keyguardTransitionInteractor.lockscreenToAodTransition, + keyguardTransitionInteractor.transition(LOCKSCREEN, AOD), ).filter { it.transitionState != TransitionState.FINISHED } @@ -562,6 +568,17 @@ constructor( } @VisibleForTesting + internal fun listenForAnyStateToLockscreenTransition(scope: CoroutineScope): Job { + return scope.launch { + keyguardTransitionInteractor + .transitionStepsToState(LOCKSCREEN) + .filter { it.transitionState == TransitionState.STARTED } + .filter { it.from != AOD } + .collect { handleDoze(0f) } + } + } + + @VisibleForTesting internal fun listenForDozing(scope: CoroutineScope): Job { return scope.launch { combine( @@ -628,7 +645,7 @@ constructor( } companion object { - private val TAG = ClockEventController::class.simpleName!! - private val DOZE_TICKRATE_THRESHOLD = 0.99f + private const val TAG = "ClockEventController" + private const val DOZE_TICKRATE_THRESHOLD = 0.99f } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt index ec54e4ce5e86..9816896e3ea8 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt @@ -282,7 +282,8 @@ open class UdfpsKeyguardViewControllerLegacy( @VisibleForTesting suspend fun listenForGoneToAodTransition(scope: CoroutineScope): Job { return scope.launch { - transitionInteractor.goneToAodTransition.collect { transitionStep -> + transitionInteractor.transition(KeyguardState.GONE, KeyguardState.AOD).collect { + transitionStep -> view.onDozeAmountChanged( transitionStep.value, transitionStep.value, diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt index 7525ce0f98ac..fa19bf478453 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractor.kt @@ -27,9 +27,9 @@ import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.KeyguardState -import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.util.kotlin.BooleanFlowOperators.or import com.android.systemui.util.time.SystemClock import dagger.Lazy import javax.inject.Inject @@ -78,15 +78,14 @@ constructor( bouncerRepository.alternateBouncerUIAvailable } private val isDozingOrAod: Flow<Boolean> = - keyguardTransitionInteractor - .get() - .transitions - .map { - it.to == KeyguardState.DOZING || - it.to == KeyguardState.AOD || - ((it.from == KeyguardState.DOZING || it.from == KeyguardState.AOD) && - it.transitionState != TransitionState.FINISHED) - } + or( + keyguardTransitionInteractor.get().transitionValue(KeyguardState.DOZING).map { + it > 0f + }, + keyguardTransitionInteractor.get().transitionValue(KeyguardState.AOD).map { + it > 0f + }, + ) .distinctUntilChanged() /** diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt index aeb564d53195..02a40d93ab65 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt @@ -16,6 +16,7 @@ package com.android.systemui.bouncer.domain.interactor +import com.android.compose.animation.scene.SceneKey import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor import com.android.systemui.authentication.domain.interactor.AuthenticationResult import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Sim @@ -26,6 +27,8 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.scene.shared.model.Scenes import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.async @@ -47,6 +50,7 @@ constructor( private val deviceEntryFaceAuthInteractor: DeviceEntryFaceAuthInteractor, private val falsingInteractor: FalsingInteractor, private val powerInteractor: PowerInteractor, + sceneInteractor: SceneInteractor, ) { private val _onIncorrectBouncerInput = MutableSharedFlow<Unit>() val onIncorrectBouncerInput: SharedFlow<Unit> = _onIncorrectBouncerInput @@ -80,6 +84,10 @@ constructor( } .map {} + /** The scene to show when bouncer is dismissed. */ + val dismissDestination: Flow<SceneKey> = + sceneInteractor.previousScene.map { it ?: Scenes.Lockscreen } + /** Notifies that the user has places down a pointer, not necessarily dragging just yet. */ fun onDown() { falsingInteractor.avoidGesture() diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt index 5c07cc57c620..7c41b75d7105 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt @@ -21,6 +21,12 @@ import android.app.admin.DevicePolicyResources import android.content.Context import android.graphics.Bitmap import androidx.core.graphics.drawable.toBitmap +import com.android.compose.animation.scene.Back +import com.android.compose.animation.scene.SceneKey +import com.android.compose.animation.scene.Swipe +import com.android.compose.animation.scene.SwipeDirection +import com.android.compose.animation.scene.UserAction +import com.android.compose.animation.scene.UserActionResult import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.authentication.shared.model.AuthenticationWipeModel @@ -35,6 +41,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.inputmethod.domain.interactor.InputMethodInteractor +import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.user.domain.interactor.SelectedUserInteractor import com.android.systemui.user.ui.viewmodel.UserActionViewModel import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel @@ -82,6 +89,15 @@ class BouncerViewModel( initialValue = null, ) + val destinationScenes: StateFlow<Map<UserAction, UserActionResult>> = + bouncerInteractor.dismissDestination + .map(::destinationSceneMap) + .stateIn( + applicationScope, + SharingStarted.WhileSubscribed(), + initialValue = destinationSceneMap(Scenes.Lockscreen), + ) + val message: BouncerMessageViewModel = bouncerMessageViewModel val userSwitcherDropdown: StateFlow<List<UserSwitcherDropdownItemViewModel>> = @@ -310,8 +326,7 @@ class BouncerViewModel( { message }, failedAttempts, remainingAttempts, - ) - ?: message + ) ?: message } else { message } @@ -328,8 +343,7 @@ class BouncerViewModel( .KEYGUARD_DIALOG_FAILED_ATTEMPTS_ERASING_PROFILE, { message }, failedAttempts, - ) - ?: message + ) ?: message } else { message } @@ -357,6 +371,12 @@ class BouncerViewModel( } } + private fun destinationSceneMap(prevScene: SceneKey) = + mapOf( + Back to UserActionResult(prevScene), + Swipe(SwipeDirection.Down) to UserActionResult(prevScene), + ) + data class DialogViewModel( val text: String, @@ -400,13 +420,13 @@ object BouncerViewModelModule { simBouncerInteractor = simBouncerInteractor, authenticationInteractor = authenticationInteractor, selectedUserInteractor = selectedUserInteractor, + devicePolicyManager = devicePolicyManager, + bouncerMessageViewModel = bouncerMessageViewModel, flags = flags, selectedUser = userSwitcherViewModel.selectedUser, users = userSwitcherViewModel.users, userSwitcherMenu = userSwitcherViewModel.menu, actionButton = actionButtonInteractor.actionButton, - devicePolicyManager = devicePolicyManager, - bouncerMessageViewModel = bouncerMessageViewModel, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt index 1eba0662a43d..a8f30297ff07 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt @@ -295,7 +295,8 @@ constructor( } private fun listenForSchedulingWatchdog() { - keyguardTransitionInteractor.anyStateToGoneTransition + keyguardTransitionInteractor + .transition(from = null, to = KeyguardState.GONE) .filter { it.transitionState == TransitionState.FINISHED } .onEach { // We deliberately want to run this in background because scheduleWatchdog does diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt index a7266503b7a1..03819ed9e2fe 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/SystemUIDeviceEntryFaceAuthInteractor.kt @@ -37,6 +37,10 @@ import com.android.systemui.deviceentry.shared.model.ErrorFaceAuthenticationStat import com.android.systemui.deviceentry.shared.model.FaceAuthenticationStatus import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.KeyguardState.AOD +import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING +import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN +import com.android.systemui.keyguard.shared.model.KeyguardState.OFF import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.log.FaceAuthenticationLogger import com.android.systemui.power.domain.interactor.PowerInteractor @@ -121,9 +125,9 @@ constructor( .launchIn(applicationScope) merge( - keyguardTransitionInteractor.aodToLockscreenTransition, - keyguardTransitionInteractor.offToLockscreenTransition, - keyguardTransitionInteractor.dozingToLockscreenTransition + keyguardTransitionInteractor.transition(AOD, LOCKSCREEN), + keyguardTransitionInteractor.transition(OFF, LOCKSCREEN), + keyguardTransitionInteractor.transition(DOZING, LOCKSCREEN), ) .filter { it.transitionState == TransitionState.STARTED } .sample(powerInteractor.detailedWakefulness) diff --git a/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamViewModel.kt b/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamViewModel.kt index ac03463da545..04edd252f98f 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamViewModel.kt @@ -23,6 +23,7 @@ import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dump.DumpManager import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.ui.viewmodel.DreamingToGlanceableHubTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel @@ -97,7 +98,7 @@ constructor( .distinctUntilChanged() val transitionEnded = - keyguardTransitionInteractor.fromDreamingTransition.filter { step -> + keyguardTransitionInteractor.transition(from = DREAMING, to = null).filter { step -> step.transitionState == TransitionState.FINISHED || step.transitionState == TransitionState.CANCELED } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java index 6b53f4ed554f..a5d7e04bf4d0 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java @@ -63,6 +63,7 @@ import android.view.SurfaceControl; import android.view.WindowManagerPolicyConstants; import android.window.IRemoteTransition; import android.window.IRemoteTransitionFinishedCallback; +import android.window.RemoteTransitionStub; import android.window.TransitionInfo; import com.android.internal.annotations.GuardedBy; @@ -187,7 +188,7 @@ public class KeyguardService extends Service { // Note: Also used for wrapping occlude by Dream animation. It works (with some redundancy). public static IRemoteTransition wrap(final KeyguardViewMediator keyguardViewMediator, final IRemoteAnimationRunner runner) { - return new IRemoteTransition.Stub() { + return new RemoteTransitionStub() { @GuardedBy("mLeashMap") private final ArrayMap<SurfaceControl, SurfaceControl> mLeashMap = new ArrayMap<>(); @@ -253,11 +254,6 @@ public class KeyguardService extends Service { } } - @Override - public void onTransitionConsumed(IBinder transition, boolean aborted) { - // No-op. - } - private static void initAlphaForAnimationTargets(@NonNull SurfaceControl.Transaction t, @NonNull RemoteAnimationTarget[] targets) { for (RemoteAnimationTarget target : targets) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt index e101b0ab64aa..c835599abfe1 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt @@ -29,6 +29,7 @@ import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.KeyguardState.GONE import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.utils.GlobalWindowManager @@ -83,7 +84,7 @@ constructor( applicationScope.launch(bgDispatcher) { // We drop 1 to avoid triggering on initial collect(). - keyguardTransitionInteractor.anyStateToGoneTransition.collect { transition -> + keyguardTransitionInteractor.transition(from = null, to = GONE).collect { transition -> if (transition.transitionState == TransitionState.FINISHED) { onKeyguardGone() } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt index 97081d93892a..d3ad0c2ac7e1 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt @@ -20,19 +20,14 @@ package com.android.systemui.keyguard.domain.interactor import android.util.Log import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyguard.data.repository.KeyguardRepository import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.Edge import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.KeyguardState.ALTERNATE_BOUNCER import com.android.systemui.keyguard.shared.model.KeyguardState.AOD import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING -import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING -import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING_LOCKSCREEN_HOSTED -import com.android.systemui.keyguard.shared.model.KeyguardState.GLANCEABLE_HUB -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.KeyguardState.OCCLUDED import com.android.systemui.keyguard.shared.model.KeyguardState.OFF import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER import com.android.systemui.keyguard.shared.model.TransitionInfo @@ -40,7 +35,6 @@ import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.util.kotlin.pairwise import javax.inject.Inject -import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.BufferOverflow @@ -53,6 +47,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -64,7 +59,6 @@ class KeyguardTransitionInteractor @Inject constructor( @Application val scope: CoroutineScope, - @Main private val mainDispatcher: CoroutineDispatcher, private val keyguardRepository: KeyguardRepository, private val repository: KeyguardTransitionRepository, private val fromLockscreenTransitionInteractor: dagger.Lazy<FromLockscreenTransitionInteractor>, @@ -75,14 +69,13 @@ constructor( dagger.Lazy<FromAlternateBouncerTransitionInteractor>, private val fromDozingTransitionInteractor: dagger.Lazy<FromDozingTransitionInteractor>, ) { - private val TAG = this::class.simpleName - - private val transitionValueCache = mutableMapOf<KeyguardState, MutableSharedFlow<Float>>() + private val transitionMap = mutableMapOf<Edge, MutableSharedFlow<TransitionStep>>() /** * Numerous flows are derived from, or care directly about, the transition value in and out of a * single state. This prevent the redundant filters from running. */ + private val transitionValueCache = mutableMapOf<KeyguardState, MutableSharedFlow<Float>>() private fun getTransitionValueFlow(state: KeyguardState): MutableSharedFlow<Float> { return transitionValueCache.getOrPut(state) { MutableSharedFlow<Float>( @@ -94,6 +87,7 @@ constructor( } } + @Deprecated("Not performant - Use something else in this class") val transitions = repository.transitions /** @@ -106,14 +100,14 @@ constructor( * from when we were canceled. */ val startedStepWithPrecedingStep = - transitions + repository.transitions .pairwise() .filter { it.newValue.transitionState == TransitionState.STARTED } .shareIn(scope, SharingStarted.Eagerly) init { // Collect non-canceled steps and emit transition values. - scope.launch(mainDispatcher) { + scope.launch { repository.transitions .filter { it.transitionState != TransitionState.CANCELED } .collect { step -> @@ -122,11 +116,22 @@ constructor( } } + scope.launch { + repository.transitions.collect { + // FROM->TO + transitionMap[Edge(it.from, it.to)]?.emit(it) + // FROM->(ANY) + transitionMap[Edge(it.from, null)]?.emit(it) + // (ANY)->TO + transitionMap[Edge(null, it.to)]?.emit(it) + } + } + // If a transition from state A -> B is canceled in favor of a transition from B -> C, we // need to ensure we emit transitionValue(A) = 0f, since no further steps will be emitted // where the from or to states are A. This would leave transitionValue(A) stuck at an // arbitrary non-zero value. - scope.launch(mainDispatcher) { + scope.launch { startedStepWithPrecedingStep.collect { (prevStep, startedStep) -> if ( prevStep.transitionState == TransitionState.CANCELED && @@ -138,116 +143,42 @@ constructor( } } - /** (any)->GONE transition information */ - val anyStateToGoneTransition: Flow<TransitionStep> = - repository.transitions.filter { step -> step.to == GONE } - - /** (any)->AOD transition information */ - val anyStateToAodTransition: Flow<TransitionStep> = - repository.transitions.filter { step -> step.to == AOD } - - /** DREAMING->(any) transition information. */ - val fromDreamingTransition: Flow<TransitionStep> = - repository.transitions.filter { step -> step.from == DREAMING } - - /** LOCKSCREEN->(any) transition information. */ - val fromLockscreenTransition: Flow<TransitionStep> = - repository.transitions.filter { step -> step.from == LOCKSCREEN } - - /** (any)->Lockscreen transition information */ - val anyStateToLockscreenTransition: Flow<TransitionStep> = - repository.transitions.filter { step -> step.to == LOCKSCREEN } - - /** (any)->Occluded transition information */ - val anyStateToOccludedTransition: Flow<TransitionStep> = - repository.transitions.filter { step -> step.to == OCCLUDED } - - /** (any)->PrimaryBouncer transition information */ - val anyStateToPrimaryBouncerTransition: Flow<TransitionStep> = - repository.transitions.filter { step -> step.to == PRIMARY_BOUNCER } - - /** (any)->Dreaming transition information */ - val anyStateToDreamingTransition: Flow<TransitionStep> = - repository.transitions.filter { step -> step.to == DREAMING } - - /** (any)->AlternateBouncer transition information */ - val anyStateToAlternateBouncerTransition: Flow<TransitionStep> = - repository.transitions.filter { step -> step.to == ALTERNATE_BOUNCER } - - /** AOD->LOCKSCREEN transition information. */ - val aodToLockscreenTransition: Flow<TransitionStep> = repository.transition(AOD, LOCKSCREEN) - - /** DREAMING->LOCKSCREEN transition information. */ - val dreamingToLockscreenTransition: Flow<TransitionStep> = - repository.transition(DREAMING, LOCKSCREEN) - - /** DREAMING_LOCKSCREEN_HOSTED->LOCKSCREEN transition information. */ - val dreamingLockscreenHostedToLockscreenTransition: Flow<TransitionStep> = - repository.transition(DREAMING_LOCKSCREEN_HOSTED, LOCKSCREEN) - - /** GONE->AOD transition information. */ - val goneToAodTransition: Flow<TransitionStep> = repository.transition(GONE, AOD) - - /** GONE->DREAMING transition information. */ - val goneToDreamingTransition: Flow<TransitionStep> = repository.transition(GONE, DREAMING) - - /** GONE->DREAMING_LOCKSCREEN_HOSTED transition information. */ - val goneToDreamingLockscreenHostedTransition: Flow<TransitionStep> = - repository.transition(GONE, DREAMING_LOCKSCREEN_HOSTED) - - /** GONE->LOCKSCREEN transition information. */ - val goneToLockscreenTransition: Flow<TransitionStep> = repository.transition(GONE, LOCKSCREEN) - - /** LOCKSCREEN->AOD transition information. */ - val lockscreenToAodTransition: Flow<TransitionStep> = repository.transition(LOCKSCREEN, AOD) - - /** LOCKSCREEN->DOZING transition information. */ - val lockscreenToDozingTransition: Flow<TransitionStep> = - repository.transition(LOCKSCREEN, DOZING) - - /** LOCKSCREEN->DREAMING transition information. */ - val lockscreenToDreamingTransition: Flow<TransitionStep> = - repository.transition(LOCKSCREEN, DREAMING) - - /** LOCKSCREEN->DREAMING_LOCKSCREEN_HOSTED transition information. */ - val lockscreenToDreamingLockscreenHostedTransition: Flow<TransitionStep> = - repository.transition(LOCKSCREEN, DREAMING_LOCKSCREEN_HOSTED) - - /** LOCKSCREEN->GLANCEABLE_HUB transition information. */ - val lockscreenToGlanceableHubTransition: Flow<TransitionStep> = - repository.transition(LOCKSCREEN, GLANCEABLE_HUB) - - /** LOCKSCREEN->OCCLUDED transition information. */ - val lockscreenToOccludedTransition: Flow<TransitionStep> = - repository.transition(LOCKSCREEN, OCCLUDED) - - /** GLANCEABLE_HUB->LOCKSCREEN transition information. */ - val glanceableHubToLockscreenTransition: Flow<TransitionStep> = - repository.transition(GLANCEABLE_HUB, LOCKSCREEN) - - /** OCCLUDED->LOCKSCREEN transition information. */ - val occludedToLockscreenTransition: Flow<TransitionStep> = - repository.transition(OCCLUDED, LOCKSCREEN) - - /** PRIMARY_BOUNCER->GONE transition information. */ - val primaryBouncerToGoneTransition: Flow<TransitionStep> = - repository.transition(PRIMARY_BOUNCER, GONE) - - /** OFF->LOCKSCREEN transition information. */ - val offToLockscreenTransition: Flow<TransitionStep> = repository.transition(OFF, LOCKSCREEN) + /** Given an [edge], return a SharedFlow to collect only relevant [TransitionStep]. */ + fun getOrCreateFlow(edge: Edge): MutableSharedFlow<TransitionStep> { + return transitionMap.getOrPut(edge) { + MutableSharedFlow<TransitionStep>( + extraBufferCapacity = 10, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + } + } - /** DOZING->LOCKSCREEN transition information. */ - val dozingToLockscreenTransition: Flow<TransitionStep> = - repository.transition(DOZING, LOCKSCREEN) + /** + * Receive all [TransitionStep] matching a filter of [from]->[to]. Allow nulls in order to match + * any transition, for instance (any)->GONE. + */ + fun transition(from: KeyguardState?, to: KeyguardState?): Flow<TransitionStep> { + if (from == null && to == null) { + throw IllegalArgumentException("from and to cannot both be null") + } + return getOrCreateFlow(Edge(from = from, to = to)) + } - /** Receive all [TransitionStep] matching a filter of [from]->[to] */ - fun transition(from: KeyguardState, to: KeyguardState): Flow<TransitionStep> { - return repository.transition(from, to) + /** + * The amount of transition into or out of the given [KeyguardState]. + * + * The value will be `0` (or close to `0`, due to float point arithmetic) if not in this step or + * `1` when fully in the given state. + */ + fun transitionValue( + state: KeyguardState, + ): Flow<Float> { + return getTransitionValueFlow(state) } /** - * AOD<->LOCKSCREEN transition information, mapped to dozeAmount range of AOD (1f) <-> - * Lockscreen (0f). + * AOD<->* transition information, mapped to dozeAmount range of AOD (1f) <-> + * * (0f). */ val dozeAmountTransition: Flow<TransitionStep> = repository.transitions @@ -265,13 +196,10 @@ constructor( val startedKeyguardTransitionStep: Flow<TransitionStep> = repository.transitions.filter { step -> step.transitionState == TransitionState.STARTED } - /** The last [TransitionStep] with a [TransitionState] of CANCELED */ - val canceledKeyguardTransitionStep: Flow<TransitionStep> = - repository.transitions.filter { step -> step.transitionState == TransitionState.CANCELED } - /** The last [TransitionStep] with a [TransitionState] of FINISHED */ val finishedKeyguardTransitionStep: Flow<TransitionStep> = - repository.transitions.filter { step -> step.transitionState == TransitionState.FINISHED } + repository.transitions + .filter { step -> step.transitionState == TransitionState.FINISHED } /** The destination state of the last [TransitionState.STARTED] transition. */ val startedKeyguardState: SharedFlow<KeyguardState> = @@ -364,10 +292,6 @@ constructor( * case, the smartspace will never be set to alpha = 1f and you'll have a half-faded smartspace * during the LS -> GONE transition. * - * If you need special-case handling for cancellations (such as conditional handling depending - * on which [KeyguardState] was canceled) you can collect [canceledKeyguardTransitionStep] - * directly. - * * As a helpful footnote, here's the values of [finishedKeyguardState] and * [currentKeyguardState] during a sequence with two cancellations: * 1. We're FINISHED in GONE. currentKeyguardState=GONE; finishedKeyguardState=GONE. @@ -390,7 +314,7 @@ constructor( } } .distinctUntilChanged() - .shareIn(scope, SharingStarted.Eagerly, replay = 1) + .stateIn(scope, SharingStarted.Eagerly, KeyguardState.OFF) /** * The [TransitionInfo] of the most recent call to @@ -420,24 +344,12 @@ constructor( /** Whether we've currently STARTED a transition and haven't yet FINISHED it. */ val isInTransitionToAnyState = isInTransitionWhere({ true }, { true }) - /** - * The amount of transition into or out of the given [KeyguardState]. - * - * The value will be `0` (or close to `0`, due to float point arithmetic) if not in this step or - * `1` when fully in the given state. - */ - fun transitionValue( - state: KeyguardState, - ): Flow<Float> { - return getTransitionValueFlow(state) - } - fun transitionStepsFromState(fromState: KeyguardState): Flow<TransitionStep> { - return repository.transitions.filter { step -> step.from == fromState } + return getOrCreateFlow(Edge(from = fromState, to = null)) } fun transitionStepsToState(toState: KeyguardState): Flow<TransitionStep> { - return repository.transitions.filter { step -> step.to == toState } + return getOrCreateFlow(Edge(from = null, to = toState)) } /** @@ -464,17 +376,24 @@ constructor( fun isInTransitionToState( state: KeyguardState, ): Flow<Boolean> { - return isInTransitionToStateWhere { it == state } + return getOrCreateFlow(Edge(from = null, to = state)) + .mapLatest { it.transitionState.isActive() } + .onStart { emit(false) } + .distinctUntilChanged() } /** - * Whether we're in a transition to a [KeyguardState] that matches the given predicate, but - * haven't yet completed it. + * Whether we're in a transition to and from the given [KeyguardState]s, but haven't yet + * completed it. */ - fun isInTransitionToStateWhere( - stateMatcher: (KeyguardState) -> Boolean, + fun isInTransition( + from: KeyguardState, + to: KeyguardState, ): Flow<Boolean> { - return isInTransitionWhere(fromStatePredicate = { true }, toStatePredicate = stateMatcher) + return getOrCreateFlow(Edge(from = from, to = to)) + .mapLatest { it.transitionState.isActive() } + .onStart { emit(false) } + .distinctUntilChanged() } /** @@ -483,12 +402,29 @@ constructor( fun isInTransitionFromState( state: KeyguardState, ): Flow<Boolean> { - return isInTransitionFromStateWhere { it == state } + return getOrCreateFlow(Edge(from = state, to = null)) + .mapLatest { it.transitionState.isActive() } + .onStart { emit(false) } + .distinctUntilChanged() + } + + /** + * Whether we're in a transition to a [KeyguardState] that matches the given predicate, but + * haven't yet completed it. + * + * If you only care about a single state, instead use the optimized [isInTransitionToState]. + */ + fun isInTransitionToStateWhere( + stateMatcher: (KeyguardState) -> Boolean, + ): Flow<Boolean> { + return isInTransitionWhere(fromStatePredicate = { true }, toStatePredicate = stateMatcher) } /** * Whether we're in a transition out of a [KeyguardState] that matches the given predicate, but * haven't yet completed it. + * + * If you only care about a single state, instead use the optimized [isInTransitionFromState]. */ fun isInTransitionFromStateWhere( stateMatcher: (KeyguardState) -> Boolean, @@ -499,6 +435,9 @@ constructor( /** * Whether we're in a transition between two [KeyguardState]s that match the given predicates, * but haven't yet completed it. + * + * If you only care about a single state for both from and to, instead use the optimized + * [isInTransition]. */ fun isInTransitionWhere( fromStatePredicate: (KeyguardState) -> Boolean, @@ -507,6 +446,13 @@ constructor( return isInTransitionWhere { from, to -> fromStatePredicate(from) && toStatePredicate(to) } } + /** + * Whether we're in a transition between two [KeyguardState]s that match the given predicates, + * but haven't yet completed it. + * + * If you only care about a single state for both from and to, instead use the optimized + * [isInTransition]. + */ fun isInTransitionWhere( fromToStatePredicate: (KeyguardState, KeyguardState) -> Boolean ): Flow<Boolean> { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionState.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionState.kt index 38a93b50ea97..f6567a6ccb53 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionState.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionState.kt @@ -18,11 +18,21 @@ package com.android.systemui.keyguard.shared.model /** Possible states for a running transition between [State] */ enum class TransitionState { /* Transition has begun. */ - STARTED, + STARTED { + override fun isActive() = true + }, /* Transition is actively running. */ - RUNNING, + RUNNING { + override fun isActive() = true + }, /* Transition has completed successfully. */ - FINISHED, + FINISHED { + override fun isActive() = false + }, /* Transition has been interrupted, and not completed successfully. */ - CANCELED, + CANCELED { + override fun isActive() = false + }; + + abstract fun isActive(): Boolean } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt index 5de1a61d61b5..735b10907c73 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt @@ -19,8 +19,6 @@ import android.view.animation.Interpolator import com.android.app.animation.Interpolators.LINEAR import com.android.keyguard.logging.KeyguardTransitionAnimationLogger import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.Edge import com.android.systemui.keyguard.shared.model.KeyguardState @@ -35,15 +33,10 @@ import kotlin.math.max import kotlin.math.min import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.launch /** * Assists in creating sub-flows for a KeyguardTransition. Call [setup] once for a transition, and @@ -53,35 +46,9 @@ import kotlinx.coroutines.launch class KeyguardTransitionAnimationFlow @Inject constructor( - @Application private val scope: CoroutineScope, - @Main private val mainDispatcher: CoroutineDispatcher, private val transitionInteractor: KeyguardTransitionInteractor, private val logger: KeyguardTransitionAnimationLogger, ) { - private val transitionMap = mutableMapOf<Edge, MutableSharedFlow<TransitionStep>>() - - init { - scope.launch(mainDispatcher) { - transitionInteractor.transitions.collect { - // FROM->TO - transitionMap[Edge(it.from, it.to)]?.emit(it) - // FROM->(ANY) - transitionMap[Edge(it.from, null)]?.emit(it) - // (ANY)->TO - transitionMap[Edge(null, it.to)]?.emit(it) - } - } - } - - private fun getOrCreateFlow(edge: Edge): MutableSharedFlow<TransitionStep> { - return transitionMap.getOrPut(edge) { - MutableSharedFlow<TransitionStep>( - extraBufferCapacity = 10, - onBufferOverflow = BufferOverflow.DROP_OLDEST - ) - } - } - /** Invoke once per transition between FROM->TO states to get access to a shared flow. */ fun setup( duration: Duration, @@ -185,7 +152,8 @@ constructor( }?.let { onStep(interpolator.getInterpolation(it)) } } - return getOrCreateFlow(edge) + return transitionInteractor + .getOrCreateFlow(edge) .map { step -> StateToValue( from = step.from, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt index 4d3a78d32b3a..91f76a4df771 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt @@ -169,10 +169,7 @@ class ClockSizeTransition( return@OnPreDrawListener true } - anim.duration = duration - anim.startDelay = startDelay - anim.interpolator = interpolator - anim.addListener( + val listener = object : AnimatorListenerAdapter() { override fun onAnimationStart(anim: Animator) { assignAnimValues("start", 0f, fromVis) @@ -183,8 +180,21 @@ class ClockSizeTransition( if (sendToBack) toView.translationZ = 0f toView.viewTreeObserver.removeOnPreDrawListener(predrawCallback) } + + override fun onAnimationPause(anim: Animator) { + toView.viewTreeObserver.removeOnPreDrawListener(predrawCallback) + } + + override fun onAnimationResume(anim: Animator) { + toView.viewTreeObserver.addOnPreDrawListener(predrawCallback) + } } - ) + + anim.duration = duration + anim.startDelay = startDelay + anim.interpolator = interpolator + anim.addListener(listener) + anim.addPauseListener(listener) assignAnimValues("init", 0f, fromVis) toView.viewTreeObserver.addOnPreDrawListener(predrawCallback) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerWindowViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerWindowViewModel.kt index 7814576eff01..5cf100e78e6e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerWindowViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerWindowViewModel.kt @@ -19,7 +19,6 @@ package com.android.systemui.keyguard.ui.viewmodel import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.KeyguardState -import com.android.systemui.keyguard.shared.model.TransitionState import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow @@ -38,12 +37,9 @@ constructor( private val deviceSupportsAlternateBouncer: Flow<Boolean> = alternateBouncerInteractor.alternateBouncerSupported private val isTransitioningToOrFromOrShowingAlternateBouncer: Flow<Boolean> = - keyguardTransitionInteractor.transitions - .map { - it.to == KeyguardState.ALTERNATE_BOUNCER || - (it.from == KeyguardState.ALTERNATE_BOUNCER && - it.transitionState != TransitionState.FINISHED) - } + keyguardTransitionInteractor + .transitionValue(KeyguardState.ALTERNATE_BOUNCER) + .map { it > 0f } .distinctUntilChanged() val alternateBouncerWindowRequired: Flow<Boolean> = diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt index 4ddd57110b38..e2177e61d954 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt @@ -29,9 +29,6 @@ import com.android.systemui.keyguard.domain.interactor.BurnInInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.BurnInModel -import com.android.systemui.keyguard.shared.model.TransitionState -import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING -import com.android.systemui.keyguard.shared.model.TransitionState.STARTED import com.android.systemui.keyguard.ui.StateToValue import com.android.systemui.res.R import javax.inject.Inject @@ -97,9 +94,9 @@ constructor( occludedToLockscreen, aodToLockscreen -> val translationY = - if (isInTransition(aodToLockscreen.transitionState)) { + if (aodToLockscreen.transitionState.isActive()) { aodToLockscreen.value ?: 0f - } else if (isInTransition(goneToAod.transitionState)) { + } else if (goneToAod.transitionState.isActive()) { (goneToAod.value ?: 0f) + burnInModel.translationY } else { burnInModel.translationY + occludedToLockscreen + keyguardTranslationY @@ -110,10 +107,6 @@ constructor( .distinctUntilChanged() } - private fun isInTransition(state: TransitionState): Boolean { - return state == STARTED || state == RUNNING - } - private fun burnIn( params: BurnInParameters, ): Flow<BurnInModel> { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlows.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlows.kt index 6d0f96c2f635..24429fae93ac 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlows.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlows.kt @@ -102,7 +102,7 @@ constructor( to = GONE, ) - return shadeInteractor.shadeExpansion.flatMapLatest { shadeExpansion -> + return shadeInteractor.isAnyExpanded.flatMapLatest { isAnyExpanded -> transitionAnimation .sharedFlow( duration = duration, @@ -110,7 +110,7 @@ constructor( onStart = { leaveShadeOpen = statusBarStateController.leaveOpenOnKeyguardHide() willRunDismissFromKeyguard = willRunAnimationOnKeyguard() - isShadeExpanded = shadeExpansion > 0f + isShadeExpanded = isAnyExpanded }, onStep = { 1f - it }, ) 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 ac67f94fa9cf..24a7c512a6a9 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 @@ -42,6 +42,7 @@ import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.notification.domain.interactor.NotificationsKeyguardInteractor import com.android.systemui.statusbar.phone.DozeParameters import com.android.systemui.statusbar.phone.ScreenOffAnimationController +import com.android.systemui.util.kotlin.BooleanFlowOperators.or import com.android.systemui.util.kotlin.pairwise import com.android.systemui.util.kotlin.sample import com.android.systemui.util.ui.AnimatableEvent @@ -133,22 +134,18 @@ constructor( private val isOnLockscreen: Flow<Boolean> = combine( keyguardTransitionInteractor.isFinishedInState(LOCKSCREEN).onStart { emit(false) }, - keyguardTransitionInteractor - .isInTransitionWhere { from, to -> from == LOCKSCREEN || to == LOCKSCREEN } - .onStart { emit(false) } + or( + keyguardTransitionInteractor.isInTransitionToState(LOCKSCREEN), + keyguardTransitionInteractor.isInTransitionFromState(LOCKSCREEN), + ), ) { onLockscreen, transitioningToOrFromLockscreen -> onLockscreen || transitioningToOrFromLockscreen } .distinctUntilChanged() - private val lockscreenToGoneTransitionRunning: Flow<Boolean> = - keyguardTransitionInteractor - .isInTransitionWhere { from, to -> from == LOCKSCREEN && to == GONE } - .onStart { emit(false) } - private val alphaOnShadeExpansion: Flow<Float> = combineTransform( - lockscreenToGoneTransitionRunning, + keyguardTransitionInteractor.isInTransition(from = LOCKSCREEN, to = GONE), isOnLockscreen, shadeInteractor.qsExpansion, shadeInteractor.shadeExpansion, 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 df34169c9a50..9dc5900296c2 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 @@ -19,7 +19,9 @@ package com.android.systemui.media.controls.data.repository import com.android.internal.logging.InstanceId import com.android.systemui.dagger.SysUISingleton import com.android.systemui.media.controls.shared.model.MediaData +import com.android.systemui.media.controls.shared.model.MediaDataLoadingModel import com.android.systemui.media.controls.shared.model.SmartspaceMediaData +import com.android.systemui.media.controls.shared.model.SmartspaceMediaLoadingModel import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -46,6 +48,16 @@ class MediaFilterRepository @Inject constructor() { MutableStateFlow(LinkedHashMap()) val allUserEntries: StateFlow<Map<String, MediaData>> = _allUserEntries.asStateFlow() + private val _mediaDataLoadedStates: MutableStateFlow<List<MediaDataLoadingModel>> = + MutableStateFlow(mutableListOf()) + val mediaDataLoadedStates: StateFlow<List<MediaDataLoadingModel>> = + _mediaDataLoadedStates.asStateFlow() + + private val _recommendationsLoadingState: MutableStateFlow<SmartspaceMediaLoadingModel> = + MutableStateFlow(SmartspaceMediaLoadingModel.Unknown) + val recommendationsLoadingState: StateFlow<SmartspaceMediaLoadingModel> = + _recommendationsLoadingState.asStateFlow() + fun addMediaEntry(key: String, data: MediaData) { val entries = LinkedHashMap<String, MediaData>(_allUserEntries.value) entries[key] = data @@ -110,4 +122,25 @@ class MediaFilterRepository @Inject constructor() { fun setReactivatedId(instanceId: InstanceId?) { _reactivatedId.value = instanceId } + + fun addMediaDataLoadingState(mediaDataLoadingModel: MediaDataLoadingModel) { + // Filter out previous loading state that has same [InstanceId]. + val loadedStates = + _mediaDataLoadedStates.value.filter { loadedModel -> + loadedModel !is MediaDataLoadingModel.Loaded || + !loadedModel.equalInstanceIds(mediaDataLoadingModel) + } + + _mediaDataLoadedStates.value = + loadedStates + + if (mediaDataLoadingModel is MediaDataLoadingModel.Loaded) { + listOf(mediaDataLoadingModel) + } else { + emptyList() + } + } + + fun setRecommedationsLoadingState(smartspaceMediaLoadingModel: SmartspaceMediaLoadingModel) { + _recommendationsLoadingState.value = smartspaceMediaLoadingModel + } } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImpl.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImpl.kt index d40069c4b3da..a30e5826529a 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImpl.kt @@ -28,7 +28,9 @@ import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.media.controls.data.repository.MediaFilterRepository import com.android.systemui.media.controls.shared.model.EXTRA_KEY_TRIGGER_RESUME import com.android.systemui.media.controls.shared.model.MediaData +import com.android.systemui.media.controls.shared.model.MediaDataLoadingModel import com.android.systemui.media.controls.shared.model.SmartspaceMediaData +import com.android.systemui.media.controls.shared.model.SmartspaceMediaLoadingModel import com.android.systemui.media.controls.util.MediaFlags import com.android.systemui.media.controls.util.MediaUiEventLogger import com.android.systemui.settings.UserTracker @@ -67,9 +69,6 @@ constructor( private val mediaFlags: MediaFlags, private val mediaFilterRepository: MediaFilterRepository, ) : MediaDataManager.Listener { - private val _listeners: MutableSet<Listener> = mutableSetOf() - val listeners: Set<Listener> - get() = _listeners.toSet() lateinit var mediaDataManager: MediaDataManager // Ensure the field (and associated reference) isn't removed during optimization. @@ -111,8 +110,9 @@ constructor( mediaFilterRepository.addSelectedUserMediaEntry(data) - // Notify listeners - listeners.forEach { it.onMediaDataLoaded(data.instanceId) } + mediaFilterRepository.addMediaDataLoadingState( + MediaDataLoadingModel.Loaded(data.instanceId) + ) } override fun onSmartspaceMediaDataLoaded( @@ -159,7 +159,7 @@ constructor( // reactivate. if (shouldReactivate) { val lastActiveId = sorted.lastKey() // most recently active id - // Notify listeners to consider this media active + // Update loading state to consider this media active Log.d(TAG, "reactivating $lastActiveId instead of smartspace") mediaFilterRepository.setReactivatedId(lastActiveId) val mediaData = sorted[lastActiveId]!!.copy(active = true) @@ -168,15 +168,9 @@ constructor( mediaData.packageName, mediaData.instanceId ) - listeners.forEach { - it.onMediaDataLoaded( - lastActiveId, - receivedSmartspaceCardLatency = - (systemClock.currentTimeMillis() - data.headphoneConnectionTimeMillis) - .toInt(), - isSsReactivated = true - ) - } + mediaFilterRepository.addMediaDataLoadingState( + MediaDataLoadingModel.Loaded(lastActiveId) + ) } } else if (data.isActive) { // Mark to prioritize Smartspace card if no recent media. @@ -192,15 +186,18 @@ constructor( smartspaceMediaData.packageName, smartspaceMediaData.instanceId ) - listeners.forEach { it.onSmartspaceMediaDataLoaded(key, shouldPrioritizeMutable) } + mediaFilterRepository.setRecommedationsLoadingState( + SmartspaceMediaLoadingModel.Loaded(key, shouldPrioritizeMutable) + ) } override fun onMediaDataRemoved(key: String) { mediaFilterRepository.removeMediaEntry(key)?.let { mediaData -> val instanceId = mediaData.instanceId mediaFilterRepository.removeSelectedUserMediaEntry(instanceId)?.let { - // Only notify listeners if something actually changed - listeners.forEach { it.onMediaDataRemoved(instanceId) } + mediaFilterRepository.addMediaDataLoadingState( + MediaDataLoadingModel.Removed(instanceId) + ) } } } @@ -210,11 +207,11 @@ constructor( mediaFilterRepository.reactivatedId.value?.let { lastActiveId -> mediaFilterRepository.setReactivatedId(null) Log.d(TAG, "expiring reactivated key $lastActiveId") - // Notify listeners to update with actual active value + // Update loading state with actual active value mediaFilterRepository.selectedUserEntries.value[lastActiveId]?.let { - listeners.forEach { listener -> - listener.onMediaDataLoaded(lastActiveId, immediately) - } + mediaFilterRepository.addMediaDataLoadingState( + MediaDataLoadingModel.Loaded(lastActiveId, immediately) + ) } } @@ -227,7 +224,9 @@ constructor( ) ) } - listeners.forEach { it.onSmartspaceMediaDataRemoved(key, immediately) } + mediaFilterRepository.setRecommedationsLoadingState( + SmartspaceMediaLoadingModel.Removed(key, immediately) + ) } @VisibleForTesting @@ -238,29 +237,37 @@ constructor( // Only remove media when the profile is unavailable. if (DEBUG) Log.d(TAG, "Removing $key after profile change") mediaFilterRepository.removeSelectedUserMediaEntry(data.instanceId, data) - listeners.forEach { listener -> listener.onMediaDataRemoved(data.instanceId) } + mediaFilterRepository.addMediaDataLoadingState( + MediaDataLoadingModel.Removed(data.instanceId) + ) } } } @VisibleForTesting internal fun handleUserSwitched() { - // If the user changes, remove all current MediaData objects and inform listeners - val listenersCopy = listeners + // If the user changes, remove all current MediaData objects. val keyCopy = mediaFilterRepository.selectedUserEntries.value.keys.toMutableList() - // Clear the list first, to make sure callbacks from listeners if we have any entries - // are up to date + // Clear the list first and update loading state to remove media from UI. mediaFilterRepository.clearSelectedUserMedia() keyCopy.forEach { instanceId -> if (DEBUG) Log.d(TAG, "Removing $instanceId after user change") - listenersCopy.forEach { listener -> listener.onMediaDataRemoved(instanceId) } + mediaFilterRepository.addMediaDataLoadingState( + MediaDataLoadingModel.Removed(instanceId) + ) } mediaFilterRepository.allUserEntries.value.forEach { (key, data) -> if (lockscreenUserManager.isCurrentProfile(data.userId)) { - if (DEBUG) Log.d(TAG, "Re-adding $key after user change") + if (DEBUG) + Log.d( + TAG, + "Re-adding $key with instanceId=${data.instanceId} after user change" + ) mediaFilterRepository.addSelectedUserMediaEntry(data) - listenersCopy.forEach { listener -> listener.onMediaDataLoaded(data.instanceId) } + mediaFilterRepository.addMediaDataLoadingState( + MediaDataLoadingModel.Loaded(data.instanceId) + ) } } } @@ -310,12 +317,6 @@ constructor( } } - /** Add a listener for filtered [MediaData] changes */ - fun addListener(listener: Listener) = _listeners.add(listener) - - /** Remove a listener that was registered with addListener */ - fun removeListener(listener: Listener) = _listeners.remove(listener) - /** * Return the time since last active for the most-recent media. * @@ -335,48 +336,6 @@ constructor( return sortedEntries[lastActiveInstanceId]?.let { now - it.lastActive } ?: Long.MAX_VALUE } - interface Listener { - /** - * Called whenever there's new MediaData Loaded for the consumption in views. - * - * @param immediately indicates should apply the UI changes immediately, otherwise wait - * until the next refresh-round before UI becomes visible. True by default to take in - * place immediately. - * @param receivedSmartspaceCardLatency is the latency between headphone connects and sysUI - * displays Smartspace media targets. Will be 0 if the data is not activated by Smartspace - * signal. - * @param isSsReactivated indicates resume media card is reactivated by Smartspace - * recommendation signal - */ - fun onMediaDataLoaded( - instanceId: InstanceId, - immediately: Boolean = true, - receivedSmartspaceCardLatency: Int = 0, - isSsReactivated: Boolean = false, - ) - - /** - * Called whenever there's new Smartspace media data loaded. - * - * @param shouldPrioritize indicates the sorting priority of the Smartspace card. If true, - * it will be prioritized as the first card. Otherwise, it will show up as the last card - * as default. - */ - fun onSmartspaceMediaDataLoaded(key: String, shouldPrioritize: Boolean = false) - - /** Called whenever a previously existing Media notification was removed. */ - fun onMediaDataRemoved(instanceId: InstanceId) - - /** - * Called whenever a previously existing Smartspace media data was removed. - * - * @param immediately indicates should apply the UI changes immediately, otherwise wait - * until the next refresh-round before UI becomes visible. True by default to take in - * place immediately. - */ - fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean = true) - } - companion object { /** * Maximum age of a media control to re-activate on smartspace signal. If there is no media diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt index 7dbca0ae4cda..cdcf3636e148 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt @@ -34,11 +34,14 @@ import com.android.systemui.media.controls.domain.pipeline.MediaDeviceManager import com.android.systemui.media.controls.domain.pipeline.MediaSessionBasedFilter import com.android.systemui.media.controls.domain.pipeline.MediaTimeoutListener import com.android.systemui.media.controls.domain.resume.MediaResumeListener +import com.android.systemui.media.controls.shared.model.MediaDataLoadingModel +import com.android.systemui.media.controls.shared.model.SmartspaceMediaLoadingModel import com.android.systemui.media.controls.util.MediaFlags import java.io.PrintWriter import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine @@ -109,6 +112,14 @@ constructor( .distinctUntilChanged() .stateIn(applicationScope, SharingStarted.WhileSubscribed(), false) + /** The most recent list of loaded media controls. */ + val mediaDataLoadedStates: Flow<List<MediaDataLoadingModel>> = + mediaFilterRepository.mediaDataLoadedStates + + /** The most recent change to loaded media recommendations. */ + val recommendationsLoadingState: Flow<SmartspaceMediaLoadingModel> = + mediaFilterRepository.recommendationsLoadingState + override fun start() { if (!mediaFlags.isMediaControlsRefactorEnabled()) { return diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaDataLoadingModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaDataLoadingModel.kt new file mode 100644 index 000000000000..bd42a4df7262 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaDataLoadingModel.kt @@ -0,0 +1,46 @@ +/* + * 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.media.controls.shared.model + +import com.android.internal.logging.InstanceId + +/** Models media data loading state. */ +sealed class MediaDataLoadingModel { + /** The initial loading state when no media data has yet loaded. */ + data object Unknown : MediaDataLoadingModel() + + /** Media data has been loaded. */ + data class Loaded( + val instanceId: InstanceId, + val immediatelyUpdateUi: Boolean = true, + ) : MediaDataLoadingModel() { + + /** Returns true if [other] has the same instance id, false otherwise. */ + fun equalInstanceIds(other: MediaDataLoadingModel): Boolean { + return when (other) { + is Loaded -> other.instanceId == instanceId + is Removed -> other.instanceId == instanceId + Unknown -> false + } + } + } + + /** Media data has been removed. */ + data class Removed( + val instanceId: InstanceId, + ) : MediaDataLoadingModel() +} diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/SmartspaceMediaLoadingModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/SmartspaceMediaLoadingModel.kt new file mode 100644 index 000000000000..6c1e536f8c02 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/SmartspaceMediaLoadingModel.kt @@ -0,0 +1,35 @@ +/* + * 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.media.controls.shared.model + +/** Models smartspace media loading state. */ +sealed class SmartspaceMediaLoadingModel { + /** The initial loading state when no smartspace media has yet loaded. */ + data object Unknown : SmartspaceMediaLoadingModel() + + /** Smartspace media has been loaded. */ + data class Loaded( + val key: String, + val isPrioritized: Boolean = false, + ) : SmartspaceMediaLoadingModel() + + /** Smartspace media has been removed. */ + data class Removed( + val key: String, + val immediatelyUpdateUi: Boolean = true, + ) : SmartspaceMediaLoadingModel() +} diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaController.kt index 963c602b3d1e..c02ce3b0a6c0 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaController.kt @@ -297,6 +297,7 @@ constructor( } } - private val activeContainer: ViewGroup? = - if (useSplitShade) splitShadeContainer else singlePaneContainer + // This field is only used to log current active container. + private val activeContainer: ViewGroup? + get() = if (useSplitShade) splitShadeContainer else singlePaneContainer } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt index c3c1e83546df..5b39ed34cb75 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt @@ -46,6 +46,8 @@ import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.KeyguardState +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.TransitionState import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.media.controls.domain.pipeline.MediaDataManager @@ -600,7 +602,8 @@ constructor( @VisibleForTesting internal fun listenForAnyStateToGoneKeyguardTransition(scope: CoroutineScope): Job { return scope.launch { - keyguardTransitionInteractor.anyStateToGoneTransition + keyguardTransitionInteractor + .transition(from = null, to = GONE) .filter { it.transitionState == TransitionState.FINISHED } .collect { showMediaCarousel() @@ -612,7 +615,8 @@ constructor( @VisibleForTesting internal fun listenForAnyStateToLockscreenTransition(scope: CoroutineScope): Job { return scope.launch { - keyguardTransitionInteractor.anyStateToLockscreenTransition + keyguardTransitionInteractor + .transition(from = null, to = LOCKSCREEN) .filter { it.transitionState == TransitionState.FINISHED } .collect { if (!allowMediaPlayerOnLockScreen) { diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputSwitcherDialogUI.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputSwitcherDialogUI.java index 6e7e0f241dd8..da852348b4e6 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputSwitcherDialogUI.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputSwitcherDialogUI.java @@ -18,6 +18,7 @@ package com.android.systemui.media.dialog; import android.annotation.MainThread; import android.content.Context; +import android.os.UserHandle; import android.text.TextUtils; import android.util.Log; @@ -52,8 +53,9 @@ public class MediaOutputSwitcherDialogUI implements CoreStartable, CommandQueue. @Override @MainThread - public void showMediaOutputSwitcher(String packageName) { + public void showMediaOutputSwitcher(String packageName, UserHandle userHandle) { if (!TextUtils.isEmpty(packageName)) { + // TODO: b/279555229 - Pass the userHandle into the output dialog manager. mMediaOutputDialogManager.createAndShow(packageName, false, null); } else { Log.e(TAG, "Unable to launch media output dialog. Package name is empty."); 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 1f935f97e771..0e66c28d4b8d 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 @@ -62,6 +62,7 @@ import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged @@ -69,10 +70,12 @@ import kotlinx.coroutines.flow.distinctUntilChangedBy import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.filterNot import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch /** @@ -246,6 +249,12 @@ constructor( private fun handleDeviceUnlockStatus() { applicationScope.launch { + // Track the previous scene (sans Bouncer), so that we know where to go when the device + // is unlocked whilst on the bouncer. + val previousScene = + sceneInteractor.previousScene + .filterNot { it == Scenes.Bouncer } + .stateIn(this, SharingStarted.Eagerly, initialValue = null) deviceUnlockedInteractor.deviceUnlockStatus .mapNotNull { deviceUnlockStatus -> val renderedScenes = @@ -273,8 +282,15 @@ constructor( when { isOnBouncer -> - // When the device becomes unlocked in Bouncer, go to Gone. - Scenes.Gone to "device was unlocked in Bouncer scene" + // When the device becomes unlocked in Bouncer, go to previous scene, + // or Gone. + if (previousScene.value == Scenes.Lockscreen) { + Scenes.Gone to "device was unlocked in Bouncer scene" + } else { + val prevScene = previousScene.value + (prevScene ?: Scenes.Gone) to + "device was unlocked in Bouncer scene, from sceneKey=$prevScene" + } isOnLockscreen -> // The lockscreen should be dismissed automatically in 2 scenarios: // 1. When face auth bypass is enabled and authentication happens while diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 4660831b77af..6b08a9ae52f2 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -30,6 +30,11 @@ import static com.android.systemui.classifier.Classifier.BOUNCER_UNLOCK; import static com.android.systemui.classifier.Classifier.GENERIC; import static com.android.systemui.classifier.Classifier.QUICK_SETTINGS; import static com.android.systemui.classifier.Classifier.UNLOCK; +import static com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING; +import static com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING_LOCKSCREEN_HOSTED; +import static com.android.systemui.keyguard.shared.model.KeyguardState.GONE; +import static com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN; +import static com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED; import static com.android.systemui.navigationbar.gestural.Utilities.isTrackpadScroll; import static com.android.systemui.navigationbar.gestural.Utilities.isTrackpadThreeFingerSwipe; import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_CLOSED; @@ -1119,7 +1124,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump controller.setup(mNotificationContainerParent)); // Dreaming->Lockscreen - collectFlow(mView, mKeyguardTransitionInteractor.getDreamingToLockscreenTransition(), + collectFlow(mView, mKeyguardTransitionInteractor.transition(DREAMING, LOCKSCREEN), mDreamingToLockscreenTransition, mMainDispatcher); collectFlow(mView, mDreamingToLockscreenTransitionViewModel.getLockscreenAlpha(), setDreamLockscreenTransitionAlpha(mNotificationStackScrollLayoutController), @@ -1130,7 +1135,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump // Gone -> Dreaming hosted in lockscreen collectFlow(mView, mKeyguardTransitionInteractor - .getGoneToDreamingLockscreenHostedTransition(), + .transition(GONE, DREAMING_LOCKSCREEN_HOSTED), mGoneToDreamingLockscreenHostedTransition, mMainDispatcher); collectFlow(mView, mGoneToDreamingLockscreenHostedTransitionViewModel.getLockscreenAlpha(), setTransitionAlpha(mNotificationStackScrollLayoutController), @@ -1138,16 +1143,16 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump // Lockscreen -> Dreaming hosted in lockscreen collectFlow(mView, mKeyguardTransitionInteractor - .getLockscreenToDreamingLockscreenHostedTransition(), + .transition(LOCKSCREEN, DREAMING_LOCKSCREEN_HOSTED), mLockscreenToDreamingLockscreenHostedTransition, mMainDispatcher); // Dreaming hosted in lockscreen -> Lockscreen collectFlow(mView, mKeyguardTransitionInteractor - .getDreamingLockscreenHostedToLockscreenTransition(), + .transition(DREAMING_LOCKSCREEN_HOSTED, LOCKSCREEN), mDreamingLockscreenHostedToLockscreenTransition, mMainDispatcher); // Occluded->Lockscreen - collectFlow(mView, mKeyguardTransitionInteractor.getOccludedToLockscreenTransition(), + collectFlow(mView, mKeyguardTransitionInteractor.transition(OCCLUDED, LOCKSCREEN), mOccludedToLockscreenTransition, mMainDispatcher); if (!MigrateClocksToBlueprint.isEnabled()) { collectFlow(mView, mOccludedToLockscreenTransitionViewModel.getLockscreenAlpha(), @@ -1158,7 +1163,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } // Lockscreen->Dreaming - collectFlow(mView, mKeyguardTransitionInteractor.getLockscreenToDreamingTransition(), + collectFlow(mView, mKeyguardTransitionInteractor.transition(LOCKSCREEN, DREAMING), mLockscreenToDreamingTransition, mMainDispatcher); if (!MigrateClocksToBlueprint.isEnabled()) { collectFlow(mView, mLockscreenToDreamingTransitionViewModel.getLockscreenAlpha(), @@ -1170,7 +1175,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump setTransitionY(mNotificationStackScrollLayoutController), mMainDispatcher); // Gone->Dreaming - collectFlow(mView, mKeyguardTransitionInteractor.getGoneToDreamingTransition(), + collectFlow(mView, mKeyguardTransitionInteractor.transition(GONE, DREAMING), mGoneToDreamingTransition, mMainDispatcher); if (!MigrateClocksToBlueprint.isEnabled()) { collectFlow(mView, mGoneToDreamingTransitionViewModel.getLockscreenAlpha(), @@ -1181,7 +1186,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump setTransitionY(mNotificationStackScrollLayoutController), mMainDispatcher); // Lockscreen->Occluded - collectFlow(mView, mKeyguardTransitionInteractor.getLockscreenToOccludedTransition(), + collectFlow(mView, mKeyguardTransitionInteractor.transition(LOCKSCREEN, OCCLUDED), mLockscreenToOccludedTransition, mMainDispatcher); if (!MigrateClocksToBlueprint.isEnabled()) { collectFlow(mView, mLockscreenToOccludedTransitionViewModel.getLockscreenAlpha(), diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java index 6ac81d226eee..b2952dcbcb39 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java @@ -18,6 +18,8 @@ package com.android.systemui.shade; import static com.android.systemui.flags.Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED; import static com.android.systemui.flags.Flags.TRACKPAD_GESTURE_COMMON; +import static com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING; +import static com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN; import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow; @@ -221,7 +223,7 @@ public class NotificationShadeWindowViewController implements Dumpable { mDisableSubpixelTextTransitionListener = new DisableSubpixelTextTransitionListener(mView); bouncerViewBinder.bind(mView.findViewById(R.id.keyguard_bouncer_container)); - collectFlow(mView, keyguardTransitionInteractor.getLockscreenToDreamingTransition(), + collectFlow(mView, keyguardTransitionInteractor.transition(LOCKSCREEN, DREAMING), mLockscreenToDreamingTransition); collectFlow( mView, diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt index d68e28c930f4..0b45c0834245 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt @@ -82,7 +82,7 @@ constructor( override val isShadeTouchable: Flow<Boolean> = combine( powerInteractor.isAsleep, - keyguardTransitionInteractor.isInTransitionToStateWhere { it == KeyguardState.AOD }, + keyguardTransitionInteractor.isInTransitionToState(KeyguardState.AOD), keyguardRepository.dozeTransitionModel.map { it.to == DozeStateModel.DOZE_PULSING }, ) { isAsleep, goingToSleep, isPulsing -> when { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java index e7b159a2d057..d955349ffede 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java @@ -50,6 +50,7 @@ import android.os.Message; import android.os.ParcelFileDescriptor; import android.os.Process; import android.os.RemoteException; +import android.os.UserHandle; import android.util.Pair; import android.util.SparseArray; import android.view.KeyEvent; @@ -516,7 +517,7 @@ public class CommandQueue extends IStatusBar.Stub implements /** * @see IStatusBar#showMediaOutputSwitcher */ - default void showMediaOutputSwitcher(String packageName) {} + default void showMediaOutputSwitcher(String packageName, UserHandle userHandle) {} /** * @see IStatusBar#confirmImmersivePrompt @@ -1361,7 +1362,7 @@ public class CommandQueue extends IStatusBar.Stub implements } } @Override - public void showMediaOutputSwitcher(String packageName) { + public void showMediaOutputSwitcher(String packageName, UserHandle userHandle) { int callingUid = Binder.getCallingUid(); if (callingUid != 0 && callingUid != Process.SYSTEM_UID) { throw new SecurityException("Call only allowed from system server."); @@ -1369,6 +1370,7 @@ public class CommandQueue extends IStatusBar.Stub implements synchronized (mLock) { SomeArgs args = SomeArgs.obtain(); args.arg1 = packageName; + args.arg2 = userHandle; mHandler.obtainMessage(MSG_SHOW_MEDIA_OUTPUT_SWITCHER, args).sendToTarget(); } } @@ -1939,8 +1941,10 @@ public class CommandQueue extends IStatusBar.Stub implements case MSG_SHOW_MEDIA_OUTPUT_SWITCHER: args = (SomeArgs) msg.obj; String clientPackageName = (String) args.arg1; + UserHandle clientUserHandle = (UserHandle) args.arg2; for (int i = 0; i < mCallbacks.size(); i++) { - mCallbacks.get(i).showMediaOutputSwitcher(clientPackageName); + mCallbacks.get(i).showMediaOutputSwitcher(clientPackageName, + clientUserHandle); } break; case MSG_CONFIRM_IMMERSIVE_PROMPT: diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java index 815236e0820c..09985f842185 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java @@ -195,20 +195,20 @@ public class KeyguardIndicationController { private boolean mOrganizationOwnedDevice; // these all assume the device is plugged in (wired/wireless/docked) AND chargingOrFull: - private boolean mPowerPluggedIn; - private boolean mPowerPluggedInWired; - private boolean mPowerPluggedInWireless; - private boolean mPowerPluggedInDock; + protected boolean mPowerPluggedIn; + protected boolean mPowerPluggedInWired; + protected boolean mPowerPluggedInWireless; + protected boolean mPowerPluggedInDock; private boolean mPowerCharged; private boolean mBatteryDefender; private boolean mEnableBatteryDefender; private boolean mIncompatibleCharger; - private int mChargingSpeed; + protected int mChargingSpeed; private int mChargingWattage; private int mBatteryLevel; private boolean mBatteryPresent = true; - private long mChargingTimeRemaining; + protected long mChargingTimeRemaining; private Pair<String, BiometricSourceType> mBiometricErrorMessageToShowOnScreenOn; private final Set<Integer> mCoExFaceAcquisitionMsgIdsToShow; private final FaceHelpMessageDeferral mFaceAcquiredMessageDeferral; @@ -1053,20 +1053,24 @@ public class KeyguardIndicationController { * Assumption: device is charging */ protected String computePowerIndication() { - int chargingId; if (mBatteryDefender) { - chargingId = R.string.keyguard_plugged_in_charging_limited; String percentage = NumberFormat.getPercentInstance().format(mBatteryLevel / 100f); - return mContext.getResources().getString(chargingId, percentage); + return mContext.getResources().getString( + R.string.keyguard_plugged_in_charging_limited, percentage); } else if (mPowerPluggedIn && mIncompatibleCharger) { - chargingId = R.string.keyguard_plugged_in_incompatible_charger; String percentage = NumberFormat.getPercentInstance().format(mBatteryLevel / 100f); - return mContext.getResources().getString(chargingId, percentage); + return mContext.getResources().getString( + R.string.keyguard_plugged_in_incompatible_charger, percentage); } else if (mPowerCharged) { return mContext.getResources().getString(R.string.keyguard_charged); } + return computePowerChargingStringIndication(); + } + + protected String computePowerChargingStringIndication() { final boolean hasChargingTime = mChargingTimeRemaining > 0; + int chargingId; if (mPowerPluggedInWired) { switch (mChargingSpeed) { case BatteryStatus.CHARGING_FAST: diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/HeadsUpRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/HeadsUpRepository.kt index ed8c05688a66..77660eb7d864 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/HeadsUpRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/HeadsUpRepository.kt @@ -29,9 +29,9 @@ interface HeadsUpRepository { /** * True if we are exiting the headsUp pinned mode, and some notifications might still be - * animating out. This is used to keep the touchable regions in a reasonable state. + * animating out. This is used to keep their view container visible. */ - val headsUpAnimatingAway: Flow<Boolean> + val isHeadsUpAnimatingAway: Flow<Boolean> /** The heads up row that should be displayed on top. */ val topHeadsUpRow: Flow<HeadsUpRowRepository?> diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt index d1dd7b55c11f..7f94da3c8c6a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractor.kt @@ -60,7 +60,7 @@ class HeadsUpNotificationInteractor @Inject constructor(repository: HeadsUpRepos } val isHeadsUpOrAnimatingAway: Flow<Boolean> = - combine(hasPinnedRows, repository.headsUpAnimatingAway) { hasPinnedRows, animatingAway -> + combine(hasPinnedRows, repository.isHeadsUpAnimatingAway) { hasPinnedRows, animatingAway -> hasPinnedRows || animatingAway } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java index 5eaccd924344..e980794d23dd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java @@ -594,7 +594,11 @@ public class StackScrollAlgorithm { ); if (view instanceof FooterView) { if (FooterViewRefactor.isEnabled()) { - if (((FooterView) view).shouldBeHidden()) { + // TODO(b/333445519): shouldBeHidden should reflect whether the shade is closed + // already, so we shouldn't need to use ambientState here. However, currently it + // doesn't get updated quickly enough and can cause the footer to flash when + // closing the shade. As such, we temporarily also check the ambientState directly. + if (((FooterView) view).shouldBeHidden() || !ambientState.isShadeExpanded()) { viewState.hidden = true; } else { final float footerEnd = algorithmState.mCurrentExpandedYPosition 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 13e36d58f6a9..77a0c2e9224c 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 @@ -207,9 +207,7 @@ constructor( keyguardTransitionInteractor.finishedKeyguardState.map { statesForConstrainedNotifications.contains(it) }, - keyguardTransitionInteractor - .isInTransitionWhere { from, to -> from == LOCKSCREEN || to == LOCKSCREEN } - .onStart { emit(false) } + keyguardTransitionInteractor.transitionValue(LOCKSCREEN).map { it > 0f }, ) { constrainedNotificationState, transitioningToOrFromLockscreen -> constrainedNotificationState || transitioningToOrFromLockscreen } @@ -241,11 +239,10 @@ constructor( keyguardTransitionInteractor.finishedKeyguardState.map { state -> state == GLANCEABLE_HUB }, - keyguardTransitionInteractor - .isInTransitionWhere { from, to -> - from == GLANCEABLE_HUB || to == GLANCEABLE_HUB - } - .onStart { emit(false) } + or( + keyguardTransitionInteractor.isInTransitionToState(GLANCEABLE_HUB), + keyguardTransitionInteractor.isInTransitionFromState(GLANCEABLE_HUB), + ), ) { isOnGlanceableHub, transitioningToOrFromHub -> isOnGlanceableHub || transitioningToOrFromHub } @@ -290,12 +287,10 @@ constructor( var aodTransitionIsComplete = true return combine( isOnLockscreenWithoutShade, - keyguardTransitionInteractor - .isInTransitionWhere( - fromStatePredicate = { it == LOCKSCREEN }, - toStatePredicate = { it == AOD } - ) - .onStart { emit(false) }, + keyguardTransitionInteractor.isInTransition( + from = LOCKSCREEN, + to = AOD, + ), ::Pair ) .transformWhile { (isOnLockscreenWithoutShade, aodTransitionIsRunning) -> diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java index 3f200d578261..0ddf37db6078 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java @@ -91,7 +91,8 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements StateFlowKt.MutableStateFlow(null); private final MutableStateFlow<Set<HeadsUpRowRepository>> mHeadsUpNotificationRows = StateFlowKt.MutableStateFlow(new HashSet<>()); - private final MutableStateFlow<Boolean> mHeadsUpGoingAway = StateFlowKt.MutableStateFlow(false); + private final MutableStateFlow<Boolean> mHeadsUpAnimatingAway = + StateFlowKt.MutableStateFlow(false); private boolean mReleaseOnExpandFinish; private boolean mTrackingHeadsUp; private final HashSet<String> mSwipedOutKeys = new HashSet<>(); @@ -184,7 +185,7 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements // Public methods: /** - * Add a listener to receive callbacks onHeadsUpGoingAway + * Add a listener to receive callbacks {@link #setHeadsUpAnimatingAway(boolean)} */ @Override public void addHeadsUpPhoneListener(OnHeadsUpPhoneListenerChange listener) { @@ -264,7 +265,7 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements if (isExpanded != mIsExpanded) { mIsExpanded = isExpanded; if (isExpanded) { - mHeadsUpGoingAway.setValue(false); + mHeadsUpAnimatingAway.setValue(false); } } } @@ -274,20 +275,15 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements * animating out. This is used to keep the touchable regions in a reasonable state. */ @Override - public void setHeadsUpGoingAway(boolean headsUpGoingAway) { - if (headsUpGoingAway != mHeadsUpGoingAway.getValue()) { + public void setHeadsUpAnimatingAway(boolean headsUpAnimatingAway) { + if (headsUpAnimatingAway != mHeadsUpAnimatingAway.getValue()) { for (OnHeadsUpPhoneListenerChange listener : mHeadsUpPhoneListeners) { - listener.onHeadsUpGoingAwayStateChanged(headsUpGoingAway); + listener.onHeadsUpAnimatingAwayStateChanged(headsUpAnimatingAway); } - mHeadsUpGoingAway.setValue(headsUpGoingAway); + mHeadsUpAnimatingAway.setValue(headsUpAnimatingAway); } } - @Override - public boolean isHeadsUpGoingAway() { - return mHeadsUpGoingAway.getValue(); - } - /** * Notifies that a remote input textbox in notification gets active or inactive. * @@ -504,8 +500,13 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements @Override @NonNull - public Flow<Boolean> getHeadsUpAnimatingAway() { - return mHeadsUpGoingAway; + public Flow<Boolean> isHeadsUpAnimatingAway() { + return mHeadsUpAnimatingAway; + } + + @Override + public boolean isHeadsUpAnimatingAwayValue() { + return mHeadsUpAnimatingAway.getValue(); } /////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHeadsUpChangeListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHeadsUpChangeListener.java index ed1f6ff7e513..87139ac0cada 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHeadsUpChangeListener.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHeadsUpChangeListener.java @@ -98,11 +98,11 @@ public class StatusBarHeadsUpChangeListener implements OnHeadsUpChangedListener, // we need to keep the panel open artificially, let's wait until the //animation // is finished. - mHeadsUpManager.setHeadsUpGoingAway(true); + mHeadsUpManager.setHeadsUpAnimatingAway(true); mNsslController.runAfterAnimationFinished(() -> { if (!mHeadsUpManager.hasPinnedHeadsUp()) { mNotificationShadeWindowController.setHeadsUpShowing(false); - mHeadsUpManager.setHeadsUpGoingAway(false); + mHeadsUpManager.setHeadsUpAnimatingAway(false); } mNotificationRemoteInputManager.onPanelCollapsed(); }); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java index c615887d5c25..8e8de46957ed 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java @@ -121,7 +121,7 @@ public final class StatusBarTouchableRegionManager implements Dumpable { updateTouchableRegion(); } }); - mHeadsUpManager.addHeadsUpPhoneListener(this::onHeadsUpGoingAwayStateChanged); + mHeadsUpManager.addHeadsUpPhoneListener(this::onHeadsUpAnimatingAwayStateChanged); mNotificationShadeWindowController = notificationShadeWindowController; mNotificationShadeWindowController.setForcePluginOpenListener((forceOpen) -> { @@ -214,7 +214,7 @@ public final class StatusBarTouchableRegionManager implements Dumpable { && (mNotificationShadeWindowView.getRootWindowInsets() != null) && (mNotificationShadeWindowView.getRootWindowInsets().getDisplayCutout() != null); boolean shouldObserve = mHeadsUpManager.hasPinnedHeadsUp() - || mHeadsUpManager.isHeadsUpGoingAway() + || mHeadsUpManager.isHeadsUpAnimatingAwayValue() || mForceCollapsedUntilLayout || hasCutoutInset || mNotificationShadeWindowController.getForcePluginOpen(); @@ -288,8 +288,8 @@ public final class StatusBarTouchableRegionManager implements Dumpable { || mUnlockedScreenOffAnimationController.isAnimationPlaying(); } - private void onHeadsUpGoingAwayStateChanged(boolean headsUpGoingAway) { - if (!headsUpGoingAway) { + private void onHeadsUpAnimatingAwayStateChanged(boolean headsUpAnimatingAway) { + if (!headsUpAnimatingAway) { updateTouchableRegionAfterLayout(); } else { updateTouchableRegion(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModel.kt index 52a6d8cf0952..cc87e8a45d13 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModel.kt @@ -19,6 +19,9 @@ package com.android.systemui.statusbar.pipeline.shared.ui.viewmodel import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING +import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN +import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor @@ -77,15 +80,13 @@ constructor( @Application coroutineScope: CoroutineScope, ) : CollapsedStatusBarViewModel { override val isTransitioningFromLockscreenToOccluded: StateFlow<Boolean> = - keyguardTransitionInteractor.lockscreenToOccludedTransition - .map { - it.transitionState == TransitionState.STARTED || - it.transitionState == TransitionState.RUNNING - } + keyguardTransitionInteractor + .isInTransition(LOCKSCREEN, OCCLUDED) .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), initialValue = false) override val transitionFromLockscreenToDreamStartedEvent: Flow<Unit> = - keyguardTransitionInteractor.lockscreenToDreamingTransition + keyguardTransitionInteractor + .transition(LOCKSCREEN, DREAMING) .filter { it.transitionState == TransitionState.STARTED } .map {} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.kt index 52a2e9ccc163..28a2a1f49bf6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.kt @@ -73,7 +73,8 @@ interface HeadsUpManager : Dumpable { /** Returns whether or not the given notification is managed by this manager. */ fun isHeadsUpEntry(key: String): Boolean - fun isHeadsUpGoingAway(): Boolean + /** @see setHeadsUpAnimatingAway */ + fun isHeadsUpAnimatingAwayValue(): Boolean /** Returns if the given notification is snoozed or not. */ fun isSnoozed(packageName: String): Boolean @@ -130,7 +131,7 @@ interface HeadsUpManager : Dumpable { * Set that we are exiting the headsUp pinned mode, but some notifications might still be * animating out. This is used to keep the touchable regions in a reasonable state. */ - fun setHeadsUpGoingAway(headsUpGoingAway: Boolean) + fun setHeadsUpAnimatingAway(headsUpAnimatingAway: Boolean) /** * Notifies that a remote input textbox in notification gets active or inactive. @@ -194,10 +195,10 @@ interface AnimationStateHandler { interface OnHeadsUpPhoneListenerChange { /** * Called when a heads up notification is 'going away' or no longer 'going away'. See - * [HeadsUpManager.setHeadsUpGoingAway]. + * [HeadsUpManager.setHeadsUpAnimatingAway]. */ // TODO(b/325936094) delete this callback, and listen to the flow instead - fun onHeadsUpGoingAwayStateChanged(headsUpGoingAway: Boolean) + fun onHeadsUpAnimatingAwayStateChanged(headsUpAnimatingAway: Boolean) } /* No op impl of HeadsUpManager. */ @@ -215,7 +216,7 @@ class HeadsUpManagerEmptyImpl @Inject constructor() : HeadsUpManager { override fun getTopEntry() = null override fun hasPinnedHeadsUp() = false override fun isHeadsUpEntry(key: String) = false - override fun isHeadsUpGoingAway() = false + override fun isHeadsUpAnimatingAwayValue() = false override fun isSnoozed(packageName: String) = false override fun isSticky(key: String?) = false override fun isTrackingHeadsUp() = false @@ -228,7 +229,7 @@ class HeadsUpManagerEmptyImpl @Inject constructor() : HeadsUpManager { override fun setAnimationStateHandler(handler: AnimationStateHandler) {} override fun setExpanded(entry: NotificationEntry, expanded: Boolean) {} override fun setGutsShown(entry: NotificationEntry, gutsShown: Boolean) {} - override fun setHeadsUpGoingAway(headsUpGoingAway: Boolean) {} + override fun setHeadsUpAnimatingAway(headsUpAnimatingAway: Boolean) {} override fun setRemoteInputActive(entry: NotificationEntry, remoteInputActive: Boolean) {} override fun setTrackingHeadsUp(tracking: Boolean) {} override fun setUser(user: Int) {} diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/UiEventLoggerStartableModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/UiEventLoggerStartableModule.kt new file mode 100644 index 000000000000..9b84090d72cd --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/UiEventLoggerStartableModule.kt @@ -0,0 +1,33 @@ +/* + * 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.volume.dagger + +import com.android.systemui.volume.domain.startable.AudioModeLoggerStartable +import com.android.systemui.volume.panel.domain.VolumePanelStartable +import dagger.Binds +import dagger.Module +import dagger.multibindings.IntoSet + +@Module +interface UiEventLoggerStartableModule { + + @Binds + @IntoSet + fun bindAudioModeLoggerStartable( + audioModeLoggerStartable: AudioModeLoggerStartable, + ): VolumePanelStartable +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/domain/startable/AudioModeLoggerStartable.kt b/packages/SystemUI/src/com/android/systemui/volume/domain/startable/AudioModeLoggerStartable.kt new file mode 100644 index 000000000000..12447577e945 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/domain/startable/AudioModeLoggerStartable.kt @@ -0,0 +1,49 @@ +/* + * 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.volume.domain.startable + +import com.android.internal.logging.UiEventLogger +import com.android.settingslib.volume.domain.interactor.AudioModeInteractor +import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope +import com.android.systemui.volume.panel.domain.VolumePanelStartable +import com.android.systemui.volume.panel.ui.VolumePanelUiEvent +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.launch + +/** Logger for audio mode */ +@VolumePanelScope +class AudioModeLoggerStartable +@Inject +constructor( + @VolumePanelScope private val scope: CoroutineScope, + private val uiEventLogger: UiEventLogger, + private val audioModeInteractor: AudioModeInteractor, +) : VolumePanelStartable { + + override fun start() { + scope.launch { + audioModeInteractor.isOngoingCall.distinctUntilChanged().collect { ongoingCall -> + uiEventLogger.log( + if (ongoingCall) VolumePanelUiEvent.VOLUME_PANEL_AUDIO_MODE_CHANGE_TO_CALLING + else VolumePanelUiEvent.VOLUME_PANEL_AUDIO_MODE_CHANGE_TO_NORMAL + ) + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModel.kt index 04d7b1fa6532..3ca9cdfe285c 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModel.kt @@ -18,8 +18,10 @@ package com.android.systemui.volume.panel.component.bottombar.ui.viewmodel import android.content.Intent import android.provider.Settings +import com.android.internal.logging.UiEventLogger import com.android.systemui.plugins.ActivityStarter import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope +import com.android.systemui.volume.panel.ui.VolumePanelUiEvent import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelViewModel import javax.inject.Inject @@ -29,6 +31,7 @@ class BottomBarViewModel constructor( private val activityStarter: ActivityStarter, private val volumePanelViewModel: VolumePanelViewModel, + private val uiEventLogger: UiEventLogger, ) { fun onDoneClicked() { @@ -36,6 +39,7 @@ constructor( } fun onSettingsClicked() { + uiEventLogger.log(VolumePanelUiEvent.VOLUME_PANEL_SOUND_SETTINGS_CLICKED) activityStarter.startActivityDismissingKeyguard( /* intent = */ Intent(Settings.ACTION_SOUND_SETTINGS), /* onlyProvisioned = */ false, diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/captioning/domain/CaptioningAvailabilityCriteria.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/captioning/domain/CaptioningAvailabilityCriteria.kt index aab825fb9f5e..85da1d0efe3a 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/captioning/domain/CaptioningAvailabilityCriteria.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/captioning/domain/CaptioningAvailabilityCriteria.kt @@ -16,18 +16,36 @@ package com.android.systemui.volume.panel.component.captioning.domain +import com.android.internal.logging.UiEventLogger import com.android.settingslib.view.accessibility.domain.interactor.CaptioningInteractor import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope import com.android.systemui.volume.panel.domain.ComponentAvailabilityCriteria +import com.android.systemui.volume.panel.ui.VolumePanelUiEvent import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.shareIn @VolumePanelScope class CaptioningAvailabilityCriteria @Inject -constructor(private val captioningInteractor: CaptioningInteractor) : - ComponentAvailabilityCriteria { +constructor( + captioningInteractor: CaptioningInteractor, + @VolumePanelScope private val scope: CoroutineScope, + private val uiEventLogger: UiEventLogger, +) : ComponentAvailabilityCriteria { - override fun isAvailable(): Flow<Boolean> = + private val availability = captioningInteractor.isSystemAudioCaptioningUiEnabled + .onEach { visible -> + uiEventLogger.log( + if (visible) VolumePanelUiEvent.VOLUME_PANEL_LIVE_CAPTION_TOGGLE_SHOWN + else VolumePanelUiEvent.VOLUME_PANEL_LIVE_CAPTION_TOGGLE_GONE + ) + } + .shareIn(scope, SharingStarted.WhileSubscribed(), replay = 1) + + override fun isAvailable(): Flow<Boolean> = availability } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/captioning/ui/viewmodel/CaptioningViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/captioning/ui/viewmodel/CaptioningViewModel.kt index 92f8f221d918..01421f86311f 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/captioning/ui/viewmodel/CaptioningViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/captioning/ui/viewmodel/CaptioningViewModel.kt @@ -17,11 +17,13 @@ package com.android.systemui.volume.panel.component.captioning.ui.viewmodel import android.content.Context +import com.android.internal.logging.UiEventLogger import com.android.settingslib.view.accessibility.domain.interactor.CaptioningInteractor import com.android.systemui.common.shared.model.Icon import com.android.systemui.res.R import com.android.systemui.volume.panel.component.button.ui.viewmodel.ToggleButtonViewModel import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope +import com.android.systemui.volume.panel.ui.VolumePanelUiEvent import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted @@ -38,6 +40,7 @@ constructor( private val context: Context, private val captioningInteractor: CaptioningInteractor, @VolumePanelScope private val coroutineScope: CoroutineScope, + private val uiEventLogger: UiEventLogger, ) { val buttonViewModel: StateFlow<ToggleButtonViewModel?> = @@ -57,6 +60,13 @@ constructor( .stateIn(coroutineScope, SharingStarted.Eagerly, null) fun setIsSystemAudioCaptioningEnabled(enabled: Boolean) { + uiEventLogger.logWithPosition( + VolumePanelUiEvent.VOLUME_PANEL_LIVE_CAPTION_TOGGLE_CLICKED, + 0, + null, + if (enabled) VolumePanelUiEvent.LIVE_CAPTION_TOGGLE_ENABLED + else VolumePanelUiEvent.LIVE_CAPTION_TOGGLE_DISABLED + ) coroutineScope.launch { captioningInteractor.setIsSystemAudioCaptioningEnabled(enabled) } } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt index fc9602e6017f..6b237f8e329b 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt @@ -17,6 +17,7 @@ package com.android.systemui.volume.panel.component.mediaoutput.ui.viewmodel import android.content.Context +import com.android.internal.logging.UiEventLogger import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.Color import com.android.systemui.common.shared.model.Icon @@ -26,6 +27,7 @@ import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputInteractor import com.android.systemui.volume.panel.component.mediaoutput.shared.model.SessionWithPlayback import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope +import com.android.systemui.volume.panel.ui.VolumePanelUiEvent import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -48,6 +50,7 @@ constructor( private val actionsInteractor: MediaOutputActionsInteractor, private val mediaDeviceSessionInteractor: MediaDeviceSessionInteractor, interactor: MediaOutputInteractor, + private val uiEventLogger: UiEventLogger, ) { private val sessionWithPlayback: StateFlow<SessionWithPlayback?> = @@ -126,6 +129,7 @@ constructor( ) fun onBarClick(expandable: Expandable) { + uiEventLogger.log(VolumePanelUiEvent.VOLUME_PANEL_MEDIA_OUTPUT_CLICKED) actionsInteractor.onBarClick(sessionWithPlayback.value, expandable) } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/ui/viewmodel/SpatialAudioViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/ui/viewmodel/SpatialAudioViewModel.kt index f022039e9cde..4ecdd46163f9 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/ui/viewmodel/SpatialAudioViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/ui/viewmodel/SpatialAudioViewModel.kt @@ -17,6 +17,7 @@ package com.android.systemui.volume.panel.component.spatial.ui.viewmodel import android.content.Context +import com.android.internal.logging.UiEventLogger import com.android.systemui.common.shared.model.Color import com.android.systemui.common.shared.model.Icon import com.android.systemui.dagger.qualifiers.Application @@ -29,6 +30,7 @@ import com.android.systemui.volume.panel.component.spatial.domain.interactor.Spa import com.android.systemui.volume.panel.component.spatial.domain.model.SpatialAudioAvailabilityModel import com.android.systemui.volume.panel.component.spatial.domain.model.SpatialAudioEnabledModel import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope +import com.android.systemui.volume.panel.ui.VolumePanelUiEvent import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted @@ -46,6 +48,7 @@ constructor( @VolumePanelScope private val scope: CoroutineScope, availabilityCriteria: SpatialAudioAvailabilityCriteria, private val interactor: SpatialAudioComponentInteractor, + private val uiEventLogger: UiEventLogger, ) { val spatialAudioButton: StateFlow<ButtonViewModel?> = @@ -101,6 +104,19 @@ constructor( .stateIn(scope, SharingStarted.Eagerly, emptyList()) fun setEnabled(model: SpatialAudioEnabledModel) { + uiEventLogger.logWithPosition( + VolumePanelUiEvent.VOLUME_PANEL_SPATIAL_AUDIO_TOGGLE_CLICKED, + 0, + null, + when (model) { + SpatialAudioEnabledModel.Disabled -> 0 + SpatialAudioEnabledModel.SpatialAudioEnabled -> 1 + SpatialAudioEnabledModel.HeadTrackingEnabled -> 2 + else -> { + -1 + } + } + ) scope.launch { interactor.setEnabled(model) } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt index 1ae1ebb99f8b..c8cd6fdbea70 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt @@ -18,12 +18,14 @@ package com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel import android.content.Context import android.media.AudioManager +import com.android.internal.logging.UiEventLogger import com.android.settingslib.volume.domain.interactor.AudioVolumeInteractor import com.android.settingslib.volume.shared.model.AudioStream import com.android.settingslib.volume.shared.model.AudioStreamModel import com.android.settingslib.volume.shared.model.RingerMode import com.android.systemui.common.shared.model.Icon import com.android.systemui.res.R +import com.android.systemui.volume.panel.ui.VolumePanelUiEvent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -43,6 +45,7 @@ constructor( @Assisted private val coroutineScope: CoroutineScope, private val context: Context, private val audioVolumeInteractor: AudioVolumeInteractor, + private val uiEventLogger: UiEventLogger, ) : SliderViewModel { private val audioStream = audioStreamWrapper.audioStream @@ -69,6 +72,19 @@ constructor( AudioStream(AudioManager.STREAM_ALARM) to R.string.stream_alarm_unavailable, AudioStream(AudioManager.STREAM_MUSIC) to R.string.stream_media_unavailable, ) + private val uiEventByStream = + mapOf( + AudioStream(AudioManager.STREAM_MUSIC) to + VolumePanelUiEvent.VOLUME_PANEL_MUSIC_SLIDER_TOUCHED, + AudioStream(AudioManager.STREAM_VOICE_CALL) to + VolumePanelUiEvent.VOLUME_PANEL_VOICE_CALL_SLIDER_TOUCHED, + AudioStream(AudioManager.STREAM_RING) to + VolumePanelUiEvent.VOLUME_PANEL_RING_SLIDER_TOUCHED, + AudioStream(AudioManager.STREAM_NOTIFICATION) to + VolumePanelUiEvent.VOLUME_PANEL_NOTIFICATION_SLIDER_TOUCHED, + AudioStream(AudioManager.STREAM_ALARM) to + VolumePanelUiEvent.VOLUME_PANEL_ALARM_SLIDER_TOUCHED, + ) override val slider: StateFlow<SliderState> = combine( @@ -88,6 +104,10 @@ constructor( } } + override fun onValueChangeFinished() { + uiEventByStream[audioStream]?.let { uiEventLogger.log(it) } + } + override fun toggleMuted(state: SliderState) { val audioViewModel = state as? State audioViewModel ?: return diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt index 3689303ab28d..956ab66ac0c3 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt @@ -54,6 +54,8 @@ constructor( } } + override fun onValueChangeFinished() {} + override fun toggleMuted(state: SliderState) { // do nothing because this action isn't supported for Cast sliders. } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderViewModel.kt index 74aee559194b..7ded8c5c9fc1 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderViewModel.kt @@ -25,5 +25,7 @@ interface SliderViewModel { fun onValueChanged(state: SliderState, newValue: Float) + fun onValueChangeFinished() + fun toggleMuted(state: SliderState) } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/dagger/DefaultMultibindsModule.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/dagger/DefaultMultibindsModule.kt index d1d539003f93..f889ed6e06be 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/dagger/DefaultMultibindsModule.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/dagger/DefaultMultibindsModule.kt @@ -17,6 +17,7 @@ package com.android.systemui.volume.panel.dagger import com.android.systemui.volume.panel.domain.ComponentAvailabilityCriteria +import com.android.systemui.volume.panel.domain.VolumePanelStartable import com.android.systemui.volume.panel.shared.model.VolumePanelComponentKey import com.android.systemui.volume.panel.shared.model.VolumePanelUiComponent import dagger.Module @@ -31,4 +32,6 @@ interface DefaultMultibindsModule { @Multibinds fun criteriaMap(): Map<VolumePanelComponentKey, ComponentAvailabilityCriteria> @Multibinds fun components(): Map<VolumePanelComponentKey, VolumePanelUiComponent> + + @Multibinds fun startables(): Set<VolumePanelStartable> } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/dagger/VolumePanelComponent.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/dagger/VolumePanelComponent.kt index d868c33d0887..ec64f3d93012 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/dagger/VolumePanelComponent.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/dagger/VolumePanelComponent.kt @@ -16,6 +16,7 @@ package com.android.systemui.volume.panel.dagger +import com.android.systemui.volume.dagger.UiEventLoggerStartableModule import com.android.systemui.volume.panel.component.anc.AncModule import com.android.systemui.volume.panel.component.bottombar.BottomBarModule import com.android.systemui.volume.panel.component.captioning.CaptioningModule @@ -25,6 +26,7 @@ import com.android.systemui.volume.panel.component.volume.VolumeSlidersModule import com.android.systemui.volume.panel.dagger.factory.VolumePanelComponentFactory import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope import com.android.systemui.volume.panel.domain.DomainModule +import com.android.systemui.volume.panel.domain.VolumePanelStartable import com.android.systemui.volume.panel.domain.interactor.ComponentsInteractor import com.android.systemui.volume.panel.ui.UiModule import com.android.systemui.volume.panel.ui.composable.ComponentsFactory @@ -47,6 +49,7 @@ import kotlinx.coroutines.CoroutineScope DefaultMultibindsModule::class, DomainModule::class, UiModule::class, + UiEventLoggerStartableModule::class, // Components modules BottomBarModule::class, AncModule::class, @@ -66,6 +69,8 @@ interface VolumePanelComponent { fun componentsLayoutManager(): ComponentsLayoutManager + fun volumePanelStartables(): Set<VolumePanelStartable> + @Subcomponent.Factory interface Factory : VolumePanelComponentFactory { diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/domain/VolumePanelStartable.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/domain/VolumePanelStartable.kt new file mode 100644 index 000000000000..9c39f5e75f88 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/domain/VolumePanelStartable.kt @@ -0,0 +1,22 @@ +/* + * 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.volume.panel.domain + +/** Code that needs to be run when Volume Panel is started.. */ +interface VolumePanelStartable { + fun start() +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/VolumePanelUiEvent.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/VolumePanelUiEvent.kt new file mode 100644 index 000000000000..8b8714fcca8c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/VolumePanelUiEvent.kt @@ -0,0 +1,56 @@ +/* + * 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.volume.panel.ui + +import com.android.internal.logging.UiEvent +import com.android.internal.logging.UiEventLogger + +/** UI events for Volume Panel. */ +enum class VolumePanelUiEvent(val metricId: Int) : UiEventLogger.UiEventEnum { + @UiEvent(doc = "The volume panel is shown") VOLUME_PANEL_SHOWN(1634), + @UiEvent(doc = "The volume panel is gone") VOLUME_PANEL_GONE(1635), + @UiEvent(doc = "Media output is clicked") VOLUME_PANEL_MEDIA_OUTPUT_CLICKED(1636), + @UiEvent(doc = "Audio mode changed to normal") VOLUME_PANEL_AUDIO_MODE_CHANGE_TO_NORMAL(1680), + @UiEvent(doc = "Audio mode changed to calling") VOLUME_PANEL_AUDIO_MODE_CHANGE_TO_CALLING(1681), + @UiEvent(doc = "Sound settings is clicked") VOLUME_PANEL_SOUND_SETTINGS_CLICKED(1638), + @UiEvent(doc = "The music volume slider is touched") VOLUME_PANEL_MUSIC_SLIDER_TOUCHED(1639), + @UiEvent(doc = "The voice call volume slider is touched") + VOLUME_PANEL_VOICE_CALL_SLIDER_TOUCHED(1640), + @UiEvent(doc = "The ring volume slider is touched") VOLUME_PANEL_RING_SLIDER_TOUCHED(1641), + @UiEvent(doc = "The notification volume slider is touched") + VOLUME_PANEL_NOTIFICATION_SLIDER_TOUCHED(1642), + @UiEvent(doc = "The alarm volume slider is touched") VOLUME_PANEL_ALARM_SLIDER_TOUCHED(1643), + @UiEvent(doc = "Live caption toggle is shown") VOLUME_PANEL_LIVE_CAPTION_TOGGLE_SHOWN(1644), + @UiEvent(doc = "Live caption toggle is gone") VOLUME_PANEL_LIVE_CAPTION_TOGGLE_GONE(1645), + @UiEvent(doc = "Live caption toggle is clicked") VOLUME_PANEL_LIVE_CAPTION_TOGGLE_CLICKED(1646), + @UiEvent(doc = "Spatial audio button is shown") VOLUME_PANEL_SPATIAL_AUDIO_BUTTON_SHOWN(1647), + @UiEvent(doc = "Spatial audio button is gone") VOLUME_PANEL_SPATIAL_AUDIO_BUTTON_GONE(1648), + @UiEvent(doc = "Spatial audio popup is shown") VOLUME_PANEL_SPATIAL_AUDIO_POP_UP_SHOWN(1649), + @UiEvent(doc = "Spatial audio toggle is clicked") + VOLUME_PANEL_SPATIAL_AUDIO_TOGGLE_CLICKED(1650), + @UiEvent(doc = "ANC button is shown") VOLUME_PANEL_ANC_BUTTON_SHOWN(1651), + @UiEvent(doc = "ANC button is gone") VOLUME_PANEL_ANC_BUTTON_GONE(1652), + @UiEvent(doc = "ANC popup is shown") VOLUME_PANEL_ANC_POPUP_SHOWN(1653), + @UiEvent(doc = "ANC toggle is clicked") VOLUME_PANEL_ANC_TOGGLE_CLICKED(1654); + + override fun getId() = metricId + + companion object { + const val LIVE_CAPTION_TOGGLE_DISABLED = 0 + const val LIVE_CAPTION_TOGGLE_ENABLED = 1 + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/activity/VolumePanelActivity.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/activity/VolumePanelActivity.kt index c728fefa77e6..ccb91ac79b6a 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/activity/VolumePanelActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/activity/VolumePanelActivity.kt @@ -21,8 +21,10 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels +import com.android.internal.logging.UiEventLogger import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.volume.panel.shared.flag.VolumePanelFlag +import com.android.systemui.volume.panel.ui.VolumePanelUiEvent import com.android.systemui.volume.panel.ui.composable.VolumePanelRoot import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelViewModel import javax.inject.Inject @@ -34,6 +36,7 @@ constructor( private val volumePanelViewModelFactory: Provider<VolumePanelViewModel.Factory>, private val volumePanelFlag: VolumePanelFlag, private val configurationController: ConfigurationController, + private val uiEventLogger: UiEventLogger, ) : ComponentActivity() { private val viewModel: VolumePanelViewModel by @@ -43,8 +46,16 @@ constructor( enableEdgeToEdge() super.onCreate(savedInstanceState) volumePanelFlag.assertNewVolumePanel() - - setContent { VolumePanelRoot(viewModel = viewModel, onDismiss = ::finish) } + uiEventLogger.log(VolumePanelUiEvent.VOLUME_PANEL_SHOWN) + setContent { + VolumePanelRoot( + viewModel = viewModel, + onDismiss = { + uiEventLogger.log(VolumePanelUiEvent.VOLUME_PANEL_GONE) + finish() + } + ) + } } override fun onContentChanged() { diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModel.kt index 5ae827ff4e3d..1de4fd1f9593 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModel.kt @@ -26,6 +26,7 @@ import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.onConfigChanged import com.android.systemui.volume.panel.dagger.VolumePanelComponent import com.android.systemui.volume.panel.dagger.factory.VolumePanelComponentFactory +import com.android.systemui.volume.panel.domain.VolumePanelStartable import com.android.systemui.volume.panel.domain.interactor.ComponentsInteractor import com.android.systemui.volume.panel.ui.composable.ComponentsFactory import com.android.systemui.volume.panel.ui.layout.ComponentsLayout @@ -109,6 +110,10 @@ class VolumePanelViewModel( replay = 1, ) + init { + volumePanelComponent.volumePanelStartables().onEach(VolumePanelStartable::start) + } + fun dismissPanel() { mutablePanelVisibility.update { false } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt index f32d5b8838b7..e72027a921b7 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt @@ -319,9 +319,19 @@ class ClockEventControllerTest : SysuiTestCase() { fun listenForDozeAmountTransition_updatesClockDozeAmount() = runBlocking(IMMEDIATE) { val transitionStep = MutableStateFlow(TransitionStep()) - whenever(keyguardTransitionInteractor.lockscreenToAodTransition) + whenever( + keyguardTransitionInteractor.transition( + KeyguardState.LOCKSCREEN, + KeyguardState.AOD + ) + ) .thenReturn(transitionStep) - whenever(keyguardTransitionInteractor.aodToLockscreenTransition) + whenever( + keyguardTransitionInteractor.transition( + KeyguardState.AOD, + KeyguardState.LOCKSCREEN + ) + ) .thenReturn(transitionStep) val job = underTest.listenForDozeAmountTransition(this) @@ -361,6 +371,27 @@ class ClockEventControllerTest : SysuiTestCase() { } @Test + fun listenForTransitionToLSFromOccluded_updatesClockDozeAmountToOne() = + runBlocking(IMMEDIATE) { + val transitionStep = MutableStateFlow(TransitionStep()) + whenever(keyguardTransitionInteractor.transitionStepsToState(KeyguardState.LOCKSCREEN)) + .thenReturn(transitionStep) + + val job = underTest.listenForAnyStateToLockscreenTransition(this) + transitionStep.value = + TransitionStep( + from = KeyguardState.OCCLUDED, + to = KeyguardState.LOCKSCREEN, + transitionState = TransitionState.STARTED, + ) + yield() + + verify(animations, times(2)).doze(0f) + + job.cancel() + } + + @Test fun listenForTransitionToAodFromLockscreen_neverUpdatesClockDozeAmount() = runBlocking(IMMEDIATE) { val transitionStep = MutableStateFlow(TransitionStep()) @@ -378,6 +409,27 @@ class ClockEventControllerTest : SysuiTestCase() { verify(animations, never()).doze(1f) + job.cancel() + } + + @Test + fun listenForAnyStateToLockscreenTransition_neverUpdatesClockDozeAmount() = + runBlocking(IMMEDIATE) { + val transitionStep = MutableStateFlow(TransitionStep()) + whenever(keyguardTransitionInteractor.transitionStepsToState(KeyguardState.LOCKSCREEN)) + .thenReturn(transitionStep) + + val job = underTest.listenForAnyStateToLockscreenTransition(this) + transitionStep.value = + TransitionStep( + from = KeyguardState.AOD, + to = KeyguardState.LOCKSCREEN, + transitionState = TransitionState.STARTED, + ) + yield() + + verify(animations, never()).doze(0f) + job.cancel() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt index dac88a340cb1..e06134bdf982 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorTest.kt @@ -119,6 +119,7 @@ class DeviceEntryFaceAuthInteractorTest : SysuiTestCase() { fun faceAuthIsRequestedWhenLockscreenBecomesVisibleFromOffState() = testScope.runTest { underTest.start() + runCurrent() powerInteractor.setAwakeForTest(reason = PowerManager.WAKE_REASON_LID) faceWakeUpTriggersConfig.setTriggerFaceAuthOnWakeUpFrom( @@ -160,6 +161,7 @@ class DeviceEntryFaceAuthInteractorTest : SysuiTestCase() { fun faceAuthIsRequestedWhenLockscreenBecomesVisibleFromAodState() = testScope.runTest { underTest.start() + runCurrent() powerInteractor.setAwakeForTest(reason = PowerManager.WAKE_REASON_LID) faceWakeUpTriggersConfig.setTriggerFaceAuthOnWakeUpFrom( @@ -207,6 +209,7 @@ class DeviceEntryFaceAuthInteractorTest : SysuiTestCase() { fun faceAuthIsRequestedWhenLockscreenBecomesVisibleFromDozingState() = testScope.runTest { underTest.start() + runCurrent() powerInteractor.setAwakeForTest(reason = PowerManager.WAKE_REASON_LID) faceWakeUpTriggersConfig.setTriggerFaceAuthOnWakeUpFrom( diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt index 2b51863117e9..b0aace6f650e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt @@ -15,6 +15,8 @@ import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepos import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.testKosmos @@ -22,7 +24,6 @@ import com.android.systemui.util.mockito.any import com.android.systemui.utils.GlobalWindowManager import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -42,8 +43,7 @@ import org.mockito.MockitoAnnotations class ResourceTrimmerTest : SysuiTestCase() { val kosmos = testKosmos() - private val testDispatcher = UnconfinedTestDispatcher() - private val testScope = TestScope(testDispatcher) + private val testScope = kosmos.testScope private val keyguardRepository = kosmos.fakeKeyguardRepository private val featureFlags = kosmos.fakeFeatureFlagsClassic private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository @@ -74,7 +74,7 @@ class ResourceTrimmerTest : SysuiTestCase() { kosmos.keyguardTransitionInteractor, globalWindowManager, testScope.backgroundScope, - testDispatcher, + kosmos.testDispatcher, featureFlags ) resourceTrimmer.start() diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt index 6d605a564022..b1a8dd1d3fdc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractorTest.kt @@ -281,6 +281,14 @@ class WindowManagerLockscreenVisibilityInteractorTest : SysuiTestCase() { // Oh no, we're still surfaceBehindAnimating=true, but no longer transitioning to GONE. transitionRepository.sendTransitionStep( TransitionStep( + transitionState = TransitionState.CANCELED, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + ) + ) + runCurrent() + transitionRepository.sendTransitionStep( + TransitionStep( transitionState = TransitionState.STARTED, from = KeyguardState.LOCKSCREEN, to = KeyguardState.AOD, diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerWindowViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerWindowViewModelTest.kt index 143c4dacb6be..1396b20a800d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerWindowViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerWindowViewModelTest.kt @@ -57,11 +57,18 @@ class AlternateBouncerWindowViewModelTest : SysuiTestCase() { stepFromAlternateBouncer(0f, TransitionState.STARTED), stepFromAlternateBouncer(.4f), stepFromAlternateBouncer(.6f), - stepFromAlternateBouncer(1f), ), testScope, ) assertThat(alternateBouncerWindowRequired).isTrue() + + transitionRepository.sendTransitionSteps( + listOf( + stepFromAlternateBouncer(1.0f, TransitionState.FINISHED), + ), + testScope, + ) + assertThat(alternateBouncerWindowRequired).isFalse() } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt index b70cc30eb3e1..fe8fdc042ae4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt @@ -29,7 +29,9 @@ import com.android.systemui.media.controls.MediaTestUtils import com.android.systemui.media.controls.data.repository.MediaFilterRepository import com.android.systemui.media.controls.shared.model.EXTRA_KEY_TRIGGER_RESUME import com.android.systemui.media.controls.shared.model.MediaData +import com.android.systemui.media.controls.shared.model.MediaDataLoadingModel import com.android.systemui.media.controls.shared.model.SmartspaceMediaData +import com.android.systemui.media.controls.shared.model.SmartspaceMediaLoadingModel import com.android.systemui.media.controls.ui.controller.MediaPlayerData import com.android.systemui.media.controls.util.MediaFlags import com.android.systemui.media.controls.util.MediaUiEventLogger @@ -48,12 +50,10 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.anyLong import org.mockito.Mock import org.mockito.Mockito.never -import org.mockito.Mockito.reset import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @@ -76,7 +76,6 @@ private val SMARTSPACE_INSTANCE_ID = InstanceId.fakeInstanceId(456)!! @TestableLooper.RunWithLooper class MediaDataFilterImplTest : SysuiTestCase() { - @Mock private lateinit var listener: MediaDataFilterImpl.Listener @Mock private lateinit var userTracker: UserTracker @Mock private lateinit var broadcastSender: BroadcastSender @Mock private lateinit var mediaDataManager: MediaDataManager @@ -89,7 +88,7 @@ class MediaDataFilterImplTest : SysuiTestCase() { @Mock private lateinit var cardAction: SmartspaceAction private lateinit var mediaDataFilter: MediaDataFilterImpl - private lateinit var mediaFilterRepository: MediaFilterRepository + private lateinit var repository: MediaFilterRepository private lateinit var testScope: TestScope private lateinit var dataMain: MediaData private lateinit var dataGuest: MediaData @@ -102,7 +101,7 @@ class MediaDataFilterImplTest : SysuiTestCase() { MediaPlayerData.clear() whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(false) testScope = TestScope() - mediaFilterRepository = MediaFilterRepository() + repository = MediaFilterRepository() mediaDataFilter = MediaDataFilterImpl( context, @@ -113,10 +112,9 @@ class MediaDataFilterImplTest : SysuiTestCase() { clock, logger, mediaFlags, - mediaFilterRepository, + repository, ) mediaDataFilter.mediaDataManager = mediaDataManager - mediaDataFilter.addListener(listener) // Start all tests as main user setUser(USER_MAIN) @@ -162,91 +160,114 @@ class MediaDataFilterImplTest : SysuiTestCase() { } @Test - fun testOnDataLoadedForCurrentUser_callsListener() { - // GIVEN a media for main user - mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain) + fun onDataLoadedForCurrentUser_updatesLoadedStates() = + testScope.runTest { + val mediaDataLoadedStates by collectLastValue(repository.mediaDataLoadedStates) + val mediaDataLoadingModel = listOf(MediaDataLoadingModel.Loaded(dataMain.instanceId)) - // THEN we should tell the listener - verify(listener).onMediaDataLoaded(eq(dataMain.instanceId), eq(true), eq(0), eq(false)) - } + mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain) + + assertThat(mediaDataLoadedStates).isEqualTo(mediaDataLoadingModel) + } @Test - fun testOnDataLoadedForGuest_doesNotCallListener() { - // GIVEN a media for guest user - mediaDataFilter.onMediaDataLoaded(KEY, null, dataGuest) + fun onDataLoadedForGuest_doesNotUpdateLoadedStates() = + testScope.runTest { + val mediaDataLoadedStates by collectLastValue(repository.mediaDataLoadedStates) + val mediaLoadedStatesModel = listOf(MediaDataLoadingModel.Loaded(dataMain.instanceId)) - // THEN we should NOT tell the listener - verify(listener, never()).onMediaDataLoaded(any(), anyBoolean(), anyInt(), anyBoolean()) - } + mediaDataFilter.onMediaDataLoaded(KEY, null, dataGuest) + + assertThat(mediaDataLoadedStates).isNotEqualTo(mediaLoadedStatesModel) + } @Test - fun testOnRemovedForCurrent_callsListener() { - // GIVEN a media was removed for main user - mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain) - mediaDataFilter.onMediaDataRemoved(KEY) + fun onRemovedForCurrent_updatesLoadedStates() = + testScope.runTest { + val mediaDataLoadedStates by collectLastValue(repository.mediaDataLoadedStates) + val mediaLoadedStatesModel = + mutableListOf(MediaDataLoadingModel.Loaded(dataMain.instanceId)) - // THEN we should tell the listener - verify(listener).onMediaDataRemoved(eq(dataMain.instanceId)) - } + // GIVEN a media was removed for main user + mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain) + + assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel) + + mediaLoadedStatesModel.remove(MediaDataLoadingModel.Loaded(dataMain.instanceId)) + mediaDataFilter.onMediaDataRemoved(KEY) + + assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel) + } @Test - fun testOnRemovedForGuest_doesNotCallListener() { - // GIVEN a media was removed for guest user - mediaDataFilter.onMediaDataLoaded(KEY, null, dataGuest) - mediaDataFilter.onMediaDataRemoved(KEY) + fun onRemovedForGuest_doesNotUpdateLoadedStates() = + testScope.runTest { + val mediaDataLoadedStates by collectLastValue(repository.mediaDataLoadedStates) - // THEN we should NOT tell the listener - verify(listener, never()).onMediaDataRemoved(eq(dataGuest.instanceId)) - } + // GIVEN a media was removed for guest user + mediaDataFilter.onMediaDataLoaded(KEY, null, dataGuest) + mediaDataFilter.onMediaDataRemoved(KEY) + + assertThat(mediaDataLoadedStates).isEmpty() + } @Test - fun testOnUserSwitched_removesOldUserControls() { - // GIVEN that we have a media loaded for main user - mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain) + fun onUserSwitched_removesOldUserControls() = + testScope.runTest { + val mediaDataLoadedStates by collectLastValue(repository.mediaDataLoadedStates) + val mediaLoadedStatesModel = listOf(MediaDataLoadingModel.Loaded(dataMain.instanceId)) - // and we switch to guest user - setUser(USER_GUEST) + // GIVEN that we have a media loaded for main user + mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain) - // THEN we should remove the main user's media - verify(listener).onMediaDataRemoved(eq(dataMain.instanceId)) - } + assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel) + + // and we switch to guest user + setUser(USER_GUEST) + + // THEN we should remove the main user's media + assertThat(mediaDataLoadedStates).isEmpty() + } @Test - fun testOnUserSwitched_addsNewUserControls() { - // GIVEN that we had some media for both users - mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain) - mediaDataFilter.onMediaDataLoaded(KEY_ALT, null, dataGuest) - reset(listener) + fun onUserSwitched_addsNewUserControls() = + testScope.runTest { + val mediaDataLoadedStates by collectLastValue(repository.mediaDataLoadedStates) + val guestLoadedStatesModel = listOf(MediaDataLoadingModel.Loaded(dataGuest.instanceId)) + val mainLoadedStatesModel = listOf(MediaDataLoadingModel.Loaded(dataMain.instanceId)) - // and we switch to guest user - setUser(USER_GUEST) + // GIVEN that we had some media for both users + mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain) + mediaDataFilter.onMediaDataLoaded(KEY_ALT, null, dataGuest) - // THEN we should add back the guest user media - verify(listener).onMediaDataLoaded(eq(dataGuest.instanceId), eq(true), eq(0), eq(false)) + // and we switch to guest user + setUser(USER_GUEST) - // but not the main user's - verify(listener, never()) - .onMediaDataLoaded(eq(dataMain.instanceId), anyBoolean(), anyInt(), anyBoolean()) - } + assertThat(mediaDataLoadedStates).isEqualTo(guestLoadedStatesModel) + assertThat(mediaDataLoadedStates).isNotEqualTo(mainLoadedStatesModel) + } @Test - fun testOnProfileChanged_profileUnavailable_loadControls() { - // GIVEN that we had some media for both profiles - mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain) - mediaDataFilter.onMediaDataLoaded(KEY_ALT, null, dataPrivateProfile) - reset(listener) + fun onProfileChanged_profileUnavailable_updateStates() = + testScope.runTest { + val mediaDataLoadedStates by collectLastValue(repository.mediaDataLoadedStates) - // and we change profile status - setPrivateProfileUnavailable() + // GIVEN that we had some media for both profiles + mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain) + mediaDataFilter.onMediaDataLoaded(KEY_ALT, null, dataPrivateProfile) - // THEN we should add the private profile media - verify(listener).onMediaDataRemoved(eq(dataPrivateProfile.instanceId)) - } + // and we change profile status + setPrivateProfileUnavailable() + + val mediaLoadedStatesModel = listOf(MediaDataLoadingModel.Loaded(dataMain.instanceId)) + // THEN we should remove the private profile media + assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel) + } @Test fun hasAnyMedia_mediaSet_returnsTrue() = testScope.runTest { - val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + val selectedUserEntries by collectLastValue(repository.selectedUserEntries) mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = dataMain) assertThat(hasAnyMedia(selectedUserEntries)).isTrue() @@ -255,7 +276,7 @@ class MediaDataFilterImplTest : SysuiTestCase() { @Test fun hasAnyMedia_recommendationSet_returnsFalse() = testScope.runTest { - val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + val selectedUserEntries by collectLastValue(repository.selectedUserEntries) mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) assertThat(hasAnyMedia(selectedUserEntries)).isFalse() @@ -264,8 +285,8 @@ class MediaDataFilterImplTest : SysuiTestCase() { @Test fun hasAnyMediaOrRecommendation_mediaSet_returnsTrue() = testScope.runTest { - val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) - val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) + val selectedUserEntries by collectLastValue(repository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData) mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = dataMain) assertThat(hasAnyMediaOrRecommendation(selectedUserEntries, smartspaceMediaData)) @@ -275,8 +296,8 @@ class MediaDataFilterImplTest : SysuiTestCase() { @Test fun hasAnyMediaOrRecommendation_recommendationSet_returnsTrue() = testScope.runTest { - val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) - val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) + val selectedUserEntries by collectLastValue(repository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData) mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) assertThat(hasAnyMediaOrRecommendation(selectedUserEntries, smartspaceMediaData)) @@ -286,7 +307,7 @@ class MediaDataFilterImplTest : SysuiTestCase() { @Test fun hasActiveMedia_inactiveMediaSet_returnsFalse() = testScope.runTest { - val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + val selectedUserEntries by collectLastValue(repository.selectedUserEntries) val data = dataMain.copy(active = false) mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = data) @@ -297,7 +318,7 @@ class MediaDataFilterImplTest : SysuiTestCase() { @Test fun hasActiveMedia_activeMediaSet_returnsTrue() = testScope.runTest { - val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + val selectedUserEntries by collectLastValue(repository.selectedUserEntries) val data = dataMain.copy(active = true) mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = data) @@ -307,9 +328,9 @@ class MediaDataFilterImplTest : SysuiTestCase() { @Test fun hasActiveMediaOrRecommendation_inactiveMediaSet_returnsFalse() = testScope.runTest { - val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) - val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) - val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedId) + val selectedUserEntries by collectLastValue(repository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData) + val reactivatedKey by collectLastValue(repository.reactivatedId) val data = dataMain.copy(active = false) mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = data) @@ -326,9 +347,9 @@ class MediaDataFilterImplTest : SysuiTestCase() { @Test fun hasActiveMediaOrRecommendation_activeMediaSet_returnsTrue() = testScope.runTest { - val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) - val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) - val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedId) + val selectedUserEntries by collectLastValue(repository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData) + val reactivatedKey by collectLastValue(repository.reactivatedId) val data = dataMain.copy(active = true) mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = data) @@ -345,9 +366,9 @@ class MediaDataFilterImplTest : SysuiTestCase() { @Test fun hasActiveMediaOrRecommendation_inactiveRecommendationSet_returnsFalse() = testScope.runTest { - val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) - val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) - val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedId) + val selectedUserEntries by collectLastValue(repository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData) + val reactivatedKey by collectLastValue(repository.reactivatedId) whenever(smartspaceData.isActive).thenReturn(false) mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) @@ -364,9 +385,9 @@ class MediaDataFilterImplTest : SysuiTestCase() { @Test fun hasActiveMediaOrRecommendation_invalidRecommendationSet_returnsFalse() = testScope.runTest { - val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) - val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) - val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedId) + val selectedUserEntries by collectLastValue(repository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData) + val reactivatedKey by collectLastValue(repository.reactivatedId) whenever(smartspaceData.isValid()).thenReturn(false) mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) @@ -383,9 +404,9 @@ class MediaDataFilterImplTest : SysuiTestCase() { @Test fun hasActiveMediaOrRecommendation_activeAndValidRecommendationSet_returnsTrue() = testScope.runTest { - val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) - val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) - val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedId) + val selectedUserEntries by collectLastValue(repository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData) + val reactivatedKey by collectLastValue(repository.reactivatedId) whenever(smartspaceData.isActive).thenReturn(true) whenever(smartspaceData.isValid()).thenReturn(true) mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) @@ -401,10 +422,10 @@ class MediaDataFilterImplTest : SysuiTestCase() { } @Test - fun testHasAnyMediaOrRecommendation_onlyCurrentUser() = + fun hasAnyMediaOrRecommendation_onlyCurrentUser() = testScope.runTest { - val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) - val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) + val selectedUserEntries by collectLastValue(repository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData) assertThat(hasAnyMediaOrRecommendation(selectedUserEntries, smartspaceMediaData)) .isFalse() @@ -415,11 +436,11 @@ class MediaDataFilterImplTest : SysuiTestCase() { } @Test - fun testHasActiveMediaOrRecommendation_onlyCurrentUser() = + fun hasActiveMediaOrRecommendation_onlyCurrentUser() = testScope.runTest { - val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) - val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) - val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedId) + val selectedUserEntries by collectLastValue(repository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData) + val reactivatedKey by collectLastValue(repository.reactivatedId) assertThat( hasActiveMediaOrRecommendation( selectedUserEntries, @@ -443,10 +464,10 @@ class MediaDataFilterImplTest : SysuiTestCase() { } @Test - fun testOnNotificationRemoved_doesNotHaveMedia() = + fun onNotificationRemoved_doesNotHaveMedia() = testScope.runTest { - val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) - val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) + val selectedUserEntries by collectLastValue(repository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData) mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = dataMain) mediaDataFilter.onMediaDataRemoved(KEY) @@ -456,7 +477,7 @@ class MediaDataFilterImplTest : SysuiTestCase() { } @Test - fun testOnSwipeToDismiss_setsTimedOut() { + fun onSwipeToDismiss_setsTimedOut() { mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain) mediaDataFilter.onSwipeToDismiss() @@ -464,15 +485,19 @@ class MediaDataFilterImplTest : SysuiTestCase() { } @Test - fun testOnSmartspaceMediaDataLoaded_noMedia_activeValidRec_prioritizesSmartspace() = + fun onSmartspaceMediaDataLoaded_noMedia_activeValidRec_prioritizesSmartspace() = testScope.runTest { - val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) - val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) - val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedId) + val selectedUserEntries by collectLastValue(repository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData) + val reactivatedKey by collectLastValue(repository.reactivatedId) + val recommendationsLoadingState by + collectLastValue(repository.recommendationsLoadingState) + val recommendationsLoadingModel = + SmartspaceMediaLoadingModel.Loaded(SMARTSPACE_KEY, isPrioritized = true) mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) - verify(listener).onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(true)) + assertThat(recommendationsLoadingState).isEqualTo(recommendationsLoadingModel) assertThat( hasActiveMediaOrRecommendation( selectedUserEntries, @@ -487,18 +512,19 @@ class MediaDataFilterImplTest : SysuiTestCase() { } @Test - fun testOnSmartspaceMediaDataLoaded_noMedia_inactiveRec_showsNothing() = + fun onSmartspaceMediaDataLoaded_noMedia_inactiveRec_showsNothing() = testScope.runTest { - val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) - val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) - val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedId) + val selectedUserEntries by collectLastValue(repository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData) + val reactivatedKey by collectLastValue(repository.reactivatedId) + val recommendationsLoadingState by + collectLastValue(repository.recommendationsLoadingState) whenever(smartspaceData.isActive).thenReturn(false) mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) - verify(listener, never()).onMediaDataLoaded(any(), anyBoolean(), anyInt(), anyBoolean()) - verify(listener, never()).onSmartspaceMediaDataLoaded(any(), anyBoolean()) + assertThat(recommendationsLoadingState).isEqualTo(SmartspaceMediaLoadingModel.Unknown) assertThat( hasActiveMediaOrRecommendation( selectedUserEntries, @@ -513,17 +539,21 @@ class MediaDataFilterImplTest : SysuiTestCase() { } @Test - fun testOnSmartspaceMediaDataLoaded_noRecentMedia_activeValidRec_prioritizesSmartspace() = + fun onSmartspaceMediaDataLoaded_noRecentMedia_activeValidRec_prioritizesSmartspace() = testScope.runTest { - val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) - val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) - val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedId) + val selectedUserEntries by collectLastValue(repository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData) + val reactivatedKey by collectLastValue(repository.reactivatedId) + val recommendationsLoadingState by + collectLastValue(repository.recommendationsLoadingState) + val recommendationsLoadingModel = + SmartspaceMediaLoadingModel.Loaded(SMARTSPACE_KEY, isPrioritized = true) val dataOld = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) mediaDataFilter.onMediaDataLoaded(KEY, null, dataOld) clock.advanceTime(MediaDataFilterImpl.SMARTSPACE_MAX_AGE + 100) mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) - verify(listener).onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(true)) + assertThat(recommendationsLoadingState).isEqualTo(recommendationsLoadingModel) assertThat( hasActiveMediaOrRecommendation( selectedUserEntries, @@ -538,11 +568,13 @@ class MediaDataFilterImplTest : SysuiTestCase() { } @Test - fun testOnSmartspaceMediaDataLoaded_noRecentMedia_inactiveRec_showsNothing() = + fun onSmartspaceMediaDataLoaded_noRecentMedia_inactiveRec_showsNothing() = testScope.runTest { - val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) - val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) - val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedId) + val selectedUserEntries by collectLastValue(repository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData) + val reactivatedKey by collectLastValue(repository.reactivatedId) + val recommendationsLoadingState by + collectLastValue(repository.recommendationsLoadingState) whenever(smartspaceData.isActive).thenReturn(false) val dataOld = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) @@ -550,7 +582,7 @@ class MediaDataFilterImplTest : SysuiTestCase() { clock.advanceTime(MediaDataFilterImpl.SMARTSPACE_MAX_AGE + 100) mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) - verify(listener, never()).onSmartspaceMediaDataLoaded(any(), anyBoolean()) + assertThat(recommendationsLoadingState).isEqualTo(SmartspaceMediaLoadingModel.Unknown) assertThat( hasActiveMediaOrRecommendation( selectedUserEntries, @@ -565,27 +597,29 @@ class MediaDataFilterImplTest : SysuiTestCase() { } @Test - fun testOnSmartspaceMediaDataLoaded_hasRecentMedia_inactiveRec_showsNothing() = + fun onSmartspaceMediaDataLoaded_hasRecentMedia_inactiveRec_showsNothing() = testScope.runTest { - val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) - val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) - val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedId) + val selectedUserEntries by collectLastValue(repository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData) + val reactivatedKey by collectLastValue(repository.reactivatedId) + val recommendationsLoadingState by + collectLastValue(repository.recommendationsLoadingState) + val mediaDataLoadedStates by collectLastValue(repository.mediaDataLoadedStates) whenever(smartspaceData.isActive).thenReturn(false) // WHEN we have media that was recently played, but not currently active val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) + val mediaLoadedStatesModel = listOf(MediaDataLoadingModel.Loaded(dataMain.instanceId)) mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent) - verify(listener) - .onMediaDataLoaded(eq(dataCurrent.instanceId), eq(true), eq(0), eq(false)) - reset(listener) + assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel) + // AND we get a smartspace signal mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) - // THEN we should tell listeners to treat the media as not active instead - verify(listener, never()).onMediaDataLoaded(any(), anyBoolean(), anyInt(), anyBoolean()) - verify(listener, never()).onSmartspaceMediaDataLoaded(any(), anyBoolean()) + // THEN we should treat the media as not active instead + assertThat(recommendationsLoadingState).isEqualTo(SmartspaceMediaLoadingModel.Unknown) assertThat( hasActiveMediaOrRecommendation( selectedUserEntries, @@ -600,27 +634,28 @@ class MediaDataFilterImplTest : SysuiTestCase() { } @Test - fun testOnSmartspaceMediaDataLoaded_hasRecentMedia_activeInvalidRec_usesMedia() = + fun onSmartspaceMediaDataLoaded_hasRecentMedia_activeInvalidRec_usesMedia() = testScope.runTest { - val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) - val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) - val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedId) + val selectedUserEntries by collectLastValue(repository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData) + val reactivatedKey by collectLastValue(repository.reactivatedId) + val recommendationsLoadingState by + collectLastValue(repository.recommendationsLoadingState) + val mediaDataLoadedStates by collectLastValue(repository.mediaDataLoadedStates) whenever(smartspaceData.isValid()).thenReturn(false) // WHEN we have media that was recently played, but not currently active val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) + val mediaLoadedStatesModel = listOf(MediaDataLoadingModel.Loaded(dataMain.instanceId)) mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent) - verify(listener) - .onMediaDataLoaded(eq(dataCurrent.instanceId), eq(true), eq(0), eq(false)) + assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel) // AND we get a smartspace signal runCurrent() mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) - // THEN we should tell listeners to treat the media as active instead - val dataCurrentAndActive = dataCurrent.copy(active = true) - verify(listener) - .onMediaDataLoaded(eq(dataCurrentAndActive.instanceId), eq(true), eq(100), eq(true)) + // THEN we should treat the media as active instead + assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel) assertThat( hasActiveMediaOrRecommendation( selectedUserEntries, @@ -630,31 +665,35 @@ class MediaDataFilterImplTest : SysuiTestCase() { ) .isTrue() // Smartspace update shouldn't be propagated for the empty rec list. - verify(listener, never()).onSmartspaceMediaDataLoaded(any(), anyBoolean()) + assertThat(recommendationsLoadingState).isEqualTo(SmartspaceMediaLoadingModel.Unknown) verify(logger, never()).logRecommendationAdded(any(), any()) verify(logger).logRecommendationActivated(eq(APP_UID), eq(PACKAGE), eq(INSTANCE_ID)) } @Test - fun testOnSmartspaceMediaDataLoaded_hasRecentMedia_activeValidRec_usesBoth() = + fun onSmartspaceMediaDataLoaded_hasRecentMedia_activeValidRec_usesBoth() = testScope.runTest { - val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) - val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) - val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedId) + val selectedUserEntries by collectLastValue(repository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData) + val reactivatedKey by collectLastValue(repository.reactivatedId) + val recommendationsLoadingState by + collectLastValue(repository.recommendationsLoadingState) + val mediaDataLoadedStates by collectLastValue(repository.mediaDataLoadedStates) // WHEN we have media that was recently played, but not currently active val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) + val mediaDataLoadingModel = listOf(MediaDataLoadingModel.Loaded(dataMain.instanceId)) + val recommendationsLoadingModel = SmartspaceMediaLoadingModel.Loaded(SMARTSPACE_KEY) + mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent) - verify(listener) - .onMediaDataLoaded(eq(dataCurrent.instanceId), eq(true), eq(0), eq(false)) + + assertThat(mediaDataLoadedStates).isEqualTo(mediaDataLoadingModel) // AND we get a smartspace signal runCurrent() mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) - // THEN we should tell listeners to treat the media as active instead - val dataCurrentAndActive = dataCurrent.copy(active = true) - verify(listener) - .onMediaDataLoaded(eq(dataCurrentAndActive.instanceId), eq(true), eq(100), eq(true)) + // THEN we should treat the media as active instead + assertThat(mediaDataLoadedStates).isEqualTo(mediaDataLoadingModel) assertThat( hasActiveMediaOrRecommendation( selectedUserEntries, @@ -664,22 +703,25 @@ class MediaDataFilterImplTest : SysuiTestCase() { ) .isTrue() // Smartspace update should also be propagated but not prioritized. - verify(listener).onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(false)) + assertThat(recommendationsLoadingState).isEqualTo(recommendationsLoadingModel) verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID) verify(logger).logRecommendationActivated(eq(APP_UID), eq(PACKAGE), eq(INSTANCE_ID)) } @Test - fun testOnSmartspaceMediaDataRemoved_usedSmartspace_clearsSmartspace() = + fun onSmartspaceMediaDataRemoved_usedSmartspace_clearsSmartspace() = testScope.runTest { - val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) - val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) - val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedId) + val selectedUserEntries by collectLastValue(repository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData) + val reactivatedKey by collectLastValue(repository.reactivatedId) + val recommendationsLoadingState by + collectLastValue(repository.recommendationsLoadingState) + val recommendationsLoadingModel = SmartspaceMediaLoadingModel.Removed(SMARTSPACE_KEY) mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) mediaDataFilter.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY) - verify(listener).onSmartspaceMediaDataRemoved(SMARTSPACE_KEY) + assertThat(recommendationsLoadingState).isEqualTo(recommendationsLoadingModel) assertThat( hasActiveMediaOrRecommendation( selectedUserEntries, @@ -692,26 +734,28 @@ class MediaDataFilterImplTest : SysuiTestCase() { } @Test - fun testOnSmartspaceMediaDataRemoved_usedMediaAndSmartspace_clearsBoth() = + fun onSmartspaceMediaDataRemoved_usedMediaAndSmartspace_clearsBoth() = testScope.runTest { - val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) - val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) - val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedId) + val selectedUserEntries by collectLastValue(repository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData) + val reactivatedKey by collectLastValue(repository.reactivatedId) + val mediaDataLoadedStates by collectLastValue(repository.mediaDataLoadedStates) + val recommendationsLoadingState by + collectLastValue(repository.recommendationsLoadingState) + val recommendationsLoadingModel = SmartspaceMediaLoadingModel.Removed(SMARTSPACE_KEY) + val mediaLoadedStatesModel = listOf(MediaDataLoadingModel.Loaded(dataMain.instanceId)) val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent) - verify(listener) - .onMediaDataLoaded(eq(dataCurrent.instanceId), eq(true), eq(0), eq(false)) + assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel) runCurrent() mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) - val dataCurrentAndActive = dataCurrent.copy(active = true) - verify(listener) - .onMediaDataLoaded(eq(dataCurrentAndActive.instanceId), eq(true), eq(100), eq(true)) + assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel) mediaDataFilter.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY) - verify(listener).onSmartspaceMediaDataRemoved(SMARTSPACE_KEY) + assertThat(recommendationsLoadingState).isEqualTo(recommendationsLoadingModel) assertThat( hasActiveMediaOrRecommendation( selectedUserEntries, @@ -724,17 +768,20 @@ class MediaDataFilterImplTest : SysuiTestCase() { } @Test - fun testOnSmartspaceLoaded_persistentEnabled_isInactive_notifiesListeners() = + fun onSmartspaceLoaded_persistentEnabled_isInactive() = testScope.runTest { - val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) - val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) - val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedId) + val selectedUserEntries by collectLastValue(repository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData) + val reactivatedKey by collectLastValue(repository.reactivatedId) + val recommendationsLoadingState by + collectLastValue(repository.recommendationsLoadingState) + val recommendationsLoadingModel = SmartspaceMediaLoadingModel.Loaded(SMARTSPACE_KEY) whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true) whenever(smartspaceData.isActive).thenReturn(false) mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) - verify(listener).onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(false)) + assertThat(recommendationsLoadingState).isEqualTo(recommendationsLoadingModel) assertThat( hasActiveMediaOrRecommendation( selectedUserEntries, @@ -748,11 +795,16 @@ class MediaDataFilterImplTest : SysuiTestCase() { } @Test - fun testOnSmartspaceLoaded_persistentEnabled_inactive_hasRecentMedia_staysInactive() = + fun onSmartspaceLoaded_persistentEnabled_inactive_hasRecentMedia_staysInactive() = testScope.runTest { - val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) - val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) - val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedId) + val selectedUserEntries by collectLastValue(repository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData) + val reactivatedKey by collectLastValue(repository.reactivatedId) + val mediaDataLoadedStates by collectLastValue(repository.mediaDataLoadedStates) + val recommendationsLoadingState by + collectLastValue(repository.recommendationsLoadingState) + val recommendationsLoadingModel = SmartspaceMediaLoadingModel.Loaded(SMARTSPACE_KEY) + val mediaLoadedStatesModel = listOf(MediaDataLoadingModel.Loaded(dataMain.instanceId)) whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true) whenever(smartspaceData.isActive).thenReturn(false) @@ -760,16 +812,14 @@ class MediaDataFilterImplTest : SysuiTestCase() { // If there is media that was recently played but inactive val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent) - verify(listener) - .onMediaDataLoaded(eq(dataCurrent.instanceId), eq(true), eq(0), eq(false)) - reset(listener) + assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel) + // And an inactive recommendation is loaded mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) // Smartspace is loaded but the media stays inactive - verify(listener).onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(false)) - verify(listener, never()).onMediaDataLoaded(any(), anyBoolean(), anyInt(), anyBoolean()) + assertThat(recommendationsLoadingState).isEqualTo(recommendationsLoadingModel) assertThat( hasActiveMediaOrRecommendation( selectedUserEntries, @@ -783,7 +833,7 @@ class MediaDataFilterImplTest : SysuiTestCase() { } @Test - fun testOnSwipeToDismiss_persistentEnabled_recommendationSetInactive() { + fun onSwipeToDismiss_persistentEnabled_recommendationSetInactive() { whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true) val data = @@ -802,16 +852,21 @@ class MediaDataFilterImplTest : SysuiTestCase() { } @Test - fun testSmartspaceLoaded_shouldTriggerResume_doesTrigger() = + fun smartspaceLoaded_shouldTriggerResume_doesTrigger() = testScope.runTest { - val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) - val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) - val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedId) + val selectedUserEntries by collectLastValue(repository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(repository.smartspaceMediaData) + val reactivatedKey by collectLastValue(repository.reactivatedId) + val mediaDataLoadedStates by collectLastValue(repository.mediaDataLoadedStates) + val recommendationsLoadingState by + collectLastValue(repository.recommendationsLoadingState) + val recommendationsLoadingModel = SmartspaceMediaLoadingModel.Loaded(SMARTSPACE_KEY) + val mediaLoadedStatesModel = listOf(MediaDataLoadingModel.Loaded(dataMain.instanceId)) // WHEN we have media that was recently played, but not currently active val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent) - verify(listener) - .onMediaDataLoaded(eq(dataCurrent.instanceId), eq(true), eq(0), eq(false)) + + assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel) // AND we get a smartspace signal with extra to trigger resume runCurrent() @@ -819,10 +874,8 @@ class MediaDataFilterImplTest : SysuiTestCase() { whenever(cardAction.extras).thenReturn(extras) mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) - // THEN we should tell listeners to treat the media as active instead - val dataCurrentAndActive = dataCurrent.copy(active = true) - verify(listener) - .onMediaDataLoaded(eq(dataCurrentAndActive.instanceId), eq(true), eq(100), eq(true)) + // THEN we should treat the media as active instead + assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel) assertThat( hasActiveMediaOrRecommendation( selectedUserEntries, @@ -831,27 +884,33 @@ class MediaDataFilterImplTest : SysuiTestCase() { ) ) .isTrue() - // And send the smartspace data, but not prioritized - verify(listener).onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(false)) + // And update the smartspace data state, but not prioritized + assertThat(recommendationsLoadingState).isEqualTo(recommendationsLoadingModel) } @Test - fun testSmartspaceLoaded_notShouldTriggerResume_doesNotTrigger() { - // WHEN we have media that was recently played, but not currently active - val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) - mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent) - verify(listener).onMediaDataLoaded(eq(dataCurrent.instanceId), eq(true), eq(0), eq(false)) + fun smartspaceLoaded_notShouldTriggerResume_doesNotTrigger() = + testScope.runTest { + val mediaDataLoadedStates by collectLastValue(repository.mediaDataLoadedStates) + val recommendationsLoadingState by + collectLastValue(repository.recommendationsLoadingState) + val recommendationsLoadingModel = SmartspaceMediaLoadingModel.Loaded(SMARTSPACE_KEY) + val mediaLoadedStatesModel = listOf(MediaDataLoadingModel.Loaded(dataMain.instanceId)) - // AND we get a smartspace signal with extra to not trigger resume - val extras = Bundle().apply { putBoolean(EXTRA_KEY_TRIGGER_RESUME, false) } - whenever(cardAction.extras).thenReturn(extras) - mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) + // WHEN we have media that was recently played, but not currently active + val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) + mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent) - // THEN listeners are not updated to show media - verify(listener, never()).onMediaDataLoaded(any(), eq(true), eq(100), eq(true)) - // But the smartspace update is still propagated - verify(listener).onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(false)) - } + assertThat(mediaDataLoadedStates).isEqualTo(mediaLoadedStatesModel) + + // AND we get a smartspace signal with extra to not trigger resume + val extras = Bundle().apply { putBoolean(EXTRA_KEY_TRIGGER_RESUME, false) } + whenever(cardAction.extras).thenReturn(extras) + mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) + + // But the smartspace update is still propagated + assertThat(recommendationsLoadingState).isEqualTo(recommendationsLoadingModel) + } private fun hasActiveMediaOrRecommendation( entries: Map<InstanceId, MediaData>?, diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java index e7b29d826a0c..0a8e470f8a7c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java @@ -410,6 +410,8 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { mock(DeviceEntryUdfpsInteractor.class); when(deviceEntryUdfpsInteractor.isUdfpsSupported()).thenReturn(MutableStateFlow(false)); + when(mKeyguardTransitionInteractor.isInTransitionToState(any())).thenReturn(emptyFlow()); + mShadeInteractor = new ShadeInteractorImpl( mTestScope.getBackgroundScope(), mKosmos.getDeviceProvisioningInteractor(), @@ -539,7 +541,7 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { }).when(mView).setOnTouchListener(any(NotificationPanelViewController.TouchHandler.class)); // Dreaming->Lockscreen - when(mKeyguardTransitionInteractor.getDreamingToLockscreenTransition()) + when(mKeyguardTransitionInteractor.transition(any(), any())) .thenReturn(emptyFlow()); when(mDreamingToLockscreenTransitionViewModel.getLockscreenAlpha()) .thenReturn(emptyFlow()); @@ -547,46 +549,28 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { .thenReturn(emptyFlow()); // Occluded->Lockscreen - when(mKeyguardTransitionInteractor.getOccludedToLockscreenTransition()) - .thenReturn(emptyFlow()); when(mOccludedToLockscreenTransitionViewModel.getLockscreenAlpha()) .thenReturn(emptyFlow()); when(mOccludedToLockscreenTransitionViewModel.getLockscreenTranslationY()) .thenReturn(emptyFlow()); // Lockscreen->Dreaming - when(mKeyguardTransitionInteractor.getLockscreenToDreamingTransition()) - .thenReturn(emptyFlow()); when(mLockscreenToDreamingTransitionViewModel.getLockscreenAlpha()) .thenReturn(emptyFlow()); when(mLockscreenToDreamingTransitionViewModel.lockscreenTranslationY(anyInt())) .thenReturn(emptyFlow()); // Gone->Dreaming - when(mKeyguardTransitionInteractor.getGoneToDreamingTransition()) - .thenReturn(emptyFlow()); when(mGoneToDreamingTransitionViewModel.getLockscreenAlpha()) .thenReturn(emptyFlow()); when(mGoneToDreamingTransitionViewModel.lockscreenTranslationY(anyInt())) .thenReturn(emptyFlow()); // Gone->Dreaming lockscreen hosted - when(mKeyguardTransitionInteractor.getGoneToDreamingLockscreenHostedTransition()) - .thenReturn(emptyFlow()); when(mGoneToDreamingLockscreenHostedTransitionViewModel.getLockscreenAlpha()) .thenReturn(emptyFlow()); - // Dreaming lockscreen hosted->Lockscreen - when(mKeyguardTransitionInteractor.getDreamingLockscreenHostedToLockscreenTransition()) - .thenReturn(emptyFlow()); - - // Lockscreen->Dreaming lockscreen hosted - when(mKeyguardTransitionInteractor.getLockscreenToDreamingLockscreenHostedTransition()) - .thenReturn(emptyFlow()); - // Lockscreen->Occluded - when(mKeyguardTransitionInteractor.getLockscreenToOccludedTransition()) - .thenReturn(emptyFlow()); when(mLockscreenToOccludedTransitionViewModel.getLockscreenAlpha()) .thenReturn(emptyFlow()); when(mLockscreenToOccludedTransitionViewModel.getLockscreenTranslationY()) 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 2c0a15dd4e5a..b04503b8e031 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt @@ -43,6 +43,8 @@ import com.android.systemui.flags.Flags.TRACKPAD_GESTURE_FEATURES import com.android.systemui.keyevent.domain.interactor.SysUIKeyEventHandler import com.android.systemui.keyguard.KeyguardUnlockAnimationController import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING +import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.res.R import com.android.systemui.shade.NotificationShadeWindowView.InteractionEventHandler @@ -160,7 +162,7 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { .thenReturn(keyguardBouncerComponent) whenever(keyguardBouncerComponent.securityContainerController) .thenReturn(keyguardSecurityContainerController) - whenever(keyguardTransitionInteractor.lockscreenToDreamingTransition) + whenever(keyguardTransitionInteractor.transition(LOCKSCREEN, DREAMING)) .thenReturn(emptyFlow<TransitionStep>()) featureFlagsClassic = FakeFeatureFlagsClassic() 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 98a815cabe83..ba8eb6f4ba36 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt @@ -36,6 +36,8 @@ import com.android.systemui.flags.Flags import com.android.systemui.keyevent.domain.interactor.SysUIKeyEventHandler import com.android.systemui.keyguard.KeyguardUnlockAnimationController import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING +import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN import com.android.systemui.res.R import com.android.systemui.shade.NotificationShadeWindowView.InteractionEventHandler import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor @@ -149,7 +151,7 @@ class NotificationShadeWindowViewTest : SysuiTestCase() { whenever(statusBarStateController.isDozing).thenReturn(false) mDependency.injectTestDependency(ShadeController::class.java, shadeController) whenever(dockManager.isDocked).thenReturn(false) - whenever(keyguardTransitionInteractor.lockscreenToDreamingTransition) + whenever(keyguardTransitionInteractor.transition(LOCKSCREEN, DREAMING)) .thenReturn(emptyFlow()) val featureFlags = FakeFeatureFlags() diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModelTest.kt index 4eb7daa1eac7..894e02e80997 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModelTest.kt @@ -188,6 +188,9 @@ class NotificationIconContainerAlwaysOnDisplayViewModelTest : SysuiTestCase() { @Test fun animationsEnabled_isTrue_whenStartingToSleepAndControlScreenOff() = testComponent.runTest { + val animationsEnabled by collectLastValue(underTest.areContainerChangesAnimated) + assertThat(animationsEnabled).isTrue() + powerRepository.updateWakefulness( rawState = WakefulnessState.STARTING_TO_SLEEP, lastWakeReason = WakeSleepReason.POWER_BUTTON, @@ -201,8 +204,6 @@ class NotificationIconContainerAlwaysOnDisplayViewModelTest : SysuiTestCase() { ) ) whenever(dozeParams.shouldControlScreenOff()).thenReturn(true) - val animationsEnabled by collectLastValue(underTest.areContainerChangesAnimated) - runCurrent() assertThat(animationsEnabled).isTrue() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt index 35b84939b05d..78b76151e7e6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt @@ -195,6 +195,9 @@ class NotificationIconContainerStatusBarViewModelTest : SysuiTestCase() { @Test fun animationsEnabled_isTrue_whenStartingToSleepAndControlScreenOff() = testComponent.runTest { + val animationsEnabled by collectLastValue(underTest.animationsEnabled) + assertThat(animationsEnabled).isTrue() + powerRepository.updateWakefulness( rawState = WakefulnessState.STARTING_TO_SLEEP, lastWakeReason = WakeSleepReason.POWER_BUTTON, @@ -208,7 +211,7 @@ class NotificationIconContainerStatusBarViewModelTest : SysuiTestCase() { ) ) whenever(dozeParams.shouldControlScreenOff()).thenReturn(true) - val animationsEnabled by collectLastValue(underTest.animationsEnabled) + runCurrent() assertThat(animationsEnabled).isTrue() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java index 06a4d0820386..01492f629fe8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java @@ -398,7 +398,7 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { } @Test - public void testAboveShelfChangedListenerCalledHeadsUpGoingAway() throws Exception { + public void testAboveShelfChangedListenerCalledHeadsUpAnimatingAway() throws Exception { ExpandableNotificationRow row = mNotificationTestHelper.createRow(); AboveShelfChangedListener listener = mock(AboveShelfChangedListener.class); row.setAboveShelfChangedListener(listener); diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorKosmos.kt index c06554573bd7..9ce9ff2faf21 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorKosmos.kt @@ -24,6 +24,7 @@ import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.kosmos.testScope import com.android.systemui.power.domain.interactor.powerInteractor +import com.android.systemui.scene.domain.interactor.sceneInteractor val Kosmos.bouncerInteractor by Fixture { BouncerInteractor( @@ -33,5 +34,6 @@ val Kosmos.bouncerInteractor by Fixture { deviceEntryFaceAuthInteractor = deviceEntryFaceAuthInteractor, falsingInteractor = falsingInteractor, powerInteractor = powerInteractor, + sceneInteractor = sceneInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt index 0f6c7cf13211..c3dad748064d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt @@ -18,6 +18,7 @@ package com.android.systemui.bouncer.ui.viewmodel +import android.app.admin.devicePolicyManager import android.content.applicationContext import com.android.systemui.authentication.domain.interactor.authenticationInteractor import com.android.systemui.bouncer.domain.interactor.bouncerActionButtonInteractor @@ -31,7 +32,6 @@ import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.user.domain.interactor.selectedUserInteractor import com.android.systemui.user.ui.viewmodel.userSwitcherViewModel -import com.android.systemui.util.mockito.mock import kotlinx.coroutines.ExperimentalCoroutinesApi val Kosmos.bouncerViewModel by Fixture { @@ -44,12 +44,12 @@ val Kosmos.bouncerViewModel by Fixture { simBouncerInteractor = simBouncerInteractor, authenticationInteractor = authenticationInteractor, selectedUserInteractor = selectedUserInteractor, + devicePolicyManager = devicePolicyManager, + bouncerMessageViewModel = bouncerMessageViewModel, flags = composeBouncerFlags, selectedUser = userSwitcherViewModel.selectedUser, users = userSwitcherViewModel.users, userSwitcherMenu = userSwitcherViewModel.menu, actionButton = bouncerActionButtonInteractor.actionButton, - devicePolicyManager = mock(), - bouncerMessageViewModel = bouncerMessageViewModel, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorKosmos.kt index 185deda950c6..6cc1e8eba73d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorKosmos.kt @@ -20,13 +20,11 @@ import com.android.systemui.keyguard.data.repository.keyguardRepository import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope -import com.android.systemui.kosmos.testDispatcher val Kosmos.keyguardTransitionInteractor: KeyguardTransitionInteractor by Kosmos.Fixture { KeyguardTransitionInteractor( scope = applicationCoroutineScope, - mainDispatcher = testDispatcher, repository = keyguardTransitionRepository, keyguardRepository = keyguardRepository, fromLockscreenTransitionInteractor = { fromLockscreenTransitionInteractor }, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowKosmos.kt index f7de5a4c20c7..1a05d21cc30a 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowKosmos.kt @@ -22,14 +22,10 @@ import com.android.keyguard.logging.keyguardTransitionAnimationLogger import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture -import com.android.systemui.kosmos.applicationCoroutineScope -import com.android.systemui.kosmos.testDispatcher import kotlinx.coroutines.ExperimentalCoroutinesApi val Kosmos.keyguardTransitionAnimationFlow by Fixture { KeyguardTransitionAnimationFlow( - scope = applicationCoroutineScope, - mainDispatcher = testDispatcher, transitionInteractor = keyguardTransitionInteractor, logger = keyguardTransitionAnimationLogger, ) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationRepositoryKosmos.kt index 165c9429c917..dc1b9feea88f 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/data/repository/HeadsUpNotificationRepositoryKosmos.kt @@ -26,7 +26,7 @@ import kotlinx.coroutines.flow.MutableStateFlow val Kosmos.headsUpNotificationRepository by Fixture { FakeHeadsUpNotificationRepository() } class FakeHeadsUpNotificationRepository : HeadsUpRepository { - override val headsUpAnimatingAway: MutableStateFlow<Boolean> = MutableStateFlow(false) + override val isHeadsUpAnimatingAway: MutableStateFlow<Boolean> = MutableStateFlow(false) override val topHeadsUpRow: Flow<HeadsUpRowRepository?> = MutableStateFlow(null) override val activeHeadsUpRows: MutableStateFlow<Set<HeadsUpRowRepository>> = MutableStateFlow(emptySet()) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/truth/TruthUtils.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/truth/TruthUtils.kt new file mode 100644 index 000000000000..64fed689d7a9 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/truth/TruthUtils.kt @@ -0,0 +1,31 @@ +/* + * 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.truth + +import com.google.common.truth.MapSubject +import com.google.common.truth.Ordered + +fun MapSubject.containsEntriesExactly(entry: Pair<*, *>, vararg entries: Pair<*, *>): Ordered = + containsExactly( + entry.first, + entry.second, + *entries + .asSequence() + .flatMap { (key, value) -> sequenceOf(key, value) } + .toList() + .toTypedArray() + ) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/VolumePanelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/VolumePanelKosmos.kt index d3410737a432..348a02e1da04 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/VolumePanelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/VolumePanelKosmos.kt @@ -24,6 +24,7 @@ import com.android.systemui.util.mockito.mock import com.android.systemui.volume.panel.dagger.factory.KosmosVolumePanelComponentFactory import com.android.systemui.volume.panel.domain.ComponentAvailabilityCriteria import com.android.systemui.volume.panel.domain.TestComponentAvailabilityCriteria +import com.android.systemui.volume.panel.domain.VolumePanelStartable import com.android.systemui.volume.panel.domain.interactor.ComponentsInteractor import com.android.systemui.volume.panel.domain.interactor.ComponentsInteractorImpl import com.android.systemui.volume.panel.shared.model.VolumePanelComponentKey @@ -44,6 +45,8 @@ val Kosmos.componentsFactory: ComponentsFactory by var Kosmos.componentsLayoutManager: ComponentsLayoutManager by Kosmos.Fixture() var Kosmos.enabledComponents: Collection<VolumePanelComponentKey> by Kosmos.Fixture { componentByKey.keys } +var Kosmos.volumePanelStartables: Set<VolumePanelStartable> by + Kosmos.Fixture { emptySet<VolumePanelStartable>() } val Kosmos.unavailableCriteria: Provider<ComponentAvailabilityCriteria> by Kosmos.Fixture { Provider { TestComponentAvailabilityCriteria(false) } } val Kosmos.availableCriteria: Provider<ComponentAvailabilityCriteria> by diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/dagger/factory/KosmosVolumePanelComponentFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/dagger/factory/KosmosVolumePanelComponentFactory.kt index 49041ed0d652..e5f5d4e389f1 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/dagger/factory/KosmosVolumePanelComponentFactory.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/dagger/factory/KosmosVolumePanelComponentFactory.kt @@ -22,10 +22,12 @@ import com.android.systemui.volume.panel.componentsFactory import com.android.systemui.volume.panel.componentsInteractor import com.android.systemui.volume.panel.componentsLayoutManager import com.android.systemui.volume.panel.dagger.VolumePanelComponent +import com.android.systemui.volume.panel.domain.VolumePanelStartable import com.android.systemui.volume.panel.domain.interactor.ComponentsInteractor import com.android.systemui.volume.panel.ui.composable.ComponentsFactory import com.android.systemui.volume.panel.ui.layout.ComponentsLayoutManager import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelViewModel +import com.android.systemui.volume.panel.volumePanelStartables import kotlinx.coroutines.CoroutineScope class KosmosVolumePanelComponentFactory(private val kosmos: Kosmos) : VolumePanelComponentFactory { @@ -41,5 +43,8 @@ class KosmosVolumePanelComponentFactory(private val kosmos: Kosmos) : VolumePane override fun componentsLayoutManager(): ComponentsLayoutManager = kosmos.componentsLayoutManager + + override fun volumePanelStartables(): Set<VolumePanelStartable> = + kosmos.volumePanelStartables } } diff --git a/services/Android.bp b/services/Android.bp index 623519521a5a..cd974c5f562d 100644 --- a/services/Android.bp +++ b/services/Android.bp @@ -324,34 +324,34 @@ non_updatable_exportable_droidstubs { baseline_file: "api/lint-baseline.txt", }, }, - dists: [ - { - targets: ["sdk"], - dir: "apistubs/android/system-server/api", - dest: "android-non-updatable.txt", - }, - { - targets: ["sdk"], - dir: "apistubs/android/system-server/api", - dest: "android-non-updatable-removed.txt", - }, - ], soong_config_variables: { release_hidden_api_exportable_stubs: { dists: [ { + targets: ["sdk"], + dir: "apistubs/android/system-server/api", + dest: "android-non-updatable.txt", tag: ".exportable.api.txt", }, { + targets: ["sdk"], + dir: "apistubs/android/system-server/api", + dest: "android-non-updatable-removed.txt", tag: ".exportable.removed-api.txt", }, ], conditions_default: { dists: [ { + targets: ["sdk"], + dir: "apistubs/android/system-server/api", + dest: "android-non-updatable.txt", tag: ".api.txt", }, { + targets: ["sdk"], + dir: "apistubs/android/system-server/api", + dest: "android-non-updatable-removed.txt", tag: ".removed-api.txt", }, ], diff --git a/services/core/java/com/android/server/SystemServiceManager.java b/services/core/java/com/android/server/SystemServiceManager.java index 20816a1b22c8..73300e45ed43 100644 --- a/services/core/java/com/android/server/SystemServiceManager.java +++ b/services/core/java/com/android/server/SystemServiceManager.java @@ -44,6 +44,9 @@ import com.android.server.am.EventLogTags; import com.android.server.pm.ApexManager; import com.android.server.pm.UserManagerInternal; import com.android.server.utils.TimingsTraceAndSlog; +import com.android.tools.r8.keepanno.annotations.KeepTarget; +import com.android.tools.r8.keepanno.annotations.TypePattern; +import com.android.tools.r8.keepanno.annotations.UsesReflection; import dalvik.system.PathClassLoader; @@ -207,6 +210,11 @@ public final class SystemServiceManager implements Dumpable { * @throws RuntimeException if the service fails to start. */ @android.ravenwood.annotation.RavenwoodKeep + @UsesReflection( + @KeepTarget( + instanceOfClassConstantExclusive = SystemService.class, + methodName = "<init>", + methodParameterTypePatterns = {@TypePattern(constant = Context.class)})) public <T extends SystemService> T startService(Class<T> serviceClass) { try { final String name = serviceClass.getName(); diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index ad15ea90c45c..8022eb37fce7 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -497,6 +497,8 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -629,6 +631,9 @@ public class ActivityManagerService extends IActivityManager.Stub private static final int MAX_BUGREPORT_TITLE_SIZE = 100; private static final int MAX_BUGREPORT_DESCRIPTION_SIZE = 150; + private static final DateTimeFormatter DROPBOX_TIME_FORMATTER = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSSZ"); + OomAdjuster mOomAdjuster; static final String EXTRA_TITLE = "android.intent.extra.TITLE"; @@ -2167,22 +2172,25 @@ public class ActivityManagerService extends IActivityManager.Stub */ static class VolatileDropboxEntryStates { private final Boolean mIsProcessFrozen; + private final ZonedDateTime mTimestamp; - private VolatileDropboxEntryStates(Boolean frozenState) { + private VolatileDropboxEntryStates(Boolean frozenState, ZonedDateTime timestamp) { this.mIsProcessFrozen = frozenState; + this.mTimestamp = timestamp; } - public static VolatileDropboxEntryStates withProcessFrozenState(boolean frozenState) { - return new VolatileDropboxEntryStates(frozenState); - } - - public static VolatileDropboxEntryStates emptyVolatileDropboxEnytyStates() { - return new VolatileDropboxEntryStates(null); + public static VolatileDropboxEntryStates withProcessFrozenStateAndTimestamp( + boolean frozenState, ZonedDateTime timestamp) { + return new VolatileDropboxEntryStates(frozenState, timestamp); } public Boolean isProcessFrozen() { return mIsProcessFrozen; } + + public ZonedDateTime getTimestamp() { + return mTimestamp; + } } static class MemBinder extends Binder { @@ -9678,6 +9686,11 @@ public class ActivityManagerService extends IActivityManager.Stub ? volatileStates.isProcessFrozen() : process.mOptRecord.isFrozen() ).append("\n"); } + if (volatileStates != null && volatileStates.getTimestamp() != null) { + String formattedTime = DROPBOX_TIME_FORMATTER.format( + volatileStates.getTimestamp()); + sb.append("Timestamp: ").append(formattedTime).append("\n"); + } int flags = process.info.flags; final IPackageManager pm = AppGlobals.getPackageManager(); sb.append("Flags: 0x").append(Integer.toHexString(flags)).append("\n"); diff --git a/services/core/java/com/android/server/am/ProcessErrorStateRecord.java b/services/core/java/com/android/server/am/ProcessErrorStateRecord.java index 0aa1a69334d7..76c59520d4ea 100644 --- a/services/core/java/com/android/server/am/ProcessErrorStateRecord.java +++ b/services/core/java/com/android/server/am/ProcessErrorStateRecord.java @@ -66,7 +66,7 @@ import java.io.PrintWriter; import java.io.StringWriter; import java.time.Instant; import java.time.ZoneId; -import java.time.format.DateTimeFormatter; +import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.UUID; import java.util.concurrent.ExecutionException; @@ -79,9 +79,6 @@ import java.util.concurrent.atomic.AtomicLong; * The error state of the process, such as if it's crashing/ANR etc. */ class ProcessErrorStateRecord { - private static final DateTimeFormatter DROPBOX_TIME_FORMATTER = - DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSSZ"); - final ProcessRecord mApp; private final ActivityManagerService mService; @@ -355,9 +352,18 @@ class ProcessErrorStateRecord { synchronized (mProcLock) { latencyTracker.waitingOnProcLockEnded(); setNotResponding(true); + + ZonedDateTime timestamp = null; + if (timeoutRecord != null && timeoutRecord.mEndUptimeMillis > 0) { + long millisSinceEndUptimeMs = anrTime - timeoutRecord.mEndUptimeMillis; + timestamp = Instant.now().minusMillis(millisSinceEndUptimeMs) + .atZone(ZoneId.systemDefault()); + } + volatileDropboxEntriyStates = ActivityManagerService.VolatileDropboxEntryStates - .withProcessFrozenState(mApp.mOptRecord.isFrozen()); + .withProcessFrozenStateAndTimestamp( + mApp.mOptRecord.isFrozen(), timestamp); } // Log the ANR to the event log. @@ -450,13 +456,6 @@ class ProcessErrorStateRecord { info.append("ErrorId: ").append(errorId.toString()).append("\n"); } info.append("Frozen: ").append(mApp.mOptRecord.isFrozen()).append("\n"); - if (timeoutRecord != null && timeoutRecord.mEndUptimeMillis > 0) { - long millisSinceEndUptimeMs = anrTime - timeoutRecord.mEndUptimeMillis; - String formattedTime = DROPBOX_TIME_FORMATTER.format( - Instant.now().minusMillis(millisSinceEndUptimeMs) - .atZone(ZoneId.systemDefault())); - info.append("Timestamp: ").append(formattedTime).append("\n"); - } // Retrieve controller with max ANR delay from AnrControllers // Note that we retrieve the controller before dumping stacks because dumping stacks can diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java index 3e3ec17eb0e0..1f89ca70ce8d 100644 --- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java +++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java @@ -170,6 +170,7 @@ public class SettingsToPropertiesMapper { "pixel_biometrics_face", "pixel_bluetooth", "pixel_connectivity_gps", + "pixel_continuity", "pixel_sensors", "pixel_system_sw_video", "pixel_watch", diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java index debd9d0f0c83..be39778372ca 100644 --- a/services/core/java/com/android/server/appop/AppOpsService.java +++ b/services/core/java/com/android/server/appop/AppOpsService.java @@ -1364,6 +1364,9 @@ public class AppOpsService extends IAppOpsService.Stub { @GuardedBy("this") private void packageRemovedLocked(int uid, String packageName) { + mHandler.post(PooledLambda.obtainRunnable(HistoricalRegistry::clearHistory, + mHistoricalRegistry, uid, packageName)); + UidState uidState = mUidStates.get(uid); if (uidState == null) { return; @@ -1398,9 +1401,6 @@ public class AppOpsService extends IAppOpsService.Stub { } } } - - mHandler.post(PooledLambda.obtainRunnable(HistoricalRegistry::clearHistory, - mHistoricalRegistry, uid, packageName)); } public void uidRemoved(int uid) { diff --git a/services/core/java/com/android/server/inputmethod/IInputMethodManagerImpl.java b/services/core/java/com/android/server/inputmethod/IInputMethodManagerImpl.java new file mode 100644 index 000000000000..7890fe0ed461 --- /dev/null +++ b/services/core/java/com/android/server/inputmethod/IInputMethodManagerImpl.java @@ -0,0 +1,464 @@ +/* + * 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.server.inputmethod; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.SOURCE; + +import android.Manifest; +import android.annotation.BinderThread; +import android.annotation.EnforcePermission; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.UserIdInt; +import android.os.Binder; +import android.os.IBinder; +import android.os.ResultReceiver; +import android.os.ShellCallback; +import android.view.MotionEvent; +import android.view.WindowManager; +import android.view.inputmethod.CursorAnchorInfo; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.ImeTracker; +import android.view.inputmethod.InputMethodInfo; +import android.view.inputmethod.InputMethodManager; +import android.view.inputmethod.InputMethodSubtype; +import android.window.ImeOnBackInvokedDispatcher; + +import com.android.internal.inputmethod.DirectBootAwareness; +import com.android.internal.inputmethod.IBooleanListener; +import com.android.internal.inputmethod.IConnectionlessHandwritingCallback; +import com.android.internal.inputmethod.IImeTracker; +import com.android.internal.inputmethod.IInputMethodClient; +import com.android.internal.inputmethod.IRemoteAccessibilityInputConnection; +import com.android.internal.inputmethod.IRemoteInputConnection; +import com.android.internal.inputmethod.InputBindResult; +import com.android.internal.inputmethod.SoftInputShowHideReason; +import com.android.internal.inputmethod.StartInputFlags; +import com.android.internal.inputmethod.StartInputReason; +import com.android.internal.view.IInputMethodManager; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.util.List; + +/** + * An actual implementation class of {@link IInputMethodManager.Stub} to allow other classes to + * focus on handling IPC callbacks. + */ +final class IInputMethodManagerImpl extends IInputMethodManager.Stub { + + /** + * Tells that the given permission is already verified before the annotated method gets called. + */ + @Retention(SOURCE) + @Target({METHOD}) + @interface PermissionVerified { + String value() default ""; + } + + @BinderThread + interface Callback { + void addClient(IInputMethodClient client, IRemoteInputConnection inputConnection, + int selfReportedDisplayId); + + InputMethodInfo getCurrentInputMethodInfoAsUser(@UserIdInt int userId); + + List<InputMethodInfo> getInputMethodList(@UserIdInt int userId, + @DirectBootAwareness int directBootAwareness); + + List<InputMethodInfo> getEnabledInputMethodList(@UserIdInt int userId); + + List<InputMethodSubtype> getEnabledInputMethodSubtypeList(String imiId, + boolean allowsImplicitlyEnabledSubtypes, @UserIdInt int userId); + + InputMethodSubtype getLastInputMethodSubtype(@UserIdInt int userId); + + boolean showSoftInput(IInputMethodClient client, IBinder windowToken, + @Nullable ImeTracker.Token statsToken, @InputMethodManager.ShowFlags int flags, + @MotionEvent.ToolType int lastClickToolType, ResultReceiver resultReceiver, + @SoftInputShowHideReason int reason); + + boolean hideSoftInput(IInputMethodClient client, IBinder windowToken, + @Nullable ImeTracker.Token statsToken, @InputMethodManager.HideFlags int flags, + ResultReceiver resultReceiver, @SoftInputShowHideReason int reason); + + @PermissionVerified(Manifest.permission.TEST_INPUT_METHOD) + void hideSoftInputFromServerForTest(); + + void startInputOrWindowGainedFocusAsync( + @StartInputReason int startInputReason, IInputMethodClient client, + IBinder windowToken, @StartInputFlags int startInputFlags, + @WindowManager.LayoutParams.SoftInputModeFlags int softInputMode, int windowFlags, + @Nullable EditorInfo editorInfo, IRemoteInputConnection inputConnection, + IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection, + int unverifiedTargetSdkVersion, @UserIdInt int userId, + @NonNull ImeOnBackInvokedDispatcher imeDispatcher, int startInputSeq); + + InputBindResult startInputOrWindowGainedFocus( + @StartInputReason int startInputReason, IInputMethodClient client, + IBinder windowToken, @StartInputFlags int startInputFlags, + @WindowManager.LayoutParams.SoftInputModeFlags int softInputMode, int windowFlags, + @Nullable EditorInfo editorInfo, IRemoteInputConnection inputConnection, + IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection, + int unverifiedTargetSdkVersion, @UserIdInt int userId, + @NonNull ImeOnBackInvokedDispatcher imeDispatcher); + + void showInputMethodPickerFromClient(IInputMethodClient client, int auxiliarySubtypeMode); + + @PermissionVerified(Manifest.permission.WRITE_SECURE_SETTINGS) + void showInputMethodPickerFromSystem(int auxiliarySubtypeMode, int displayId); + + @PermissionVerified(Manifest.permission.TEST_INPUT_METHOD) + boolean isInputMethodPickerShownForTest(); + + InputMethodSubtype getCurrentInputMethodSubtype(@UserIdInt int userId); + + void setAdditionalInputMethodSubtypes(String imiId, InputMethodSubtype[] subtypes, + @UserIdInt int userId); + + void setExplicitlyEnabledInputMethodSubtypes(String imeId, + @NonNull int[] subtypeHashCodes, @UserIdInt int userId); + + int getInputMethodWindowVisibleHeight(IInputMethodClient client); + + void reportPerceptibleAsync(IBinder windowToken, boolean perceptible); + + @PermissionVerified(Manifest.permission.INTERNAL_SYSTEM_WINDOW) + void removeImeSurface(); + + void removeImeSurfaceFromWindowAsync(IBinder windowToken); + + void startProtoDump(byte[] bytes, int i, String s); + + boolean isImeTraceEnabled(); + + @PermissionVerified(Manifest.permission.CONTROL_UI_TRACING) + void startImeTrace(); + + @PermissionVerified(Manifest.permission.CONTROL_UI_TRACING) + void stopImeTrace(); + + void startStylusHandwriting(IInputMethodClient client); + + void startConnectionlessStylusHandwriting(IInputMethodClient client, @UserIdInt int userId, + @Nullable CursorAnchorInfo cursorAnchorInfo, @Nullable String delegatePackageName, + @Nullable String delegatorPackageName, + @NonNull IConnectionlessHandwritingCallback callback); + + boolean acceptStylusHandwritingDelegation(@NonNull IInputMethodClient client, + @UserIdInt int userId, @NonNull String delegatePackageName, + @NonNull String delegatorPackageName, + @InputMethodManager.HandwritingDelegateFlags int flags); + + void acceptStylusHandwritingDelegationAsync(@NonNull IInputMethodClient client, + @UserIdInt int userId, @NonNull String delegatePackageName, + @NonNull String delegatorPackageName, + @InputMethodManager.HandwritingDelegateFlags int flags, IBooleanListener callback); + + void prepareStylusHandwritingDelegation(@NonNull IInputMethodClient client, + @UserIdInt int userId, @NonNull String delegatePackageName, + @NonNull String delegatorPackageName); + + boolean isStylusHandwritingAvailableAsUser(@UserIdInt int userId, boolean connectionless); + + @PermissionVerified(Manifest.permission.TEST_INPUT_METHOD) + void addVirtualStylusIdForTestSession(IInputMethodClient client); + + @PermissionVerified(Manifest.permission.TEST_INPUT_METHOD) + void setStylusWindowIdleTimeoutForTest(IInputMethodClient client, long timeout); + + IImeTracker getImeTrackerService(); + + void onShellCommand(@Nullable FileDescriptor in, @Nullable FileDescriptor out, + @Nullable FileDescriptor err, @NonNull String[] args, + @Nullable ShellCallback callback, @NonNull ResultReceiver resultReceiver, + @NonNull Binder self); + + void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter fout, @Nullable String[] args); + } + + @NonNull + private final Callback mCallback; + + private IInputMethodManagerImpl(@NonNull Callback callback) { + mCallback = callback; + } + + static IInputMethodManagerImpl create(@NonNull Callback callback) { + return new IInputMethodManagerImpl(callback); + } + + @Override + public void addClient(IInputMethodClient client, IRemoteInputConnection inputmethod, + int untrustedDisplayId) { + mCallback.addClient(client, inputmethod, untrustedDisplayId); + } + + @Override + public InputMethodInfo getCurrentInputMethodInfoAsUser(@UserIdInt int userId) { + return mCallback.getCurrentInputMethodInfoAsUser(userId); + } + + @Override + public List<InputMethodInfo> getInputMethodList(@UserIdInt int userId, + int directBootAwareness) { + return mCallback.getInputMethodList(userId, directBootAwareness); + } + + @Override + public List<InputMethodInfo> getEnabledInputMethodList(@UserIdInt int userId) { + return mCallback.getEnabledInputMethodList(userId); + } + + @Override + public List<InputMethodSubtype> getEnabledInputMethodSubtypeList(String imiId, + boolean allowsImplicitlyEnabledSubtypes, @UserIdInt int userId) { + return mCallback.getEnabledInputMethodSubtypeList(imiId, allowsImplicitlyEnabledSubtypes, + userId); + } + + @Override + public InputMethodSubtype getLastInputMethodSubtype(@UserIdInt int userId) { + return mCallback.getLastInputMethodSubtype(userId); + } + + @Override + public boolean showSoftInput(IInputMethodClient client, IBinder windowToken, + @NonNull ImeTracker.Token statsToken, @InputMethodManager.ShowFlags int flags, + @MotionEvent.ToolType int lastClickToolType, ResultReceiver resultReceiver, + @SoftInputShowHideReason int reason) { + return mCallback.showSoftInput(client, windowToken, statsToken, flags, lastClickToolType, + resultReceiver, reason); + } + + @Override + public boolean hideSoftInput(IInputMethodClient client, IBinder windowToken, + @NonNull ImeTracker.Token statsToken, @InputMethodManager.HideFlags int flags, + ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) { + return mCallback.hideSoftInput(client, windowToken, statsToken, flags, resultReceiver, + reason); + } + + @EnforcePermission(Manifest.permission.TEST_INPUT_METHOD) + @Override + public void hideSoftInputFromServerForTest() { + super.hideSoftInputFromServerForTest_enforcePermission(); + + mCallback.hideSoftInputFromServerForTest(); + } + + @Override + public InputBindResult startInputOrWindowGainedFocus( + @StartInputReason int startInputReason, IInputMethodClient client, IBinder windowToken, + @StartInputFlags int startInputFlags, + @WindowManager.LayoutParams.SoftInputModeFlags int softInputMode, + int windowFlags, @Nullable EditorInfo editorInfo, + IRemoteInputConnection inputConnection, + IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection, + int unverifiedTargetSdkVersion, @UserIdInt int userId, + @NonNull ImeOnBackInvokedDispatcher imeDispatcher) { + return mCallback.startInputOrWindowGainedFocus( + startInputReason, client, windowToken, startInputFlags, softInputMode, + windowFlags, editorInfo, inputConnection, remoteAccessibilityInputConnection, + unverifiedTargetSdkVersion, userId, imeDispatcher); + } + + @Override + public void startInputOrWindowGainedFocusAsync(@StartInputReason int startInputReason, + IInputMethodClient client, IBinder windowToken, + @StartInputFlags int startInputFlags, + @WindowManager.LayoutParams.SoftInputModeFlags int softInputMode, + int windowFlags, @Nullable EditorInfo editorInfo, + IRemoteInputConnection inputConnection, + IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection, + int unverifiedTargetSdkVersion, @UserIdInt int userId, + @NonNull ImeOnBackInvokedDispatcher imeDispatcher, int startInputSeq) { + mCallback.startInputOrWindowGainedFocusAsync( + startInputReason, client, windowToken, startInputFlags, softInputMode, + windowFlags, editorInfo, inputConnection, remoteAccessibilityInputConnection, + unverifiedTargetSdkVersion, userId, imeDispatcher, startInputSeq); + } + + @Override + public void showInputMethodPickerFromClient(IInputMethodClient client, + int auxiliarySubtypeMode) { + mCallback.showInputMethodPickerFromClient(client, auxiliarySubtypeMode); + } + + @EnforcePermission(Manifest.permission.WRITE_SECURE_SETTINGS) + @Override + public void showInputMethodPickerFromSystem(int auxiliarySubtypeMode, int displayId) { + super.showInputMethodPickerFromSystem_enforcePermission(); + + mCallback.showInputMethodPickerFromSystem(auxiliarySubtypeMode, displayId); + + } + + @EnforcePermission(Manifest.permission.TEST_INPUT_METHOD) + @Override + public boolean isInputMethodPickerShownForTest() { + super.isInputMethodPickerShownForTest_enforcePermission(); + + return mCallback.isInputMethodPickerShownForTest(); + } + + @Override + public InputMethodSubtype getCurrentInputMethodSubtype(@UserIdInt int userId) { + return mCallback.getCurrentInputMethodSubtype(userId); + } + + @Override + public void setAdditionalInputMethodSubtypes(String id, InputMethodSubtype[] subtypes, + @UserIdInt int userId) { + mCallback.setAdditionalInputMethodSubtypes(id, subtypes, userId); + } + + @Override + public void setExplicitlyEnabledInputMethodSubtypes(String imeId, int[] subtypeHashCodes, + @UserIdInt int userId) { + mCallback.setExplicitlyEnabledInputMethodSubtypes(imeId, subtypeHashCodes, userId); + } + + @Override + public int getInputMethodWindowVisibleHeight(IInputMethodClient client) { + return mCallback.getInputMethodWindowVisibleHeight(client); + } + + @Override + public void reportPerceptibleAsync(IBinder windowToken, boolean perceptible) { + mCallback.reportPerceptibleAsync(windowToken, perceptible); + } + + @EnforcePermission(Manifest.permission.INTERNAL_SYSTEM_WINDOW) + @Override + public void removeImeSurface() { + super.removeImeSurface_enforcePermission(); + + mCallback.removeImeSurface(); + } + + @Override + public void removeImeSurfaceFromWindowAsync(IBinder windowToken) { + mCallback.removeImeSurfaceFromWindowAsync(windowToken); + } + + @Override + public void startProtoDump(byte[] protoDump, int source, String where) { + mCallback.startProtoDump(protoDump, source, where); + } + + @Override + public boolean isImeTraceEnabled() { + return mCallback.isImeTraceEnabled(); + } + + @EnforcePermission(Manifest.permission.CONTROL_UI_TRACING) + @Override + public void startImeTrace() { + super.startImeTrace_enforcePermission(); + + mCallback.startImeTrace(); + } + + @EnforcePermission(Manifest.permission.CONTROL_UI_TRACING) + @Override + public void stopImeTrace() { + super.stopImeTrace_enforcePermission(); + + mCallback.stopImeTrace(); + } + + @Override + public void startStylusHandwriting(IInputMethodClient client) { + mCallback.startStylusHandwriting(client); + } + + @Override + public void startConnectionlessStylusHandwriting(IInputMethodClient client, + @UserIdInt int userId, CursorAnchorInfo cursorAnchorInfo, + String delegatePackageName, String delegatorPackageName, + IConnectionlessHandwritingCallback callback) { + mCallback.startConnectionlessStylusHandwriting(client, userId, cursorAnchorInfo, + delegatePackageName, delegatorPackageName, callback); + } + + @Override + public void prepareStylusHandwritingDelegation(IInputMethodClient client, @UserIdInt int userId, + String delegatePackageName, String delegatorPackageName) { + mCallback.prepareStylusHandwritingDelegation(client, userId, + delegatePackageName, delegatorPackageName); + } + + @Override + public boolean acceptStylusHandwritingDelegation(IInputMethodClient client, + @UserIdInt int userId, String delegatePackageName, String delegatorPackageName, + @InputMethodManager.HandwritingDelegateFlags int flags) { + return mCallback.acceptStylusHandwritingDelegation(client, userId, + delegatePackageName, delegatorPackageName, flags); + } + + @Override + public void acceptStylusHandwritingDelegationAsync(IInputMethodClient client, + @UserIdInt int userId, String delegatePackageName, String delegatorPackageName, + @InputMethodManager.HandwritingDelegateFlags int flags, + IBooleanListener callback) { + mCallback.acceptStylusHandwritingDelegationAsync(client, userId, + delegatePackageName, delegatorPackageName, flags, callback); + } + + @Override + public boolean isStylusHandwritingAvailableAsUser(@UserIdInt int userId, + boolean connectionless) { + return mCallback.isStylusHandwritingAvailableAsUser(userId, connectionless); + } + + @EnforcePermission(Manifest.permission.TEST_INPUT_METHOD) + @Override + public void addVirtualStylusIdForTestSession(IInputMethodClient client) { + super.addVirtualStylusIdForTestSession_enforcePermission(); + + mCallback.addVirtualStylusIdForTestSession(client); + } + + @EnforcePermission(Manifest.permission.TEST_INPUT_METHOD) + @Override + public void setStylusWindowIdleTimeoutForTest(IInputMethodClient client, long timeout) { + super.setStylusWindowIdleTimeoutForTest_enforcePermission(); + + mCallback.setStylusWindowIdleTimeoutForTest(client, timeout); + } + + @Override + public IImeTracker getImeTrackerService() { + return mCallback.getImeTrackerService(); + } + + @Override + public void onShellCommand(@Nullable FileDescriptor in, @Nullable FileDescriptor out, + @Nullable FileDescriptor err, @NonNull String[] args, @Nullable ShellCallback callback, + @NonNull ResultReceiver resultReceiver) { + mCallback.onShellCommand(in, out, err, args, callback, resultReceiver, this); + } + + @Override + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + mCallback.dump(fd, pw, args); + } +} diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 1d07eee927b2..03a85c40ef31 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -61,7 +61,6 @@ import android.annotation.AnyThread; import android.annotation.BinderThread; import android.annotation.DrawableRes; import android.annotation.DurationMillisLong; -import android.annotation.EnforcePermission; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -172,7 +171,6 @@ import com.android.internal.util.ArrayUtils; import com.android.internal.util.ConcurrentUtils; import com.android.internal.util.DumpUtils; import com.android.internal.util.Preconditions; -import com.android.internal.view.IInputMethodManager; import com.android.server.AccessibilityManagerInternal; import com.android.server.EventLogTags; import com.android.server.LocalServices; @@ -211,8 +209,9 @@ import java.util.function.IntConsumer; /** * This class provides a system service that manages input methods. */ -public final class InputMethodManagerService extends IInputMethodManager.Stub - implements Handler.Callback { +public final class InputMethodManagerService implements IInputMethodManagerImpl.Callback, + ZeroJankProxy.Callback, Handler.Callback { + // Virtual device id for test. private static final Integer VIRTUAL_STYLUS_ID_FOR_TEST = 999999; static final boolean DEBUG = false; @@ -1236,21 +1235,14 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @Override public void onStart() { mService.publishLocalService(); - IInputMethodManager.Stub service; + IInputMethodManagerImpl.Callback service; if (Flags.useZeroJankProxy()) { - service = - new ZeroJankProxy( - mService.mHandler::post, - mService, - () -> { - synchronized (ImfLock.class) { - return mService.isInputShown(); - } - }); + service = new ZeroJankProxy(mService.mHandler::post, mService); } else { service = mService; } - publishBinderService(Context.INPUT_METHOD_SERVICE, service, false /*allowIsolated*/, + publishBinderService(Context.INPUT_METHOD_SERVICE, + IInputMethodManagerImpl.create(service), false /*allowIsolated*/, DUMP_FLAG_PRIORITY_CRITICAL | DUMP_FLAG_PRIORITY_NORMAL | DUMP_FLAG_PROTO); } @@ -1935,10 +1927,10 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } @Nullable - ClientState getClientState(IInputMethodClient client) { - synchronized (ImfLock.class) { - return mClientController.getClient(client.asBinder()); - } + @GuardedBy("ImfLock.class") + @Override + public ClientState getClientStateLocked(IInputMethodClient client) { + return mClientController.getClient(client.asBinder()); } // TODO(b/314150112): Move this to ClientController. @@ -2017,7 +2009,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } @GuardedBy("ImfLock.class") - private boolean isInputShown() { + @Override + public boolean isInputShownLocked() { return mVisibilityStateComputer.isInputShown(); } @@ -3133,11 +3126,16 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub public void startConnectionlessStylusHandwriting(IInputMethodClient client, int userId, @Nullable CursorAnchorInfo cursorAnchorInfo, @Nullable String delegatePackageName, @Nullable String delegatorPackageName, - @NonNull IConnectionlessHandwritingCallback callback) throws RemoteException { + @NonNull IConnectionlessHandwritingCallback callback) { synchronized (ImfLock.class) { if (!mBindingController.supportsConnectionlessStylusHandwriting()) { Slog.w(TAG, "Connectionless stylus handwriting mode unsupported by IME."); - callback.onError(CONNECTIONLESS_HANDWRITING_ERROR_UNSUPPORTED); + try { + callback.onError(CONNECTIONLESS_HANDWRITING_ERROR_UNSUPPORTED); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to report CONNECTIONLESS_HANDWRITING_ERROR_UNSUPPORTED", e); + e.rethrowAsRuntimeException(); + } return; } } @@ -3148,7 +3146,12 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub synchronized (ImfLock.class) { if (!mClientController.verifyClientAndPackageMatch(client, delegatorPackageName)) { Slog.w(TAG, "startConnectionlessStylusHandwriting() fail"); - callback.onError(CONNECTIONLESS_HANDWRITING_ERROR_OTHER); + try { + callback.onError(CONNECTIONLESS_HANDWRITING_ERROR_OTHER); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to report CONNECTIONLESS_HANDWRITING_ERROR_OTHER", e); + e.rethrowAsRuntimeException(); + } throw new IllegalArgumentException("Delegator doesn't match UID"); } } @@ -3172,7 +3175,12 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (!startStylusHandwriting( client, false, immsCallback, cursorAnchorInfo, isForDelegation)) { - callback.onError(CONNECTIONLESS_HANDWRITING_ERROR_OTHER); + try { + callback.onError(CONNECTIONLESS_HANDWRITING_ERROR_OTHER); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to report CONNECTIONLESS_HANDWRITING_ERROR_OTHER", e); + e.rethrowAsRuntimeException(); + } } } @@ -3272,11 +3280,15 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @UserIdInt int userId, @NonNull String delegatePackageName, @NonNull String delegatorPackageName, - @InputMethodManager.HandwritingDelegateFlags int flags, IBooleanListener callback) - throws RemoteException { + @InputMethodManager.HandwritingDelegateFlags int flags, IBooleanListener callback) { boolean result = acceptStylusHandwritingDelegation( client, userId, delegatePackageName, delegatorPackageName, flags); - callback.onResult(result); + try { + callback.onResult(result); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to report result=" + result, e); + e.rethrowAsRuntimeException(); + } } @Override @@ -3367,7 +3379,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") boolean showCurrentInputLocked(IBinder windowToken, @NonNull ImeTracker.Token statsToken, @InputMethodManager.ShowFlags int flags, - int lastClickToolType, @Nullable ResultReceiver resultReceiver, + @MotionEvent.ToolType int lastClickToolType, @Nullable ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) { if (!mVisibilityStateComputer.onImeShowFlags(statsToken, flags)) { return false; @@ -3413,7 +3425,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub "InputMethodManagerService#hideSoftInput"); synchronized (ImfLock.class) { if (!canInteractWithImeLocked(uid, client, "hideSoftInput", statsToken)) { - if (isInputShown()) { + if (isInputShownLocked()) { ImeTracker.forLogging().onFailed( statsToken, ImeTracker.PHASE_SERVER_CLIENT_FOCUSED); } else { @@ -3436,10 +3448,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } @Override - @EnforcePermission(Manifest.permission.TEST_INPUT_METHOD) + @IInputMethodManagerImpl.PermissionVerified(Manifest.permission.TEST_INPUT_METHOD) public void hideSoftInputFromServerForTest() { - super.hideSoftInputFromServerForTest_enforcePermission(); - synchronized (ImfLock.class) { hideCurrentInputLocked(mImeBindingState.mFocusedWindow, 0 /* flags */, SoftInputShowHideReason.HIDE_SOFT_INPUT); @@ -3472,7 +3482,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // TODO(b/246309664): Clean up IMMS#mImeWindowVis IInputMethodInvoker curMethod = getCurMethodLocked(); final boolean shouldHideSoftInput = curMethod != null - && (isInputShown() || (mImeWindowVis & InputMethodService.IME_ACTIVE) != 0); + && (isInputShownLocked() || (mImeWindowVis & InputMethodService.IME_ACTIVE) != 0); mVisibilityStateComputer.requestImeVisibility(windowToken, false); if (shouldHideSoftInput) { @@ -3845,13 +3855,11 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } } - @EnforcePermission(Manifest.permission.WRITE_SECURE_SETTINGS) + @IInputMethodManagerImpl.PermissionVerified(Manifest.permission.WRITE_SECURE_SETTINGS) @Override public void showInputMethodPickerFromSystem(int auxiliarySubtypeMode, int displayId) { // Always call subtype picker, because subtype picker is a superset of input method // picker. - super.showInputMethodPickerFromSystem_enforcePermission(); - mHandler.obtainMessage(MSG_SHOW_IM_SUBTYPE_PICKER, auxiliarySubtypeMode, displayId) .sendToTarget(); } @@ -3859,10 +3867,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub /** * A test API for CTS to make sure that the input method menu is showing. */ - @EnforcePermission(Manifest.permission.TEST_INPUT_METHOD) + @IInputMethodManagerImpl.PermissionVerified(Manifest.permission.TEST_INPUT_METHOD) public boolean isInputMethodPickerShownForTest() { - super.isInputMethodPickerShownForTest_enforcePermission(); - synchronized (ImfLock.class) { return mMenuController.isisInputMethodPickerShownForTestLocked(); } @@ -4162,11 +4168,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub }); } - @EnforcePermission(Manifest.permission.INTERNAL_SYSTEM_WINDOW) + @IInputMethodManagerImpl.PermissionVerified(Manifest.permission.INTERNAL_SYSTEM_WINDOW) @Override public void removeImeSurface() { - super.removeImeSurface_enforcePermission(); - mHandler.obtainMessage(MSG_REMOVE_IME_SURFACE).sendToTarget(); } @@ -4275,11 +4279,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub * a stylus deviceId is not already registered on device. */ @BinderThread - @EnforcePermission(Manifest.permission.TEST_INPUT_METHOD) + @IInputMethodManagerImpl.PermissionVerified(Manifest.permission.TEST_INPUT_METHOD) @Override public void addVirtualStylusIdForTestSession(IInputMethodClient client) { - super.addVirtualStylusIdForTestSession_enforcePermission(); - int uid = Binder.getCallingUid(); synchronized (ImfLock.class) { if (!canInteractWithImeLocked(uid, client, "addVirtualStylusIdForTestSession", @@ -4302,12 +4304,10 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub * @param timeout to set in milliseconds. To reset to default, use a value <= zero. */ @BinderThread - @EnforcePermission(Manifest.permission.TEST_INPUT_METHOD) + @IInputMethodManagerImpl.PermissionVerified(Manifest.permission.TEST_INPUT_METHOD) @Override public void setStylusWindowIdleTimeoutForTest( IInputMethodClient client, @DurationMillisLong long timeout) { - super.setStylusWindowIdleTimeoutForTest_enforcePermission(); - int uid = Binder.getCallingUid(); synchronized (ImfLock.class) { if (!canInteractWithImeLocked(uid, client, "setStylusWindowIdleTimeoutForTest", @@ -4403,10 +4403,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } @BinderThread - @EnforcePermission(Manifest.permission.CONTROL_UI_TRACING) + @IInputMethodManagerImpl.PermissionVerified(Manifest.permission.CONTROL_UI_TRACING) @Override public void startImeTrace() { - super.startImeTrace_enforcePermission(); ImeTracing.getInstance().startTrace(null /* printwriter */); synchronized (ImfLock.class) { mClientController.forAllClients(c -> c.mClient.setImeTraceEnabled(true /* enabled */)); @@ -4414,11 +4413,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } @BinderThread - @EnforcePermission(Manifest.permission.CONTROL_UI_TRACING) + @IInputMethodManagerImpl.PermissionVerified(Manifest.permission.CONTROL_UI_TRACING) @Override public void stopImeTrace() { - super.stopImeTrace_enforcePermission(); - ImeTracing.getInstance().stopTrace(null /* printwriter */); synchronized (ImfLock.class) { mClientController.forAllClients(c -> c.mClient.setImeTraceEnabled(false /* enabled */)); @@ -4698,7 +4695,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // implemented so that auxiliary subtypes will be excluded when the soft // keyboard is invisible. synchronized (ImfLock.class) { - showAuxSubtypes = isInputShown(); + showAuxSubtypes = isInputShownLocked(); } break; case InputMethodManager.SHOW_IM_PICKER_MODE_INCLUDE_AUXILIARY_SUBTYPES: @@ -5845,7 +5842,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @BinderThread @Override - protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return; PriorityDump.dump(mPriorityDumper, fd, pw, args); @@ -5975,7 +5972,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub public void onShellCommand(@Nullable FileDescriptor in, @Nullable FileDescriptor out, @Nullable FileDescriptor err, @NonNull String[] args, @Nullable ShellCallback callback, - @NonNull ResultReceiver resultReceiver) throws RemoteException { + @NonNull ResultReceiver resultReceiver, @NonNull Binder self) { final int callingUid = Binder.getCallingUid(); // Reject any incoming calls from non-shell users, including ones from the system user. if (callingUid != Process.ROOT_UID && callingUid != Process.SHELL_UID) { @@ -5996,7 +5993,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub throw new SecurityException(errorMsg); } new ShellCommandImpl(this).exec( - this, in, out, err, args, callback, resultReceiver); + self, in, out, err, args, callback, resultReceiver); } private static final class ShellCommandImpl extends ShellCommand { diff --git a/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java b/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java index ffc2319b60af..1cd1ddce78fd 100644 --- a/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java +++ b/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java @@ -36,17 +36,16 @@ import static com.android.server.inputmethod.InputMethodManagerService.TAG; import android.Manifest; import android.annotation.BinderThread; -import android.annotation.EnforcePermission; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.UserIdInt; import android.os.Binder; import android.os.IBinder; -import android.os.RemoteException; import android.os.ResultReceiver; import android.os.ShellCallback; import android.util.Slog; +import android.view.MotionEvent; import android.view.WindowManager; import android.view.inputmethod.CursorAnchorInfo; import android.view.inputmethod.EditorInfo; @@ -56,6 +55,7 @@ import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodSubtype; import android.window.ImeOnBackInvokedDispatcher; +import com.android.internal.annotations.GuardedBy; import com.android.internal.inputmethod.DirectBootAwareness; import com.android.internal.inputmethod.IBooleanListener; import com.android.internal.inputmethod.IConnectionlessHandwritingCallback; @@ -76,22 +76,25 @@ import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; -import java.util.function.BooleanSupplier; /** * A proxy that processes all {@link IInputMethodManager} calls asynchronously. - * @hide */ -public class ZeroJankProxy extends IInputMethodManager.Stub { +final class ZeroJankProxy implements IInputMethodManagerImpl.Callback { - private final IInputMethodManager mInner; + interface Callback extends IInputMethodManagerImpl.Callback { + @GuardedBy("ImfLock.class") + ClientState getClientStateLocked(IInputMethodClient client); + @GuardedBy("ImfLock.class") + boolean isInputShownLocked(); + } + + private final Callback mInner; private final Executor mExecutor; - private final BooleanSupplier mIsInputShown; - ZeroJankProxy(Executor executor, IInputMethodManager inner, BooleanSupplier isInputShown) { + ZeroJankProxy(Executor executor, Callback inner) { mInner = inner; mExecutor = executor; - mIsInputShown = isInputShown; } private void offload(ThrowingRunnable r) { @@ -126,45 +129,43 @@ public class ZeroJankProxy extends IInputMethodManager.Stub { @Override public void addClient(IInputMethodClient client, IRemoteInputConnection inputConnection, - int selfReportedDisplayId) throws RemoteException { + int selfReportedDisplayId) { offload(() -> mInner.addClient(client, inputConnection, selfReportedDisplayId)); } @Override - public InputMethodInfo getCurrentInputMethodInfoAsUser(int userId) throws RemoteException { + public InputMethodInfo getCurrentInputMethodInfoAsUser(int userId) { return mInner.getCurrentInputMethodInfoAsUser(userId); } @Override public List<InputMethodInfo> getInputMethodList( - int userId, @DirectBootAwareness int directBootAwareness) throws RemoteException { + int userId, @DirectBootAwareness int directBootAwareness) { return mInner.getInputMethodList(userId, directBootAwareness); } @Override - public List<InputMethodInfo> getEnabledInputMethodList(int userId) throws RemoteException { + public List<InputMethodInfo> getEnabledInputMethodList(int userId) { return mInner.getEnabledInputMethodList(userId); } @Override public List<InputMethodSubtype> getEnabledInputMethodSubtypeList(String imiId, - boolean allowsImplicitlyEnabledSubtypes, int userId) - throws RemoteException { + boolean allowsImplicitlyEnabledSubtypes, int userId) { return mInner.getEnabledInputMethodSubtypeList(imiId, allowsImplicitlyEnabledSubtypes, userId); } @Override - public InputMethodSubtype getLastInputMethodSubtype(int userId) throws RemoteException { + public InputMethodSubtype getLastInputMethodSubtype(int userId) { return mInner.getLastInputMethodSubtype(userId); } @Override public boolean showSoftInput(IInputMethodClient client, IBinder windowToken, @Nullable ImeTracker.Token statsToken, @InputMethodManager.ShowFlags int flags, - int lastClickTooType, ResultReceiver resultReceiver, - @SoftInputShowHideReason int reason) - throws RemoteException { + @MotionEvent.ToolType int lastClickToolType, ResultReceiver resultReceiver, + @SoftInputShowHideReason int reason) { offload( () -> { if (!mInner.showSoftInput( @@ -172,7 +173,7 @@ public class ZeroJankProxy extends IInputMethodManager.Stub { windowToken, statsToken, flags, - lastClickTooType, + lastClickToolType, resultReceiver, reason)) { sendResultReceiverFailure(resultReceiver); @@ -184,8 +185,7 @@ public class ZeroJankProxy extends IInputMethodManager.Stub { @Override public boolean hideSoftInput(IInputMethodClient client, IBinder windowToken, @Nullable ImeTracker.Token statsToken, @InputMethodManager.HideFlags int flags, - ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) - throws RemoteException { + ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) { offload( () -> { if (!mInner.hideSoftInput( @@ -200,17 +200,19 @@ public class ZeroJankProxy extends IInputMethodManager.Stub { if (resultReceiver == null) { return; } - resultReceiver.send( - mIsInputShown.getAsBoolean() + final boolean isInputShown; + synchronized (ImfLock.class) { + isInputShown = mInner.isInputShownLocked(); + } + resultReceiver.send(isInputShown ? InputMethodManager.RESULT_UNCHANGED_SHOWN : InputMethodManager.RESULT_UNCHANGED_HIDDEN, null); } @Override - @EnforcePermission(Manifest.permission.TEST_INPUT_METHOD) - public void hideSoftInputFromServerForTest() throws RemoteException { - super.hideSoftInputFromServerForTest_enforcePermission(); + @IInputMethodManagerImpl.PermissionVerified(Manifest.permission.TEST_INPUT_METHOD) + public void hideSoftInputFromServerForTest() { mInner.hideSoftInputFromServerForTest(); } @@ -225,8 +227,7 @@ public class ZeroJankProxy extends IInputMethodManager.Stub { IRemoteInputConnection inputConnection, IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection, int unverifiedTargetSdkVersion, @UserIdInt int userId, - @NonNull ImeOnBackInvokedDispatcher imeDispatcher, int startInputSeq) - throws RemoteException { + @NonNull ImeOnBackInvokedDispatcher imeDispatcher, int startInputSeq) { offload(() -> { InputBindResult result = mInner.startInputOrWindowGainedFocus(startInputReason, client, windowToken, startInputFlags, softInputMode, windowFlags, @@ -249,99 +250,92 @@ public class ZeroJankProxy extends IInputMethodManager.Stub { IRemoteInputConnection inputConnection, IRemoteAccessibilityInputConnection remoteAccessibilityInputConnection, int unverifiedTargetSdkVersion, @UserIdInt int userId, - @NonNull ImeOnBackInvokedDispatcher imeDispatcher) - throws RemoteException { + @NonNull ImeOnBackInvokedDispatcher imeDispatcher) { // Should never be called when flag is enabled i.e. when this proxy is used. return null; } @Override public void showInputMethodPickerFromClient(IInputMethodClient client, - int auxiliarySubtypeMode) - throws RemoteException { + int auxiliarySubtypeMode) { offload(() -> mInner.showInputMethodPickerFromClient(client, auxiliarySubtypeMode)); } - @EnforcePermission(Manifest.permission.WRITE_SECURE_SETTINGS) + @IInputMethodManagerImpl.PermissionVerified(Manifest.permission.WRITE_SECURE_SETTINGS) @Override - public void showInputMethodPickerFromSystem(int auxiliarySubtypeMode, int displayId) - throws RemoteException { + public void showInputMethodPickerFromSystem(int auxiliarySubtypeMode, int displayId) { mInner.showInputMethodPickerFromSystem(auxiliarySubtypeMode, displayId); } - @EnforcePermission(Manifest.permission.TEST_INPUT_METHOD) + @IInputMethodManagerImpl.PermissionVerified(Manifest.permission.TEST_INPUT_METHOD) @Override - public boolean isInputMethodPickerShownForTest() throws RemoteException { - super.isInputMethodPickerShownForTest_enforcePermission(); + public boolean isInputMethodPickerShownForTest() { return mInner.isInputMethodPickerShownForTest(); } @Override - public InputMethodSubtype getCurrentInputMethodSubtype(int userId) throws RemoteException { + public InputMethodSubtype getCurrentInputMethodSubtype(int userId) { return mInner.getCurrentInputMethodSubtype(userId); } @Override public void setAdditionalInputMethodSubtypes(String imiId, InputMethodSubtype[] subtypes, - @UserIdInt int userId) throws RemoteException { + @UserIdInt int userId) { mInner.setAdditionalInputMethodSubtypes(imiId, subtypes, userId); } @Override public void setExplicitlyEnabledInputMethodSubtypes(String imeId, - @NonNull int[] subtypeHashCodes, @UserIdInt int userId) throws RemoteException { + @NonNull int[] subtypeHashCodes, @UserIdInt int userId) { mInner.setExplicitlyEnabledInputMethodSubtypes(imeId, subtypeHashCodes, userId); } @Override - public int getInputMethodWindowVisibleHeight(IInputMethodClient client) - throws RemoteException { + public int getInputMethodWindowVisibleHeight(IInputMethodClient client) { return mInner.getInputMethodWindowVisibleHeight(client); } @Override - public void reportPerceptibleAsync(IBinder windowToken, boolean perceptible) - throws RemoteException { + public void reportPerceptibleAsync(IBinder windowToken, boolean perceptible) { // Already async TODO(b/293640003): ordering issues? mInner.reportPerceptibleAsync(windowToken, perceptible); } - @EnforcePermission(Manifest.permission.INTERNAL_SYSTEM_WINDOW) + @IInputMethodManagerImpl.PermissionVerified(Manifest.permission.INTERNAL_SYSTEM_WINDOW) @Override - public void removeImeSurface() throws RemoteException { + public void removeImeSurface() { mInner.removeImeSurface(); } @Override - public void removeImeSurfaceFromWindowAsync(IBinder windowToken) throws RemoteException { + public void removeImeSurfaceFromWindowAsync(IBinder windowToken) { mInner.removeImeSurfaceFromWindowAsync(windowToken); } @Override - public void startProtoDump(byte[] bytes, int i, String s) throws RemoteException { + public void startProtoDump(byte[] bytes, int i, String s) { mInner.startProtoDump(bytes, i, s); } @Override - public boolean isImeTraceEnabled() throws RemoteException { + public boolean isImeTraceEnabled() { return mInner.isImeTraceEnabled(); } - @EnforcePermission(Manifest.permission.CONTROL_UI_TRACING) + @IInputMethodManagerImpl.PermissionVerified(Manifest.permission.CONTROL_UI_TRACING) @Override - public void startImeTrace() throws RemoteException { + public void startImeTrace() { mInner.startImeTrace(); } - @EnforcePermission(Manifest.permission.CONTROL_UI_TRACING) + @IInputMethodManagerImpl.PermissionVerified(Manifest.permission.CONTROL_UI_TRACING) @Override - public void stopImeTrace() throws RemoteException { + public void stopImeTrace() { mInner.stopImeTrace(); } @Override - public void startStylusHandwriting(IInputMethodClient client) - throws RemoteException { + public void startStylusHandwriting(IInputMethodClient client) { offload(() -> mInner.startStylusHandwriting(client)); } @@ -349,7 +343,7 @@ public class ZeroJankProxy extends IInputMethodManager.Stub { public void startConnectionlessStylusHandwriting(IInputMethodClient client, int userId, @Nullable CursorAnchorInfo cursorAnchorInfo, @Nullable String delegatePackageName, @Nullable String delegatorPackageName, - @NonNull IConnectionlessHandwritingCallback callback) throws RemoteException { + @NonNull IConnectionlessHandwritingCallback callback) { offload(() -> mInner.startConnectionlessStylusHandwriting( client, userId, cursorAnchorInfo, delegatePackageName, delegatorPackageName, callback)); @@ -363,14 +357,11 @@ public class ZeroJankProxy extends IInputMethodManager.Stub { @NonNull String delegatorPackageName, @InputMethodManager.HandwritingDelegateFlags int flags) { try { - return CompletableFuture.supplyAsync(() -> { - try { - return mInner.acceptStylusHandwritingDelegation( - client, userId, delegatePackageName, delegatorPackageName, flags); - } catch (RemoteException e) { - throw new RuntimeException(e); - } - }, this::offload).get(); + return CompletableFuture.supplyAsync(() -> + mInner.acceptStylusHandwritingDelegation( + client, userId, delegatePackageName, delegatorPackageName, + flags), + this::offload).get(); } catch (InterruptedException e) { throw new RuntimeException(e); } catch (ExecutionException e) { @@ -384,8 +375,7 @@ public class ZeroJankProxy extends IInputMethodManager.Stub { @UserIdInt int userId, @NonNull String delegatePackageName, @NonNull String delegatorPackageName, - @InputMethodManager.HandwritingDelegateFlags int flags, IBooleanListener callback) - throws RemoteException { + @InputMethodManager.HandwritingDelegateFlags int flags, IBooleanListener callback) { offload(() -> mInner.acceptStylusHandwritingDelegationAsync( client, userId, delegatePackageName, delegatorPackageName, flags, callback)); } @@ -401,52 +391,45 @@ public class ZeroJankProxy extends IInputMethodManager.Stub { } @Override - public boolean isStylusHandwritingAvailableAsUser(int userId, boolean connectionless) - throws RemoteException { + public boolean isStylusHandwritingAvailableAsUser(int userId, boolean connectionless) { return mInner.isStylusHandwritingAvailableAsUser(userId, connectionless); } - @EnforcePermission("android.permission.TEST_INPUT_METHOD") + @IInputMethodManagerImpl.PermissionVerified("android.permission.TEST_INPUT_METHOD") @Override - public void addVirtualStylusIdForTestSession(IInputMethodClient client) - throws RemoteException { + public void addVirtualStylusIdForTestSession(IInputMethodClient client) { mInner.addVirtualStylusIdForTestSession(client); } - @EnforcePermission("android.permission.TEST_INPUT_METHOD") + @IInputMethodManagerImpl.PermissionVerified("android.permission.TEST_INPUT_METHOD") @Override - public void setStylusWindowIdleTimeoutForTest(IInputMethodClient client, long timeout) - throws RemoteException { + public void setStylusWindowIdleTimeoutForTest(IInputMethodClient client, long timeout) { mInner.setStylusWindowIdleTimeoutForTest(client, timeout); } @Override - public IImeTracker getImeTrackerService() throws RemoteException { + public IImeTracker getImeTrackerService() { return mInner.getImeTrackerService(); } @BinderThread @Override public void onShellCommand(@Nullable FileDescriptor in, @Nullable FileDescriptor out, - @Nullable FileDescriptor err, - @NonNull String[] args, @Nullable ShellCallback callback, - @NonNull ResultReceiver resultReceiver) throws RemoteException { - ((InputMethodManagerService) mInner).onShellCommand( - in, out, err, args, callback, resultReceiver); + @Nullable FileDescriptor err, @NonNull String[] args, @Nullable ShellCallback callback, + @NonNull ResultReceiver resultReceiver, @NonNull Binder self) { + mInner.onShellCommand(in, out, err, args, callback, resultReceiver, self); } @Override - protected void dump(@NonNull FileDescriptor fd, - @NonNull PrintWriter fout, + public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter fout, @Nullable String[] args) { - ((InputMethodManagerService) mInner).dump(fd, fout, args); + mInner.dump(fd, fout, args); } private void sendOnStartInputResult( IInputMethodClient client, InputBindResult res, int startInputSeq) { synchronized (ImfLock.class) { - InputMethodManagerService service = (InputMethodManagerService) mInner; - final ClientState cs = service.getClientState(client); + final ClientState cs = mInner.getClientStateLocked(client); if (cs != null && cs.mClient != null) { cs.mClient.onStartInputResult(res, startInputSeq); } else { diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java index 19562ef79fbb..dbdb155eb2e3 100644 --- a/services/core/java/com/android/server/locksettings/LockSettingsService.java +++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java @@ -950,13 +950,18 @@ public class LockSettingsService extends ILockSettings.Stub { && android.multiuser.Flags.enablePrivateSpaceFeatures() && android.multiuser.Flags.enableBiometricsToUnlockPrivateSpace()) { mHandler.post(() -> { - UserProperties userProperties = - mUserManager.getUserProperties(UserHandle.of(userId)); - if (userProperties != null - && userProperties.getAllowStoppingUserWithDelayedLocking()) { - int strongAuthRequired = LockPatternUtils.StrongAuthTracker - .getDefaultFlags(mContext); - requireStrongAuth(strongAuthRequired, userId); + try { + UserProperties userProperties = + mUserManager.getUserProperties(UserHandle.of(userId)); + if (userProperties != null && userProperties + .getAllowStoppingUserWithDelayedLocking()) { + int strongAuthRequired = LockPatternUtils.StrongAuthTracker + .getDefaultFlags(mContext); + requireStrongAuth(strongAuthRequired, userId); + } + } catch (IllegalArgumentException e) { + Slogf.d(TAG, "User %d does not exist or has been removed", + userId); } }); } diff --git a/services/core/java/com/android/server/media/MediaRouterService.java b/services/core/java/com/android/server/media/MediaRouterService.java index 1a129cb080a8..064443ce7d10 100644 --- a/services/core/java/com/android/server/media/MediaRouterService.java +++ b/services/core/java/com/android/server/media/MediaRouterService.java @@ -267,9 +267,11 @@ public final class MediaRouterService extends IMediaRouterService.Stub // Binder call @Override public boolean showMediaOutputSwitcher(String packageName) { - if (!validatePackageName(Binder.getCallingUid(), packageName)) { + int uid = Binder.getCallingUid(); + if (!validatePackageName(uid, packageName)) { throw new SecurityException("packageName must match the calling identity"); } + UserHandle userHandle = UserHandle.getUserHandleForUid(uid); final long token = Binder.clearCallingIdentity(); try { if (mContext.getSystemService(ActivityManager.class).getPackageImportance(packageName) @@ -280,7 +282,7 @@ public final class MediaRouterService extends IMediaRouterService.Stub synchronized (mLock) { StatusBarManagerInternal statusBar = LocalServices.getService(StatusBarManagerInternal.class); - statusBar.showMediaOutputSwitcher(packageName); + statusBar.showMediaOutputSwitcher(packageName, userHandle); } } finally { Binder.restoreCallingIdentity(token); diff --git a/services/core/java/com/android/server/media/MediaSession2Record.java b/services/core/java/com/android/server/media/MediaSession2Record.java index 0cd7654f70ea..dfb2b0a750e3 100644 --- a/services/core/java/com/android/server/media/MediaSession2Record.java +++ b/services/core/java/com/android/server/media/MediaSession2Record.java @@ -157,6 +157,11 @@ public class MediaSession2Record extends MediaSessionRecordImpl { } @Override + public void expireTempEngaged() { + // NA as MediaSession2 doesn't support UserEngagementStates for FGS. + } + + @Override public boolean sendMediaButton(String packageName, int pid, int uid, boolean asSystemService, KeyEvent ke, int sequenceId, ResultReceiver cb) { // TODO(jaewan): Implement. diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java index 93f68a831705..194ab04817ec 100644 --- a/services/core/java/com/android/server/media/MediaSessionRecord.java +++ b/services/core/java/com/android/server/media/MediaSessionRecord.java @@ -24,6 +24,7 @@ import static android.media.session.MediaController.PlaybackInfo.PLAYBACK_TYPE_L import static android.media.session.MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE; import android.Manifest; +import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; @@ -85,6 +86,8 @@ import com.android.server.LocalServices; import com.android.server.uri.UriGrantsManagerInternal; import java.io.PrintWriter; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -225,6 +228,49 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde private int mPolicies; + private @UserEngagementState int mUserEngagementState = USER_DISENGAGED; + + @IntDef({USER_PERMANENTLY_ENGAGED, USER_TEMPORARY_ENGAGED, USER_DISENGAGED}) + @Retention(RetentionPolicy.SOURCE) + private @interface UserEngagementState {} + + /** + * Indicates that the session is active and in one of the user engaged states. + * + * @see #updateUserEngagedStateIfNeededLocked(boolean) () + */ + private static final int USER_PERMANENTLY_ENGAGED = 0; + + /** + * Indicates that the session is active and in {@link PlaybackState#STATE_PAUSED} state. + * + * @see #updateUserEngagedStateIfNeededLocked(boolean) () + */ + private static final int USER_TEMPORARY_ENGAGED = 1; + + /** + * Indicates that the session is either not active or in one of the user disengaged states + * + * @see #updateUserEngagedStateIfNeededLocked(boolean) () + */ + private static final int USER_DISENGAGED = 2; + + /** + * Indicates the duration of the temporary engaged states. + * + * <p>Some {@link MediaSession} states like {@link PlaybackState#STATE_PAUSED} are temporarily + * engaged, meaning the corresponding session is only considered in an engaged state for the + * duration of this timeout, and only if coming from an engaged state. + * + * <p>For example, if a session is transitioning from a user-engaged state {@link + * PlaybackState#STATE_PLAYING} to a temporary user-engaged state {@link + * PlaybackState#STATE_PAUSED}, then the session will be considered in a user-engaged state for + * the duration of this timeout, starting at the transition instant. However, a temporary + * user-engaged state is not considered user-engaged when transitioning from a non-user engaged + * state {@link PlaybackState#STATE_STOPPED}. + */ + private static final int TEMP_USER_ENGAGED_TIMEOUT = 600000; + public MediaSessionRecord( int ownerPid, int ownerUid, @@ -548,6 +594,7 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde mSessionCb.mCb.asBinder().unlinkToDeath(this, 0); mDestroyed = true; mPlaybackState = null; + updateUserEngagedStateIfNeededLocked(/* isTimeoutExpired= */ true); mHandler.post(MessageHandler.MSG_DESTROYED); } } @@ -559,6 +606,12 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde } } + @Override + public void expireTempEngaged() { + mHandler.removeCallbacks(mHandleTempEngagedSessionTimeout); + updateUserEngagedStateIfNeededLocked(/* isTimeoutExpired= */ true); + } + /** * Sends media button. * @@ -1129,6 +1182,11 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde } }; + private final Runnable mHandleTempEngagedSessionTimeout = + () -> { + updateUserEngagedStateIfNeededLocked(/* isTimeoutExpired= */ true); + }; + @RequiresPermission(Manifest.permission.INTERACT_ACROSS_USERS) private static boolean componentNameExists( @NonNull ComponentName componentName, @NonNull Context context, int userId) { @@ -1145,6 +1203,40 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde return !resolveInfos.isEmpty(); } + private void updateUserEngagedStateIfNeededLocked(boolean isTimeoutExpired) { + int oldUserEngagedState = mUserEngagementState; + int newUserEngagedState; + if (!isActive() || mPlaybackState == null) { + newUserEngagedState = USER_DISENGAGED; + } else if (isActive() && mPlaybackState.isActive()) { + newUserEngagedState = USER_PERMANENTLY_ENGAGED; + } else if (mPlaybackState.getState() == PlaybackState.STATE_PAUSED) { + newUserEngagedState = + oldUserEngagedState == USER_PERMANENTLY_ENGAGED || !isTimeoutExpired + ? USER_TEMPORARY_ENGAGED + : USER_DISENGAGED; + } else { + newUserEngagedState = USER_DISENGAGED; + } + if (oldUserEngagedState == newUserEngagedState) { + return; + } + + if (newUserEngagedState == USER_TEMPORARY_ENGAGED) { + mHandler.postDelayed(mHandleTempEngagedSessionTimeout, TEMP_USER_ENGAGED_TIMEOUT); + } else if (oldUserEngagedState == USER_TEMPORARY_ENGAGED) { + mHandler.removeCallbacks(mHandleTempEngagedSessionTimeout); + } + + boolean wasUserEngaged = oldUserEngagedState != USER_DISENGAGED; + boolean isNowUserEngaged = newUserEngagedState != USER_DISENGAGED; + mUserEngagementState = newUserEngagedState; + if (wasUserEngaged != isNowUserEngaged) { + mService.onSessionUserEngagementStateChange( + /* mediaSessionRecord= */ this, /* isUserEngaged= */ isNowUserEngaged); + } + } + private final class SessionStub extends ISession.Stub { @Override public void destroySession() throws RemoteException { @@ -1182,8 +1274,10 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde .logFgsApiEnd(ActivityManager.FOREGROUND_SERVICE_API_TYPE_MEDIA_PLAYBACK, callingUid, callingPid); } - - mIsActive = active; + synchronized (mLock) { + mIsActive = active; + updateUserEngagedStateIfNeededLocked(/* isTimeoutExpired= */ false); + } long token = Binder.clearCallingIdentity(); try { mService.onSessionActiveStateChanged(MediaSessionRecord.this, mPlaybackState); @@ -1341,6 +1435,7 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde && TRANSITION_PRIORITY_STATES.contains(newState)); synchronized (mLock) { mPlaybackState = state; + updateUserEngagedStateIfNeededLocked(/* isTimeoutExpired= */ false); } final long token = Binder.clearCallingIdentity(); try { diff --git a/services/core/java/com/android/server/media/MediaSessionRecordImpl.java b/services/core/java/com/android/server/media/MediaSessionRecordImpl.java index 09991995099e..b57b14835987 100644 --- a/services/core/java/com/android/server/media/MediaSessionRecordImpl.java +++ b/services/core/java/com/android/server/media/MediaSessionRecordImpl.java @@ -196,6 +196,12 @@ public abstract class MediaSessionRecordImpl { */ public abstract boolean isClosed(); + /** + * Note: This method is only used for testing purposes If the session is temporary engaged, the + * timeout will expire and it will become disengaged. + */ + public abstract void expireTempEngaged(); + @Override public final boolean equals(Object o) { if (this == o) return true; diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java index 53c32cf31d21..74adf5e0d52c 100644 --- a/services/core/java/com/android/server/media/MediaSessionService.java +++ b/services/core/java/com/android/server/media/MediaSessionService.java @@ -367,11 +367,13 @@ public class MediaSessionService extends SystemService implements Monitor { } boolean isUserEngaged = isUserEngaged(record, playbackState); - Log.d(TAG, "onSessionActiveStateChanged: " - + "record=" + record - + "playbackState=" + playbackState - + "allowRunningInForeground=" + isUserEngaged); - setForegroundServiceAllowance(record, /* allowRunningInForeground= */ isUserEngaged); + Log.d( + TAG, + "onSessionActiveStateChanged:" + + " record=" + + record + + " playbackState=" + + playbackState); reportMediaInteractionEvent(record, isUserEngaged); mHandler.postSessionsChanged(record); } @@ -479,11 +481,13 @@ public class MediaSessionService extends SystemService implements Monitor { } user.mPriorityStack.onPlaybackStateChanged(record, shouldUpdatePriority); boolean isUserEngaged = isUserEngaged(record, playbackState); - Log.d(TAG, "onSessionPlaybackStateChanged: " - + "record=" + record - + "playbackState=" + playbackState - + "allowRunningInForeground=" + isUserEngaged); - setForegroundServiceAllowance(record, /* allowRunningInForeground= */ isUserEngaged); + Log.d( + TAG, + "onSessionPlaybackStateChanged:" + + " record=" + + record + + " playbackState=" + + playbackState); reportMediaInteractionEvent(record, isUserEngaged); } } @@ -650,68 +654,112 @@ public class MediaSessionService extends SystemService implements Monitor { session.close(); Log.d(TAG, "destroySessionLocked: record=" + session); - setForegroundServiceAllowance(session, /* allowRunningInForeground= */ false); + reportMediaInteractionEvent(session, /* userEngaged= */ false); mHandler.postSessionsChanged(session); } - private void setForegroundServiceAllowance( - MediaSessionRecordImpl record, boolean allowRunningInForeground) { - if (!Flags.enableNotifyingActivityManagerWithMediaSessionStatusChange()) { - return; - } - ForegroundServiceDelegationOptions foregroundServiceDelegationOptions = - record.getForegroundServiceDelegationOptions(); - if (foregroundServiceDelegationOptions == null) { - return; - } - if (allowRunningInForeground) { - onUserSessionEngaged(record); + void onSessionUserEngagementStateChange( + MediaSessionRecordImpl mediaSessionRecord, boolean isUserEngaged) { + if (isUserEngaged) { + addUserEngagedSession(mediaSessionRecord); + startFgsIfSessionIsLinkedToNotification(mediaSessionRecord); } else { - onUserDisengaged(record); + removeUserEngagedSession(mediaSessionRecord); + stopFgsIfNoSessionIsLinkedToNotification(mediaSessionRecord); } } - private void onUserSessionEngaged(MediaSessionRecordImpl mediaSessionRecord) { + private void addUserEngagedSession(MediaSessionRecordImpl mediaSessionRecord) { synchronized (mLock) { int uid = mediaSessionRecord.getUid(); mUserEngagedSessionsForFgs.putIfAbsent(uid, new HashSet<>()); mUserEngagedSessionsForFgs.get(uid).add(mediaSessionRecord); + } + } + + private void removeUserEngagedSession(MediaSessionRecordImpl mediaSessionRecord) { + synchronized (mLock) { + int uid = mediaSessionRecord.getUid(); + Set<MediaSessionRecordImpl> mUidUserEngagedSessionsForFgs = + mUserEngagedSessionsForFgs.get(uid); + if (mUidUserEngagedSessionsForFgs == null) { + return; + } + + mUidUserEngagedSessionsForFgs.remove(mediaSessionRecord); + if (mUidUserEngagedSessionsForFgs.isEmpty()) { + mUserEngagedSessionsForFgs.remove(uid); + } + } + } + + private void startFgsIfSessionIsLinkedToNotification( + MediaSessionRecordImpl mediaSessionRecord) { + Log.d(TAG, "startFgsIfSessionIsLinkedToNotification: record=" + mediaSessionRecord); + if (!Flags.enableNotifyingActivityManagerWithMediaSessionStatusChange()) { + return; + } + synchronized (mLock) { + int uid = mediaSessionRecord.getUid(); for (Notification mediaNotification : mMediaNotifications.getOrDefault(uid, Set.of())) { if (mediaSessionRecord.isLinkedToNotification(mediaNotification)) { - mActivityManagerInternal.startForegroundServiceDelegate( - mediaSessionRecord.getForegroundServiceDelegationOptions(), - /* connection= */ null); + startFgsDelegate(mediaSessionRecord.getForegroundServiceDelegationOptions()); return; } } } } - private void onUserDisengaged(MediaSessionRecordImpl mediaSessionRecord) { + private void startFgsDelegate( + ForegroundServiceDelegationOptions foregroundServiceDelegationOptions) { + final long token = Binder.clearCallingIdentity(); + try { + mActivityManagerInternal.startForegroundServiceDelegate( + foregroundServiceDelegationOptions, /* connection= */ null); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + private void stopFgsIfNoSessionIsLinkedToNotification( + MediaSessionRecordImpl mediaSessionRecord) { + Log.d(TAG, "stopFgsIfNoSessionIsLinkedToNotification: record=" + mediaSessionRecord); + if (!Flags.enableNotifyingActivityManagerWithMediaSessionStatusChange()) { + return; + } synchronized (mLock) { int uid = mediaSessionRecord.getUid(); - if (mUserEngagedSessionsForFgs.containsKey(uid)) { - mUserEngagedSessionsForFgs.get(uid).remove(mediaSessionRecord); - if (mUserEngagedSessionsForFgs.get(uid).isEmpty()) { - mUserEngagedSessionsForFgs.remove(uid); - } + ForegroundServiceDelegationOptions foregroundServiceDelegationOptions = + mediaSessionRecord.getForegroundServiceDelegationOptions(); + if (foregroundServiceDelegationOptions == null) { + return; } - boolean shouldStopFgs = true; - for (MediaSessionRecordImpl sessionRecord : + for (MediaSessionRecordImpl record : mUserEngagedSessionsForFgs.getOrDefault(uid, Set.of())) { - for (Notification mediaNotification : mMediaNotifications.getOrDefault(uid, - Set.of())) { - if (sessionRecord.isLinkedToNotification(mediaNotification)) { - shouldStopFgs = false; + for (Notification mediaNotification : + mMediaNotifications.getOrDefault(uid, Set.of())) { + if (record.isLinkedToNotification(mediaNotification)) { + // A user engaged session linked with a media notification is found. + // We shouldn't call stop FGS in this case. + return; } } } - if (shouldStopFgs) { - mActivityManagerInternal.stopForegroundServiceDelegate( - mediaSessionRecord.getForegroundServiceDelegationOptions()); - } + + stopFgsDelegate(foregroundServiceDelegationOptions); + } + } + + private void stopFgsDelegate( + ForegroundServiceDelegationOptions foregroundServiceDelegationOptions) { + final long token = Binder.clearCallingIdentity(); + try { + mActivityManagerInternal.stopForegroundServiceDelegate( + foregroundServiceDelegationOptions); + } finally { + Binder.restoreCallingIdentity(token); } } @@ -2502,7 +2550,6 @@ public class MediaSessionService extends SystemService implements Monitor { } MediaSessionRecord session = null; MediaButtonReceiverHolder mediaButtonReceiverHolder = null; - if (mCustomMediaKeyDispatcher != null) { MediaSession.Token token = mCustomMediaKeyDispatcher.getMediaSession( keyEvent, uid, asSystemService); @@ -2630,6 +2677,18 @@ public class MediaSessionService extends SystemService implements Monitor { && streamType <= AudioManager.STREAM_NOTIFICATION; } + @Override + public void expireTempEngagedSessions() { + synchronized (mLock) { + for (Set<MediaSessionRecordImpl> uidSessions : + mUserEngagedSessionsForFgs.values()) { + for (MediaSessionRecordImpl sessionRecord : uidSessions) { + sessionRecord.expireTempEngaged(); + } + } + } + } + private class MediaKeyListenerResultReceiver extends ResultReceiver implements Runnable { private final String mPackageName; private final int mPid; @@ -3127,7 +3186,6 @@ public class MediaSessionService extends SystemService implements Monitor { super.onNotificationPosted(sbn); Notification postedNotification = sbn.getNotification(); int uid = sbn.getUid(); - if (!postedNotification.isMediaNotification()) { return; } @@ -3138,11 +3196,9 @@ public class MediaSessionService extends SystemService implements Monitor { mUserEngagedSessionsForFgs.getOrDefault(uid, Set.of())) { ForegroundServiceDelegationOptions foregroundServiceDelegationOptions = mediaSessionRecord.getForegroundServiceDelegationOptions(); - if (mediaSessionRecord.isLinkedToNotification(postedNotification) - && foregroundServiceDelegationOptions != null) { - mActivityManagerInternal.startForegroundServiceDelegate( - foregroundServiceDelegationOptions, - /* connection= */ null); + if (foregroundServiceDelegationOptions != null + && mediaSessionRecord.isLinkedToNotification(postedNotification)) { + startFgsDelegate(foregroundServiceDelegationOptions); return; } } @@ -3173,21 +3229,7 @@ public class MediaSessionService extends SystemService implements Monitor { return; } - boolean shouldStopFgs = true; - for (MediaSessionRecordImpl mediaSessionRecord : - mUserEngagedSessionsForFgs.getOrDefault(uid, Set.of())) { - for (Notification mediaNotification : - mMediaNotifications.getOrDefault(uid, Set.of())) { - if (mediaSessionRecord.isLinkedToNotification(mediaNotification)) { - shouldStopFgs = false; - } - } - } - if (shouldStopFgs - && notificationRecord.getForegroundServiceDelegationOptions() != null) { - mActivityManagerInternal.stopForegroundServiceDelegate( - notificationRecord.getForegroundServiceDelegationOptions()); - } + stopFgsIfNoSessionIsLinkedToNotification(notificationRecord); } } diff --git a/services/core/java/com/android/server/media/MediaShellCommand.java b/services/core/java/com/android/server/media/MediaShellCommand.java index a56380827f2c..a20de3198d2c 100644 --- a/services/core/java/com/android/server/media/MediaShellCommand.java +++ b/services/core/java/com/android/server/media/MediaShellCommand.java @@ -92,6 +92,8 @@ public class MediaShellCommand extends ShellCommand { runMonitor(); } else if (cmd.equals("volume")) { runVolume(); + } else if (cmd.equals("expire-temp-engaged-sessions")) { + expireTempEngagedSessions(); } else { showError("Error: unknown command '" + cmd + "'"); return -1; @@ -367,4 +369,8 @@ public class MediaShellCommand extends ShellCommand { private void runVolume() throws Exception { VolumeCtrl.run(this); } + + private void expireTempEngagedSessions() throws Exception { + mSessionService.expireTempEngagedSessions(); + } } diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index ebc1a2a45579..b14242ef8e08 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -22,6 +22,7 @@ import static android.Manifest.permission.STATUS_BAR_SERVICE; import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND; import static android.app.ActivityManagerInternal.ServiceNotificationPolicy.NOT_FOREGROUND_SERVICE; import static android.app.AppOpsManager.MODE_ALLOWED; +import static android.app.AppOpsManager.MODE_DEFAULT; import static android.app.Flags.FLAG_LIFETIME_EXTENSION_REFACTOR; import static android.app.Flags.lifetimeExtensionRefactor; import static android.app.Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION; @@ -701,7 +702,7 @@ public class NotificationManagerService extends SystemService { private static final int MY_UID = Process.myUid(); private static final int MY_PID = Process.myPid(); - private static final IBinder ALLOWLIST_TOKEN = new Binder(); + static final IBinder ALLOWLIST_TOKEN = new Binder(); protected RankingHandler mRankingHandler; private long mLastOverRateLogTime; private float mMaxPackageEnqueueRate = DEFAULT_MAX_NOTIFICATION_ENQUEUE_RATE; @@ -3598,8 +3599,8 @@ public class NotificationManagerService extends SystemService { } } - @android.annotation.EnforcePermission(android.Manifest.permission.MANAGE_TOAST_RATE_LIMITING) @Override + @EnforcePermission(android.Manifest.permission.MANAGE_TOAST_RATE_LIMITING) public void setToastRateLimitingEnabled(boolean enable) { super.setToastRateLimitingEnabled_enforcePermission(); @@ -4524,7 +4525,6 @@ public class NotificationManagerService extends SystemService { return getActiveNotificationsWithAttribution(callingPkg, null); } - @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_NOTIFICATIONS) /** * System-only API for getting a list of current (i.e. not cleared) notifications. * @@ -4532,6 +4532,7 @@ public class NotificationManagerService extends SystemService { * @returns A list of all the notifications, in natural order. */ @Override + @EnforcePermission(android.Manifest.permission.ACCESS_NOTIFICATIONS) public StatusBarNotification[] getActiveNotificationsWithAttribution(String callingPkg, String callingAttributionTag) { // enforce() will ensure the calling uid has the correct permission @@ -4549,9 +4550,9 @@ public class NotificationManagerService extends SystemService { }); // noteOp will check to make sure the callingPkg matches the uid - if (mAppOps.noteOpNoThrow(AppOpsManager.OP_ACCESS_NOTIFICATIONS, uid, callingPkg, - callingAttributionTag, null) - == MODE_ALLOWED) { + int mode = mAppOps.noteOpNoThrow(AppOpsManager.OP_ACCESS_NOTIFICATIONS, uid, callingPkg, + callingAttributionTag, null); + if (mode == MODE_ALLOWED || mode == MODE_DEFAULT) { synchronized (mNotificationLock) { final int N = mNotificationList.size(); for (int i = 0; i < N; i++) { @@ -4627,7 +4628,7 @@ public class NotificationManagerService extends SystemService { // Remove background token before returning notification to untrusted app, this // ensures the app isn't able to perform background operations that are // associated with notification interactions. - notification.clearAllowlistToken(); + notification.overrideAllowlistToken(null); return new StatusBarNotification( sbn.getPackageName(), sbn.getOpPkg(), @@ -4651,12 +4652,12 @@ public class NotificationManagerService extends SystemService { includeSnoozed); } - @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_NOTIFICATIONS) /** * System-only API for getting a list of recent (cleared, no longer shown) notifications. */ @Override @RequiresPermission(android.Manifest.permission.ACCESS_NOTIFICATIONS) + @EnforcePermission(android.Manifest.permission.ACCESS_NOTIFICATIONS) public StatusBarNotification[] getHistoricalNotificationsWithAttribution(String callingPkg, String callingAttributionTag, int count, boolean includeSnoozed) { // enforce() will ensure the calling uid has the correct permission @@ -4666,9 +4667,9 @@ public class NotificationManagerService extends SystemService { int uid = Binder.getCallingUid(); // noteOp will check to make sure the callingPkg matches the uid - if (mAppOps.noteOpNoThrow(AppOpsManager.OP_ACCESS_NOTIFICATIONS, uid, callingPkg, - callingAttributionTag, null) - == MODE_ALLOWED) { + int mode = mAppOps.noteOpNoThrow(AppOpsManager.OP_ACCESS_NOTIFICATIONS, uid, callingPkg, + callingAttributionTag, null); + if (mode == MODE_ALLOWED || mode == MODE_DEFAULT) { synchronized (mArchive) { tmp = mArchive.getArray(mUm, count, includeSnoozed); } @@ -4676,7 +4677,6 @@ public class NotificationManagerService extends SystemService { return tmp; } - @android.annotation.EnforcePermission(android.Manifest.permission.ACCESS_NOTIFICATIONS) /** * System-only API for getting a list of historical notifications. May contain multiple days * of notifications. @@ -4684,6 +4684,7 @@ public class NotificationManagerService extends SystemService { @Override @WorkerThread @RequiresPermission(android.Manifest.permission.ACCESS_NOTIFICATIONS) + @EnforcePermission(android.Manifest.permission.ACCESS_NOTIFICATIONS) public NotificationHistory getNotificationHistory(String callingPkg, String callingAttributionTag) { // enforce() will ensure the calling uid has the correct permission @@ -4691,9 +4692,9 @@ public class NotificationManagerService extends SystemService { int uid = Binder.getCallingUid(); // noteOp will check to make sure the callingPkg matches the uid - if (mAppOps.noteOpNoThrow(AppOpsManager.OP_ACCESS_NOTIFICATIONS, uid, callingPkg, - callingAttributionTag, null) - == MODE_ALLOWED) { + int mode = mAppOps.noteOpNoThrow(AppOpsManager.OP_ACCESS_NOTIFICATIONS, uid, callingPkg, + callingAttributionTag, null); + if (mode == MODE_ALLOWED || mode == MODE_DEFAULT) { IntArray currentUserIds = mUserProfiles.getCurrentProfileIds(); Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "notifHistoryReadHistory"); try { @@ -7214,6 +7215,17 @@ public class NotificationManagerService extends SystemService { + " trying to post for invalid pkg " + pkg + " in user " + incomingUserId); } + if (android.app.Flags.secureAllowlistToken()) { + IBinder allowlistToken = notification.getAllowlistToken(); + if (allowlistToken != null && allowlistToken != ALLOWLIST_TOKEN) { + throw new SecurityException( + "Unexpected allowlist token received from " + callingUid); + } + // allowlistToken is populated by unparceling, so it can be null if the notification was + // posted from inside system_server. Ensure it's the expected value. + notification.overrideAllowlistToken(ALLOWLIST_TOKEN); + } + checkRestrictedCategories(notification); // Notifications passed to setForegroundService() have FLAG_FOREGROUND_SERVICE, diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java index b5d49b36affe..53863aa83ab4 100644 --- a/services/core/java/com/android/server/power/PowerManagerService.java +++ b/services/core/java/com/android/server/power/PowerManagerService.java @@ -622,6 +622,7 @@ public final class PowerManagerService extends SystemService // Value we store for tracking face down behavior. @VisibleForTesting boolean mIsFaceDown = false; + private boolean mUseFaceDownDetector = true; private long mLastFlipTime = 0L; // The screen brightness setting override from the window manager @@ -3253,7 +3254,7 @@ public final class PowerManagerService extends SystemService mScreenTimeoutOverridePolicy.getScreenTimeoutOverrideLocked( mWakeLockSummary, screenOffTimeout); } - if (mIsFaceDown) { + if (mIsFaceDown && mUseFaceDownDetector) { shortestScreenOffTimeout = Math.min(screenDimDuration, shortestScreenOffTimeout); } @@ -4701,6 +4702,7 @@ public final class PowerManagerService extends SystemService pw.println(" mHoldingDisplaySuspendBlocker=" + mHoldingDisplaySuspendBlocker); pw.println(" mLastFlipTime=" + mLastFlipTime); pw.println(" mIsFaceDown=" + mIsFaceDown); + pw.println(" mUseFaceDownDetector=" + mUseFaceDownDetector); pw.println(); pw.println("Settings and Configuration:"); @@ -6921,6 +6923,16 @@ public final class PowerManagerService extends SystemService Binder.restoreCallingIdentity(ident); } } + + public void setUseFaceDownDetector(boolean enable) { + final long ident = Binder.clearCallingIdentity(); + try { + mUseFaceDownDetector = enable; + } finally { + Binder.restoreCallingIdentity(ident); + } + } + } @VisibleForTesting diff --git a/services/core/java/com/android/server/power/PowerManagerShellCommand.java b/services/core/java/com/android/server/power/PowerManagerShellCommand.java index 9439b762fde0..20184e9fd1a7 100644 --- a/services/core/java/com/android/server/power/PowerManagerShellCommand.java +++ b/services/core/java/com/android/server/power/PowerManagerShellCommand.java @@ -63,6 +63,8 @@ class PowerManagerShellCommand extends ShellCommand { return runListAmbientDisplaySuppressionTokens(); case "set-prox": return runSetProx(); + case "set-face-down-detector": + return runSetFaceDownDetector(); default: return handleDefaultCommands(cmd); } @@ -178,6 +180,20 @@ class PowerManagerShellCommand extends ShellCommand { return 0; } + /** + * To be used for testing - allowing us to disable the usage of face down detector. + */ + private int runSetFaceDownDetector() { + try { + mService.setUseFaceDownDetector(Boolean.parseBoolean(getNextArgRequired())); + } catch (Exception e) { + PrintWriter pw = getOutPrintWriter(); + pw.println("Error: " + e); + return -1; + } + return 0; + } + @Override public void onHelp() { final PrintWriter pw = getOutPrintWriter(); @@ -203,6 +219,8 @@ class PowerManagerShellCommand extends ShellCommand { pw.println(" Acquires the proximity sensor wakelock. Wakelock is associated with"); pw.println(" a specific display if specified. 'list' lists wakelocks previously"); pw.println(" created by set-prox including their held status."); + pw.println(" set-face-down-detector [true|false]"); + pw.println(" sets whether we use face down detector timeouts or not"); pw.println(); Intent.printIntentArgsHelp(pw , ""); diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java index 2ff38616fce5..e4f60ec2cdb8 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java @@ -22,6 +22,7 @@ import android.content.ComponentName; import android.hardware.fingerprint.IUdfpsRefreshRateRequestCallback; import android.os.Bundle; import android.os.IBinder; +import android.os.UserHandle; import android.view.WindowInsets.Type.InsetsType; import android.view.WindowInsetsController.Appearance; import android.view.WindowInsetsController.Behavior; @@ -248,10 +249,10 @@ public interface StatusBarManagerInternal { /** * Shows the media output switcher dialog. * - * @param packageName of the session for which the output switcher is shown. + * @param targetPackageName of the session for which the output switcher is shown. * @see com.android.internal.statusbar.IStatusBar#showMediaOutputSwitcher */ - void showMediaOutputSwitcher(String packageName); + void showMediaOutputSwitcher(String targetPackageName, UserHandle targetUserHandle); /** * Add a tile to the Quick Settings Panel diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java index cca5beb13405..2c67207f407c 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java @@ -850,11 +850,11 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D } @Override - public void showMediaOutputSwitcher(String packageName) { + public void showMediaOutputSwitcher(String targetPackageName, UserHandle targetUserHandle) { IStatusBar bar = mBar; if (bar != null) { try { - bar.showMediaOutputSwitcher(packageName); + bar.showMediaOutputSwitcher(targetPackageName, targetUserHandle); } catch (RemoteException ex) { } } diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 88f86cc7cecb..a0902cdd7c27 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -8967,8 +8967,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A : mDisplayContent.getDisplayInfo(); final Task task = getTask(); task.calculateInsetFrames(mTmpBounds /* outNonDecorBounds */, - outStableBounds /* outStableBounds */, parentBounds /* bounds */, di, - true /* useLegacyInsetsForStableBounds */); + outStableBounds /* outStableBounds */, parentBounds /* bounds */, di); final int orientationWithInsets = outStableBounds.height() >= outStableBounds.width() ? ORIENTATION_PORTRAIT : ORIENTATION_LANDSCAPE; // If orientation does not match the orientation with insets applied, then a diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java index e157318543f6..f8aa69b80253 100644 --- a/services/core/java/com/android/server/wm/Session.java +++ b/services/core/java/com/android/server/wm/Session.java @@ -912,7 +912,7 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient { @Override public void grantInputChannel(int displayId, SurfaceControl surface, IBinder clientToken, @Nullable InputTransferToken hostInputTransferToken, int flags, - int privateFlags, int type, int inputFeatures, IBinder windowToken, + int privateFlags, int inputFeatures, int type, IBinder windowToken, InputTransferToken inputTransferToken, String inputHandleName, InputChannel outInputChannel) { if (hostInputTransferToken == null && !mCanAddInternalSystemWindow) { @@ -925,7 +925,7 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient { try { mService.grantInputChannel(this, mUid, mPid, displayId, surface, clientToken, hostInputTransferToken, flags, mCanAddInternalSystemWindow ? privateFlags : 0, - type, inputFeatures, windowToken, inputTransferToken, inputHandleName, + inputFeatures, type, windowToken, inputTransferToken, inputHandleName, outInputChannel); } finally { Binder.restoreCallingIdentity(identity); diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index 129af90793b3..218fb7f6b817 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -2309,8 +2309,7 @@ class TaskFragment extends WindowContainer<WindowContainer> { // area, i.e. the screen area without the system bars. // The non decor inset are areas that could never be removed in Honeycomb. See // {@link WindowManagerPolicy#getNonDecorInsetsLw}. - calculateInsetFrames(mTmpNonDecorBounds, mTmpStableBounds, mTmpFullBounds, di, - false /* useLegacyInsetsForStableBounds */); + calculateInsetFrames(mTmpNonDecorBounds, mTmpStableBounds, mTmpFullBounds, di); } else { // Apply the given non-decor and stable insets to calculate the corresponding bounds // for screen size of configuration. @@ -2408,11 +2407,9 @@ class TaskFragment extends WindowContainer<WindowContainer> { * @param outNonDecorBounds where to place bounds with non-decor insets applied. * @param outStableBounds where to place bounds with stable insets applied. * @param bounds the bounds to inset. - * @param useLegacyInsetsForStableBounds {@code true} if we need to use the legacy insets frame - * for apps targeting U or before when calculating stable bounds. */ void calculateInsetFrames(Rect outNonDecorBounds, Rect outStableBounds, Rect bounds, - DisplayInfo displayInfo, boolean useLegacyInsetsForStableBounds) { + DisplayInfo displayInfo) { outNonDecorBounds.set(bounds); outStableBounds.set(bounds); if (mDisplayContent == null) { @@ -2424,11 +2421,7 @@ class TaskFragment extends WindowContainer<WindowContainer> { final DisplayPolicy.DecorInsets.Info info = policy.getDecorInsetsInfo( displayInfo.rotation, displayInfo.logicalWidth, displayInfo.logicalHeight); intersectWithInsetsIfFits(outNonDecorBounds, mTmpBounds, info.mNonDecorInsets); - if (!useLegacyInsetsForStableBounds) { - intersectWithInsetsIfFits(outStableBounds, mTmpBounds, info.mConfigInsets); - } else { - intersectWithInsetsIfFits(outStableBounds, mTmpBounds, info.mOverrideConfigInsets); - } + intersectWithInsetsIfFits(outStableBounds, mTmpBounds, info.mConfigInsets); } /** diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java index 222abc35ee0b..ce53290da49c 100644 --- a/services/core/java/com/android/server/wm/TransitionController.java +++ b/services/core/java/com/android/server/wm/TransitionController.java @@ -565,6 +565,9 @@ class TransitionController { if (isTransientCollect(ar)) { return true; } + for (int i = mWaitingTransitions.size() - 1; i >= 0; --i) { + if (mWaitingTransitions.get(i).isTransientLaunch(ar)) return true; + } for (int i = mPlayingTransitions.size() - 1; i >= 0; --i) { if (mPlayingTransitions.get(i).isTransientLaunch(ar)) return true; } diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 7a0245bc1acc..4d9fc6c14bc0 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -3701,22 +3701,11 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP mDragResizingChangeReported = true; mWindowFrames.clearReportResizeHints(); - // App window resize may trigger Activity#onConfigurationChanged, so we need to update - // ActivityWindowInfo as well. - final IBinder activityToken; - final ActivityWindowInfo activityWindowInfo; - if (mLastReportedActivityWindowInfo != null) { - activityToken = mActivityRecord.token; - activityWindowInfo = mLastReportedActivityWindowInfo; - } else { - activityToken = null; - activityWindowInfo = null; - } - final int prevRotation = mLastReportedConfiguration .getMergedConfiguration().windowConfiguration.getRotation(); fillClientWindowFramesAndConfiguration(mClientWindowFrames, mLastReportedConfiguration, - activityWindowInfo, true /* useLatestConfig */, false /* relayoutVisible */); + mLastReportedActivityWindowInfo, true /* useLatestConfig */, + false /* relayoutVisible */); final boolean syncRedraw = shouldSendRedrawForSync(); final boolean syncWithBuffers = syncRedraw && shouldSyncWithBuffers(); final boolean reportDraw = syncRedraw || drawPending; @@ -3740,14 +3729,15 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP mLastReportedConfiguration, getCompatInsetsState(), forceRelayout, alwaysConsumeSystemBars, displayId, syncWithBuffers ? mSyncSeqId : -1, isDragResizing, - activityToken, activityWindowInfo)); + mLastReportedActivityWindowInfo)); onResizePostDispatched(drawPending, prevRotation, displayId); } else { // TODO(b/301870955): cleanup after launch try { mClient.resized(mClientWindowFrames, reportDraw, mLastReportedConfiguration, getCompatInsetsState(), forceRelayout, alwaysConsumeSystemBars, displayId, - syncWithBuffers ? mSyncSeqId : -1, isDragResizing, activityWindowInfo); + syncWithBuffers ? mSyncSeqId : -1, isDragResizing, + mLastReportedActivityWindowInfo); onResizePostDispatched(drawPending, prevRotation, displayId); } catch (RemoteException e) { // Cancel orientation change of this window to avoid blocking unfreeze display. diff --git a/services/permission/java/com/android/server/permission/access/AccessPolicy.kt b/services/permission/java/com/android/server/permission/access/AccessPolicy.kt index 36758341d4d7..69a88e982f96 100644 --- a/services/permission/java/com/android/server/permission/access/AccessPolicy.kt +++ b/services/permission/java/com/android/server/permission/access/AccessPolicy.kt @@ -16,7 +16,6 @@ package com.android.server.permission.access -import android.permission.flags.Flags import android.util.Slog import com.android.modules.utils.BinaryXmlPullParser import com.android.modules.utils.BinaryXmlSerializer @@ -79,7 +78,7 @@ private constructor( setPackageStates(packageStates) setDisabledSystemPackageStates(disabledSystemPackageStates) packageStates.forEach { (_, packageState) -> - if (Flags.ignoreApexPermissions() && packageState.isApex) { + if (packageState.isApex) { return@forEach } mutateAppIdPackageNames() @@ -107,7 +106,7 @@ private constructor( newState.mutateUserStatesNoWrite()[userId] = MutableUserState() forEachSchemePolicy { with(it) { onUserAdded(userId) } } newState.externalState.packageStates.forEach { (_, packageState) -> - if (Flags.ignoreApexPermissions() && packageState.isApex) { + if (packageState.isApex) { return@forEach } upgradePackageVersion(packageState, userId) @@ -133,7 +132,7 @@ private constructor( setPackageStates(packageStates) setDisabledSystemPackageStates(disabledSystemPackageStates) packageStates.forEach { (packageName, packageState) -> - if (Flags.ignoreApexPermissions() && packageState.isApex) { + if (packageState.isApex) { return@forEach } if (packageState.volumeUuid == volumeUuid) { @@ -161,7 +160,7 @@ private constructor( with(it) { onStorageVolumeMounted(volumeUuid, packageNames, isSystemUpdated) } } packageStates.forEach { (_, packageState) -> - if (Flags.ignoreApexPermissions() && packageState.isApex) { + if (packageState.isApex) { return@forEach } if (packageState.volumeUuid == volumeUuid) { diff --git a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt index 63fb468c8f54..87af841b901c 100644 --- a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt +++ b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt @@ -81,7 +81,7 @@ class AppIdPermissionPolicy : SchemePolicy() { override fun MutateStateScope.onUserAdded(userId: Int) { newState.externalState.packageStates.forEach { (_, packageState) -> - if (Flags.ignoreApexPermissions() && packageState.isApex) { + if (packageState.isApex) { return@forEach } evaluateAllPermissionStatesForPackageAndUser(packageState, userId, null) 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 44ed3df34f24..65feeb027b3e 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 @@ -1445,7 +1445,7 @@ class PermissionService(private val service: AccessCheckingService) : val packageStates = packageManagerLocal.withUnfilteredSnapshot().use { it.packageStates } service.mutateState { packageStates.forEach { (packageName, packageState) -> - if (Flags.ignoreApexPermissions() && packageState.isApex) { + if (packageState.isApex) { return@forEach } val androidPackage = packageState.androidPackage ?: return@forEach @@ -1880,7 +1880,7 @@ class PermissionService(private val service: AccessCheckingService) : packageManagerLocal.withUnfilteredSnapshot().use { snapshot -> service.mutateState { snapshot.packageStates.forEach { (_, packageState) -> - if (Flags.ignoreApexPermissions() && packageState.isApex) { + if (packageState.isApex) { return@forEach } with(policy) { resetRuntimePermissions(packageState.packageName, userId) } @@ -1925,7 +1925,7 @@ class PermissionService(private val service: AccessCheckingService) : packageManagerLocal.withUnfilteredSnapshot().use { snapshot -> snapshot.packageStates.forEach { (_, packageState) -> - if (Flags.ignoreApexPermissions() && packageState.isApex) { + if (packageState.isApex) { return@forEach } val androidPackage = packageState.androidPackage ?: return@forEach @@ -1943,7 +1943,7 @@ class PermissionService(private val service: AccessCheckingService) : val permissions = service.getState { with(policy) { getPermissions() } } packageManagerLocal.withUnfilteredSnapshot().use { snapshot -> snapshot.packageStates.forEach packageStates@{ (_, packageState) -> - if (Flags.ignoreApexPermissions() && packageState.isApex) { + if (packageState.isApex) { return@packageStates } val androidPackage = packageState.androidPackage ?: return@packageStates @@ -2072,7 +2072,7 @@ class PermissionService(private val service: AccessCheckingService) : val appIdPackageNames = MutableIndexedMap<Int, MutableIndexedSet<String>>() packageStates.forEach { (_, packageState) -> - if (Flags.ignoreApexPermissions() && packageState.isApex) { + if (packageState.isApex) { return@forEach } appIdPackageNames @@ -2328,7 +2328,7 @@ class PermissionService(private val service: AccessCheckingService) : isInstantApp: Boolean, oldPackage: AndroidPackage? ) { - if (Flags.ignoreApexPermissions() && packageState.isApex) { + if (packageState.isApex) { return } @@ -2358,7 +2358,7 @@ class PermissionService(private val service: AccessCheckingService) : params: PermissionManagerServiceInternal.PackageInstalledParams, userId: Int ) { - if (Flags.ignoreApexPermissions() && androidPackage.isApex) { + if (androidPackage.isApex) { return } @@ -2414,7 +2414,7 @@ class PermissionService(private val service: AccessCheckingService) : sharedUserPkgs: List<AndroidPackage>, userId: Int ) { - if (Flags.ignoreApexPermissions() && packageState.isApex) { + if (packageState.isApex) { return } diff --git a/services/proguard.flags b/services/proguard.flags index f84eff79bb15..bf30781b434e 100644 --- a/services/proguard.flags +++ b/services/proguard.flags @@ -29,11 +29,6 @@ public protected *; } -# Derivatives of SystemService and other services created via reflection --keep,allowoptimization,allowaccessmodification class * extends com.android.server.SystemService { - public <methods>; -} - # Accessed from com.android.compos APEX -keep,allowoptimization,allowaccessmodification class com.android.internal.art.ArtStatsLog { public static void write(...); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index f08fbde962ef..4a61d32d00b2 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -14011,6 +14011,314 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + @EnableFlags(android.app.Flags.FLAG_SECURE_ALLOWLIST_TOKEN) + public void enqueueNotification_acceptsCorrectToken() throws RemoteException { + Notification sent = new Notification.Builder(mContext, TEST_CHANNEL_ID) + .setContentIntent(createPendingIntent("content")) + .build(); + Notification received = parcelAndUnparcel(sent, Notification.CREATOR); + assertThat(received.getAllowlistToken()).isEqualTo( + NotificationManagerService.ALLOWLIST_TOKEN); + + mBinderService.enqueueNotificationWithTag(mPkg, mPkg, "tag", 1, + parcelAndUnparcel(received, Notification.CREATOR), mUserId); + waitForIdle(); + + assertThat(mService.mNotificationList).hasSize(1); + assertThat(mService.mNotificationList.get(0).getNotification().getAllowlistToken()) + .isEqualTo(NotificationManagerService.ALLOWLIST_TOKEN); + } + + @Test + @EnableFlags(android.app.Flags.FLAG_SECURE_ALLOWLIST_TOKEN) + public void enqueueNotification_acceptsNullToken_andPopulatesIt() throws RemoteException { + Notification receivedWithoutParceling = new Notification.Builder(mContext, TEST_CHANNEL_ID) + .setContentIntent(createPendingIntent("content")) + .build(); + assertThat(receivedWithoutParceling.getAllowlistToken()).isNull(); + + mBinderService.enqueueNotificationWithTag(mPkg, mPkg, "tag", 1, + parcelAndUnparcel(receivedWithoutParceling, Notification.CREATOR), mUserId); + waitForIdle(); + + assertThat(mService.mNotificationList).hasSize(1); + assertThat(mService.mNotificationList.get(0).getNotification().getAllowlistToken()) + .isEqualTo(NotificationManagerService.ALLOWLIST_TOKEN); + } + + @Test + @EnableFlags(android.app.Flags.FLAG_SECURE_ALLOWLIST_TOKEN) + public void enqueueNotification_rejectsOtherToken() throws RemoteException { + Notification sent = new Notification.Builder(mContext, TEST_CHANNEL_ID) + .setContentIntent(createPendingIntent("content")) + .build(); + sent.overrideAllowlistToken(new Binder()); + Notification received = parcelAndUnparcel(sent, Notification.CREATOR); + assertThat(received.getAllowlistToken()).isEqualTo(sent.getAllowlistToken()); + + assertThrows(SecurityException.class, () -> + mBinderService.enqueueNotificationWithTag(mPkg, mPkg, "tag", 1, + parcelAndUnparcel(received, Notification.CREATOR), mUserId)); + waitForIdle(); + + assertThat(mService.mNotificationList).isEmpty(); + } + + @Test + @EnableFlags(android.app.Flags.FLAG_SECURE_ALLOWLIST_TOKEN) + public void enqueueNotification_customParcelingWithFakeInnerToken_hasCorrectTokenInIntents() + throws RemoteException { + Notification sentFromApp = new Notification.Builder(mContext, TEST_CHANNEL_ID) + .setContentIntent(createPendingIntent("content")) + .setPublicVersion(new Notification.Builder(mContext, TEST_CHANNEL_ID) + .setContentIntent(createPendingIntent("public")) + .build()) + .build(); + sentFromApp.publicVersion.overrideAllowlistToken(new Binder()); + + // Instead of using the normal parceling, assume the caller parcels it by hand, including a + // null token in the outer notification (as would be expected, and as is verified by + // enqueue) but trying to sneak in a different one in the inner notification, hoping it gets + // propagated to the PendingIntents. + Parcel parcelSentFromApp = Parcel.obtain(); + writeNotificationToParcelCustom(parcelSentFromApp, sentFromApp, new ArraySet<>( + Lists.newArrayList(sentFromApp.contentIntent, + sentFromApp.publicVersion.contentIntent))); + + // Use the unparceling as received in enqueueNotificationWithTag() + parcelSentFromApp.setDataPosition(0); + Notification receivedByNms = new Notification(parcelSentFromApp); + + // Verify that all the pendingIntents have the correct token. + assertThat(receivedByNms.contentIntent.getWhitelistToken()).isEqualTo( + NotificationManagerService.ALLOWLIST_TOKEN); + assertThat(receivedByNms.publicVersion.contentIntent.getWhitelistToken()).isEqualTo( + NotificationManagerService.ALLOWLIST_TOKEN); + } + + /** + * Replicates the behavior of {@link Notification#writeToParcel} but excluding the + * "always use the same allowlist token as the root notification" parts. + */ + private static void writeNotificationToParcelCustom(Parcel parcel, Notification notif, + ArraySet<PendingIntent> allPendingIntents) { + int flags = 0; + parcel.writeInt(1); // version? + + parcel.writeStrongBinder(notif.getAllowlistToken()); + parcel.writeLong(notif.when); + parcel.writeLong(notif.creationTime); + if (notif.getSmallIcon() != null) { + parcel.writeInt(1); + notif.getSmallIcon().writeToParcel(parcel, 0); + } else { + parcel.writeInt(0); + } + parcel.writeInt(notif.number); + if (notif.contentIntent != null) { + parcel.writeInt(1); + notif.contentIntent.writeToParcel(parcel, 0); + } else { + parcel.writeInt(0); + } + if (notif.deleteIntent != null) { + parcel.writeInt(1); + notif.deleteIntent.writeToParcel(parcel, 0); + } else { + parcel.writeInt(0); + } + if (notif.tickerText != null) { + parcel.writeInt(1); + TextUtils.writeToParcel(notif.tickerText, parcel, flags); + } else { + parcel.writeInt(0); + } + if (notif.tickerView != null) { + parcel.writeInt(1); + notif.tickerView.writeToParcel(parcel, 0); + } else { + parcel.writeInt(0); + } + if (notif.contentView != null) { + parcel.writeInt(1); + notif.contentView.writeToParcel(parcel, 0); + } else { + parcel.writeInt(0); + } + if (notif.getLargeIcon() != null) { + parcel.writeInt(1); + notif.getLargeIcon().writeToParcel(parcel, 0); + } else { + parcel.writeInt(0); + } + + parcel.writeInt(notif.defaults); + parcel.writeInt(notif.flags); + + if (notif.sound != null) { + parcel.writeInt(1); + notif.sound.writeToParcel(parcel, 0); + } else { + parcel.writeInt(0); + } + parcel.writeInt(notif.audioStreamType); + + if (notif.audioAttributes != null) { + parcel.writeInt(1); + notif.audioAttributes.writeToParcel(parcel, 0); + } else { + parcel.writeInt(0); + } + + parcel.writeLongArray(notif.vibrate); + parcel.writeInt(notif.ledARGB); + parcel.writeInt(notif.ledOnMS); + parcel.writeInt(notif.ledOffMS); + parcel.writeInt(notif.iconLevel); + + if (notif.fullScreenIntent != null) { + parcel.writeInt(1); + notif.fullScreenIntent.writeToParcel(parcel, 0); + } else { + parcel.writeInt(0); + } + + parcel.writeInt(notif.priority); + + parcel.writeString8(notif.category); + + parcel.writeString8(notif.getGroup()); + + parcel.writeString8(notif.getSortKey()); + + parcel.writeBundle(notif.extras); // null ok + + parcel.writeTypedArray(notif.actions, 0); // null ok + + if (notif.bigContentView != null) { + parcel.writeInt(1); + notif.bigContentView.writeToParcel(parcel, 0); + } else { + parcel.writeInt(0); + } + + if (notif.headsUpContentView != null) { + parcel.writeInt(1); + notif.headsUpContentView.writeToParcel(parcel, 0); + } else { + parcel.writeInt(0); + } + + parcel.writeInt(notif.visibility); + + if (notif.publicVersion != null) { + parcel.writeInt(1); + writeNotificationToParcelCustom(parcel, notif.publicVersion, new ArraySet<>()); + } else { + parcel.writeInt(0); + } + + parcel.writeInt(notif.color); + + if (notif.getChannelId() != null) { + parcel.writeInt(1); + parcel.writeString8(notif.getChannelId()); + } else { + parcel.writeInt(0); + } + parcel.writeLong(notif.getTimeoutAfter()); + + if (notif.getShortcutId() != null) { + parcel.writeInt(1); + parcel.writeString8(notif.getShortcutId()); + } else { + parcel.writeInt(0); + } + + if (notif.getLocusId() != null) { + parcel.writeInt(1); + notif.getLocusId().writeToParcel(parcel, 0); + } else { + parcel.writeInt(0); + } + + parcel.writeInt(notif.getBadgeIconType()); + + if (notif.getSettingsText() != null) { + parcel.writeInt(1); + TextUtils.writeToParcel(notif.getSettingsText(), parcel, flags); + } else { + parcel.writeInt(0); + } + + parcel.writeInt(notif.getGroupAlertBehavior()); + + if (notif.getBubbleMetadata() != null) { + parcel.writeInt(1); + notif.getBubbleMetadata().writeToParcel(parcel, 0); + } else { + parcel.writeInt(0); + } + + parcel.writeBoolean(notif.getAllowSystemGeneratedContextualActions()); + + parcel.writeInt(Notification.FOREGROUND_SERVICE_DEFAULT); // no getter for mFgsDeferBehavior + + // mUsesStandardHeader is not written because it should be recomputed in listeners + + parcel.writeArraySet(allPendingIntents); + } + + @Test + @SuppressWarnings("unchecked") + @EnableFlags(android.app.Flags.FLAG_SECURE_ALLOWLIST_TOKEN) + public void getActiveNotifications_doesNotLeakAllowlistToken() throws RemoteException { + Notification sentFromApp = new Notification.Builder(mContext, TEST_CHANNEL_ID) + .setContentIntent(createPendingIntent("content")) + .setPublicVersion(new Notification.Builder(mContext, TEST_CHANNEL_ID) + .setContentIntent(createPendingIntent("public")) + .build()) + .extend(new Notification.WearableExtender() + .addPage(new Notification.Builder(mContext, TEST_CHANNEL_ID) + .setContentIntent(createPendingIntent("wearPage")) + .build())) + .build(); + // Binder transition: app -> NMS + Notification receivedByNms = parcelAndUnparcel(sentFromApp, Notification.CREATOR); + assertThat(receivedByNms.getAllowlistToken()).isEqualTo( + NotificationManagerService.ALLOWLIST_TOKEN); + mBinderService.enqueueNotificationWithTag(mPkg, mPkg, "tag", 1, + parcelAndUnparcel(receivedByNms, Notification.CREATOR), mUserId); + waitForIdle(); + assertThat(mService.mNotificationList).hasSize(1); + Notification posted = mService.mNotificationList.get(0).getNotification(); + assertThat(posted.getAllowlistToken()).isEqualTo( + NotificationManagerService.ALLOWLIST_TOKEN); + assertThat(posted.contentIntent.getWhitelistToken()).isEqualTo( + NotificationManagerService.ALLOWLIST_TOKEN); + + ParceledListSlice<StatusBarNotification> listSentFromNms = + mBinderService.getAppActiveNotifications(mPkg, mUserId); + // Binder transition: NMS -> app. App doesn't have the allowlist token so clear it + // (having a different one would produce the same effect; the relevant thing is to not let + // out ALLOWLIST_TOKEN). + // Note: for other tests, this is restored by constructing TestableNMS in setup(). + Notification.processAllowlistToken = null; + ParceledListSlice<StatusBarNotification> listReceivedByApp = parcelAndUnparcel( + listSentFromNms, ParceledListSlice.CREATOR); + Notification gottenBackByApp = listReceivedByApp.getList().get(0).getNotification(); + + assertThat(gottenBackByApp.getAllowlistToken()).isNull(); + assertThat(gottenBackByApp.contentIntent.getWhitelistToken()).isNull(); + assertThat(gottenBackByApp.publicVersion.getAllowlistToken()).isNull(); + assertThat(gottenBackByApp.publicVersion.contentIntent.getWhitelistToken()).isNull(); + assertThat(new Notification.WearableExtender(gottenBackByApp).getPages() + .get(0).getAllowlistToken()).isNull(); + assertThat(new Notification.WearableExtender(gottenBackByApp).getPages() + .get(0).contentIntent.getWhitelistToken()).isNull(); + } + + @Test public void enqueueNotification_allowlistsPendingIntents() throws RemoteException { PendingIntent contentIntent = createPendingIntent("content"); PendingIntent actionIntent1 = createPendingIntent("action1"); diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java index 018600641853..42fe3a747b64 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java @@ -1630,6 +1630,7 @@ public class TransitionTests extends WindowTestsBase { assertTrue(controller.mWaitingTransitions.contains(transition)); assertTrue(controller.isTransientHide(appTask)); assertTrue(controller.isTransientVisible(appTask)); + assertTrue(controller.isTransientLaunch(recent)); } @Test diff --git a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromOverviewTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromOverviewTest.kt index 92b865542257..063088d54b45 100644 --- a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromOverviewTest.kt +++ b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromOverviewTest.kt @@ -50,6 +50,9 @@ class ShowImeOnAppStartWhenLaunchingAppFromOverviewTest(flicker: LegacyFlickerTe testApp.launchViaIntent(wmHelper) testApp.openIME(wmHelper) this.setRotation(flicker.scenario.startRotation) + if (flicker.scenario.isTablet) { + tapl.launchedAppState.swipeUpToUnstashTaskbar() + } tapl.launchedAppState.switchToOverview() wmHelper.StateSyncBuilder().withRecentsActivityVisible().waitForAndVerify() } |