diff options
183 files changed, 3965 insertions, 1484 deletions
diff --git a/apex/jobscheduler/service/aconfig/device_idle.aconfig b/apex/jobscheduler/service/aconfig/device_idle.aconfig index fc24b3075f14..e4cb5ad81ba0 100644 --- a/apex/jobscheduler/service/aconfig/device_idle.aconfig +++ b/apex/jobscheduler/service/aconfig/device_idle.aconfig @@ -4,5 +4,5 @@ flag { name: "disable_wakelocks_in_light_idle" namespace: "backstage_power" description: "Disable wakelocks for background apps while Light Device Idle is active" - bug: "299329948" + bug: "326607666" } diff --git a/apex/jobscheduler/service/aconfig/job.aconfig b/apex/jobscheduler/service/aconfig/job.aconfig index ef9ac73d6f8e..5e6d3775f6a2 100644 --- a/apex/jobscheduler/service/aconfig/job.aconfig +++ b/apex/jobscheduler/service/aconfig/job.aconfig @@ -4,7 +4,7 @@ flag { name: "batch_active_bucket_jobs" namespace: "backstage_power" description: "Include jobs in the ACTIVE bucket in the job batching effort. Don't let them run as freely as they're ready." - bug: "299329948" + bug: "326607666" } flag { diff --git a/core/api/current.txt b/core/api/current.txt index 5781e9507160..b4c3f44b4ec4 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -691,7 +691,6 @@ package android { field public static final int defaultHeight = 16844021; // 0x10104f5 field @FlaggedApi("android.content.res.default_locale") public static final int defaultLocale; field public static final int defaultToDeviceProtectedStorage = 16844036; // 0x1010504 - field @FlaggedApi("android.nfc.Flags.FLAG_OBSERVE_MODE") public static final int defaultToObserveMode; field public static final int defaultValue = 16843245; // 0x10101ed field public static final int defaultWidth = 16844020; // 0x10104f4 field public static final int delay = 16843212; // 0x10101cc @@ -1501,6 +1500,7 @@ package android { field public static final int shortcutId = 16844072; // 0x1010528 field public static final int shortcutLongLabel = 16844074; // 0x101052a field public static final int shortcutShortLabel = 16844073; // 0x1010529 + field @FlaggedApi("android.nfc.Flags.FLAG_OBSERVE_MODE") public static final int shouldDefaultToObserveMode; field public static final int shouldDisableView = 16843246; // 0x10101ee field public static final int shouldUseDefaultUnfoldTransition = 16844364; // 0x101064c field public static final int showAsAction = 16843481; // 0x10102d9 @@ -13508,7 +13508,7 @@ package android.content.pm { field public static final int FLAG_USE_APP_ZYGOTE = 8; // 0x8 field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_CAMERA}, anyOf={android.Manifest.permission.CAMERA}, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_CAMERA = 64; // 0x40 field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE}, anyOf={android.Manifest.permission.BLUETOOTH_ADVERTISE, android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_SCAN, android.Manifest.permission.CHANGE_NETWORK_STATE, android.Manifest.permission.CHANGE_WIFI_STATE, android.Manifest.permission.CHANGE_WIFI_MULTICAST_STATE, android.Manifest.permission.NFC, android.Manifest.permission.TRANSMIT_IR, android.Manifest.permission.UWB_RANGING}, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE = 16; // 0x10 - field @Deprecated @RequiresPermission(value=android.Manifest.permission.FOREGROUND_SERVICE_DATA_SYNC, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_DATA_SYNC = 1; // 0x1 + field @RequiresPermission(value=android.Manifest.permission.FOREGROUND_SERVICE_DATA_SYNC, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_DATA_SYNC = 1; // 0x1 field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_HEALTH}, anyOf={android.Manifest.permission.ACTIVITY_RECOGNITION, android.Manifest.permission.BODY_SENSORS, android.Manifest.permission.HIGH_SAMPLING_RATE_SENSORS}) public static final int FOREGROUND_SERVICE_TYPE_HEALTH = 256; // 0x100 field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_LOCATION}, anyOf={android.Manifest.permission.ACCESS_COARSE_LOCATION, android.Manifest.permission.ACCESS_FINE_LOCATION}, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_LOCATION = 8; // 0x8 field public static final int FOREGROUND_SERVICE_TYPE_MANIFEST = -1; // 0xffffffff @@ -39799,6 +39799,7 @@ package android.security.keystore { method @Deprecated public boolean isInsideSecureHardware(); method public boolean isInvalidatedByBiometricEnrollment(); method public boolean isTrustedUserPresenceRequired(); + method @FlaggedApi("android.security.keyinfo_unlocked_device_required") public boolean isUnlockedDeviceRequired(); method public boolean isUserAuthenticationRequired(); method public boolean isUserAuthenticationRequirementEnforcedBySecureHardware(); method public boolean isUserAuthenticationValidWhileOnBody(); diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 67ccd9d86c83..1718452548b3 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -10418,7 +10418,6 @@ package android.nfc.cardemulation { @FlaggedApi("android.nfc.enable_nfc_mainline") public final class ApduServiceInfo implements android.os.Parcelable { ctor @FlaggedApi("android.nfc.enable_nfc_mainline") public ApduServiceInfo(@NonNull android.content.pm.PackageManager, @NonNull android.content.pm.ResolveInfo, boolean) throws java.io.IOException, org.xmlpull.v1.XmlPullParserException; method @FlaggedApi("android.nfc.nfc_read_polling_loop") public void addPollingLoopFilter(@NonNull String, boolean); - method @FlaggedApi("android.nfc.nfc_observe_mode") public boolean defaultToObserveMode(); method @FlaggedApi("android.nfc.enable_nfc_mainline") public int describeContents(); method @FlaggedApi("android.nfc.enable_nfc_mainline") public void dump(@NonNull android.os.ParcelFileDescriptor, @NonNull java.io.PrintWriter, @NonNull String[]); method @FlaggedApi("android.nfc.enable_nfc_mainline") public void dumpDebug(@NonNull android.util.proto.ProtoOutputStream); @@ -10448,9 +10447,10 @@ package android.nfc.cardemulation { method @FlaggedApi("android.nfc.enable_nfc_mainline") public boolean requiresUnlock(); method @FlaggedApi("android.nfc.enable_nfc_mainline") public void resetOffHostSecureElement(); method @FlaggedApi("android.nfc.enable_nfc_mainline") public void setCategoryOtherServiceEnabled(boolean); - method @FlaggedApi("android.nfc.nfc_observe_mode") public void setDefaultToObserveMode(boolean); method @FlaggedApi("android.nfc.enable_nfc_mainline") public void setDynamicAidGroup(@NonNull android.nfc.cardemulation.AidGroup); method @FlaggedApi("android.nfc.enable_nfc_mainline") public void setOffHostSecureElement(@NonNull String); + method @FlaggedApi("android.nfc.nfc_observe_mode") public void setShouldDefaultToObserveMode(boolean); + method @FlaggedApi("android.nfc.nfc_observe_mode") public boolean shouldDefaultToObserveMode(); method @FlaggedApi("android.nfc.enable_nfc_mainline") public void writeToParcel(@NonNull android.os.Parcel, int); field @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public static final android.os.Parcelable.Creator<android.nfc.cardemulation.ApduServiceInfo> CREATOR; } diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 074f7e993eb4..41151c0dc647 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -717,7 +717,7 @@ public final class ActivityThread extends ClientTransactionHandler } activity.mMainThread.handleActivityConfigurationChanged( ActivityClientRecord.this, overrideConfig, newDisplayId, - false /* alwaysReportChange */); + mActivityWindowInfo, false /* alwaysReportChange */); } @Override @@ -6659,11 +6659,12 @@ public final class ActivityThread extends ClientTransactionHandler /** * Sets the supplied {@code overrideConfig} as pending for the {@code token}. Calling * this method prevents any calls to - * {@link #handleActivityConfigurationChanged(ActivityClientRecord, Configuration, int)} from - * processing any configurations older than {@code overrideConfig}. + * {@link #handleActivityConfigurationChanged(ActivityClientRecord, Configuration, int, + * ActivityWindowInfo)} from processing any configurations older than {@code overrideConfig}. */ @Override - public void updatePendingActivityConfiguration(IBinder token, Configuration overrideConfig) { + public void updatePendingActivityConfiguration(@NonNull IBinder token, + @NonNull Configuration overrideConfig) { synchronized (mPendingOverrideConfigs) { final Configuration pendingOverrideConfig = mPendingOverrideConfigs.get(token); if (pendingOverrideConfig != null @@ -6680,9 +6681,10 @@ public final class ActivityThread extends ClientTransactionHandler } @Override - public void handleActivityConfigurationChanged(ActivityClientRecord r, - @NonNull Configuration overrideConfig, int displayId) { - handleActivityConfigurationChanged(r, overrideConfig, displayId, + public void handleActivityConfigurationChanged(@NonNull ActivityClientRecord r, + @NonNull Configuration overrideConfig, int displayId, + @NonNull ActivityWindowInfo activityWindowInfo) { + handleActivityConfigurationChanged(r, overrideConfig, displayId, activityWindowInfo, // This is the only place that uses alwaysReportChange=true. The entry point should // be from ActivityConfigurationChangeItem or MoveToDisplayItem, so the server side // has confirmed the activity should handle the configuration instead of relaunch. @@ -6700,9 +6702,11 @@ public final class ActivityThread extends ClientTransactionHandler * @param overrideConfig Activity override config. * @param displayId Id of the display where activity was moved to, -1 if there was no move and * value didn't change. + * @param activityWindowInfo the window info of the given activity. */ - void handleActivityConfigurationChanged(ActivityClientRecord r, - @NonNull Configuration overrideConfig, int displayId, boolean alwaysReportChange) { + void handleActivityConfigurationChanged(@NonNull ActivityClientRecord r, + @NonNull Configuration overrideConfig, int displayId, + @NonNull ActivityWindowInfo activityWindowInfo, boolean alwaysReportChange) { synchronized (mPendingOverrideConfigs) { final Configuration pendingOverrideConfig = mPendingOverrideConfigs.get(r.token); if (overrideConfig.isOtherSeqNewer(pendingOverrideConfig)) { @@ -6735,6 +6739,8 @@ public final class ActivityThread extends ClientTransactionHandler // Perform updates. r.overrideConfig = overrideConfig; + r.mActivityWindowInfo = activityWindowInfo; + // TODO(b/287582673): notify on ActivityWindowInfo change final ViewRootImpl viewRoot = r.activity.mDecor != null ? r.activity.mDecor.getViewRootImpl() : null; diff --git a/core/java/android/app/AutomaticZenRule.java b/core/java/android/app/AutomaticZenRule.java index 6ad03135ea02..f6ec370478a9 100644 --- a/core/java/android/app/AutomaticZenRule.java +++ b/core/java/android/app/AutomaticZenRule.java @@ -705,7 +705,15 @@ public final class AutomaticZenRule implements Parcelable { } /** - * Sets the component (service or activity) that owns this rule. + * Sets the component name of the + * {@link android.service.notification.ConditionProviderService} that manages this rule + * (but note that {@link android.service.notification.ConditionProviderService} is + * deprecated in favor of using {@link NotificationManager#setAutomaticZenRuleState} to + * notify the system about the state of your rule). + * + * <p>This is exclusive with {@link #setConfigurationActivity}; rules where a configuration + * activity is set will not use the component set here to determine whether the rule + * should be active. */ public @NonNull Builder setOwner(@Nullable ComponentName owner) { mOwner = owner; @@ -743,6 +751,11 @@ public final class AutomaticZenRule implements Parcelable { * information about this rule and/or allows them to configure it. This is required to be * non-null for rules that are not backed by a * {@link android.service.notification.ConditionProviderService}. + * + * <p>This is exclusive with {@link #setOwner}; rules where a configuration + * activity is set will not use the + * {@link android.service.notification.ConditionProviderService} supplied there to determine + * whether the rule should be active. */ public @NonNull Builder setConfigurationActivity( @Nullable ComponentName configurationActivity) { diff --git a/core/java/android/app/ClientTransactionHandler.java b/core/java/android/app/ClientTransactionHandler.java index 4c92dee6ff17..b5b3669c1d80 100644 --- a/core/java/android/app/ClientTransactionHandler.java +++ b/core/java/android/app/ClientTransactionHandler.java @@ -167,11 +167,12 @@ public abstract class ClientTransactionHandler { /** Set pending activity configuration in case it will be updated by other transaction item. */ public abstract void updatePendingActivityConfiguration(@NonNull IBinder token, - Configuration overrideConfig); + @NonNull Configuration overrideConfig); /** Deliver activity (override) configuration change. */ public abstract void handleActivityConfigurationChanged(@NonNull ActivityClientRecord r, - Configuration overrideConfig, int displayId); + @NonNull Configuration overrideConfig, int displayId, + @NonNull ActivityWindowInfo activityWindowInfo); /** Deliver {@link android.window.WindowContextInfo} change. */ public abstract void handleWindowContextInfoChanged(@NonNull IBinder clientToken, diff --git a/core/java/android/app/ForegroundServiceTypePolicy.java b/core/java/android/app/ForegroundServiceTypePolicy.java index 7e06735791ff..d1e517bbd03c 100644 --- a/core/java/android/app/ForegroundServiceTypePolicy.java +++ b/core/java/android/app/ForegroundServiceTypePolicy.java @@ -62,7 +62,6 @@ import android.content.pm.ServiceInfo.ForegroundServiceType; import android.hardware.usb.UsbAccessory; import android.hardware.usb.UsbDevice; import android.hardware.usb.UsbManager; -import android.os.Build; import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; @@ -128,14 +127,10 @@ public abstract class ForegroundServiceTypePolicy { * The FGS type enforcement: * deprecating the {@link android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_DATA_SYNC}. * - * <p>Starting a FGS with this type from apps with targetSdkVersion - * {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM} or later will result in a warning - * in the log. - * * @hide */ @ChangeId - @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + @Disabled @Overridable public static final long FGS_TYPE_DATA_SYNC_DEPRECATION_CHANGE_ID = 255039210L; diff --git a/core/java/android/app/servertransaction/ActivityConfigurationChangeItem.java b/core/java/android/app/servertransaction/ActivityConfigurationChangeItem.java index bc8fac5fa0ce..48ea846e8d50 100644 --- a/core/java/android/app/servertransaction/ActivityConfigurationChangeItem.java +++ b/core/java/android/app/servertransaction/ActivityConfigurationChangeItem.java @@ -29,6 +29,7 @@ import android.content.res.Configuration; import android.os.IBinder; import android.os.Parcel; import android.os.Trace; +import android.window.ActivityWindowInfo; import java.util.Objects; @@ -49,11 +50,13 @@ public class ActivityConfigurationChangeItem extends ActivityTransactionItem { } @Override - public void execute(@NonNull ClientTransactionHandler client, @Nullable ActivityClientRecord r, + public void execute(@NonNull ClientTransactionHandler client, @NonNull ActivityClientRecord r, @NonNull PendingTransactionActions pendingActions) { // TODO(lifecycler): detect if PIP or multi-window mode changed and report it here. Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityConfigChanged"); - client.handleActivityConfigurationChanged(r, mConfiguration, INVALID_DISPLAY); + client.handleActivityConfigurationChanged(r, mConfiguration, INVALID_DISPLAY, + // TODO(b/287582673): add ActivityWindowInfo + new ActivityWindowInfo()); Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER); } diff --git a/core/java/android/app/servertransaction/MoveToDisplayItem.java b/core/java/android/app/servertransaction/MoveToDisplayItem.java index 1353d1679427..0702c4594075 100644 --- a/core/java/android/app/servertransaction/MoveToDisplayItem.java +++ b/core/java/android/app/servertransaction/MoveToDisplayItem.java @@ -28,6 +28,7 @@ import android.content.res.Configuration; import android.os.IBinder; import android.os.Parcel; import android.os.Trace; +import android.window.ActivityWindowInfo; import java.util.Objects; @@ -39,6 +40,7 @@ public class MoveToDisplayItem extends ActivityTransactionItem { private int mTargetDisplayId; private Configuration mConfiguration; + private ActivityWindowInfo mActivityWindowInfo; @Override public void preExecute(@NonNull ClientTransactionHandler client) { @@ -52,7 +54,8 @@ public class MoveToDisplayItem extends ActivityTransactionItem { public void execute(@NonNull ClientTransactionHandler client, @NonNull ActivityClientRecord r, @NonNull PendingTransactionActions pendingActions) { Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityMovedToDisplay"); - client.handleActivityConfigurationChanged(r, mConfiguration, mTargetDisplayId); + client.handleActivityConfigurationChanged(r, mConfiguration, mTargetDisplayId, + mActivityWindowInfo); Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER); } @@ -69,7 +72,7 @@ public class MoveToDisplayItem extends ActivityTransactionItem { /** Obtain an instance initialized with provided params. */ @NonNull public static MoveToDisplayItem obtain(@NonNull IBinder activityToken, int targetDisplayId, - @NonNull Configuration configuration) { + @NonNull Configuration configuration, @NonNull ActivityWindowInfo activityWindowInfo) { MoveToDisplayItem instance = ObjectPool.obtain(MoveToDisplayItem.class); if (instance == null) { instance = new MoveToDisplayItem(); @@ -77,6 +80,7 @@ public class MoveToDisplayItem extends ActivityTransactionItem { instance.setActivityToken(activityToken); instance.mTargetDisplayId = targetDisplayId; instance.mConfiguration = new Configuration(configuration); + instance.mActivityWindowInfo = new ActivityWindowInfo(activityWindowInfo); return instance; } @@ -86,6 +90,7 @@ public class MoveToDisplayItem extends ActivityTransactionItem { super.recycle(); mTargetDisplayId = 0; mConfiguration = null; + mActivityWindowInfo = null; ObjectPool.recycle(this); } @@ -97,6 +102,7 @@ public class MoveToDisplayItem extends ActivityTransactionItem { super.writeToParcel(dest, flags); dest.writeInt(mTargetDisplayId); dest.writeTypedObject(mConfiguration, flags); + dest.writeTypedObject(mActivityWindowInfo, flags); } /** Read from Parcel. */ @@ -104,6 +110,7 @@ public class MoveToDisplayItem extends ActivityTransactionItem { super(in); mTargetDisplayId = in.readInt(); mConfiguration = in.readTypedObject(Configuration.CREATOR); + mActivityWindowInfo = in.readTypedObject(ActivityWindowInfo.CREATOR); } public static final @NonNull Creator<MoveToDisplayItem> CREATOR = new Creator<>() { @@ -126,7 +133,8 @@ public class MoveToDisplayItem extends ActivityTransactionItem { } final MoveToDisplayItem other = (MoveToDisplayItem) o; return mTargetDisplayId == other.mTargetDisplayId - && Objects.equals(mConfiguration, other.mConfiguration); + && Objects.equals(mConfiguration, other.mConfiguration) + && Objects.equals(mActivityWindowInfo, other.mActivityWindowInfo); } @Override @@ -135,6 +143,7 @@ public class MoveToDisplayItem extends ActivityTransactionItem { result = 31 * result + super.hashCode(); result = 31 * result + mTargetDisplayId; result = 31 * result + mConfiguration.hashCode(); + result = 31 * result + Objects.hashCode(mActivityWindowInfo); return result; } @@ -142,6 +151,7 @@ public class MoveToDisplayItem extends ActivityTransactionItem { public String toString() { return "MoveToDisplayItem{" + super.toString() + ",targetDisplayId=" + mTargetDisplayId - + ",configuration=" + mConfiguration + "}"; + + ",configuration=" + mConfiguration + + ",activityWindowInfo=" + mActivityWindowInfo + "}"; } } diff --git a/core/java/android/content/pm/ServiceInfo.java b/core/java/android/content/pm/ServiceInfo.java index 9c6aab4bc9fb..5b0cee75e591 100644 --- a/core/java/android/content/pm/ServiceInfo.java +++ b/core/java/android/content/pm/ServiceInfo.java @@ -163,25 +163,12 @@ public class ServiceInfo extends ComponentInfo * Because of this, developers must make sure to stop the foreground service even if * {@link android.app.Service#onTimeout(int, int)} is not called on such versions. * - * <p>Apps targeting API level {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM} and - * later should <b>NOT</b> use this type: calling - * {@link android.app.Service#startForeground(int, android.app.Notification, int)} with - * this type on devices running {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM} is - * still allowed, but it may throw an {@link android.app.InvalidForegroundServiceTypeException} - * in future platform releases. - * - * <p class="note"> - * Use the {@link android.app.job.JobInfo.Builder#setUserInitiated(boolean)} API for - * user-initiated, network data transfers. - * - * @deprecated Use {@link android.app.job.JobInfo.Builder} APIs or alternate FGS types - * (like {@link #FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING}) applicable to your use-case. + * @see android.app.Service#onTimeout(int, int) */ @RequiresPermission( value = Manifest.permission.FOREGROUND_SERVICE_DATA_SYNC, conditional = true ) - @Deprecated public static final int FOREGROUND_SERVICE_TYPE_DATA_SYNC = 1 << 0; /** diff --git a/core/java/android/database/sqlite/SQLiteDatabase.java b/core/java/android/database/sqlite/SQLiteDatabase.java index 5dfeac7fca9b..d683d72f17be 100644 --- a/core/java/android/database/sqlite/SQLiteDatabase.java +++ b/core/java/android/database/sqlite/SQLiteDatabase.java @@ -40,15 +40,12 @@ import android.os.Looper; import android.os.OperationCanceledException; import android.os.SystemProperties; import android.text.TextUtils; -import android.util.ArrayMap; import android.util.ArraySet; import android.util.EventLog; import android.util.Log; import android.util.Pair; import android.util.Printer; -import com.android.internal.annotations.GuardedBy; -import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.Preconditions; import dalvik.annotation.optimization.NeverCompile; @@ -106,14 +103,8 @@ public final class SQLiteDatabase extends SQLiteClosable { // Stores reference to all databases opened in the current process. // (The referent Object is not used at this time.) // INVARIANT: Guarded by sActiveDatabases. - @GuardedBy("sActiveDatabases") private static WeakHashMap<SQLiteDatabase, Object> sActiveDatabases = new WeakHashMap<>(); - // Tracks which database files are currently open. If a database file is opened more than - // once at any given moment, the associated databases are marked as "concurrent". - @GuardedBy("sActiveDatabases") - private static final OpenTracker sOpenTracker = new OpenTracker(); - // Thread-local for database sessions that belong to this database. // Each thread has its own database session. // INVARIANT: Immutable. @@ -519,7 +510,6 @@ public final class SQLiteDatabase extends SQLiteClosable { private void dispose(boolean finalized) { final SQLiteConnectionPool pool; - final String path; synchronized (mLock) { if (mCloseGuardLocked != null) { if (finalized) { @@ -530,12 +520,10 @@ public final class SQLiteDatabase extends SQLiteClosable { pool = mConnectionPoolLocked; mConnectionPoolLocked = null; - path = isInMemoryDatabase() ? null : getPath(); } if (!finalized) { synchronized (sActiveDatabases) { - sOpenTracker.close(path); sActiveDatabases.remove(this); } @@ -1144,74 +1132,6 @@ public final class SQLiteDatabase extends SQLiteClosable { } } - /** - * Track the number of times a database file has been opened. There is a primary connection - * associated with every open database, and these can contend with each other, leading to - * unexpected SQLiteDatabaseLockedException exceptions. The tracking here is only advisory: - * multiply-opened databases are logged but no other action is taken. - * - * This class is not thread-safe. - */ - private static class OpenTracker { - // The list of currently-open databases. This maps the database file to the number of - // currently-active opens. - private final ArrayMap<String, Integer> mOpens = new ArrayMap<>(); - - // The maximum number of concurrently open database paths that will be stored. Once this - // many paths have been recorded, further paths are logged but not saved. - private static final int MAX_RECORDED_PATHS = 20; - - // The list of databases that were ever concurrently opened. - private final ArraySet<String> mConcurrent = new ArraySet<>(); - - /** Return the canonical path. On error, just return the input path. */ - private static String normalize(String path) { - try { - return new File(path).toPath().toRealPath().toString(); - } catch (Exception e) { - // If there is an IO or security exception, just continue, using the input path. - return path; - } - } - - /** Return true if the path is currently open in another SQLiteDatabase instance. */ - void open(@Nullable String path) { - if (path == null) return; - path = normalize(path); - - Integer count = mOpens.get(path); - if (count == null || count == 0) { - mOpens.put(path, 1); - return; - } else { - mOpens.put(path, count + 1); - if (mConcurrent.size() < MAX_RECORDED_PATHS) { - mConcurrent.add(path); - } - Log.w(TAG, "multiple primary connections on " + path); - return; - } - } - - void close(@Nullable String path) { - if (path == null) return; - path = normalize(path); - Integer count = mOpens.get(path); - if (count == null || count <= 0) { - Log.e(TAG, "open database counting failure on " + path); - } else if (count == 1) { - // Implicitly set the count to zero, and make mOpens smaller. - mOpens.remove(path); - } else { - mOpens.put(path, count - 1); - } - } - - ArraySet<String> getConcurrentDatabasePaths() { - return new ArraySet<>(mConcurrent); - } - } - private void open() { try { try { @@ -1233,17 +1153,14 @@ public final class SQLiteDatabase extends SQLiteClosable { } private void openInner() { - final String path; synchronized (mLock) { assert mConnectionPoolLocked == null; mConnectionPoolLocked = SQLiteConnectionPool.open(mConfigurationLocked); mCloseGuardLocked.open("close"); - path = isInMemoryDatabase() ? null : getPath(); } synchronized (sActiveDatabases) { sActiveDatabases.put(this, null); - sOpenTracker.open(path); } } @@ -2428,17 +2345,6 @@ public final class SQLiteDatabase extends SQLiteClosable { } /** - * Return list of databases that have been concurrently opened. - * @hide - */ - @VisibleForTesting - public static ArraySet<String> getConcurrentDatabasePaths() { - synchronized (sActiveDatabases) { - return sOpenTracker.getConcurrentDatabasePaths(); - } - } - - /** * Returns true if the new version code is greater than the current database version. * * @param newVersion The new version code. @@ -2860,19 +2766,6 @@ public final class SQLiteDatabase extends SQLiteClosable { dumpDatabaseDirectory(printer, new File(dir), isSystem); } } - - // Dump concurrently-opened database files, if any - final ArraySet<String> concurrent; - synchronized (sActiveDatabases) { - concurrent = sOpenTracker.getConcurrentDatabasePaths(); - } - if (concurrent.size() > 0) { - printer.println(""); - printer.println("Concurrently opened database files"); - for (String f : concurrent) { - printer.println(" " + f); - } - } } private static void dumpDatabaseDirectory(Printer pw, File dir, boolean isSystem) { diff --git a/core/java/android/security/flags.aconfig b/core/java/android/security/flags.aconfig index 76314546b4f0..5e7edda31c19 100644 --- a/core/java/android/security/flags.aconfig +++ b/core/java/android/security/flags.aconfig @@ -31,6 +31,13 @@ flag { } flag { + name: "keyinfo_unlocked_device_required" + namespace: "hardware_backed_security" + description: "Add the API android.security.keystore.KeyInfo#isUnlockedDeviceRequired()" + bug: "296475382" +} + +flag { name: "deprecate_fsv_sig" namespace: "hardware_backed_security" description: "Feature flag for deprecating .fsv_sig" diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index b5f3b9a8fa2d..333cbb39d9c7 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -95,6 +95,7 @@ import static android.view.WindowManagerGlobal.RELAYOUT_RES_SURFACE_CHANGED; import static android.view.accessibility.Flags.fixMergedContentChangeEvent; import static android.view.accessibility.Flags.forceInvertColor; import static android.view.accessibility.Flags.reduceWindowContentChangedEventThrottle; +import static android.view.flags.Flags.toolkitFrameRateTypingReadOnly; import static android.view.flags.Flags.toolkitMetricsForFrameRateDecision; import static android.view.flags.Flags.toolkitSetFrameRateReadOnly; import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto.ClientSideProto.IME_FOCUS_CONTROLLER; @@ -1061,9 +1062,6 @@ public final class ViewRootImpl implements ViewParent, * the variables below are used to determine whther a dVRR feature should be enabled */ - // Used to determine whether to suppress boost on typing - private boolean mShouldSuppressBoostOnTyping = false; - /** * A temporary object used so relayoutWindow can return the latest SyncSeqId * system. The SyncSeqId system was designed to work without synchronous relayout @@ -1117,10 +1115,12 @@ public final class ViewRootImpl implements ViewParent, private static boolean sToolkitSetFrameRateReadOnlyFlagValue; private static boolean sToolkitMetricsForFrameRateDecisionFlagValue; + private static boolean sToolkitFrameRateTypingReadOnlyFlagValue; static { sToolkitSetFrameRateReadOnlyFlagValue = toolkitSetFrameRateReadOnly(); sToolkitMetricsForFrameRateDecisionFlagValue = toolkitMetricsForFrameRateDecision(); + sToolkitFrameRateTypingReadOnlyFlagValue = toolkitFrameRateTypingReadOnly(); } // The latest input event from the gesture that was used to resolve the pointer icon. @@ -12417,7 +12417,8 @@ public final class ViewRootImpl implements ViewParent, boolean desiredAction = motionEventAction == MotionEvent.ACTION_DOWN || motionEventAction == MotionEvent.ACTION_MOVE || motionEventAction == MotionEvent.ACTION_UP; - boolean undesiredType = windowType == TYPE_INPUT_METHOD && mShouldSuppressBoostOnTyping; + boolean undesiredType = windowType == TYPE_INPUT_METHOD + && sToolkitFrameRateTypingReadOnlyFlagValue; // use toolkitSetFrameRate flag to gate the change return desiredAction && !undesiredType && shouldEnableDvrr() && getFrameRateBoostOnTouchEnabled(); diff --git a/core/java/android/view/flags/refresh_rate_flags.aconfig b/core/java/android/view/flags/refresh_rate_flags.aconfig index 9d613bcae29a..05cabd56f532 100644 --- a/core/java/android/view/flags/refresh_rate_flags.aconfig +++ b/core/java/android/view/flags/refresh_rate_flags.aconfig @@ -74,4 +74,12 @@ flag { description: "Feature flag for setting frame rate based on velocity" bug: "239979904" is_fixed_read_only: true +} + +flag { + name: "toolkit_frame_rate_typing_read_only" + namespace: "toolkit" + description: "Feature flag for suppressing boost on typing" + bug: "239979904" + is_fixed_read_only: true }
\ No newline at end of file diff --git a/core/java/android/view/inputmethod/ImeTracker.java b/core/java/android/view/inputmethod/ImeTracker.java index 74e1d10cdd44..b1fdaa97ffe0 100644 --- a/core/java/android/view/inputmethod/ImeTracker.java +++ b/core/java/android/view/inputmethod/ImeTracker.java @@ -340,15 +340,6 @@ public interface ImeTracker { @SoftInputShowHideReason int reason, boolean fromUser); /** - * Alias for {@link #onRequestShow(String, int, int, int, boolean)} with - * {@code fromUser} set to {@code false}. - */ - default Token onRequestShow(@Nullable String component, int uid, @Origin int origin, - @SoftInputShowHideReason int reason) { - return onRequestShow(component, uid, origin, reason, false /* fromUser */); - } - - /** * Creates an IME hide request tracking token. * * @param component the name of the component that created the IME request, or {@code null} @@ -365,15 +356,6 @@ public interface ImeTracker { @SoftInputShowHideReason int reason, boolean fromUser); /** - * Alias for {@link #onRequestHide(String, int, int, int, boolean)} with - * {@code fromUser} set to {@code false}. - */ - default Token onRequestHide(@Nullable String component, int uid, @Origin int origin, - @SoftInputShowHideReason int reason) { - return onRequestHide(component, uid, origin, reason, false /* fromUser */); - } - - /** * Called when an IME request progresses to a further phase. * * @param token the token tracking the current IME request or {@code null} otherwise. diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index f349ae9d8d79..fcc8344cbcd9 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -2328,7 +2328,7 @@ public final class InputMethodManager { synchronized (mH) { final ImeTracker.Token statsToken = ImeTracker.forLogging().onRequestShow( null /* component */, Process.myUid(), ImeTracker.ORIGIN_CLIENT_SHOW_SOFT_INPUT, - SoftInputShowHideReason.SHOW_SOFT_INPUT); + SoftInputShowHideReason.SHOW_SOFT_INPUT, false /* fromUser */); Log.w(TAG, "showSoftInputUnchecked() is a hidden method, which will be" + " removed soon. If you are using androidx.appcompat.widget.SearchView," @@ -3538,7 +3538,7 @@ public final class InputMethodManager { void closeCurrentInput() { final ImeTracker.Token statsToken = ImeTracker.forLogging().onRequestHide( null /* component */, Process.myUid(), ImeTracker.ORIGIN_CLIENT_HIDE_SOFT_INPUT, - SoftInputShowHideReason.HIDE_CLOSE_CURRENT_SESSION); + SoftInputShowHideReason.HIDE_CLOSE_CURRENT_SESSION, false /* fromUser */); ImeTracker.forLatency().onRequestHide(statsToken, ImeTracker.ORIGIN_CLIENT_HIDE_SOFT_INPUT, SoftInputShowHideReason.HIDE_CLOSE_CURRENT_SESSION, ActivityThread::currentApplication); @@ -3638,7 +3638,7 @@ public final class InputMethodManager { if (statsToken == null) { statsToken = ImeTracker.forLogging().onRequestHide(null /* component */, Process.myUid(), ImeTracker.ORIGIN_CLIENT_HIDE_SOFT_INPUT, - SoftInputShowHideReason.HIDE_SOFT_INPUT_BY_INSETS_API); + SoftInputShowHideReason.HIDE_SOFT_INPUT_BY_INSETS_API, false /* fromUser */); } ImeTracker.forLatency().onRequestHide(statsToken, ImeTracker.ORIGIN_CLIENT_HIDE_SOFT_INPUT, SoftInputShowHideReason.HIDE_SOFT_INPUT_BY_INSETS_API, diff --git a/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig b/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig index 82067defd336..254f4f77c100 100644 --- a/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig +++ b/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig @@ -77,3 +77,10 @@ flag { bug: "309593314" is_fixed_read_only: true } + +flag { + name: "letterbox_background_wallpaper" + namespace: "large_screen_experiences_app_compat" + description: "Whether the blurred letterbox wallpaper background is enabled by default" + bug: "297195682" +} diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl index d463b62e62a3..6ffc63869f26 100644 --- a/core/java/com/android/internal/statusbar/IStatusBar.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl @@ -360,8 +360,12 @@ oneway interface IStatusBar /** Shows rear display educational dialog */ void showRearDisplayDialog(int currentBaseState); - /** Called when requested to go to fullscreen from the active split app. */ - void goToFullscreenFromSplit(); + /** + * Called when requested to go to fullscreen from the focused app. + * + * @param displayId the id of the current display. + */ + void moveFocusedTaskToFullscreen(int displayId); /** * Enters stage split from a current running app. diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index c1fd61948e68..48cf09a84e57 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -4409,7 +4409,7 @@ <attr name="requireDeviceScreenOn" format="boolean"/> <!-- Whether the device should default to observe mode when this service is default or in the foreground. --> - <attr name="defaultToObserveMode" format="boolean"/> + <attr name="shouldDefaultToObserveMode" format="boolean"/> </declare-styleable> <!-- Use <code>offhost-apdu-service</code> as the root tag of the XML resource that @@ -4436,7 +4436,7 @@ <attr name="requireDeviceScreenOn"/> <!-- Whether the device should default to observe mode when this service is default or in the foreground. --> - <attr name="defaultToObserveMode"/> + <attr name="shouldDefaultToObserveMode"/> </declare-styleable> <!-- Specify one or more <code>aid-group</code> elements inside a diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml index b2e0be7c2201..c882938b63ce 100644 --- a/core/res/res/values/attrs_manifest.xml +++ b/core/res/res/values/attrs_manifest.xml @@ -1618,15 +1618,13 @@ <!-- Data (photo, file, account) upload/download, backup/restore, import/export, fetch, transfer over network between device and cloud. - <p><b>THIS TYPE IS DEPRECATED.</b> - <p><em>Note: For apps with <code>targetSdkVersion</code> - {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM} and above, this type should - <b>NOT</b> be used: calling - {@link android.app.Service#startForeground(int, android.app.Notification, int)} - with this type on devices running - {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM} is still allowed, but it may - throw an {@link android.app.InvalidForegroundServiceTypeException} in future platform - releases.</em> + <p>For apps with <code>targetSdkVersion</code> + {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and above, this type should NOT + be used: calling + {@link android.app.Service#startForeground(int, android.app.Notification, int)} with + this type on devices running {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} + is still allowed, but calling it with this type on devices running future platform + releases may get a {@link android.app.InvalidForegroundServiceTypeException}. --> <flag name="dataSync" value="0x01" /> <!-- Music, video, news or other media play. diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 1f06b0b7c62b..6134e788be82 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -6967,4 +6967,16 @@ <!-- Whether to use file hashes cache in watchlist--> <bool name="config_watchlistUseFileHashesCache">false</bool> + + <!-- Name of the package responsible to handle Contextual Search. --> + <string name="config_defaultContextualSearchPackageName" translatable="false" /> + + <!-- The key containing the entrypoint for Contextual Search. --> + <string name="config_defaultContextualSearchKey" translatable="false" /> + + <!-- The key containing the branching boolean for Contextual Search. --> + <string name="config_defaultContextualSearchEnabled" translatable="false" /> + + <!-- The key containing the branching boolean for legacy Search. --> + <string name="config_defaultContextualSearchLegacyEnabled" translatable="false" /> </resources> diff --git a/core/res/res/values/public-staging.xml b/core/res/res/values/public-staging.xml index 5987f6ea05d4..3303c076c090 100644 --- a/core/res/res/values/public-staging.xml +++ b/core/res/res/values/public-staging.xml @@ -160,7 +160,7 @@ <!-- @FlaggedApi("android.view.inputmethod.connectionless_handwriting") --> <public name="supportsConnectionlessStylusHandwriting" /> <!-- @FlaggedApi("android.nfc.Flags.FLAG_OBSERVE_MODE") --> - <public name="defaultToObserveMode"/> + <public name="shouldDefaultToObserveMode"/> <!-- @FlaggedApi("android.security.asm_restrictions_enabled") --> <public name="allowCrossUidActivitySwitchFromBelow"/> <!-- @FlaggedApi("com.android.text.flags.use_bounds_for_width") --> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index b36b1d63cbf2..2f5183fc1455 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -5368,4 +5368,8 @@ <java-symbol type="bool" name="config_evenDimmerEnabled" /> <java-symbol type="bool" name="config_watchlistUseFileHashesCache" /> + <java-symbol type="string" name="config_defaultContextualSearchPackageName" /> + <java-symbol type="string" name="config_defaultContextualSearchKey" /> + <java-symbol type="string" name="config_defaultContextualSearchEnabled" /> + <java-symbol type="string" name="config_defaultContextualSearchLegacyEnabled" /> </resources> diff --git a/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java b/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java index d95834fc0f4a..64c17bdfa731 100644 --- a/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java +++ b/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java @@ -395,11 +395,13 @@ public class ActivityThreadTest { olderConfig.seq = seq + 1; final ActivityClientRecord r = getActivityClientRecord(activity); - activityThread.handleActivityConfigurationChanged(r, olderConfig, INVALID_DISPLAY); + activityThread.handleActivityConfigurationChanged(r, olderConfig, INVALID_DISPLAY, + new ActivityWindowInfo()); assertEquals(numOfConfig, activity.mNumOfConfigChanges); assertEquals(olderConfig.orientation, activity.mConfig.orientation); - activityThread.handleActivityConfigurationChanged(r, newerConfig, INVALID_DISPLAY); + activityThread.handleActivityConfigurationChanged(r, newerConfig, INVALID_DISPLAY, + new ActivityWindowInfo()); assertEquals(numOfConfig + 1, activity.mNumOfConfigChanges); assertEquals(newerConfig.orientation, activity.mConfig.orientation); }); @@ -417,7 +419,7 @@ public class ActivityThreadTest { config.orientation = ORIENTATION_PORTRAIT; activityThread.handleActivityConfigurationChanged(getActivityClientRecord(activity), - config, INVALID_DISPLAY); + config, INVALID_DISPLAY, new ActivityWindowInfo()); }); final IApplicationThread appThread = activityThread.getApplicationThread(); @@ -488,7 +490,7 @@ public class ActivityThreadTest { config.orientation = ORIENTATION_PORTRAIT; activityThread.handleActivityConfigurationChanged(getActivityClientRecord(activity), - config, INVALID_DISPLAY); + config, INVALID_DISPLAY, new ActivityWindowInfo()); }); final int numOfConfig = activity.mNumOfConfigChanges; @@ -618,7 +620,7 @@ public class ActivityThreadTest { activityThread.updatePendingActivityConfiguration(activity.getActivityToken(), newActivityConfig); activityThread.handleActivityConfigurationChanged(r, newActivityConfig, - INVALID_DISPLAY); + INVALID_DISPLAY, new ActivityWindowInfo()); assertEquals("Virtual display orientation must not change when activity" + " configuration orientation changes.", @@ -783,8 +785,8 @@ public class ActivityThreadTest { /** * Calls {@link ActivityThread#handleActivityConfigurationChanged(ActivityClientRecord, - * Configuration, int)} to try to push activity configuration to the activity for the given - * sequence number. + * Configuration, int, ActivityWindowInfo)} to try to push activity configuration to the + * activity for the given sequence number. * <p> * It uses orientation to push the configuration and it tries a different orientation if the * first attempt doesn't make through, to rule out the possibility that the previous @@ -803,7 +805,8 @@ public class ActivityThreadTest { Configuration config = new Configuration(); config.orientation = ORIENTATION_PORTRAIT; config.seq = seq; - activityThread.handleActivityConfigurationChanged(r, config, INVALID_DISPLAY); + activityThread.handleActivityConfigurationChanged(r, config, INVALID_DISPLAY, + new ActivityWindowInfo()); if (activity.mNumOfConfigChanges > numOfConfig) { return config.seq; @@ -812,7 +815,8 @@ public class ActivityThreadTest { config = new Configuration(); config.orientation = ORIENTATION_LANDSCAPE; config.seq = seq + 1; - activityThread.handleActivityConfigurationChanged(r, config, INVALID_DISPLAY); + activityThread.handleActivityConfigurationChanged(r, config, INVALID_DISPLAY, + new ActivityWindowInfo()); return config.seq; } diff --git a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java index 30545f994f01..85a1b4ee3ebd 100644 --- a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java +++ b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java @@ -177,7 +177,7 @@ public class ClientTransactionItemTest { @Test public void testMoveToDisplayItem_getContextToUpdate() { final MoveToDisplayItem item = MoveToDisplayItem - .obtain(mActivityToken, DEFAULT_DISPLAY, mConfiguration); + .obtain(mActivityToken, DEFAULT_DISPLAY, mConfiguration, new ActivityWindowInfo()); final Context context = item.getContextToUpdate(mHandler); assertEquals(mActivity, context); diff --git a/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java b/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java index a8466bb092c8..906558f7603b 100644 --- a/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java +++ b/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java @@ -157,7 +157,8 @@ public class ObjectPoolTests { @Test public void testRecycleMoveToDisplayItem() { - testRecycle(() -> MoveToDisplayItem.obtain(mActivityToken, 4, config())); + testRecycle(() -> MoveToDisplayItem.obtain(mActivityToken, 4, config(), + new ActivityWindowInfo())); } @Test diff --git a/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java b/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java index 9743e84b9349..dbb090fe795b 100644 --- a/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java +++ b/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java @@ -110,8 +110,11 @@ public class TransactionParcelTests { @Test public void testMoveToDisplay() { // Write to parcel + final ActivityWindowInfo activityWindowInfo = new ActivityWindowInfo(); + activityWindowInfo.set(true /* isEmbedded */, new Rect(0, 0, 500, 1000), + new Rect(0, 0, 500, 500)); MoveToDisplayItem item = MoveToDisplayItem.obtain(mActivityToken, 4 /* targetDisplayId */, - config()); + config(), activityWindowInfo); writeAndPrepareForReading(item); // Read from parcel and assert diff --git a/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java b/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java index 3ee565f8e025..e118c98dd4da 100644 --- a/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java +++ b/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java @@ -403,41 +403,4 @@ public class SQLiteDatabaseTest { } assertFalse(allowed); } - - /** Return true if the path is in the list of strings. */ - private boolean isConcurrent(String path) throws Exception { - path = new File(path).toPath().toRealPath().toString(); - return SQLiteDatabase.getConcurrentDatabasePaths().contains(path); - } - - @Test - public void testDuplicateDatabases() throws Exception { - // The two database paths in this test are assumed not to have been opened earlier in this - // process. - - // A database path that will be opened twice. - final String dbName = "never-used-db.db"; - final File dbFile = mContext.getDatabasePath(dbName); - final String dbPath = dbFile.getPath(); - - // A database path that will be opened only once. - final String okName = "never-used-ok.db"; - final File okFile = mContext.getDatabasePath(okName); - final String okPath = okFile.getPath(); - - SQLiteDatabase db1 = SQLiteDatabase.openOrCreateDatabase(dbFile, null); - assertFalse(isConcurrent(dbPath)); - SQLiteDatabase db2 = SQLiteDatabase.openOrCreateDatabase(dbFile, null); - assertTrue(isConcurrent(dbPath)); - db1.close(); - assertTrue(isConcurrent(dbPath)); - db2.close(); - assertTrue(isConcurrent(dbPath)); - - SQLiteDatabase db3 = SQLiteDatabase.openOrCreateDatabase(okFile, null); - db3.close(); - db3 = SQLiteDatabase.openOrCreateDatabase(okFile, null); - assertFalse(isConcurrent(okPath)); - db3.close(); - } } diff --git a/keystore/java/android/security/KeyStore.java b/keystore/java/android/security/KeyStore.java index 11b827117aa3..bd9abec22325 100644 --- a/keystore/java/android/security/KeyStore.java +++ b/keystore/java/android/security/KeyStore.java @@ -21,12 +21,14 @@ import android.os.Build; import android.os.StrictMode; /** - * @hide This should not be made public in its present form because it - * assumes that private and secret key bytes are available and would - * preclude the use of hardware crypto. + * This class provides some constants and helper methods related to Android's Keystore service. + * This class was originally much larger, but its functionality was superseded by other classes. + * It now just contains a few remaining pieces for which the users haven't been updated yet. + * You may be looking for {@link java.security.KeyStore} instead. + * + * @hide */ public class KeyStore { - private static final String TAG = "KeyStore"; // ResponseCodes - see system/security/keystore/include/keystore/keystore.h @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) @@ -42,50 +44,6 @@ public class KeyStore { return KEY_STORE; } - /** @hide */ - @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) - public byte[] get(String key) { - return null; - } - - /** @hide */ - @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) - public boolean delete(String key) { - return false; - } - - /** - * List uids of all keys that are auth bound to the current user. - * Only system is allowed to call this method. - * @hide - * @deprecated This function always returns null. - */ - @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) - public int[] listUidsOfAuthBoundKeys() { - return null; - } - - - /** - * @hide - * @deprecated This function has no effect. - */ - @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) - public boolean unlock(String password) { - return false; - } - - /** - * - * @return - * @deprecated This function always returns true. - * @hide - */ - @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) - public boolean isEmpty() { - return true; - } - /** * Add an authentication record to the keystore authorization table. * @@ -105,13 +63,4 @@ public class KeyStore { public void onDeviceOffBody() { AndroidKeyStoreMaintenance.onDeviceOffBody(); } - - /** - * Returns a {@link KeyStoreException} corresponding to the provided keystore/keymaster error - * code. - */ - @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) - public static KeyStoreException getKeyStoreException(int errorCode) { - return new KeyStoreException(-10000, "Should not be called."); - } } diff --git a/keystore/java/android/security/keystore/KeyInfo.java b/keystore/java/android/security/keystore/KeyInfo.java index f50efd2c3328..5cffe46936a2 100644 --- a/keystore/java/android/security/keystore/KeyInfo.java +++ b/keystore/java/android/security/keystore/KeyInfo.java @@ -16,6 +16,7 @@ package android.security.keystore; +import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; @@ -81,6 +82,7 @@ public class KeyInfo implements KeySpec { private final @KeyProperties.AuthEnum int mUserAuthenticationType; private final boolean mUserAuthenticationRequirementEnforcedBySecureHardware; private final boolean mUserAuthenticationValidWhileOnBody; + private final boolean mUnlockedDeviceRequired; private final boolean mTrustedUserPresenceRequired; private final boolean mInvalidatedByBiometricEnrollment; private final boolean mUserConfirmationRequired; @@ -107,6 +109,7 @@ public class KeyInfo implements KeySpec { @KeyProperties.AuthEnum int userAuthenticationType, boolean userAuthenticationRequirementEnforcedBySecureHardware, boolean userAuthenticationValidWhileOnBody, + boolean unlockedDeviceRequired, boolean trustedUserPresenceRequired, boolean invalidatedByBiometricEnrollment, boolean userConfirmationRequired, @@ -132,6 +135,7 @@ public class KeyInfo implements KeySpec { mUserAuthenticationRequirementEnforcedBySecureHardware = userAuthenticationRequirementEnforcedBySecureHardware; mUserAuthenticationValidWhileOnBody = userAuthenticationValidWhileOnBody; + mUnlockedDeviceRequired = unlockedDeviceRequired; mTrustedUserPresenceRequired = trustedUserPresenceRequired; mInvalidatedByBiometricEnrollment = invalidatedByBiometricEnrollment; mUserConfirmationRequired = userConfirmationRequired; @@ -275,6 +279,20 @@ public class KeyInfo implements KeySpec { } /** + * Returns {@code true} if the key is authorized to be used only when the device is unlocked. + * + * <p>This authorization applies only to secret key and private key operations. Public key + * operations are not restricted. + * + * @see KeyGenParameterSpec.Builder#setUnlockedDeviceRequired(boolean) + * @see KeyProtection.Builder#setUnlockedDeviceRequired(boolean) + */ + @FlaggedApi(android.security.Flags.FLAG_KEYINFO_UNLOCKED_DEVICE_REQUIRED) + public boolean isUnlockedDeviceRequired() { + return mUnlockedDeviceRequired; + } + + /** * Returns {@code true} if the key is authorized to be used only for messages confirmed by the * user. * diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreSecretKeyFactorySpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreSecretKeyFactorySpi.java index 97592b44ba2e..2682eb657963 100644 --- a/keystore/java/android/security/keystore2/AndroidKeyStoreSecretKeyFactorySpi.java +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreSecretKeyFactorySpi.java @@ -93,6 +93,7 @@ public class AndroidKeyStoreSecretKeyFactorySpi extends SecretKeyFactorySpi { long userAuthenticationValidityDurationSeconds = 0; boolean userAuthenticationRequired = true; boolean userAuthenticationValidWhileOnBody = false; + boolean unlockedDeviceRequired = false; boolean trustedUserPresenceRequired = false; boolean trustedUserConfirmationRequired = false; int remainingUsageCount = KeyProperties.UNRESTRICTED_USAGE_COUNT; @@ -184,6 +185,9 @@ public class AndroidKeyStoreSecretKeyFactorySpi extends SecretKeyFactorySpi { + userAuthenticationValidityDurationSeconds + " seconds"); } break; + case KeymasterDefs.KM_TAG_UNLOCKED_DEVICE_REQUIRED: + unlockedDeviceRequired = true; + break; case KeymasterDefs.KM_TAG_ALLOW_WHILE_ON_BODY: userAuthenticationValidWhileOnBody = KeyStore2ParameterUtils.isSecureHardware(a.securityLevel); @@ -257,6 +261,7 @@ public class AndroidKeyStoreSecretKeyFactorySpi extends SecretKeyFactorySpi { : keymasterSwEnforcedUserAuthenticators, userAuthenticationRequirementEnforcedBySecureHardware, userAuthenticationValidWhileOnBody, + unlockedDeviceRequired, trustedUserPresenceRequired, invalidatedByBiometricEnrollment, trustedUserConfirmationRequired, diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig index 0967f4e83c74..b61dda4c4e53 100644 --- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig +++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig @@ -8,14 +8,6 @@ flag { } flag { - name: "enable_desktop_windowing" - namespace: "multitasking" - description: "Enables desktop windowing" - bug: "304778354" - is_fixed_read_only: true -} - -flag { name: "enable_split_contextual" namespace: "multitasking" description: "Enables invoking split contextually" diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java index 8305fa6b0fbf..1071d728a56d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java @@ -51,4 +51,6 @@ public interface DesktopMode { /** Called when requested to go to desktop mode from the current focused app. */ void enterDesktop(int displayId); + /** Called when requested to go to fullscreen from the current focused desktop app. */ + void moveFocusedTaskToFullscreen(int displayId); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java index 88949b2a5acd..22ba70860587 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java @@ -18,21 +18,13 @@ package com.android.wm.shell.desktopmode; import android.os.SystemProperties; -import com.android.wm.shell.Flags; +import com.android.window.flags.Flags; /** * Constants for desktop mode feature */ public class DesktopModeStatus { - private static final boolean ENABLE_DESKTOP_WINDOWING = Flags.enableDesktopWindowing(); - - /** - * Flag to indicate whether desktop mode proto is available on the device - */ - private static final boolean IS_PROTO2_ENABLED = SystemProperties.getBoolean( - "persist.wm.debug.desktop_mode_2", false); - /** * Flag to indicate whether task resizing is veiled. */ @@ -75,16 +67,10 @@ public class DesktopModeStatus { "persist.wm.debug.desktop_use_rounded_corners", true); /** - * Return {@code true} is desktop windowing proto 2 is enabled + * Return {@code true} if desktop windowing is enabled */ public static boolean isEnabled() { - // Check for aconfig flag first - if (ENABLE_DESKTOP_WINDOWING) { - return true; - } - // Fall back to sysprop flag - // TODO(b/304778354): remove sysprop once desktop aconfig flag supports dynamic overriding - return IS_PROTO2_ENABLED; + return Flags.enableDesktopWindowingMode(); } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index dcffb2d3e8fa..b9d0342137c5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -381,6 +381,18 @@ class DesktopTasksController( } } + /** Enter fullscreen by moving the focused freeform task in given `displayId` to fullscreen. */ + fun enterFullscreen(displayId: Int) { + if (DesktopModeStatus.isEnabled()) { + shellTaskOrganizer + .getRunningTasks(displayId) + .find { taskInfo -> + taskInfo.isFocused && taskInfo.windowingMode == WINDOWING_MODE_FREEFORM + } + ?.let { moveToFullscreenWithAnimation(it, it.positionInParent) } + } + } + /** Move a desktop app to split screen. */ fun moveToSplit(task: RunningTaskInfo) { KtProtoLog.v( @@ -1108,6 +1120,12 @@ class DesktopTasksController( this@DesktopTasksController.enterDesktop(displayId) } } + + override fun moveFocusedTaskToFullscreen(displayId: Int) { + mainExecutor.execute { + this@DesktopTasksController.enterFullscreen(displayId) + } + } } /** The interface for calls from outside the host process. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java index 53dd981755d2..3c374677b949 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java @@ -476,7 +476,9 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, } public void goToFullscreenFromSplit() { - mStageCoordinator.goToFullscreenFromSplit(); + if (mStageCoordinator.isSplitActive()) { + mStageCoordinator.goToFullscreenFromSplit(); + } } /** Move the specified task to fullscreen, regardless of focus state. */ diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index 383621beca22..35c803b78674 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -781,6 +781,23 @@ class DesktopTasksControllerTest : ShellTestCase() { ) } + @Test + fun moveFocusedTaskToFullscreen() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + val task3 = setUpFreeformTask() + + task1.isFocused = false + task2.isFocused = true + task3.isFocused = false + + controller.enterFullscreen(DEFAULT_DISPLAY) + + val wct = getLatestExitDesktopWct() + assertThat(wct.changes[task2.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_FULLSCREEN) + } + private fun setUpFreeformTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo { val task = createFreeformTask(displayId) whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) diff --git a/nfc/api/current.txt b/nfc/api/current.txt index 0fb7c95e3680..9d0221a3ae68 100644 --- a/nfc/api/current.txt +++ b/nfc/api/current.txt @@ -206,9 +206,9 @@ package android.nfc.cardemulation { method public boolean registerAidsForService(android.content.ComponentName, String, java.util.List<java.lang.String>); method @FlaggedApi("android.nfc.nfc_read_polling_loop") public boolean registerPollingLoopFilterForService(@NonNull android.content.ComponentName, @NonNull String, boolean); method public boolean removeAidsForService(android.content.ComponentName, String); - method @FlaggedApi("android.nfc.nfc_observe_mode") public boolean setDefaultToObserveModeForService(@NonNull android.content.ComponentName, boolean); method @NonNull @RequiresPermission(android.Manifest.permission.NFC) public boolean setOffHostForService(@NonNull android.content.ComponentName, @NonNull String); method public boolean setPreferredService(android.app.Activity, android.content.ComponentName); + method @FlaggedApi("android.nfc.nfc_observe_mode") public boolean setShouldDefaultToObserveModeForService(@NonNull android.content.ComponentName, boolean); method public boolean supportsAidPrefixRegistration(); method @NonNull @RequiresPermission(android.Manifest.permission.NFC) public boolean unsetOffHostForService(@NonNull android.content.ComponentName); method public boolean unsetPreferredService(android.app.Activity); diff --git a/nfc/java/android/nfc/INfcCardEmulation.aidl b/nfc/java/android/nfc/INfcCardEmulation.aidl index 64f7fa44c12f..85a07b74871b 100644 --- a/nfc/java/android/nfc/INfcCardEmulation.aidl +++ b/nfc/java/android/nfc/INfcCardEmulation.aidl @@ -30,7 +30,7 @@ interface INfcCardEmulation boolean isDefaultServiceForAid(int userHandle, in ComponentName service, String aid); boolean setDefaultServiceForCategory(int userHandle, in ComponentName service, String category); boolean setDefaultForNextTap(int userHandle, in ComponentName service); - boolean setDefaultToObserveModeForService(int userId, in android.content.ComponentName service, boolean enable); + boolean setShouldDefaultToObserveModeForService(int userId, in android.content.ComponentName service, boolean enable); boolean registerAidGroupForService(int userHandle, in ComponentName service, in AidGroup aidGroup); boolean registerPollingLoopFilterForService(int userHandle, in ComponentName service, in String pollingLoopFilter, boolean autoTransact); boolean setOffHostForService(int userHandle, in ComponentName service, in String offHostSecureElement); diff --git a/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java b/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java index e62e37bd4ca0..2c7d61eea777 100644 --- a/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java +++ b/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java @@ -141,7 +141,7 @@ public final class ApduServiceInfo implements Parcelable { /** * Whether the NFC stack should default to Observe Mode when this preferred service. */ - private boolean mDefaultToObserveMode; + private boolean mShouldDefaultToObserveMode; /** * @hide @@ -275,8 +275,8 @@ public final class ApduServiceInfo implements Parcelable { com.android.internal.R.styleable.HostApduService_settingsActivity); mOffHostName = null; mStaticOffHostName = mOffHostName; - mDefaultToObserveMode = sa.getBoolean( - R.styleable.HostApduService_defaultToObserveMode, + mShouldDefaultToObserveMode = sa.getBoolean( + R.styleable.HostApduService_shouldDefaultToObserveMode, false); sa.recycle(); } else { @@ -297,8 +297,8 @@ public final class ApduServiceInfo implements Parcelable { com.android.internal.R.styleable.HostApduService_settingsActivity); mOffHostName = sa.getString( com.android.internal.R.styleable.OffHostApduService_secureElementName); - mDefaultToObserveMode = sa.getBoolean( - R.styleable.HostApduService_defaultToObserveMode, + mShouldDefaultToObserveMode = sa.getBoolean( + R.styleable.HostApduService_shouldDefaultToObserveMode, false); if (mOffHostName != null) { if (mOffHostName.equals("eSE")) { @@ -633,22 +633,22 @@ public final class ApduServiceInfo implements Parcelable { } /** - * Returns whether the NFC stack should default to observe mode when this servise is preferred. - * @return whether the NFC stack should default to observe mode when this servise is preferred + * Returns whether the NFC stack should default to observe mode when this service is preferred. + * @return whether the NFC stack should default to observe mode when this service is preferred */ @FlaggedApi(Flags.FLAG_NFC_OBSERVE_MODE) - public boolean defaultToObserveMode() { - return mDefaultToObserveMode; + public boolean shouldDefaultToObserveMode() { + return mShouldDefaultToObserveMode; } /** - * Sets whether the NFC stack should default to observe mode when this servise is preferred. - * @param defaultToObserveMode whether the NFC stack should default to observe mode when this - * servise is preferred + * Sets whether the NFC stack should default to observe mode when this service is preferred. + * @param shouldDefaultToObserveMode whether the NFC stack should default to observe mode when + * this service is preferred */ @FlaggedApi(Flags.FLAG_NFC_OBSERVE_MODE) - public void setDefaultToObserveMode(boolean defaultToObserveMode) { - mDefaultToObserveMode = defaultToObserveMode; + public void setShouldDefaultToObserveMode(boolean shouldDefaultToObserveMode) { + mShouldDefaultToObserveMode = shouldDefaultToObserveMode; } /** diff --git a/nfc/java/android/nfc/cardemulation/CardEmulation.java b/nfc/java/android/nfc/cardemulation/CardEmulation.java index 47ddd9de224f..ea58504063d7 100644 --- a/nfc/java/android/nfc/cardemulation/CardEmulation.java +++ b/nfc/java/android/nfc/cardemulation/CardEmulation.java @@ -348,11 +348,11 @@ public final class CardEmulation { * @return whether the change was successful. */ @FlaggedApi(Flags.FLAG_NFC_OBSERVE_MODE) - public boolean setDefaultToObserveModeForService(@NonNull ComponentName service, + public boolean setShouldDefaultToObserveModeForService(@NonNull ComponentName service, boolean enable) { try { - return sService.setDefaultToObserveModeForService(mContext.getUser().getIdentifier(), - service, enable); + return sService.setShouldDefaultToObserveModeForService( + mContext.getUser().getIdentifier(), service, enable); } catch (RemoteException e) { Log.e(TAG, "Failed to reach CardEmulationService."); } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/BottomSheet.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/BottomSheet.kt index d319e4cc9ef9..a7b5c36215cf 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/BottomSheet.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/BottomSheet.kt @@ -16,8 +16,15 @@ package com.android.credentialmanager.common.ui +import android.credentials.flags.Flags +import androidx.compose.animation.animateContentSize import androidx.compose.foundation.background -import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.rememberCoroutineScope @@ -25,6 +32,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import com.android.compose.rememberSystemUiController import com.android.compose.theme.LocalAndroidColorScheme +import androidx.compose.ui.unit.dp import com.android.credentialmanager.common.material.ModalBottomSheetLayout import com.android.credentialmanager.common.material.ModalBottomSheetValue import com.android.credentialmanager.common.material.rememberModalBottomSheetState @@ -34,40 +42,68 @@ import kotlinx.coroutines.launch /** Draws a modal bottom sheet with the same styles and effects shared by various flows. */ @Composable +@OptIn(ExperimentalMaterial3Api::class) fun ModalBottomSheet( - sheetContent: @Composable ColumnScope.() -> Unit, - onDismiss: () -> Unit, - isInitialRender: Boolean, - onInitialRenderComplete: () -> Unit, - isAutoSelectFlow: Boolean, + sheetContent: @Composable () -> Unit, + onDismiss: () -> Unit, + isInitialRender: Boolean, + onInitialRenderComplete: () -> Unit, + isAutoSelectFlow: Boolean, ) { - val scope = rememberCoroutineScope() - val state = rememberModalBottomSheetState( - initialValue = if (isAutoSelectFlow) ModalBottomSheetValue.Expanded - else ModalBottomSheetValue.Hidden, - skipHalfExpanded = true - ) - val sysUiController = rememberSystemUiController() - if (state.targetValue == ModalBottomSheetValue.Hidden || isAutoSelectFlow) { - setTransparentSystemBarsColor(sysUiController) + if (Flags.selectorUiImprovementsEnabled()) { + val state = androidx.compose.material3.rememberModalBottomSheetState( + skipPartiallyExpanded = true + ) + androidx.compose.material3.ModalBottomSheet( + onDismissRequest = onDismiss, + containerColor = LocalAndroidColorScheme.current.surfaceBright, + sheetState = state, + content = { + Box( + modifier = Modifier + .animateContentSize() + .wrapContentHeight() + .fillMaxWidth() + ) { + sheetContent() + } + }, + scrimColor = MaterialTheme.colorScheme.scrim.copy(alpha = .32f), + shape = EntryShape.TopRoundedCorner, + dragHandle = null, + // Never take over the full screen. We always want to leave some top scrim space + // for exiting and viewing the underlying app to help a user gain context. + modifier = Modifier.padding(top = 56.dp), + ) } else { - setBottomSheetSystemBarsColor(sysUiController) - } - ModalBottomSheetLayout( - sheetBackgroundColor = LocalAndroidColorScheme.current.surfaceBright, - modifier = Modifier.background(Color.Transparent), - sheetState = state, - sheetContent = sheetContent, - sheetShape = EntryShape.TopRoundedCorner, - ) {} - LaunchedEffect(state.currentValue, state.targetValue) { - if (state.currentValue == ModalBottomSheetValue.Hidden) { - if (isInitialRender) { - onInitialRenderComplete() - scope.launch { state.show() } - } else if (state.targetValue == ModalBottomSheetValue.Hidden) { - // Only dismiss ui when the motion is downwards - onDismiss() + val scope = rememberCoroutineScope() + val state = rememberModalBottomSheetState( + initialValue = if (isAutoSelectFlow) ModalBottomSheetValue.Expanded + else ModalBottomSheetValue.Hidden, + skipHalfExpanded = true + ) + val sysUiController = rememberSystemUiController() + if (state.targetValue == ModalBottomSheetValue.Hidden || isAutoSelectFlow) { + setTransparentSystemBarsColor(sysUiController) + } else { + setBottomSheetSystemBarsColor(sysUiController) + } + ModalBottomSheetLayout( + sheetBackgroundColor = LocalAndroidColorScheme.current.surfaceBright, + modifier = Modifier.background(Color.Transparent), + sheetState = state, + sheetContent = { sheetContent() }, + sheetShape = EntryShape.TopRoundedCorner, + ) {} + LaunchedEffect(state.currentValue, state.targetValue) { + if (state.currentValue == ModalBottomSheetValue.Hidden) { + if (isInitialRender) { + onInitialRenderComplete() + scope.launch { state.show() } + } else if (state.targetValue == ModalBottomSheetValue.Hidden) { + // Only dismiss ui when the motion is downwards + onDismiss() + } } } } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Cards.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Cards.kt index bdfe39920d44..c68ae8b168fb 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Cards.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Cards.kt @@ -18,7 +18,10 @@ package com.android.credentialmanager.common.ui import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.LazyColumn @@ -66,6 +69,9 @@ fun SheetContainerCard( horizontalAlignment = Alignment.CenterHorizontally, content = content, verticalArrangement = contentVerticalArrangement, + // The bottom sheet overlaps with the navigation bars but make sure the actual content + // in the bottom sheet does not. + contentPadding = WindowInsets.navigationBars.asPaddingValues(), ) } } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt index a6253b8d4e07..8ff17e0d333a 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt @@ -29,13 +29,10 @@ import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.outlined.Lock -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.SuggestionChip import androidx.compose.material3.SuggestionChipDefaults -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -278,31 +275,6 @@ fun ActionEntry( } /** - * A single row of leading icon and text describing a benefit of passkeys, used by the - * [com.android.credentialmanager.createflow.PasskeyIntroCard]. - */ -@Composable -fun PasskeyBenefitRow( - leadingIconPainter: Painter, - text: String, -) { - Row( - horizontalArrangement = Arrangement.spacedBy(16.dp), - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth() - ) { - Icon( - modifier = Modifier.size(24.dp), - painter = leadingIconPainter, - tint = LocalAndroidColorScheme.current.onSurfaceVariant, - // Decorative purpose only. - contentDescription = null, - ) - BodyMediumText(text = text) - } -} - -/** * A single row of one or two CTA buttons for continuing or cancelling the current step. */ @Composable @@ -327,40 +299,36 @@ fun CtaButtonRow( } } -@OptIn(ExperimentalMaterial3Api::class) @Composable fun MoreOptionTopAppBar( text: String, onNavigationIconClicked: () -> Unit, bottomPadding: Dp, ) { - TopAppBar( - title = { - LargeTitleText(text = text, modifier = Modifier.padding(horizontal = 4.dp)) - }, - navigationIcon = { - IconButton( + Row( + modifier = Modifier.padding(top = 12.dp, bottom = bottomPadding), + verticalAlignment = Alignment.CenterVertically, + ) { + IconButton( modifier = Modifier.padding(top = 8.dp, bottom = 8.dp, start = 4.dp).size(48.dp), onClick = onNavigationIconClicked - ) { - Box( + ) { + Box( modifier = Modifier.size(48.dp), contentAlignment = Alignment.Center, - ) { - Icon( + ) { + Icon( imageVector = Icons.Filled.ArrowBack, contentDescription = stringResource( - R.string.accessibility_back_arrow_button + R.string.accessibility_back_arrow_button ), modifier = Modifier.size(24.dp).autoMirrored(), tint = LocalAndroidColorScheme.current.onSurfaceVariant, - ) - } + ) } - }, - colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), - modifier = Modifier.padding(top = 12.dp, bottom = bottomPadding) - ) + } + LargeTitleText(text = text, modifier = Modifier.padding(horizontal = 4.dp)) + } } private fun Modifier.autoMirrored() = composed { diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt index 4ed84b908865..72775500e7c5 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt @@ -653,4 +653,4 @@ fun EmptyAuthEntrySnackBarScreen( contentText = stringResource(R.string.no_sign_in_info_in, lastLocked.providerDisplayName), ) onLog(GetCredentialEvent.CREDMAN_GET_CRED_SCREEN_EMPTY_AUTH_SNACKBAR_SCREEN) -}
\ No newline at end of file +} diff --git a/packages/CredentialManager/tests/robotests/Android.bp b/packages/CredentialManager/tests/robotests/Android.bp index baebfeb399f2..75a0dcce0b9e 100644 --- a/packages/CredentialManager/tests/robotests/Android.bp +++ b/packages/CredentialManager/tests/robotests/Android.bp @@ -37,7 +37,7 @@ android_robolectric_test { ":CredentialManagerScreenshotTestFiles", ], - // Do not add any libraries here, instead add them to the ScreenshotTestStub + // Do not add any libraries here, instead add them to the ScreenshotTestRobo static_libs: [ "androidx.compose.runtime_runtime", "androidx.test.uiautomator_uiautomator", @@ -45,6 +45,7 @@ android_robolectric_test { "inline-mockito-robolectric-prebuilt", "platform-parametric-runner-lib", "uiautomator-helpers", + "flag-junit-base", ], libs: [ "android.test.runner", diff --git a/packages/CredentialManager/tests/robotests/customization/assets/phone/dark_landscape_singleCredentialScreen_newM3BottomSheet.png b/packages/CredentialManager/tests/robotests/customization/assets/phone/dark_landscape_singleCredentialScreen_newM3BottomSheet.png Binary files differnew file mode 100644 index 000000000000..81860e538275 --- /dev/null +++ b/packages/CredentialManager/tests/robotests/customization/assets/phone/dark_landscape_singleCredentialScreen_newM3BottomSheet.png diff --git a/packages/CredentialManager/tests/robotests/customization/assets/phone/dark_portrait_singleCredentialScreen_newM3BottomSheet.png b/packages/CredentialManager/tests/robotests/customization/assets/phone/dark_portrait_singleCredentialScreen_newM3BottomSheet.png Binary files differnew file mode 100644 index 000000000000..8c1fff7df8ab --- /dev/null +++ b/packages/CredentialManager/tests/robotests/customization/assets/phone/dark_portrait_singleCredentialScreen_newM3BottomSheet.png diff --git a/packages/CredentialManager/tests/robotests/customization/assets/phone/light_landscape_singleCredentialScreen_newM3BottomSheet.png b/packages/CredentialManager/tests/robotests/customization/assets/phone/light_landscape_singleCredentialScreen_newM3BottomSheet.png Binary files differnew file mode 100644 index 000000000000..4eb025fcd190 --- /dev/null +++ b/packages/CredentialManager/tests/robotests/customization/assets/phone/light_landscape_singleCredentialScreen_newM3BottomSheet.png diff --git a/packages/CredentialManager/tests/robotests/customization/assets/phone/light_portrait_singleCredentialScreen_newM3BottomSheet.png b/packages/CredentialManager/tests/robotests/customization/assets/phone/light_portrait_singleCredentialScreen_newM3BottomSheet.png Binary files differnew file mode 100644 index 000000000000..c709f934f78d --- /dev/null +++ b/packages/CredentialManager/tests/robotests/customization/assets/phone/light_portrait_singleCredentialScreen_newM3BottomSheet.png diff --git a/packages/CredentialManager/tests/robotests/customization/assets/tablet/dark_landscape_singleCredentialScreen_newM3BottomSheet.png b/packages/CredentialManager/tests/robotests/customization/assets/tablet/dark_landscape_singleCredentialScreen_newM3BottomSheet.png Binary files differnew file mode 100644 index 000000000000..278c13f6c7da --- /dev/null +++ b/packages/CredentialManager/tests/robotests/customization/assets/tablet/dark_landscape_singleCredentialScreen_newM3BottomSheet.png diff --git a/packages/CredentialManager/tests/robotests/customization/assets/tablet/dark_portrait_singleCredentialScreen_newM3BottomSheet.png b/packages/CredentialManager/tests/robotests/customization/assets/tablet/dark_portrait_singleCredentialScreen_newM3BottomSheet.png Binary files differnew file mode 100644 index 000000000000..cb85df350ccf --- /dev/null +++ b/packages/CredentialManager/tests/robotests/customization/assets/tablet/dark_portrait_singleCredentialScreen_newM3BottomSheet.png diff --git a/packages/CredentialManager/tests/robotests/customization/assets/tablet/light_landscape_singleCredentialScreen_newM3BottomSheet.png b/packages/CredentialManager/tests/robotests/customization/assets/tablet/light_landscape_singleCredentialScreen_newM3BottomSheet.png Binary files differnew file mode 100644 index 000000000000..2eca70741a3c --- /dev/null +++ b/packages/CredentialManager/tests/robotests/customization/assets/tablet/light_landscape_singleCredentialScreen_newM3BottomSheet.png diff --git a/packages/CredentialManager/tests/robotests/customization/assets/tablet/light_portrait_singleCredentialScreen_newM3BottomSheet.png b/packages/CredentialManager/tests/robotests/customization/assets/tablet/light_portrait_singleCredentialScreen_newM3BottomSheet.png Binary files differnew file mode 100644 index 000000000000..7ee91b3705df --- /dev/null +++ b/packages/CredentialManager/tests/robotests/customization/assets/tablet/light_portrait_singleCredentialScreen_newM3BottomSheet.png diff --git a/packages/CredentialManager/tests/robotests/screenshot/src/com/android/credentialmanager/GetCredScreenshotTest.kt b/packages/CredentialManager/tests/robotests/screenshot/src/com/android/credentialmanager/GetCredScreenshotTest.kt index a0e1fed0ac96..e609d0c5c008 100644 --- a/packages/CredentialManager/tests/robotests/screenshot/src/com/android/credentialmanager/GetCredScreenshotTest.kt +++ b/packages/CredentialManager/tests/robotests/screenshot/src/com/android/credentialmanager/GetCredScreenshotTest.kt @@ -16,7 +16,10 @@ package com.android.credentialmanager +import android.credentials.flags.Flags import android.content.Context +import android.platform.test.flag.junit.SetFlagsRule +import androidx.compose.ui.test.isPopup import com.android.credentialmanager.getflow.RequestDisplayInfo import com.android.credentialmanager.model.CredentialType import com.android.credentialmanager.model.get.ProviderInfo @@ -59,8 +62,11 @@ class GetCredScreenshotTest(emulationSpec: DeviceEmulationSpec) { CredentialManagerGoldenImagePathManager(getEmulatedDevicePathConfig(emulationSpec)) ) + @get:Rule val setFlagsRule: SetFlagsRule = SetFlagsRule() + @Test - fun singleCredentialScreen() { + fun singleCredentialScreen_M3BottomSheetDisabled() { + setFlagsRule.disableFlags(Flags.FLAG_SELECTOR_UI_IMPROVEMENTS_ENABLED) val providerInfoList = buildProviderInfoList() val providerDisplayInfo = toProviderDisplayInfo(providerInfoList) val activeEntry = toActiveEntry(providerDisplayInfo) @@ -86,6 +92,39 @@ class GetCredScreenshotTest(emulationSpec: DeviceEmulationSpec) { } } + @Test + fun singleCredentialScreen_M3BottomSheetEnabled() { + setFlagsRule.enableFlags(Flags.FLAG_SELECTOR_UI_IMPROVEMENTS_ENABLED) + val providerInfoList = buildProviderInfoList() + val providerDisplayInfo = toProviderDisplayInfo(providerInfoList) + val activeEntry = toActiveEntry(providerDisplayInfo) + screenshotRule.screenshotTest( + "singleCredentialScreen_newM3BottomSheet", + // M3's ModalBottomSheet lives in a new window, meaning we have two windows with + // a root. Hence use a different matcher `isPopup`. + viewFinder = { screenshotRule.composeRule.onNode(isPopup()) }, + ) { + ModalBottomSheet( + sheetContent = { + PrimarySelectionCard( + requestDisplayInfo = REQUEST_DISPLAY_INFO, + providerDisplayInfo = providerDisplayInfo, + providerInfoList = providerInfoList, + activeEntry = activeEntry, + onEntrySelected = {}, + onConfirm = {}, + onMoreOptionSelected = {}, + onLog = {}, + ) + }, + isInitialRender = true, + onDismiss = {}, + onInitialRenderComplete = {}, + isAutoSelectFlow = false, + ) + } + } + private fun buildProviderInfoList(): List<ProviderInfo> { val context = ApplicationProvider.getApplicationContext<Context>() val provider1 = ProviderInfo( diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt index 988afd70aaed..a395266ba5f9 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt @@ -26,6 +26,7 @@ import android.content.pm.PackageManager import android.content.pm.PackageManager.ApplicationInfoFlags import android.content.pm.ResolveInfo import android.os.SystemProperties +import android.util.Log import com.android.internal.R import com.android.settingslib.spaprivileged.framework.common.userManager import kotlinx.coroutines.async @@ -85,19 +86,24 @@ class AppListRepositoryImpl( userId: Int, loadInstantApps: Boolean, matchAnyUserForAdmin: Boolean, - ): List<ApplicationInfo> = coroutineScope { - val hiddenSystemModulesDeferred = async { packageManager.getHiddenSystemModules() } - val hideWhenDisabledPackagesDeferred = async { - context.resources.getStringArray(R.array.config_hideWhenDisabled_packageNames) - } - val installedApplicationsAsUser = - getInstalledApplications(userId, matchAnyUserForAdmin) + ): List<ApplicationInfo> = try { + coroutineScope { + val hiddenSystemModulesDeferred = async { packageManager.getHiddenSystemModules() } + val hideWhenDisabledPackagesDeferred = async { + context.resources.getStringArray(R.array.config_hideWhenDisabled_packageNames) + } + val installedApplicationsAsUser = + getInstalledApplications(userId, matchAnyUserForAdmin) - val hiddenSystemModules = hiddenSystemModulesDeferred.await() - val hideWhenDisabledPackages = hideWhenDisabledPackagesDeferred.await() - installedApplicationsAsUser.filter { app -> - app.isInAppList(loadInstantApps, hiddenSystemModules, hideWhenDisabledPackages) + val hiddenSystemModules = hiddenSystemModulesDeferred.await() + val hideWhenDisabledPackages = hideWhenDisabledPackagesDeferred.await() + installedApplicationsAsUser.filter { app -> + app.isInAppList(loadInstantApps, hiddenSystemModules, hideWhenDisabledPackages) + } } + } catch (e: Exception) { + Log.e(TAG, "loadApps failed", e) + emptyList() } private suspend fun getInstalledApplications( @@ -210,6 +216,8 @@ class AppListRepositoryImpl( } companion object { + private const val TAG = "AppListRepository" + private fun ApplicationInfo.isInAppList( showInstantApps: Boolean, hiddenSystemModules: Set<String>, diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt index efd53a4c9c23..c60ce41be87e 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt @@ -28,6 +28,8 @@ import android.content.pm.PackageManager.ResolveInfoFlags import android.content.pm.ResolveInfo import android.content.pm.UserInfo import android.content.res.Resources +import android.os.BadParcelableException +import android.os.DeadObjectException import android.os.UserManager import android.platform.test.flag.junit.SetFlagsRule import androidx.test.core.app.ApplicationProvider @@ -44,6 +46,7 @@ import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.doAnswer import org.mockito.kotlin.doReturn +import org.mockito.kotlin.doThrow import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.spy @@ -311,6 +314,19 @@ class AppListRepositoryTest { } @Test + fun loadApps_hasException_returnEmptyList() = runTest { + packageManager.stub { + on { + getInstalledApplicationsAsUser(any<ApplicationInfoFlags>(), eq(ADMIN_USER_ID)) + } doThrow BadParcelableException(DeadObjectException()) + } + + val appList = repository.loadApps(userId = ADMIN_USER_ID) + + assertThat(appList).isEmpty() + } + + @Test fun showSystemPredicate_showSystem() = runTest { val app = SYSTEM_APP diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java index 6ee403d50751..bd27c896a3c4 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java @@ -19,6 +19,7 @@ package com.android.settingslib.bluetooth; import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; import android.annotation.CallbackExecutor; +import android.annotation.IntDef; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothClass; import android.bluetooth.BluetoothCsipSetCoordinator; @@ -35,6 +36,7 @@ import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothProfile.ServiceListener; import android.content.ContentResolver; import android.content.Context; +import android.content.Intent; import android.database.ContentObserver; import android.net.Uri; import android.os.Build; @@ -52,6 +54,8 @@ import com.android.settingslib.R; import com.google.common.collect.ImmutableList; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; @@ -71,6 +75,18 @@ import java.util.stream.Collectors; * result callback. */ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { + public static final String ACTION_LE_AUDIO_SHARING_STATE_CHANGE = + "com.android.settings.action.BLUETOOTH_LE_AUDIO_SHARING_STATE_CHANGE"; + public static final String EXTRA_LE_AUDIO_SHARING_STATE = "BLUETOOTH_LE_AUDIO_SHARING_STATE"; + public static final int BROADCAST_STATE_UNKNOWN = 0; + public static final int BROADCAST_STATE_ON = 1; + public static final int BROADCAST_STATE_OFF = 2; + @Retention(RetentionPolicy.SOURCE) + @IntDef( + prefix = {"BROADCAST_STATE_"}, + value = {BROADCAST_STATE_UNKNOWN, BROADCAST_STATE_ON, BROADCAST_STATE_OFF}) + public @interface BroadcastState {} + private static final String SETTINGS_PKG = "com.android.settings"; private static final String TAG = "LocalBluetoothLeBroadcast"; private static final boolean DEBUG = BluetoothUtils.D; @@ -89,7 +105,6 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { Settings.Secure.getUriFor( Settings.Secure.BLUETOOTH_LE_BROADCAST_IMPROVE_COMPATIBILITY), }; - private final Context mContext; private final CachedBluetoothDeviceManager mDeviceManager; private BluetoothLeBroadcast mServiceBroadcast; @@ -200,6 +215,7 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { Log.d(TAG, "onBroadcastMetadataChanged(), broadcastId = " + broadcastId); } setLatestBluetoothLeBroadcastMetadata(metadata); + notifyBroadcastStateChange(BROADCAST_STATE_ON); } @Override @@ -212,7 +228,7 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { + ", broadcastId = " + broadcastId); } - + notifyBroadcastStateChange(BROADCAST_STATE_OFF); stopLocalSourceReceivers(); resetCacheInfo(); } @@ -1005,10 +1021,6 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { /** Update fallback active device if needed. */ public void updateFallbackActiveDeviceIfNeeded() { - if (!isEnabled(null)) { - Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded due to no ongoing broadcast"); - return; - } if (mServiceBroadcastAssistant == null) { Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded due to assistant profile is null"); return; @@ -1078,4 +1090,15 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { "bluetooth_le_broadcast_fallback_active_group_id", BluetoothCsipSetCoordinator.GROUP_ID_INVALID); } + + private void notifyBroadcastStateChange(@BroadcastState int state) { + if (!mContext.getPackageName().equals(SETTINGS_PKG)) { + Log.d(TAG, "Skip notifyBroadcastStateChange, not triggered by Settings."); + return; + } + Intent intent = new Intent(ACTION_LE_AUDIO_SHARING_STATE_CHANGE); + intent.putExtra(EXTRA_LE_AUDIO_SHARING_STATE, state); + intent.setPackage(mContext.getPackageName()); + mContext.sendBroadcast(intent); + } } diff --git a/packages/SettingsLib/src/com/android/settingslib/media/data/repository/SpatializerRepository.kt b/packages/SettingsLib/src/com/android/settingslib/media/data/repository/SpatializerRepository.kt index 2a4658bc69a1..a5c63be3c987 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/data/repository/SpatializerRepository.kt +++ b/packages/SettingsLib/src/com/android/settingslib/media/data/repository/SpatializerRepository.kt @@ -18,33 +18,71 @@ package com.android.settingslib.media.data.repository import android.media.AudioDeviceAttributes import android.media.Spatializer +import androidx.concurrent.futures.DirectExecutor import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext interface SpatializerRepository { + /** Returns true when head tracking is enabled and false the otherwise. */ + val isHeadTrackingAvailable: StateFlow<Boolean> + /** * Returns true when Spatial audio feature is supported for the [audioDeviceAttributes] and * false the otherwise. */ - suspend fun isAvailableForDevice(audioDeviceAttributes: AudioDeviceAttributes): Boolean + suspend fun isSpatialAudioAvailableForDevice( + audioDeviceAttributes: AudioDeviceAttributes + ): Boolean /** Returns a list [AudioDeviceAttributes] that are compatible with spatial audio. */ - suspend fun getCompatibleDevices(): Collection<AudioDeviceAttributes> + suspend fun getSpatialAudioCompatibleDevices(): Collection<AudioDeviceAttributes> + + /** Adds a [audioDeviceAttributes] to [getSpatialAudioCompatibleDevices] list. */ + suspend fun addSpatialAudioCompatibleDevice(audioDeviceAttributes: AudioDeviceAttributes) + + /** Removes a [audioDeviceAttributes] from [getSpatialAudioCompatibleDevices] list. */ + suspend fun removeSpatialAudioCompatibleDevice(audioDeviceAttributes: AudioDeviceAttributes) - /** Adds a [audioDeviceAttributes] to [getCompatibleDevices] list. */ - suspend fun addCompatibleDevice(audioDeviceAttributes: AudioDeviceAttributes) + /** Checks if the head tracking is enabled for the [audioDeviceAttributes]. */ + suspend fun isHeadTrackingEnabled(audioDeviceAttributes: AudioDeviceAttributes): Boolean - /** Removes a [audioDeviceAttributes] to [getCompatibleDevices] list. */ - suspend fun removeCompatibleDevice(audioDeviceAttributes: AudioDeviceAttributes) + /** Sets head tracking [isEnabled] for the [audioDeviceAttributes]. */ + suspend fun setHeadTrackingEnabled( + audioDeviceAttributes: AudioDeviceAttributes, + isEnabled: Boolean, + ) } class SpatializerRepositoryImpl( private val spatializer: Spatializer, + coroutineScope: CoroutineScope, private val backgroundContext: CoroutineContext, ) : SpatializerRepository { - override suspend fun isAvailableForDevice( + override val isHeadTrackingAvailable: StateFlow<Boolean> = + callbackFlow { + val listener = + Spatializer.OnHeadTrackerAvailableListener { _, available -> + launch { send(available) } + } + spatializer.addOnHeadTrackerAvailableListener(DirectExecutor.INSTANCE, listener) + awaitClose { spatializer.removeOnHeadTrackerAvailableListener(listener) } + } + .onStart { emit(spatializer.isHeadTrackerAvailable) } + .flowOn(backgroundContext) + .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), false) + + override suspend fun isSpatialAudioAvailableForDevice( audioDeviceAttributes: AudioDeviceAttributes ): Boolean { return withContext(backgroundContext) { @@ -52,18 +90,36 @@ class SpatializerRepositoryImpl( } } - override suspend fun getCompatibleDevices(): Collection<AudioDeviceAttributes> = + override suspend fun getSpatialAudioCompatibleDevices(): Collection<AudioDeviceAttributes> = withContext(backgroundContext) { spatializer.compatibleAudioDevices } - override suspend fun addCompatibleDevice(audioDeviceAttributes: AudioDeviceAttributes) { + override suspend fun addSpatialAudioCompatibleDevice( + audioDeviceAttributes: AudioDeviceAttributes + ) { withContext(backgroundContext) { spatializer.addCompatibleAudioDevice(audioDeviceAttributes) } } - override suspend fun removeCompatibleDevice(audioDeviceAttributes: AudioDeviceAttributes) { + override suspend fun removeSpatialAudioCompatibleDevice( + audioDeviceAttributes: AudioDeviceAttributes + ) { withContext(backgroundContext) { spatializer.removeCompatibleAudioDevice(audioDeviceAttributes) } } + + override suspend fun isHeadTrackingEnabled( + audioDeviceAttributes: AudioDeviceAttributes + ): Boolean = + withContext(backgroundContext) { spatializer.isHeadTrackerEnabled(audioDeviceAttributes) } + + override suspend fun setHeadTrackingEnabled( + audioDeviceAttributes: AudioDeviceAttributes, + isEnabled: Boolean, + ) { + withContext(backgroundContext) { + spatializer.setHeadTrackerEnabled(isEnabled, audioDeviceAttributes) + } + } } diff --git a/packages/SettingsLib/src/com/android/settingslib/media/domain/interactor/SpatializerInteractor.kt b/packages/SettingsLib/src/com/android/settingslib/media/domain/interactor/SpatializerInteractor.kt index c3cc340d9cd8..0347403cb385 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/domain/interactor/SpatializerInteractor.kt +++ b/packages/SettingsLib/src/com/android/settingslib/media/domain/interactor/SpatializerInteractor.kt @@ -18,22 +18,40 @@ package com.android.settingslib.media.domain.interactor import android.media.AudioDeviceAttributes import com.android.settingslib.media.data.repository.SpatializerRepository +import kotlinx.coroutines.flow.StateFlow class SpatializerInteractor(private val repository: SpatializerRepository) { - suspend fun isAvailable(audioDeviceAttributes: AudioDeviceAttributes): Boolean = - repository.isAvailableForDevice(audioDeviceAttributes) + /** Checks if head tracking is available. */ + val isHeadTrackingAvailable: StateFlow<Boolean> + get() = repository.isHeadTrackingAvailable + + suspend fun isSpatialAudioAvailable(audioDeviceAttributes: AudioDeviceAttributes): Boolean = + repository.isSpatialAudioAvailableForDevice(audioDeviceAttributes) /** Checks if spatial audio is enabled for the [audioDeviceAttributes]. */ - suspend fun isEnabled(audioDeviceAttributes: AudioDeviceAttributes): Boolean = - repository.getCompatibleDevices().contains(audioDeviceAttributes) + suspend fun isSpatialAudioEnabled(audioDeviceAttributes: AudioDeviceAttributes): Boolean = + repository.getSpatialAudioCompatibleDevices().contains(audioDeviceAttributes) - /** Enblaes or disables spatial audio for [audioDeviceAttributes]. */ - suspend fun setEnabled(audioDeviceAttributes: AudioDeviceAttributes, isEnabled: Boolean) { + /** Enables or disables spatial audio for [audioDeviceAttributes]. */ + suspend fun setSpatialAudioEnabled( + audioDeviceAttributes: AudioDeviceAttributes, + isEnabled: Boolean + ) { if (isEnabled) { - repository.addCompatibleDevice(audioDeviceAttributes) + repository.addSpatialAudioCompatibleDevice(audioDeviceAttributes) } else { - repository.removeCompatibleDevice(audioDeviceAttributes) + repository.removeSpatialAudioCompatibleDevice(audioDeviceAttributes) } } + + /** Checks if head tracking is enabled for the [audioDeviceAttributes]. */ + suspend fun isHeadTrackingEnabled(audioDeviceAttributes: AudioDeviceAttributes): Boolean = + repository.isHeadTrackingEnabled(audioDeviceAttributes) + + /** Enables or disables head tracking for the [audioDeviceAttributes]. */ + suspend fun setHeadTrackingEnabled( + audioDeviceAttributes: AudioDeviceAttributes, + isEnabled: Boolean, + ) = repository.setHeadTrackingEnabled(audioDeviceAttributes, isEnabled) } diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/media/domain/interactor/FakeSpatializerRepository.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/media/domain/interactor/FakeSpatializerRepository.kt deleted file mode 100644 index 3f52f2494dfc..000000000000 --- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/media/domain/interactor/FakeSpatializerRepository.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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.settingslib.media.domain.interactor - -import android.media.AudioDeviceAttributes -import com.android.settingslib.media.data.repository.SpatializerRepository - -class FakeSpatializerRepository : SpatializerRepository { - - private val availabilityByDevice: MutableMap<AudioDeviceAttributes, Boolean> = mutableMapOf() - private val compatibleDevices: MutableList<AudioDeviceAttributes> = mutableListOf() - - override suspend fun isAvailableForDevice( - audioDeviceAttributes: AudioDeviceAttributes - ): Boolean = availabilityByDevice.getOrDefault(audioDeviceAttributes, false) - - override suspend fun getCompatibleDevices(): Collection<AudioDeviceAttributes> = - compatibleDevices - - override suspend fun addCompatibleDevice(audioDeviceAttributes: AudioDeviceAttributes) { - compatibleDevices.add(audioDeviceAttributes) - } - - override suspend fun removeCompatibleDevice(audioDeviceAttributes: AudioDeviceAttributes) { - compatibleDevices.remove(audioDeviceAttributes) - } - - fun setIsAvailable(audioDeviceAttributes: AudioDeviceAttributes, isAvailable: Boolean) { - availabilityByDevice[audioDeviceAttributes] = isAvailable - } -} diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/media/domain/interactor/SpatializerInteractorTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/media/domain/interactor/SpatializerInteractorTest.kt deleted file mode 100644 index a44baeb174bf..000000000000 --- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/media/domain/interactor/SpatializerInteractorTest.kt +++ /dev/null @@ -1,56 +0,0 @@ -/* - * 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.settingslib.media.domain.interactor - -import android.media.AudioDeviceAttributes -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.SmallTest -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runTest -import org.junit.Test -import org.junit.runner.RunWith - -@SmallTest -@RunWith(AndroidJUnit4::class) -class SpatializerInteractorTest { - - private val testScope = TestScope() - private val underTest = SpatializerInteractor(FakeSpatializerRepository()) - - @Test - fun setEnabledFalse_isEnabled_false() { - testScope.runTest { - underTest.setEnabled(deviceAttributes, false) - - assertThat(underTest.isEnabled(deviceAttributes)).isFalse() - } - } - - @Test - fun setEnabledTrue_isEnabled_true() { - testScope.runTest { - underTest.setEnabled(deviceAttributes, true) - - assertThat(underTest.isEnabled(deviceAttributes)).isTrue() - } - } - - private companion object { - val deviceAttributes = AudioDeviceAttributes(0, 0, "test_device") - } -} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/deviceinfo/WifiMacAddressPreferenceControllerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/deviceinfo/WifiMacAddressPreferenceControllerTest.java index 37052673eb4d..70ba415abde5 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/deviceinfo/WifiMacAddressPreferenceControllerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/deviceinfo/WifiMacAddressPreferenceControllerTest.java @@ -20,9 +20,7 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; import android.annotation.SuppressLint; import android.content.Context; @@ -94,19 +92,6 @@ public class WifiMacAddressPreferenceControllerTest { } @Test - public void updateConnectivity_notAvailable_notCalled() { - boolean mCalled = false; - mController = spy(new ConcreteWifiMacAddressPreferenceController(mContext, mLifecycle) { - @Override - public boolean isAvailable() { - return false; - } - }); - mController.displayPreference(mScreen); - verify(mController, never()).updateConnectivity(); - } - - @Test public void updateConnectivity_null_setMacUnavailable() { doReturn(null).when(mWifiManager).getFactoryMacAddresses(); mController.displayPreference(mScreen); diff --git a/packages/SettingsProvider/Android.bp b/packages/SettingsProvider/Android.bp index d5814e3a9b79..94ea01607714 100644 --- a/packages/SettingsProvider/Android.bp +++ b/packages/SettingsProvider/Android.bp @@ -60,6 +60,7 @@ android_test { // because this test is not an instrumentation test. (because the target runs in the system process.) "SettingsProviderLib", "androidx.test.rules", + "device_config_service_flags_java", "flag-junit", "junit", "libaconfig_java_proto_lite", diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java index 1c9e748c5f3a..ce0257f6c85b 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java @@ -359,6 +359,15 @@ final class SettingsState { @VisibleForTesting @GuardedBy("mLock") + public void addAconfigDefaultValuesFromMap( + @NonNull Map<String, Map<String, String>> defaultMap) { + if (mNamespaceDefaults != null) { + mNamespaceDefaults.putAll(defaultMap); + } + } + + @VisibleForTesting + @GuardedBy("mLock") public static void loadAconfigDefaultValues(byte[] fileContents, @NonNull Map<String, Map<String, String>> defaultMap) { try { @@ -510,6 +519,28 @@ final class SettingsState { return false; } + // Aconfig flags are always boot stable, so we anytime we write one, we staged it to be + // applied on reboot. + if (Flags.stageAllAconfigFlags() && mNamespaceDefaults != null) { + int slashIndex = name.indexOf("/"); + boolean stageFlag = isConfigSettingsKey(mKey) + && slashIndex != -1 + && slashIndex != 0 + && slashIndex != name.length(); + + if (stageFlag) { + String namespace = name.substring(0, slashIndex); + String flag = name.substring(slashIndex + 1); + + boolean isAconfig = mNamespaceDefaults.containsKey(namespace) + && mNamespaceDefaults.get(namespace).containsKey(name); + + if (isAconfig) { + name = "staged/" + namespace + "*" + flag; + } + } + } + final boolean isNameTooLong = name.length() > SettingsState.MAX_LENGTH_PER_STRING; final boolean isValueTooLong = value != null && value.length() > SettingsState.MAX_LENGTH_PER_STRING; diff --git a/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig b/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig index ecac5ee18582..e5086e87173a 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig +++ b/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig @@ -14,3 +14,14 @@ flag { bug: "311155098" is_fixed_read_only: true } + +flag { + name: "stage_all_aconfig_flags" + namespace: "core_experiments_team_internal" + description: "Stage _all_ aconfig flags on writes, even local ones." + bug: "326598713" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java index 9ecbd50fc566..ea30c69b1c45 100644 --- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java +++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java @@ -15,13 +15,25 @@ */ package com.android.providers.settings; +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertNull; +import static junit.framework.Assert.assertTrue; +import static junit.framework.Assert.fail; + import android.aconfig.Aconfig; import android.aconfig.Aconfig.parsed_flag; import android.aconfig.Aconfig.parsed_flags; import android.os.Looper; -import android.test.AndroidTestCase; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.util.Xml; +import androidx.test.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + import com.android.modules.utils.TypedXmlSerializer; import com.google.common.base.Strings; @@ -34,7 +46,18 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -public class SettingsStateTest extends AndroidTestCase { +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class SettingsStateTest { + @Rule + public final CheckFlagsRule mCheckFlagsRule = + DeviceFlagsValueProvider.createCheckFlagsRule(); + public static final String CRAZY_STRING = "\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007\u0008\u0009\n\u000b\u000c\r" + "\u000e\u000f\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017\u0018\u0019\u001a" + @@ -76,25 +99,25 @@ public class SettingsStateTest extends AndroidTestCase { private File mSettingsFile; - @Override - protected void setUp() { - mSettingsFile = new File(getContext().getCacheDir(), "setting.xml"); + @Before + public void setUp() { + mSettingsFile = new File(InstrumentationRegistry.getContext().getCacheDir(), "setting.xml"); mSettingsFile.delete(); } - @Override - protected void tearDown() throws Exception { + @After + public void tearDown() throws Exception { if (mSettingsFile != null) { mSettingsFile.delete(); } - super.tearDown(); } + @Test public void testLoadValidAconfigProto() { int configKey = SettingsState.makeKey(SettingsState.SETTINGS_TYPE_CONFIG, 0); Object lock = new Object(); SettingsState settingsState = new SettingsState( - getContext(), lock, mSettingsFile, configKey, + InstrumentationRegistry.getContext(), lock, mSettingsFile, configKey, SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper()); parsed_flags flags = parsed_flags .newBuilder() @@ -129,11 +152,12 @@ public class SettingsStateTest extends AndroidTestCase { } } + @Test public void testSkipLoadingAconfigFlagWithMissingFields() { int configKey = SettingsState.makeKey(SettingsState.SETTINGS_TYPE_CONFIG, 0); Object lock = new Object(); SettingsState settingsState = new SettingsState( - getContext(), lock, mSettingsFile, configKey, + InstrumentationRegistry.getContext(), lock, mSettingsFile, configKey, SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper()); parsed_flags flags = parsed_flags @@ -155,12 +179,97 @@ public class SettingsStateTest extends AndroidTestCase { } } + @Test + @RequiresFlagsEnabled(Flags.FLAG_STAGE_ALL_ACONFIG_FLAGS) + public void testWritingAconfigFlagStages() { + int configKey = SettingsState.makeKey(SettingsState.SETTINGS_TYPE_CONFIG, 0); + Object lock = new Object(); + SettingsState settingsState = new SettingsState( + InstrumentationRegistry.getContext(), lock, mSettingsFile, configKey, + SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper()); + parsed_flags flags = parsed_flags + .newBuilder() + .addParsedFlag(parsed_flag + .newBuilder() + .setPackage("com.android.flags") + .setName("flag5") + .setNamespace("test_namespace") + .setDescription("test flag") + .addBug("12345678") + .setState(Aconfig.flag_state.DISABLED) + .setPermission(Aconfig.flag_permission.READ_WRITE)) + .build(); + + synchronized (lock) { + Map<String, Map<String, String>> defaults = new HashMap<>(); + settingsState.loadAconfigDefaultValues(flags.toByteArray(), defaults); + settingsState.addAconfigDefaultValuesFromMap(defaults); + + settingsState.insertSettingLocked("test_namespace/com.android.flags.flag5", + "true", null, false, "com.android.flags"); + settingsState.insertSettingLocked("test_namespace/com.android.flags.flag6", + "true", null, false, "com.android.flags"); + + assertEquals("true", + settingsState + .getSettingLocked("staged/test_namespace*com.android.flags.flag5") + .getValue()); + assertEquals(null, + settingsState + .getSettingLocked("test_namespace/com.android.flags.flag5") + .getValue()); + + assertEquals(null, + settingsState + .getSettingLocked("staged/test_namespace*com.android.flags.flag6") + .getValue()); + assertEquals("true", + settingsState + .getSettingLocked("test_namespace/com.android.flags.flag6") + .getValue()); + } + } + + @Test + @RequiresFlagsDisabled(Flags.FLAG_LOAD_ACONFIG_DEFAULTS) + public void testAddingAconfigMapOnNullIsNoOp() { + int configKey = SettingsState.makeKey(SettingsState.SETTINGS_TYPE_CONFIG, 0); + Object lock = new Object(); + SettingsState settingsState = new SettingsState( + InstrumentationRegistry.getContext(), lock, mSettingsFile, configKey, + SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper()); + + parsed_flags flags = parsed_flags + .newBuilder() + .addParsedFlag(parsed_flag + .newBuilder() + .setPackage("com.android.flags") + .setName("flag5") + .setNamespace("test_namespace") + .setDescription("test flag") + .addBug("12345678") + .setState(Aconfig.flag_state.DISABLED) + .setPermission(Aconfig.flag_permission.READ_WRITE)) + .build(); + + synchronized (lock) { + Map<String, Map<String, String>> defaults = new HashMap<>(); + settingsState.loadAconfigDefaultValues(flags.toByteArray(), defaults); + settingsState.addAconfigDefaultValuesFromMap(defaults); + + assertEquals(null, settingsState.getAconfigDefaultValues()); + } + + } + + @Test public void testInvalidAconfigProtoDoesNotCrash() { Map<String, Map<String, String>> defaults = new HashMap<>(); SettingsState settingsState = getSettingStateObject(); settingsState.loadAconfigDefaultValues("invalid protobuf".getBytes(), defaults); } + @Test public void testIsBinary() { assertFalse(SettingsState.isBinary(" abc 日本語")); @@ -191,6 +300,7 @@ public class SettingsStateTest extends AndroidTestCase { } /** Make sure we won't pass invalid characters to XML serializer. */ + @Test public void testWriteReadNoCrash() throws Exception { ByteArrayOutputStream os = new ByteArrayOutputStream(); @@ -233,12 +343,15 @@ public class SettingsStateTest extends AndroidTestCase { /** * Make sure settings can be written to a file and also can be read. */ + @Test public void testReadWrite() { final Object lock = new Object(); assertFalse(mSettingsFile.exists()); - final SettingsState ssWriter = new SettingsState(getContext(), lock, mSettingsFile, 1, - SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper()); + final SettingsState ssWriter = + new SettingsState( + InstrumentationRegistry.getContext(), lock, mSettingsFile, 1, + SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper()); ssWriter.setVersionLocked(SettingsState.SETTINGS_VERSION_NEW_ENCODING); ssWriter.insertSettingLocked("k1", "\u0000", null, false, "package"); @@ -250,8 +363,10 @@ public class SettingsStateTest extends AndroidTestCase { } ssWriter.waitForHandler(); assertTrue(mSettingsFile.exists()); - final SettingsState ssReader = new SettingsState(getContext(), lock, mSettingsFile, 1, - SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper()); + final SettingsState ssReader = + new SettingsState( + InstrumentationRegistry.getContext(), lock, mSettingsFile, 1, + SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper()); synchronized (lock) { assertEquals("\u0000", ssReader.getSettingLocked("k1").getValue()); @@ -264,6 +379,7 @@ public class SettingsStateTest extends AndroidTestCase { /** * In version 120, value "null" meant {code NULL}. */ + @Test public void testUpgrade() throws Exception { final Object lock = new Object(); final PrintStream os = new PrintStream(new FileOutputStream(mSettingsFile)); @@ -276,8 +392,10 @@ public class SettingsStateTest extends AndroidTestCase { "</settings>"); os.close(); - final SettingsState ss = new SettingsState(getContext(), lock, mSettingsFile, 1, - SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper()); + final SettingsState ss = + new SettingsState( + InstrumentationRegistry.getContext(), lock, mSettingsFile, 1, + SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper()); synchronized (lock) { SettingsState.Setting s; s = ss.getSettingLocked("k0"); @@ -294,6 +412,7 @@ public class SettingsStateTest extends AndroidTestCase { } } + @Test public void testInitializeSetting_preserveFlagNotSet() { SettingsState settingsWriter = getSettingStateObject(); settingsWriter.insertSettingLocked(SETTING_NAME, "1", null, false, TEST_PACKAGE); @@ -304,6 +423,7 @@ public class SettingsStateTest extends AndroidTestCase { assertFalse(settingsReader.getSettingLocked(SETTING_NAME).isValuePreservedInRestore()); } + @Test public void testModifySetting_preserveFlagSet() { SettingsState settingsWriter = getSettingStateObject(); settingsWriter.insertSettingLocked(SETTING_NAME, "1", null, false, TEST_PACKAGE); @@ -315,6 +435,7 @@ public class SettingsStateTest extends AndroidTestCase { assertTrue(settingsReader.getSettingLocked(SETTING_NAME).isValuePreservedInRestore()); } + @Test public void testModifySettingOverrideableByRestore_preserveFlagNotSet() { SettingsState settingsWriter = getSettingStateObject(); settingsWriter.insertSettingLocked(SETTING_NAME, "1", null, false, TEST_PACKAGE); @@ -327,6 +448,7 @@ public class SettingsStateTest extends AndroidTestCase { assertFalse(settingsReader.getSettingLocked(SETTING_NAME).isValuePreservedInRestore()); } + @Test public void testModifySettingOverrideableByRestore_preserveFlagAlreadySet_flagValueUnchanged() { SettingsState settingsWriter = getSettingStateObject(); // Init the setting. @@ -344,6 +466,7 @@ public class SettingsStateTest extends AndroidTestCase { assertTrue(settingsReader.getSettingLocked(SETTING_NAME).isValuePreservedInRestore()); } + @Test public void testResetSetting_preservedFlagIsReset() { SettingsState settingsState = getSettingStateObject(); // Initialize the setting. @@ -356,6 +479,7 @@ public class SettingsStateTest extends AndroidTestCase { } + @Test public void testModifySettingBySystemPackage_sameValue_preserveFlagNotSet() { SettingsState settingsState = getSettingStateObject(); // Initialize the setting. @@ -366,6 +490,7 @@ public class SettingsStateTest extends AndroidTestCase { assertFalse(settingsState.getSettingLocked(SETTING_NAME).isValuePreservedInRestore()); } + @Test public void testModifySettingBySystemPackage_newValue_preserveFlagSet() { SettingsState settingsState = getSettingStateObject(); // Initialize the setting. @@ -377,12 +502,15 @@ public class SettingsStateTest extends AndroidTestCase { } private SettingsState getSettingStateObject() { - SettingsState settingsState = new SettingsState(getContext(), mLock, mSettingsFile, 1, - SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper()); + SettingsState settingsState = + new SettingsState( + InstrumentationRegistry.getContext(), mLock, mSettingsFile, 1, + SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper()); settingsState.setVersionLocked(SettingsState.SETTINGS_VERSION_NEW_ENCODING); return settingsState; } + @Test public void testInsertSetting_memoryUsage() { SettingsState settingsState = getSettingStateObject(); // No exception should be thrown when there is no cap @@ -390,8 +518,10 @@ public class SettingsStateTest extends AndroidTestCase { null, false, "p1"); settingsState.deleteSettingLocked(SETTING_NAME); - settingsState = new SettingsState(getContext(), mLock, mSettingsFile, 1, - SettingsState.MAX_BYTES_PER_APP_PACKAGE_LIMITED, Looper.getMainLooper()); + settingsState = + new SettingsState( + InstrumentationRegistry.getContext(), mLock, mSettingsFile, 1, + SettingsState.MAX_BYTES_PER_APP_PACKAGE_LIMITED, Looper.getMainLooper()); // System package doesn't have memory usage limit settingsState.insertSettingLocked(SETTING_NAME, Strings.repeat("A", 20001), null, false, SYSTEM_PACKAGE); @@ -425,9 +555,12 @@ public class SettingsStateTest extends AndroidTestCase { } } + @Test public void testMemoryUsagePerPackage() { - SettingsState settingsState = new SettingsState(getContext(), mLock, mSettingsFile, 1, - SettingsState.MAX_BYTES_PER_APP_PACKAGE_LIMITED, Looper.getMainLooper()); + SettingsState settingsState = + new SettingsState( + InstrumentationRegistry.getContext(), mLock, mSettingsFile, 1, + SettingsState.MAX_BYTES_PER_APP_PACKAGE_LIMITED, Looper.getMainLooper()); // Test inserting one key with default final String testKey1 = SETTING_NAME; @@ -512,9 +645,12 @@ public class SettingsStateTest extends AndroidTestCase { assertEquals(expectedMemUsage, settingsState.getMemoryUsage(TEST_PACKAGE)); } + @Test public void testLargeSettingKey() { - SettingsState settingsState = new SettingsState(getContext(), mLock, mSettingsFile, 1, - SettingsState.MAX_BYTES_PER_APP_PACKAGE_LIMITED, Looper.getMainLooper()); + SettingsState settingsState = + new SettingsState( + InstrumentationRegistry.getContext(), mLock, mSettingsFile, 1, + SettingsState.MAX_BYTES_PER_APP_PACKAGE_LIMITED, Looper.getMainLooper()); final String largeKey = Strings.repeat("A", SettingsState.MAX_LENGTH_PER_STRING + 1); final String testValue = "testValue"; synchronized (mLock) { @@ -535,9 +671,12 @@ public class SettingsStateTest extends AndroidTestCase { } } + @Test public void testLargeSettingValue() { - SettingsState settingsState = new SettingsState(getContext(), mLock, mSettingsFile, 1, - SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper()); + SettingsState settingsState = + new SettingsState( + InstrumentationRegistry.getContext(), mLock, mSettingsFile, 1, + SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper()); final String testKey = "testKey"; final String largeValue = Strings.repeat("A", SettingsState.MAX_LENGTH_PER_STRING + 1); synchronized (mLock) { @@ -558,11 +697,12 @@ public class SettingsStateTest extends AndroidTestCase { } } + @Test public void testApplyStagedConfigValues() { int configKey = SettingsState.makeKey(SettingsState.SETTINGS_TYPE_CONFIG, 0); Object lock = new Object(); SettingsState settingsState = new SettingsState( - getContext(), lock, mSettingsFile, configKey, + InstrumentationRegistry.getContext(), lock, mSettingsFile, configKey, SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper()); synchronized (lock) { @@ -578,7 +718,8 @@ public class SettingsStateTest extends AndroidTestCase { assertEquals(VALUE2, settingsState.getSettingLocked(FLAG_NAME_2).getValue()); } - settingsState = new SettingsState(getContext(), lock, mSettingsFile, configKey, + settingsState = new SettingsState( + InstrumentationRegistry.getContext(), lock, mSettingsFile, configKey, SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper()); synchronized (lock) { @@ -589,6 +730,7 @@ public class SettingsStateTest extends AndroidTestCase { } } + @Test public void testStagingTransformation() { assertEquals(INVALID_STAGED_FLAG_1, SettingsState.createRealFlagName(INVALID_STAGED_FLAG_1)); @@ -603,11 +745,12 @@ public class SettingsStateTest extends AndroidTestCase { SettingsState.createRealFlagName(VALID_STAGED_FLAG_1)); } + @Test public void testInvalidStagedFlagsUnaffectedByReboot() { int configKey = SettingsState.makeKey(SettingsState.SETTINGS_TYPE_CONFIG, 0); Object lock = new Object(); SettingsState settingsState = new SettingsState( - getContext(), lock, mSettingsFile, configKey, + InstrumentationRegistry.getContext(), lock, mSettingsFile, configKey, SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper()); synchronized (lock) { @@ -620,7 +763,8 @@ public class SettingsStateTest extends AndroidTestCase { assertEquals(VALUE2, settingsState.getSettingLocked(INVALID_STAGED_FLAG_1).getValue()); } - settingsState = new SettingsState(getContext(), lock, mSettingsFile, configKey, + settingsState = new SettingsState( + InstrumentationRegistry.getContext(), lock, mSettingsFile, configKey, SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper()); synchronized (lock) { @@ -628,6 +772,7 @@ public class SettingsStateTest extends AndroidTestCase { } } + @Test public void testsetSettingsLockedKeepTrunkDefault() throws Exception { final PrintStream os = new PrintStream(new FileOutputStream(mSettingsFile)); os.print( @@ -648,7 +793,7 @@ public class SettingsStateTest extends AndroidTestCase { int configKey = SettingsState.makeKey(SettingsState.SETTINGS_TYPE_CONFIG, 0); SettingsState settingsState = new SettingsState( - getContext(), mLock, mSettingsFile, configKey, + InstrumentationRegistry.getContext(), mLock, mSettingsFile, configKey, SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper()); String prefix = "test_namespace"; @@ -705,6 +850,7 @@ public class SettingsStateTest extends AndroidTestCase { } } + @Test public void testsetSettingsLockedNoTrunkDefault() throws Exception { final PrintStream os = new PrintStream(new FileOutputStream(mSettingsFile)); os.print( @@ -720,7 +866,7 @@ public class SettingsStateTest extends AndroidTestCase { int configKey = SettingsState.makeKey(SettingsState.SETTINGS_TYPE_CONFIG, 0); SettingsState settingsState = new SettingsState( - getContext(), mLock, mSettingsFile, configKey, + InstrumentationRegistry.getContext(), mLock, mSettingsFile, configKey, SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper()); Map<String, String> keyValues = diff --git a/packages/SoundPicker/src/com/android/soundpicker/RingtonePickerActivity.java b/packages/SoundPicker/src/com/android/soundpicker/RingtonePickerActivity.java index ea46c0cee6b9..ee81813b4245 100644 --- a/packages/SoundPicker/src/com/android/soundpicker/RingtonePickerActivity.java +++ b/packages/SoundPicker/src/com/android/soundpicker/RingtonePickerActivity.java @@ -300,6 +300,8 @@ public final class RingtonePickerActivity extends AlertActivity implements } }; installTask.execute(data.getData()); + } else if (requestCode == ADD_FILE_REQUEST_CODE && resultCode == RESULT_CANCELED) { + setupAlert(); } } diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index f5c4843e1324..ba7738005de2 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -542,6 +542,13 @@ flag { namespace: "systemui" description: "Binds Keyguard Media Controller Visibility to MediaContainerView" bug: "298213983" +} + +flag { + name: "delayed_wakelock_release_on_background_thread" + namespace: "systemui" + description: "Released delayed wakelocks on background threads to avoid janking screen transitions." + bug: "316128516" metadata { purpose: PURPOSE_BUGFIX } diff --git a/packages/SystemUI/compose/core/src/com/android/compose/PlatformSlider.kt b/packages/SystemUI/compose/core/src/com/android/compose/PlatformSlider.kt index 62dd4ac8c230..ef15c8461b95 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/PlatformSlider.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/PlatformSlider.kt @@ -152,7 +152,10 @@ fun PlatformSlider( modifier = Modifier.fillMaxHeight() .weight(1f) - .padding(start = { paddingStart.roundToPx() }), + .padding( + start = { paddingStart.roundToPx() }, + end = { sliderHeight.roundToPx() / 2 }, + ), contentAlignment = Alignment.CenterStart, ) { labelComposable(isDragging) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt index be5aa8a4c3b9..7535a51675e3 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt @@ -9,7 +9,7 @@ import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp +import androidx.compose.ui.res.dimensionResource import com.android.compose.animation.scene.Edge import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.FixedSizeEdgeDetector @@ -29,6 +29,7 @@ import com.android.systemui.communal.shared.model.ObservableCommunalTransitionSt import com.android.systemui.communal.ui.compose.extensions.allowGestures import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel import com.android.systemui.communal.ui.viewmodel.CommunalViewModel +import com.android.systemui.res.R import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.transform @@ -91,7 +92,10 @@ fun CommunalContainer( SceneTransitionLayout( state = sceneTransitionLayoutState, modifier = modifier.fillMaxSize().allowGestures(allowed = touchesAllowed), - swipeSourceDetector = FixedSizeEdgeDetector(ContainerDimensions.EdgeSwipeSize), + swipeSourceDetector = + FixedSizeEdgeDetector( + dimensionResource(id = R.dimen.communal_gesture_initiation_width) + ), ) { scene( TransitionSceneKey.Blank, @@ -167,7 +171,3 @@ fun ObservableTransitionState.toModel(): ObservableCommunalTransitionState { ) } } - -object ContainerDimensions { - val EdgeSwipeSize = 40.dp -} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt index 66cef86fb773..6875bc544a55 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt @@ -40,7 +40,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -51,6 +50,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.res.colorResource import androidx.compose.ui.unit.dp import com.android.compose.animation.scene.SceneScope import com.android.compose.animation.scene.TransitionState @@ -62,6 +62,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.qs.footer.ui.compose.FooterActions import com.android.systemui.qs.ui.viewmodel.QuickSettingsSceneViewModel +import com.android.systemui.res.R import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.ui.composable.ComposableScene import com.android.systemui.scene.ui.composable.asComposeAware @@ -168,7 +169,7 @@ private fun SceneScope.QuickSettingsScene( modifier = Modifier.element(Shade.Elements.BackgroundScrim) .fillMaxSize() - .background(MaterialTheme.colorScheme.scrim) + .background(colorResource(R.color.shade_scrim_background_dark)) ) Column( horizontalAlignment = Alignment.CenterHorizontally, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt index 2e0ce42ee713..8484b7f5273f 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt @@ -26,7 +26,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -37,6 +36,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.layout.Layout import androidx.compose.ui.layout.layout import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.unit.dp import com.android.compose.animation.scene.ElementKey @@ -171,7 +171,7 @@ private fun SceneScope.ShadeScene( modifier = modifier .element(Shade.Elements.BackgroundScrim) - .background(MaterialTheme.colorScheme.scrim), + .background(colorResource(R.color.shade_scrim_background_dark)), ) Box { Layout( diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/bottombar/ui/BottomBarComponent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/bottombar/ui/BottomBarComponent.kt index d40126198c33..c08eb94f25c0 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/bottombar/ui/BottomBarComponent.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/bottombar/ui/BottomBarComponent.kt @@ -19,17 +19,17 @@ package com.android.systemui.volume.panel.component.bottombar.ui import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import com.android.compose.PlatformButton -import com.android.compose.PlatformOutlinedButton import com.android.systemui.res.R import com.android.systemui.volume.panel.component.bottombar.ui.viewmodel.BottomBarViewModel import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope @@ -47,11 +47,11 @@ constructor( @Composable override fun VolumePanelComposeScope.Content(modifier: Modifier) { Row( - modifier = modifier.height(if (isLargeScreen) 54.dp else 48.dp).fillMaxWidth(), + modifier = modifier.heightIn(min = if (isLargeScreen) 54.dp else 48.dp).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { - PlatformOutlinedButton( + OutlinedButton( onClick = viewModel::onSettingsClicked, colors = ButtonDefaults.outlinedButtonColors( @@ -60,8 +60,8 @@ constructor( ) { Text(text = stringResource(R.string.volume_panel_dialog_settings_button)) } - PlatformButton(onClick = viewModel::onDoneClicked) { - Text(text = stringResource(R.string.inline_done_button)) + Button(onClick = viewModel::onDoneClicked) { + Text(stringResource(R.string.inline_done_button)) } } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/mediaoutput/ui/composable/MediaOutputComponent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/mediaoutput/ui/composable/MediaOutputComponent.kt index d49fed5d6e10..b3fcc305e6b5 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/mediaoutput/ui/composable/MediaOutputComponent.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/mediaoutput/ui/composable/MediaOutputComponent.kt @@ -27,6 +27,7 @@ import androidx.compose.animation.scaleOut import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically import androidx.compose.animation.togetherWith +import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -46,7 +47,6 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.android.compose.animation.Expandable import com.android.systemui.common.ui.compose.Icon @@ -78,8 +78,8 @@ constructor( color = MaterialTheme.colorScheme.surface, shape = RoundedCornerShape(28.dp), onClick = { viewModel.onBarClick(it) }, - ) { - Row { + ) { _ -> + Row(verticalAlignment = Alignment.CenterVertically) { connectedDeviceViewModel?.let { ConnectedDeviceText(it) } deviceIconViewModel?.let { ConnectedDeviceIcon(it) } @@ -90,26 +90,23 @@ constructor( @Composable private fun RowScope.ConnectedDeviceText(connectedDeviceViewModel: ConnectedDeviceViewModel) { Column( - modifier = - Modifier.weight(1f) - .padding(start = 24.dp, top = 20.dp, bottom = 20.dp) - .fillMaxHeight(), + modifier = Modifier.weight(1f).padding(start = 24.dp), verticalArrangement = Arrangement.spacedBy(4.dp), ) { Text( - connectedDeviceViewModel.label.toString(), + modifier = Modifier.basicMarquee(), + text = connectedDeviceViewModel.label.toString(), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 1, - overflow = TextOverflow.Ellipsis, ) connectedDeviceViewModel.deviceName?.let { Text( - it.toString(), + modifier = Modifier.basicMarquee(), + text = it.toString(), style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onSurface, maxLines = 1, - overflow = TextOverflow.Ellipsis, ) } } 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 ddc9252a5a4a..4d810dfce89d 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 @@ -36,8 +36,6 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.IconButtonDefaults @@ -70,7 +68,7 @@ fun ColumnVolumeSliders( require(viewModels.isNotEmpty()) var isExpanded: Boolean by remember(isExpandable) { mutableStateOf(!isExpandable) } val transition = updateTransition(isExpanded, label = "CollapsableSliders") - Column(modifier = modifier.verticalScroll(rememberScrollState())) { + Column(modifier = modifier) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp), 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 0d94bb06c06f..18a62dca3769 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 @@ -20,6 +20,7 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateContentSize import androidx.compose.animation.expandVertically import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.layout.Column import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -59,7 +60,12 @@ fun VolumeSlider( colors = sliderColors, label = { Column(modifier = Modifier.animateContentSize()) { - Text(state.label, style = MaterialTheme.typography.titleMedium) + Text( + modifier = Modifier.basicMarquee(), + text = state.label, + style = MaterialTheme.typography.titleMedium, + maxLines = 1, + ) state.disabledMessage?.let { message -> AnimatedVisibility( @@ -67,7 +73,12 @@ fun VolumeSlider( enter = expandVertically { it }, exit = shrinkVertically { it }, ) { - Text(text = message, style = MaterialTheme.typography.bodySmall) + Text( + modifier = Modifier.basicMarquee(), + text = message, + style = MaterialTheme.typography.bodySmall, + maxLines = 1, + ) } } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/HorizontalVolumePanelContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/HorizontalVolumePanelContent.kt index a838a99524a3..ac5004e16a3b 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/HorizontalVolumePanelContent.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/HorizontalVolumePanelContent.kt @@ -21,6 +21,8 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -35,7 +37,7 @@ fun VolumePanelComposeScope.HorizontalVolumePanelContent( val spacing = 20.dp Row(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(space = spacing)) { Column( - modifier = Modifier.weight(1f), + modifier = Modifier.weight(1f).verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(spacing) ) { for (component in layout.contentComponents) { @@ -46,7 +48,7 @@ fun VolumePanelComposeScope.HorizontalVolumePanelContent( } Column( - modifier = Modifier.weight(1f), + modifier = Modifier.weight(1f).verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(space = spacing, alignment = Alignment.Top) ) { for (component in layout.headerComponents) { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VerticalVolumePanelContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VerticalVolumePanelContent.kt index 4d073798c70c..dd767817a5ae 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VerticalVolumePanelContent.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VerticalVolumePanelContent.kt @@ -22,6 +22,8 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @@ -33,7 +35,7 @@ fun VolumePanelComposeScope.VerticalVolumePanelContent( modifier: Modifier = Modifier, ) { Column( - modifier = modifier, + modifier = modifier.verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(20.dp), ) { for (component in layout.headerComponents) { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelRoot.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelRoot.kt index 8a9ebc918be6..910cd5ec107b 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelRoot.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelRoot.kt @@ -21,6 +21,8 @@ import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.displayCutout import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn @@ -36,13 +38,17 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.max import com.android.compose.theme.PlatformTheme import com.android.systemui.res.R import com.android.systemui.volume.panel.ui.layout.ComponentsLayout import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelState import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelViewModel +import kotlin.math.max private val padding = 24.dp @@ -65,12 +71,12 @@ fun VolumePanelRoot( val components by viewModel.componentsLayout.collectAsState(null) with(VolumePanelComposeScope(state)) { - var boxModifier = modifier.fillMaxSize().clickable(onClick = onDismiss) - if (!isPortrait) { - boxModifier = boxModifier.padding(horizontal = 48.dp) - } Box( - modifier = boxModifier, + modifier = + modifier + .fillMaxSize() + .clickable(onClick = onDismiss) + .volumePanelPaddings(isPortrait = isPortrait), contentAlignment = Alignment.BottomCenter, ) { val radius = dimensionResource(R.dimen.volume_panel_corner_radius) @@ -80,8 +86,8 @@ fun VolumePanelRoot( interactionSource = null, indication = null, onClick = { - // prevent windowCloseOnTouchOutside from dismissing when tapped on - // the panel itself. + // prevent windowCloseOnTouchOutside from dismissing when tapped + // on the panel itself. }, ), shape = RoundedCornerShape(topStart = radius, topEnd = radius), @@ -110,7 +116,7 @@ private fun VolumePanelComposeScope.Components( layout: ComponentsLayout, modifier: Modifier = Modifier ) { - val arrangement = + val arrangement: Arrangement.Vertical = if (isLargeScreen) { Arrangement.spacedBy(20.dp) } else { @@ -120,16 +126,21 @@ private fun VolumePanelComposeScope.Components( modifier = modifier.widthIn(max = 800.dp), verticalArrangement = arrangement, ) { - val contentModifier = Modifier if (isPortrait || isLargeScreen) { - VerticalVolumePanelContent(modifier = contentModifier, layout = layout) + VerticalVolumePanelContent( + modifier = Modifier.weight(weight = 1f, fill = false), + layout = layout + ) } else { HorizontalVolumePanelContent( - modifier = contentModifier.heightIn(max = 212.dp), - layout = layout + modifier = Modifier.weight(weight = 1f, fill = false).heightIn(max = 212.dp), + layout = layout, ) } - BottomBar(layout = layout, modifier = Modifier) + BottomBar( + modifier = Modifier, + layout = layout, + ) } } @@ -149,3 +160,28 @@ private fun VolumePanelComposeScope.BottomBar( } } } + +/** + * Makes sure volume panel stays symmetrically in the middle of the screen while still avoiding + * being under the cutouts. + */ +@Composable +private fun Modifier.volumePanelPaddings(isPortrait: Boolean): Modifier { + val cutout = WindowInsets.displayCutout + return with(LocalDensity.current) { + val horizontalCutout = + max( + cutout.getLeft(density = this, layoutDirection = LocalLayoutDirection.current), + cutout.getRight(density = this, layoutDirection = LocalLayoutDirection.current) + ) + val minHorizontalPadding = if (isPortrait) 0.dp else 48.dp + val horizontalPadding = max(horizontalCutout.toDp(), minHorizontalPadding) + + padding( + start = horizontalPadding, + top = cutout.getTop(this).toDp(), + end = horizontalPadding, + bottom = cutout.getBottom(this).toDp(), + ) + } +} diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt index 187d82a9e626..b94e49bb0edc 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt @@ -36,44 +36,38 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.launch -internal class SceneGestureHandler( - internal val layoutImpl: SceneTransitionLayoutImpl, - internal val orientation: Orientation, - private val coroutineScope: CoroutineScope, -) { - private val layoutState = layoutImpl.state - val draggable: DraggableHandler = SceneDraggableHandler(this) - - private var _swipeTransition: SwipeTransition? = null - private var swipeTransition: SwipeTransition - get() = _swipeTransition ?: error("SwipeTransition needs to be initialized") - set(value) { - _swipeTransition = value - } +interface DraggableHandler { + /** + * Start a drag in the given [startedPosition], with the given [overSlop] and number of + * [pointersDown]. + * + * The returned [DragController] should be used to continue or stop the drag. + */ + fun onDragStarted(startedPosition: Offset?, overSlop: Float, pointersDown: Int): DragController +} - private fun updateTransition(newTransition: SwipeTransition, force: Boolean = false) { - if (isDrivingTransition || force) { - layoutState.startTransition(newTransition, newTransition.key) +/** + * The [DragController] provides control over the transition between two scenes through the [onDrag] + * and [onStop] methods. + */ +interface DragController { + /** Drag the current scene by [delta] pixels. */ + fun onDrag(delta: Float) - // Initialize SwipeTransition.transformationSpec and .swipeSpec. Note that this must be - // called right after layoutState.startTransition() is called, because it computes the - // current layoutState.transformationSpec(). - val transformationSpec = layoutState.transformationSpec - newTransition.transformationSpec = transformationSpec - newTransition.swipeSpec = - transformationSpec.swipeSpec ?: layoutState.transitions.defaultSwipeSpec - } else { - // We were not driving the transition and we don't force the update, so the specs won't - // be used and it doesn't matter which ones we set here. - newTransition.transformationSpec = TransformationSpec.Empty - newTransition.swipeSpec = SceneTransitions.DefaultSwipeSpec - } + /** Starts a transition to a target scene. */ + fun onStop(velocity: Float, canChangeScene: Boolean) +} - swipeTransition = newTransition - } +internal class DraggableHandlerImpl( + internal val layoutImpl: SceneTransitionLayoutImpl, + internal val orientation: Orientation, + internal val coroutineScope: CoroutineScope, +) : DraggableHandler { + /** The [DraggableHandler] can only have one active [DragController] at a time. */ + private var dragController: DragControllerImpl? = null - internal val isDrivingTransition - get() = layoutState.transitionState == _swipeTransition + internal val isDrivingTransition: Boolean + get() = dragController?.isDrivingTransition == true /** * The velocity threshold at which the intent of the user is to swipe up or down. It is the same @@ -86,14 +80,9 @@ internal class SceneGestureHandler( * The positional threshold at which the intent of the user is to swipe to the next scene. It is * the same as SwipeableV2Defaults.PositionalThreshold. */ - private val positionalThreshold + internal val positionalThreshold get() = with(layoutImpl.density) { 56.dp.toPx() } - internal var currentSource: Any? = null - - /** The [Swipes] associated to the current gesture. */ - private var swipes: Swipes? = null - /** * Whether we should immediately intercept a gesture. * @@ -102,35 +91,52 @@ internal class SceneGestureHandler( */ internal fun shouldImmediatelyIntercept(startedPosition: Offset?): Boolean { // We don't intercept the touch if we are not currently driving the transition. - if (!isDrivingTransition) { + val dragController = dragController + if (dragController?.isDrivingTransition != true) { return false } // Only intercept the current transition if one of the 2 swipes results is also a transition // between the same pair of scenes. + val swipeTransition = dragController.swipeTransition val fromScene = swipeTransition._currentScene val swipes = computeSwipes(fromScene, startedPosition, pointersDown = 1) - val (upOrLeft, downOrRight) = computeSwipesResults(fromScene, swipes) + val (upOrLeft, downOrRight) = swipes.computeSwipesResults(fromScene) return (upOrLeft != null && swipeTransition.isTransitioningBetween(fromScene.key, upOrLeft.toScene)) || (downOrRight != null && swipeTransition.isTransitioningBetween(fromScene.key, downOrRight.toScene)) } - internal fun onDragStarted(pointersDown: Int, startedPosition: Offset?, overSlop: Float) { + override fun onDragStarted( + startedPosition: Offset?, + overSlop: Float, + pointersDown: Int, + ): DragController { if (overSlop == 0f) { - check(isDrivingTransition) { - "onDragStarted() called while isDrivingTransition=false overSlop=0f" + val oldDragController = dragController + check(oldDragController != null && oldDragController.isDrivingTransition) { + val isActive = oldDragController?.isDrivingTransition + "onDragStarted(overSlop=0f) requires an active dragController, but was $isActive" } // This [transition] was already driving the animation: simply take over it. // Stop animating and start from where the current offset. - swipeTransition.cancelOffsetAnimation() - swipes!!.updateSwipesResults(swipeTransition._fromScene) - return + oldDragController.swipeTransition.cancelOffsetAnimation() + + // We need to recompute the swipe results since this is a new gesture, and the + // fromScene.userActions may have changed. + val swipes = oldDragController.swipes + swipes.updateSwipesResults(oldDragController.swipeTransition._fromScene) + + // A new gesture should always create a new SwipeTransition. This way there cannot be + // different gestures controlling the same transition. + val swipeTransition = SwipeTransition(oldDragController.swipeTransition) + swipes.updateSwipesResults(fromScene = swipeTransition._fromScene) + return updateDragController(swipes, swipeTransition) } - val transitionState = layoutState.transitionState + val transitionState = layoutImpl.state.transitionState if (transitionState is TransitionState.Transition) { // TODO(b/290184746): Better handle interruptions here if state != idle. Log.w( @@ -142,24 +148,27 @@ internal class SceneGestureHandler( } val fromScene = layoutImpl.scene(transitionState.currentScene) - val newSwipes = computeSwipes(fromScene, startedPosition, pointersDown) - swipes = newSwipes - val result = newSwipes.findUserActionResult(fromScene, overSlop, true) + val swipes = computeSwipes(fromScene, startedPosition, pointersDown) + val result = swipes.findUserActionResult(fromScene, overSlop, true) // As we were unable to locate a valid target scene, the initial SwipeTransition cannot be - // defined. - if (result == null) return + // defined. Consequently, a simple NoOp Controller will be returned. + if (result == null) return NoOpDragController - val newSwipeTransition = - SwipeTransition( - fromScene = fromScene, - result = result, - swipes = newSwipes, - layoutImpl = layoutImpl, - orientation = orientation - ) + return updateDragController( + swipes = swipes, + swipeTransition = SwipeTransition(fromScene, result, swipes, layoutImpl, orientation) + ) + } - updateTransition(newSwipeTransition, force = true) + private fun updateDragController( + swipes: Swipes, + swipeTransition: SwipeTransition + ): DragController { + val newDragController = DragControllerImpl(this, swipes, swipeTransition) + newDragController.updateTransition(swipeTransition, force = true) + dragController = newDragController + return newDragController } private fun computeSwipes( @@ -216,7 +225,58 @@ internal class SceneGestureHandler( } } - internal fun onDrag(delta: Float) { + companion object { + private const val TAG = "DraggableHandlerImpl" + } +} + +/** @param swipes The [Swipes] associated to the current gesture. */ +private class DragControllerImpl( + private val draggableHandler: DraggableHandlerImpl, + val swipes: Swipes, + var swipeTransition: SwipeTransition, +) : DragController { + val layoutState = draggableHandler.layoutImpl.state + + /** + * Whether this handle is active. If this returns false, calling [onDrag] and [onStop] will do + * nothing. We should have only one active controller at a time + */ + val isDrivingTransition: Boolean + get() = layoutState.transitionState == swipeTransition + + init { + check(!isDrivingTransition) { "Multiple controllers with the same SwipeTransition" } + } + + fun updateTransition(newTransition: SwipeTransition, force: Boolean = false) { + if (isDrivingTransition || force) { + layoutState.startTransition(newTransition, newTransition.key) + + // Initialize SwipeTransition.transformationSpec and .swipeSpec. Note that this must be + // called right after layoutState.startTransition() is called, because it computes the + // current layoutState.transformationSpec(). + val transformationSpec = layoutState.transformationSpec + newTransition.transformationSpec = transformationSpec + newTransition.swipeSpec = + transformationSpec.swipeSpec ?: layoutState.transitions.defaultSwipeSpec + } else { + // We were not driving the transition and we don't force the update, so the specs won't + // be used and it doesn't matter which ones we set here. + newTransition.transformationSpec = TransformationSpec.Empty + newTransition.swipeSpec = SceneTransitions.DefaultSwipeSpec + } + + swipeTransition = newTransition + } + + /** + * We receive a [delta] that can be consumed to change the offset of the current + * [SwipeTransition]. + * + * @return the consumed delta + */ + override fun onDrag(delta: Float) { if (delta == 0f || !isDrivingTransition) return swipeTransition.dragOffset += delta @@ -225,14 +285,14 @@ internal class SceneGestureHandler( val isNewFromScene = fromScene.key != swipeTransition.fromScene val result = - swipes!!.findUserActionResult( + swipes.findUserActionResult( fromScene = fromScene, directionOffset = swipeTransition.dragOffset, updateSwipesResults = isNewFromScene ) if (result == null) { - onDragStopped(velocity = delta, canChangeScene = true) + onStop(velocity = delta, canChangeScene = true) return } @@ -243,34 +303,18 @@ internal class SceneGestureHandler( result.toScene != swipeTransition.toScene || result.transitionKey != swipeTransition.key ) { - val newSwipeTransition = + val swipeTransition = SwipeTransition( fromScene = fromScene, result = result, - swipes = swipes!!, - layoutImpl = layoutImpl, - orientation = orientation + swipes = swipes, + layoutImpl = draggableHandler.layoutImpl, + orientation = draggableHandler.orientation, ) .apply { dragOffset = swipeTransition.dragOffset } - updateTransition(newSwipeTransition) - } - } - - private fun computeSwipesResults( - fromScene: Scene, - swipes: Swipes - ): Pair<UserActionResult?, UserActionResult?> { - val userActions = fromScene.userActions - fun sceneToSwipePair(swipe: Swipe?): UserActionResult? { - return userActions[swipe ?: return null] + updateTransition(swipeTransition) } - - val upOrLeftResult = - sceneToSwipePair(swipes.upOrLeft) ?: sceneToSwipePair(swipes.upOrLeftNoSource) - val downOrRightResult = - sceneToSwipePair(swipes.downOrRight) ?: sceneToSwipePair(swipes.downOrRightNoSource) - return Pair(upOrLeftResult, downOrRightResult) } /** @@ -302,18 +346,22 @@ internal class SceneGestureHandler( // to the next screen or go back to the previous one. val offset = swipeTransition.dragOffset val absoluteDistance = distance.absoluteValue - return if (offset <= -absoluteDistance && swipes!!.upOrLeftResult?.toScene == toScene.key) { + return if (offset <= -absoluteDistance && swipes.upOrLeftResult?.toScene == toScene.key) { toScene to absoluteDistance - } else if ( - offset >= absoluteDistance && swipes!!.downOrRightResult?.toScene == toScene.key - ) { + } else if (offset >= absoluteDistance && swipes.downOrRightResult?.toScene == toScene.key) { toScene to -absoluteDistance } else { fromScene to 0f } } - internal fun onDragStopped(velocity: Float, canChangeScene: Boolean) { + private fun snapToScene(scene: SceneKey) { + if (!isDrivingTransition) return + swipeTransition.cancelOffsetAnimation() + layoutState.finishTransition(swipeTransition, idleScene = scene) + } + + override fun onStop(velocity: Float, canChangeScene: Boolean) { // The state was changed since the drag started; don't do anything. if (!isDrivingTransition) { return @@ -332,16 +380,16 @@ internal class SceneGestureHandler( // immediately go back B => A. if (targetScene != swipeTransition._currentScene) { swipeTransition._currentScene = targetScene - with(layoutImpl.state) { coroutineScope.onChangeScene(targetScene.key) } + with(draggableHandler.layoutImpl.state) { + draggableHandler.coroutineScope.onChangeScene(targetScene.key) + } } swipeTransition.animateOffset( - coroutineScope = coroutineScope, + coroutineScope = draggableHandler.coroutineScope, initialVelocity = velocity, targetOffset = targetOffset, - onAnimationCompleted = { - layoutState.finishTransition(swipeTransition, idleScene = targetScene.key) - } + onAnimationCompleted = { snapToScene(targetScene.key) } ) } @@ -400,10 +448,10 @@ internal class SceneGestureHandler( if (startFromIdlePosition) { // If there is a target scene, we start the overscroll animation. - val result = swipes!!.findUserActionResultStrict(velocity) + val result = swipes.findUserActionResultStrict(velocity) if (result == null) { // We will not animate - layoutState.finishTransition(swipeTransition, idleScene = fromScene.key) + snapToScene(fromScene.key) return } @@ -411,9 +459,9 @@ internal class SceneGestureHandler( SwipeTransition( fromScene = fromScene, result = result, - swipes = swipes!!, - layoutImpl = layoutImpl, - orientation = orientation + swipes = swipes, + layoutImpl = draggableHandler.layoutImpl, + orientation = draggableHandler.orientation, ) .apply { _currentScene = swipeTransition._currentScene } @@ -440,6 +488,9 @@ internal class SceneGestureHandler( return (offset - distance).absoluteValue < offset.absoluteValue } + val velocityThreshold = draggableHandler.velocityThreshold + val positionalThreshold = draggableHandler.positionalThreshold + // Swiping up or left. if (distance < 0f) { return if (offset > 0f || velocity >= velocityThreshold) { @@ -460,10 +511,6 @@ internal class SceneGestureHandler( isCloserToTarget() } } - - companion object { - private const val TAG = "SceneGestureHandler" - } } private fun SwipeTransition( @@ -492,11 +539,26 @@ private fun SwipeTransition( ) } +private fun SwipeTransition(old: SwipeTransition): SwipeTransition { + return SwipeTransition( + key = old.key, + _fromScene = old._fromScene, + _toScene = old._toScene, + userActionDistanceScope = old.userActionDistanceScope, + orientation = old.orientation, + isUpOrLeft = old.isUpOrLeft + ) + .apply { + _currentScene = old._currentScene + dragOffset = old.dragOffset + } +} + private class SwipeTransition( val key: TransitionKey?, val _fromScene: Scene, val _toScene: Scene, - private val userActionDistanceScope: UserActionDistanceScope, + val userActionDistanceScope: UserActionDistanceScope, override val orientation: Orientation, override val isUpOrLeft: Boolean, ) : @@ -730,40 +792,16 @@ private class Swipes( } } -private class SceneDraggableHandler( - private val gestureHandler: SceneGestureHandler, -) : DraggableHandler { - private val source = this - - override fun onDragStarted(startedPosition: Offset, overSlop: Float, pointersDown: Int) { - gestureHandler.currentSource = source - gestureHandler.onDragStarted(pointersDown, startedPosition, overSlop) - } - - override fun onDelta(pixels: Float) { - if (gestureHandler.currentSource == source) { - gestureHandler.onDrag(delta = pixels) - } - } - - override fun onDragStopped(velocity: Float) { - if (gestureHandler.currentSource == source) { - gestureHandler.currentSource = null - gestureHandler.onDragStopped(velocity = velocity, canChangeScene = true) - } - } -} - -internal class SceneNestedScrollHandler( +internal class NestedScrollHandlerImpl( private val layoutImpl: SceneTransitionLayoutImpl, private val orientation: Orientation, private val topOrLeftBehavior: NestedScrollBehavior, private val bottomOrRightBehavior: NestedScrollBehavior, -) : NestedScrollHandler { +) { private val layoutState = layoutImpl.state - private val gestureHandler = layoutImpl.gestureHandler(orientation) + private val draggableHandler = layoutImpl.draggableHandler(orientation) - override val connection: PriorityNestedScrollConnection = nestedScrollConnection() + val connection: PriorityNestedScrollConnection = nestedScrollConnection() private fun nestedScrollConnection(): PriorityNestedScrollConnection { // If we performed a long gesture before entering priority mode, we would have to avoid @@ -808,7 +846,7 @@ internal class SceneNestedScrollHandler( return overscrollSpec != null } - val source = this + var dragController: DragController? = null var isIntercepting = false return PriorityNestedScrollConnection( @@ -819,7 +857,7 @@ internal class SceneNestedScrollHandler( val canInterceptSwipeTransition = canChangeScene && offsetAvailable != 0f && - gestureHandler.shouldImmediatelyIntercept(startedPosition = null) + draggableHandler.shouldImmediatelyIntercept(startedPosition = null) if (!canInterceptSwipeTransition) return@PriorityNestedScrollConnection false val threshold = layoutImpl.transitionInterceptionThreshold @@ -893,34 +931,28 @@ internal class SceneNestedScrollHandler( canContinueScroll = { true }, canScrollOnFling = false, onStart = { offsetAvailable -> - gestureHandler.currentSource = source - gestureHandler.onDragStarted( - pointersDown = 1, - startedPosition = null, - overSlop = if (isIntercepting) 0f else offsetAvailable, - ) + dragController = + draggableHandler.onDragStarted( + pointersDown = 1, + startedPosition = null, + overSlop = if (isIntercepting) 0f else offsetAvailable, + ) }, onScroll = { offsetAvailable -> - if (gestureHandler.currentSource != source) { - return@PriorityNestedScrollConnection 0f - } + val controller = dragController ?: error("Should be called after onStart") // TODO(b/297842071) We should handle the overscroll or slow drag if the gesture is // initiated in a nested child. - gestureHandler.onDrag(offsetAvailable) + controller.onDrag(delta = offsetAvailable) offsetAvailable }, onStop = { velocityAvailable -> - if (gestureHandler.currentSource != source) { - return@PriorityNestedScrollConnection 0f - } + val controller = dragController ?: error("Should be called after onStart") - gestureHandler.onDragStopped( - velocity = velocityAvailable, - canChangeScene = canChangeScene - ) + controller.onStop(velocity = velocityAvailable, canChangeScene = canChangeScene) + dragController = null // The onDragStopped animation consumes any remaining velocity. velocityAvailable }, @@ -935,3 +967,9 @@ internal class SceneNestedScrollHandler( // TODO(b/290184746): Have a better default visibility threshold which takes the swipe distance into // account instead. internal const val OffsetVisibilityThreshold = 0.5f + +private object NoOpDragController : DragController { + override fun onDrag(delta: Float) {} + + override fun onStop(velocity: Float, canChangeScene: Boolean) {} +} diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt deleted file mode 100644 index 58052cd60f39..000000000000 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/GestureHandler.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.android.compose.animation.scene - -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.input.nestedscroll.NestedScrollConnection - -interface DraggableHandler { - fun onDragStarted(startedPosition: Offset, overSlop: Float, pointersDown: Int = 1) - fun onDelta(pixels: Float) - fun onDragStopped(velocity: Float) -} - -interface NestedScrollHandler { - val connection: NestedScrollConnection -} diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt index 3ff869b5fdad..05dd5cc09dbf 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt @@ -24,7 +24,6 @@ import androidx.compose.foundation.gestures.awaitVerticalTouchSlopOrCancellation import androidx.compose.foundation.gestures.horizontalDrag import androidx.compose.foundation.gestures.verticalDrag import androidx.compose.runtime.Stable -import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.pointer.PointerEvent @@ -33,7 +32,6 @@ import androidx.compose.ui.input.pointer.PointerId import androidx.compose.ui.input.pointer.PointerInputChange import androidx.compose.ui.input.pointer.PointerInputScope import androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNode -import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.positionChange import androidx.compose.ui.input.pointer.util.VelocityTracker import androidx.compose.ui.input.pointer.util.addPointerInputChange @@ -69,9 +67,7 @@ internal fun Modifier.multiPointerDraggable( orientation: Orientation, enabled: () -> Boolean, startDragImmediately: (startedPosition: Offset) -> Boolean, - onDragStarted: (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> Unit, - onDragDelta: (delta: Float) -> Unit, - onDragStopped: (velocity: Float) -> Unit, + onDragStarted: (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController, ): Modifier = this.then( MultiPointerDraggableElement( @@ -79,8 +75,6 @@ internal fun Modifier.multiPointerDraggable( enabled, startDragImmediately, onDragStarted, - onDragDelta, - onDragStopped, ) ) @@ -89,9 +83,7 @@ private data class MultiPointerDraggableElement( private val enabled: () -> Boolean, private val startDragImmediately: (startedPosition: Offset) -> Boolean, private val onDragStarted: - (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> Unit, - private val onDragDelta: (Float) -> Unit, - private val onDragStopped: (velocity: Float) -> Unit, + (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController, ) : ModifierNodeElement<MultiPointerDraggableNode>() { override fun create(): MultiPointerDraggableNode = MultiPointerDraggableNode( @@ -99,8 +91,6 @@ private data class MultiPointerDraggableElement( enabled = enabled, startDragImmediately = startDragImmediately, onDragStarted = onDragStarted, - onDragDelta = onDragDelta, - onDragStopped = onDragStopped, ) override fun update(node: MultiPointerDraggableNode) { @@ -108,8 +98,6 @@ private data class MultiPointerDraggableElement( node.enabled = enabled node.startDragImmediately = startDragImmediately node.onDragStarted = onDragStarted - node.onDragDelta = onDragDelta - node.onDragStopped = onDragStopped } } @@ -117,9 +105,8 @@ internal class MultiPointerDraggableNode( orientation: Orientation, enabled: () -> Boolean, var startDragImmediately: (startedPosition: Offset) -> Boolean, - var onDragStarted: (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> Unit, - var onDragDelta: (Float) -> Unit, - var onDragStopped: (velocity: Float) -> Unit, + var onDragStarted: + (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController, ) : PointerInputModifierNode, DelegatingNode(), @@ -176,40 +163,33 @@ internal class MultiPointerDraggableNode( return } - val onDragStart: (Offset, Float, Int) -> Unit = { startedPosition, overSlop, pointersDown -> - velocityTracker.resetTracking() - onDragStarted(startedPosition, overSlop, pointersDown) - } - - val onDragCancel: () -> Unit = { onDragStopped(/* velocity= */ 0f) } - - val onDragEnd: () -> Unit = { - val maxFlingVelocity = - currentValueOf(LocalViewConfiguration).maximumFlingVelocity.let { max -> - Velocity(max, max) - } - - val velocity = velocityTracker.calculateVelocity(maxFlingVelocity) - onDragStopped( - when (orientation) { - Orientation.Horizontal -> velocity.x - Orientation.Vertical -> velocity.y - } - ) - } - - val onDrag: (change: PointerInputChange, dragAmount: Float) -> Unit = { change, amount -> - velocityTracker.addPointerInputChange(change) - onDragDelta(amount) - } - detectDragGestures( orientation = orientation, startDragImmediately = startDragImmediately, - onDragStart = onDragStart, - onDragEnd = onDragEnd, - onDragCancel = onDragCancel, - onDrag = onDrag, + onDragStart = { startedPosition, overSlop, pointersDown -> + velocityTracker.resetTracking() + onDragStarted(startedPosition, overSlop, pointersDown) + }, + onDrag = { controller, change, amount -> + velocityTracker.addPointerInputChange(change) + controller.onDrag(amount) + }, + onDragEnd = { controller -> + val viewConfiguration = currentValueOf(LocalViewConfiguration) + val maxVelocity = viewConfiguration.maximumFlingVelocity.let { Velocity(it, it) } + val velocity = velocityTracker.calculateVelocity(maxVelocity) + controller.onStop( + velocity = + when (orientation) { + Orientation.Horizontal -> velocity.x + Orientation.Vertical -> velocity.y + }, + canChangeScene = true, + ) + }, + onDragCancel = { controller -> + controller.onStop(velocity = 0f, canChangeScene = true) + }, ) } } @@ -225,10 +205,10 @@ internal class MultiPointerDraggableNode( private suspend fun PointerInputScope.detectDragGestures( orientation: Orientation, startDragImmediately: (startedPosition: Offset) -> Boolean, - onDragStart: (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> Unit, - onDragEnd: () -> Unit, - onDragCancel: () -> Unit, - onDrag: (change: PointerInputChange, dragAmount: Float) -> Unit, + onDragStart: (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> DragController, + onDragEnd: (controller: DragController) -> Unit, + onDragCancel: (controller: DragController) -> Unit, + onDrag: (controller: DragController, change: PointerInputChange, dragAmount: Float) -> Unit, ) { awaitEachGesture { val initialDown = awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Initial) @@ -282,34 +262,34 @@ private suspend fun PointerInputScope.detectDragGestures( } } - onDragStart(drag.position, overSlop, pressed.size) + val controller = onDragStart(drag.position, overSlop, pressed.size) val successful: Boolean try { - onDrag(drag, overSlop) + onDrag(controller, drag, overSlop) successful = when (orientation) { Orientation.Horizontal -> horizontalDrag(drag.id) { - onDrag(it, it.positionChange().x) + onDrag(controller, it, it.positionChange().x) it.consume() } Orientation.Vertical -> verticalDrag(drag.id) { - onDrag(it, it.positionChange().y) + onDrag(controller, it, it.positionChange().y) it.consume() } } } catch (t: Throwable) { - onDragCancel() + onDragCancel(controller) throw t } if (successful) { - onDragEnd() + onDragEnd(controller) } else { - onDragCancel() + onDragCancel(controller) } } } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt index e78f3266d664..5a2f85ad163c 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt @@ -178,7 +178,7 @@ private fun scenePriorityNestedScrollConnection( topOrLeftBehavior: NestedScrollBehavior, bottomOrRightBehavior: NestedScrollBehavior, ) = - SceneNestedScrollHandler( + NestedScrollHandlerImpl( layoutImpl = layoutImpl, orientation = orientation, topOrLeftBehavior = topOrLeftBehavior, diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt index 3093d477a24c..1670e9cee731 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt @@ -102,8 +102,8 @@ internal class SceneTransitionLayoutImpl( .also { _sharedValues = it } // TODO(b/317958526): Lazily allocate scene gesture handlers the first time they are needed. - private val horizontalGestureHandler: SceneGestureHandler - private val verticalGestureHandler: SceneGestureHandler + private val horizontalDraggableHandler: DraggableHandlerImpl + private val verticalDraggableHandler: DraggableHandlerImpl private var _userActionDistanceScope: UserActionDistanceScope? = null internal val userActionDistanceScope: UserActionDistanceScope @@ -116,27 +116,27 @@ internal class SceneTransitionLayoutImpl( init { updateScenes(builder) - // SceneGestureHandler must wait for the scenes to be initialized, in order to access the + // DraggableHandlerImpl must wait for the scenes to be initialized, in order to access the // current scene (required for SwipeTransition). - horizontalGestureHandler = - SceneGestureHandler( + horizontalDraggableHandler = + DraggableHandlerImpl( layoutImpl = this, orientation = Orientation.Horizontal, coroutineScope = coroutineScope, ) - verticalGestureHandler = - SceneGestureHandler( + verticalDraggableHandler = + DraggableHandlerImpl( layoutImpl = this, orientation = Orientation.Vertical, coroutineScope = coroutineScope, ) } - internal fun gestureHandler(orientation: Orientation): SceneGestureHandler = + internal fun draggableHandler(orientation: Orientation): DraggableHandlerImpl = when (orientation) { - Orientation.Vertical -> verticalGestureHandler - Orientation.Horizontal -> horizontalGestureHandler + Orientation.Vertical -> verticalDraggableHandler + Orientation.Horizontal -> horizontalDraggableHandler } internal fun scene(key: SceneKey): Scene { @@ -192,8 +192,8 @@ internal class SceneTransitionLayoutImpl( // Handle horizontal and vertical swipes on this layout. // Note: order here is important and will give a slight priority to the vertical // swipes. - .swipeToScene(horizontalGestureHandler) - .swipeToScene(verticalGestureHandler) + .swipeToScene(horizontalDraggableHandler) + .swipeToScene(verticalDraggableHandler) .then(LayoutElement(layoutImpl = this)) ) { LookaheadScope { diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt index 61f497818c89..b618369c2369 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt @@ -31,39 +31,39 @@ import androidx.compose.ui.unit.IntSize * Configures the swipeable behavior of a [SceneTransitionLayout] depending on the current state. */ @Stable -internal fun Modifier.swipeToScene(gestureHandler: SceneGestureHandler): Modifier { - return this.then(SwipeToSceneElement(gestureHandler)) +internal fun Modifier.swipeToScene(draggableHandler: DraggableHandlerImpl): Modifier { + return this.then(SwipeToSceneElement(draggableHandler)) } private data class SwipeToSceneElement( - val gestureHandler: SceneGestureHandler, + val draggableHandler: DraggableHandlerImpl, ) : ModifierNodeElement<SwipeToSceneNode>() { - override fun create(): SwipeToSceneNode = SwipeToSceneNode(gestureHandler) + override fun create(): SwipeToSceneNode = SwipeToSceneNode(draggableHandler) override fun update(node: SwipeToSceneNode) { - node.gestureHandler = gestureHandler + node.draggableHandler = draggableHandler } } private class SwipeToSceneNode( - gestureHandler: SceneGestureHandler, + draggableHandler: DraggableHandlerImpl, ) : DelegatingNode(), PointerInputModifierNode { private val delegate = delegate( MultiPointerDraggableNode( - orientation = gestureHandler.orientation, + orientation = draggableHandler.orientation, enabled = ::enabled, startDragImmediately = ::startDragImmediately, - onDragStarted = gestureHandler.draggable::onDragStarted, - onDragDelta = gestureHandler.draggable::onDelta, - onDragStopped = gestureHandler.draggable::onDragStopped, + onDragStarted = draggableHandler::onDragStarted, ) ) - var gestureHandler: SceneGestureHandler = gestureHandler + private var _draggableHandler = draggableHandler + var draggableHandler: DraggableHandlerImpl + get() = _draggableHandler set(value) { - if (value != field) { - field = value + if (_draggableHandler != value) { + _draggableHandler = value // Make sure to update the delegate orientation. Note that this will automatically // reset the underlying pointer input handler, so previous gestures will be @@ -81,12 +81,12 @@ private class SwipeToSceneNode( override fun onCancelPointerInput() = delegate.onCancelPointerInput() private fun enabled(): Boolean { - return gestureHandler.isDrivingTransition || - currentScene().shouldEnableSwipes(gestureHandler.orientation) + return draggableHandler.isDrivingTransition || + currentScene().shouldEnableSwipes(delegate.orientation) } private fun currentScene(): Scene { - val layoutImpl = gestureHandler.layoutImpl + val layoutImpl = draggableHandler.layoutImpl return layoutImpl.scene(layoutImpl.state.transitionState.currentScene) } @@ -98,12 +98,12 @@ private class SwipeToSceneNode( private fun startDragImmediately(startedPosition: Offset): Boolean { // Immediately start the drag if the user can't swipe in the other direction and the gesture // handler can intercept it. - return !canOppositeSwipe() && gestureHandler.shouldImmediatelyIntercept(startedPosition) + return !canOppositeSwipe() && draggableHandler.shouldImmediatelyIntercept(startedPosition) } private fun canOppositeSwipe(): Boolean { val oppositeOrientation = - when (gestureHandler.orientation) { + when (draggableHandler.orientation) { Orientation.Vertical -> Orientation.Horizontal Orientation.Horizontal -> Orientation.Vertical } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt index d28ac6ad546e..eb9b4280aacb 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt @@ -47,7 +47,7 @@ private const val SCREEN_SIZE = 100f private val LAYOUT_SIZE = IntSize(SCREEN_SIZE.toInt(), SCREEN_SIZE.toInt()) @RunWith(AndroidJUnit4::class) -class SceneGestureHandlerTest { +class DraggableHandlerTest { private class TestGestureScope( private val testScope: MonotonicClockTestScope, ) { @@ -99,19 +99,19 @@ class SceneGestureHandlerTest { ) .apply { setScenesTargetSizeForTest(LAYOUT_SIZE) } - val sceneGestureHandler = layoutImpl.gestureHandler(Orientation.Vertical) - val horizontalSceneGestureHandler = layoutImpl.gestureHandler(Orientation.Horizontal) + val draggableHandler = layoutImpl.draggableHandler(Orientation.Vertical) + val horizontalDraggableHandler = layoutImpl.draggableHandler(Orientation.Horizontal) fun nestedScrollConnection(nestedScrollBehavior: NestedScrollBehavior) = - SceneNestedScrollHandler( + NestedScrollHandlerImpl( layoutImpl = layoutImpl, - orientation = sceneGestureHandler.orientation, + orientation = draggableHandler.orientation, topOrLeftBehavior = nestedScrollBehavior, bottomOrRightBehavior = nestedScrollBehavior, ) .connection - val velocityThreshold = sceneGestureHandler.velocityThreshold + val velocityThreshold = draggableHandler.velocityThreshold fun down(fractionOfScreen: Float) = if (fractionOfScreen < 0f) error("use up()") else SCREEN_SIZE * fractionOfScreen @@ -190,20 +190,18 @@ class SceneGestureHandlerTest { fun onDragStarted( startedPosition: Offset = Offset.Zero, overSlop: Float, - pointersDown: Int = 1 - ) { + pointersDown: Int = 1, + ): DragController { // overSlop should be 0f only if the drag gesture starts with startDragImmediately if (overSlop == 0f) error("Consider using onDragStartedImmediately()") - onDragStarted(sceneGestureHandler.draggable, startedPosition, overSlop, pointersDown) + return onDragStarted(draggableHandler, startedPosition, overSlop, pointersDown) } - fun onDragStartedImmediately(startedPosition: Offset = Offset.Zero, pointersDown: Int = 1) { - onDragStarted( - sceneGestureHandler.draggable, - startedPosition, - overSlop = 0f, - pointersDown - ) + fun onDragStartedImmediately( + startedPosition: Offset = Offset.Zero, + pointersDown: Int = 1, + ): DragController { + return onDragStarted(draggableHandler, startedPosition, overSlop = 0f, pointersDown) } fun onDragStarted( @@ -211,24 +209,26 @@ class SceneGestureHandlerTest { startedPosition: Offset = Offset.Zero, overSlop: Float = 0f, pointersDown: Int = 1 - ) { - draggableHandler.onDragStarted( - startedPosition = startedPosition, - overSlop = overSlop, - pointersDown = pointersDown, - ) + ): DragController { + val dragController = + draggableHandler.onDragStarted( + startedPosition = startedPosition, + overSlop = overSlop, + pointersDown = pointersDown, + ) // MultiPointerDraggable will always call onDelta with the initial overSlop right after - onDelta(pixels = overSlop) + dragController.onDragDelta(pixels = overSlop) + + return dragController } - fun onDelta(pixels: Float) { - sceneGestureHandler.draggable.onDelta(pixels = pixels) + fun DragController.onDragDelta(pixels: Float) { + onDrag(delta = pixels) } - fun onDragStopped(velocity: Float) { - sceneGestureHandler.draggable.onDragStopped(velocity = velocity) - runCurrent() + fun DragController.onDragStopped(velocity: Float, canChangeScene: Boolean = true) { + onStop(velocity, canChangeScene) } fun NestedScrollConnection.scroll( @@ -281,20 +281,20 @@ class SceneGestureHandlerTest { @Test fun afterSceneTransitionIsStarted_interceptDragEvents() = runGestureTest { - onDragStarted(overSlop = down(fractionOfScreen = 0.1f)) + val dragController = onDragStarted(overSlop = down(fractionOfScreen = 0.1f)) assertTransition(currentScene = SceneA) assertThat(progress).isEqualTo(0.1f) - onDelta(pixels = down(fractionOfScreen = 0.1f)) + dragController.onDragDelta(pixels = down(fractionOfScreen = 0.1f)) assertThat(progress).isEqualTo(0.2f) } @Test fun onDragStoppedAfterDrag_velocityLowerThanThreshold_remainSameScene() = runGestureTest { - onDragStarted(overSlop = down(fractionOfScreen = 0.1f)) + val dragController = onDragStarted(overSlop = down(fractionOfScreen = 0.1f)) assertTransition(currentScene = SceneA) - onDragStopped(velocity = velocityThreshold - 0.01f) + dragController.onDragStopped(velocity = velocityThreshold - 0.01f) assertTransition(currentScene = SceneA) // wait for the stop animation @@ -304,10 +304,10 @@ class SceneGestureHandlerTest { @Test fun onDragStoppedAfterDrag_velocityAtLeastThreshold_goToNextScene() = runGestureTest { - onDragStarted(overSlop = down(fractionOfScreen = 0.1f)) + val dragController = onDragStarted(overSlop = down(fractionOfScreen = 0.1f)) assertTransition(currentScene = SceneA) - onDragStopped(velocity = velocityThreshold) + dragController.onDragStopped(velocity = velocityThreshold) assertTransition(currentScene = SceneC) // wait for the stop animation @@ -317,10 +317,10 @@ class SceneGestureHandlerTest { @Test fun onDragStoppedAfterStarted_returnToIdle() = runGestureTest { - onDragStarted(overSlop = down(fractionOfScreen = 0.1f)) + val dragController = onDragStarted(overSlop = down(fractionOfScreen = 0.1f)) assertTransition(currentScene = SceneA) - onDragStopped(velocity = 0f) + dragController.onDragStopped(velocity = 0f) advanceUntilIdle() assertIdle(currentScene = SceneA) } @@ -328,7 +328,7 @@ class SceneGestureHandlerTest { @Test fun onDragReversedDirection_changeToScene() = runGestureTest { // Drag A -> B with progress 0.6 - onDragStarted(overSlop = -60f) + val dragController = onDragStarted(overSlop = -60f) assertTransition( currentScene = SceneA, fromScene = SceneA, @@ -337,7 +337,7 @@ class SceneGestureHandlerTest { ) // Reverse direction such that A -> C now with 0.4 - onDelta(pixels = 100f) + dragController.onDragDelta(pixels = 100f) assertTransition( currentScene = SceneA, fromScene = SceneA, @@ -346,7 +346,7 @@ class SceneGestureHandlerTest { ) // After the drag stopped scene C should be committed - onDragStopped(velocity = velocityThreshold) + dragController.onDragStopped(velocity = velocityThreshold) assertTransition(currentScene = SceneC, fromScene = SceneA, toScene = SceneC) // wait for the stop animation @@ -356,8 +356,6 @@ class SceneGestureHandlerTest { @Test fun onDragStartedWithoutActionsInBothDirections_stayIdle() = runGestureTest { - val horizontalDraggableHandler = horizontalSceneGestureHandler.draggable - onDragStarted(horizontalDraggableHandler, overSlop = up(fractionOfScreen = 0.3f)) assertIdle(currentScene = SceneA) @@ -370,7 +368,7 @@ class SceneGestureHandlerTest { navigateToSceneC() // We are on SceneC which has no action in Down direction - onDragStarted(overSlop = 10f) + val dragController = onDragStarted(overSlop = 10f) assertTransition( currentScene = SceneC, fromScene = SceneC, @@ -379,7 +377,7 @@ class SceneGestureHandlerTest { ) // Reverse drag direction, it will consume the previous drag - onDelta(pixels = -10f) + dragController.onDragDelta(pixels = -10f) assertTransition( currentScene = SceneC, fromScene = SceneC, @@ -388,7 +386,7 @@ class SceneGestureHandlerTest { ) // Continue reverse drag direction, it should record progress to Scene B - onDelta(pixels = -10f) + dragController.onDragDelta(pixels = -10f) assertTransition( currentScene = SceneC, fromScene = SceneC, @@ -416,14 +414,14 @@ class SceneGestureHandlerTest { @Test fun onDragToExactlyZero_toSceneIsSet() = runGestureTest { - onDragStarted(overSlop = down(fractionOfScreen = 0.3f)) + val dragController = onDragStarted(overSlop = down(fractionOfScreen = 0.3f)) assertTransition( currentScene = SceneA, fromScene = SceneA, toScene = SceneC, progress = 0.3f ) - onDelta(pixels = up(fractionOfScreen = 0.3f)) + dragController.onDragDelta(pixels = up(fractionOfScreen = 0.3f)) assertTransition( currentScene = SceneA, fromScene = SceneA, @@ -434,8 +432,8 @@ class SceneGestureHandlerTest { private fun TestGestureScope.navigateToSceneC() { assertIdle(currentScene = SceneA) - onDragStarted(overSlop = down(fractionOfScreen = 1f)) - onDragStopped(velocity = 0f) + val dragController = onDragStarted(overSlop = down(fractionOfScreen = 1f)) + dragController.onDragStopped(velocity = 0f) advanceUntilIdle() assertIdle(currentScene = SceneC) } @@ -443,7 +441,7 @@ class SceneGestureHandlerTest { @Test fun onAccelaratedScroll_scrollToThirdScene() = runGestureTest { // Drag A -> B with progress 0.2 - onDragStarted(overSlop = up(fractionOfScreen = 0.2f)) + val dragController1 = onDragStarted(overSlop = up(fractionOfScreen = 0.2f)) assertTransition( currentScene = SceneA, fromScene = SceneA, @@ -452,13 +450,13 @@ class SceneGestureHandlerTest { ) // Start animation A -> B with progress 0.2 -> 1.0 - onDragStopped(velocity = -velocityThreshold) + dragController1.onDragStopped(velocity = -velocityThreshold) assertTransition(currentScene = SceneB, fromScene = SceneA, toScene = SceneB) // While at A -> B do a 100% screen drag (progress 1.2). This should go past B and change // the transition to B -> C with progress 0.2 - onDragStartedImmediately() - onDelta(pixels = up(fractionOfScreen = 1f)) + val dragController2 = onDragStartedImmediately() + dragController2.onDragDelta(pixels = up(fractionOfScreen = 1f)) assertTransition( currentScene = SceneB, fromScene = SceneB, @@ -467,7 +465,7 @@ class SceneGestureHandlerTest { ) // After the drag stopped scene C should be committed - onDragStopped(velocity = -velocityThreshold) + dragController2.onDragStopped(velocity = -velocityThreshold) assertTransition(currentScene = SceneC, fromScene = SceneB, toScene = SceneC) // wait for the stop animation @@ -477,9 +475,9 @@ class SceneGestureHandlerTest { @Test fun onAccelaratedScrollBothTargetsBecomeNull_settlesToIdle() = runGestureTest { - onDragStarted(overSlop = up(fractionOfScreen = 0.2f)) - onDelta(pixels = up(fractionOfScreen = 0.2f)) - onDragStopped(velocity = -velocityThreshold) + val dragController1 = onDragStarted(overSlop = up(fractionOfScreen = 0.2f)) + dragController1.onDragDelta(pixels = up(fractionOfScreen = 0.2f)) + dragController1.onDragStopped(velocity = -velocityThreshold) assertTransition(currentScene = SceneB, fromScene = SceneA, toScene = SceneB) mutableUserActionsA.remove(Swipe.Up) @@ -488,34 +486,34 @@ class SceneGestureHandlerTest { mutableUserActionsB.remove(Swipe.Down) // start accelaratedScroll and scroll over to B -> null - onDragStartedImmediately() - onDelta(pixels = up(fractionOfScreen = 0.5f)) - onDelta(pixels = up(fractionOfScreen = 0.5f)) + val dragController2 = onDragStartedImmediately() + dragController2.onDragDelta(pixels = up(fractionOfScreen = 0.5f)) + dragController2.onDragDelta(pixels = up(fractionOfScreen = 0.5f)) // here onDragStopped is already triggered, but subsequent onDelta/onDragStopped calls may // still be called. Make sure that they don't crash or change the scene - onDelta(pixels = up(fractionOfScreen = 0.5f)) - onDragStopped(velocity = 0f) + dragController2.onDragDelta(pixels = up(fractionOfScreen = 0.5f)) + dragController2.onDragStopped(velocity = 0f) advanceUntilIdle() assertIdle(SceneB) // These events can still come in after the animation has settled - onDelta(pixels = up(fractionOfScreen = 0.5f)) - onDragStopped(velocity = 0f) + dragController2.onDragDelta(pixels = up(fractionOfScreen = 0.5f)) + dragController2.onDragStopped(velocity = 0f) assertIdle(SceneB) } @Test fun onDragTargetsChanged_targetStaysTheSame() = runGestureTest { - onDragStarted(overSlop = up(fractionOfScreen = 0.1f)) + val dragController1 = onDragStarted(overSlop = up(fractionOfScreen = 0.1f)) assertTransition(fromScene = SceneA, toScene = SceneB, progress = 0.1f) mutableUserActionsA[Swipe.Up] = UserActionResult(SceneC) - onDelta(pixels = up(fractionOfScreen = 0.1f)) + dragController1.onDragDelta(pixels = up(fractionOfScreen = 0.1f)) // target stays B even though UserActions changed assertTransition(fromScene = SceneA, toScene = SceneB, progress = 0.2f) - onDragStopped(velocity = down(fractionOfScreen = 0.1f)) + dragController1.onDragStopped(velocity = down(fractionOfScreen = 0.1f)) advanceUntilIdle() // now target changed to C for new drag @@ -525,25 +523,26 @@ class SceneGestureHandlerTest { @Test fun onDragTargetsChanged_targetsChangeWhenStartingNewDrag() = runGestureTest { - onDragStarted(overSlop = up(fractionOfScreen = 0.1f)) + val dragController1 = onDragStarted(overSlop = up(fractionOfScreen = 0.1f)) assertTransition(fromScene = SceneA, toScene = SceneB, progress = 0.1f) mutableUserActionsA[Swipe.Up] = UserActionResult(SceneC) - onDelta(pixels = up(fractionOfScreen = 0.1f)) - onDragStopped(velocity = down(fractionOfScreen = 0.1f)) + dragController1.onDragDelta(pixels = up(fractionOfScreen = 0.1f)) + dragController1.onDragStopped(velocity = down(fractionOfScreen = 0.1f)) // now target changed to C for new drag that started before previous drag settled to Idle - onDragStartedImmediately() - onDelta(pixels = up(fractionOfScreen = 0.1f)) + val dragController2 = onDragStartedImmediately() + dragController2.onDragDelta(pixels = up(fractionOfScreen = 0.1f)) assertTransition(fromScene = SceneA, toScene = SceneC, progress = 0.3f) } @Test fun startGestureDuringAnimatingOffset_shouldImmediatelyStopTheAnimation() = runGestureTest { - onDragStarted(overSlop = down(fractionOfScreen = 0.1f)) + val dragController = onDragStarted(overSlop = down(fractionOfScreen = 0.1f)) assertTransition(currentScene = SceneA) - onDragStopped(velocity = velocityThreshold) + dragController.onDragStopped(velocity = velocityThreshold) + runCurrent() assertTransition(currentScene = SceneC) assertThat(isUserInputOngoing).isFalse() @@ -632,7 +631,7 @@ class SceneGestureHandlerTest { // stop scene transition (start the "stop animation") nestedScroll.preFling(available = Velocity.Zero) - // a pre scroll event, that could be intercepted by SceneGestureHandler + // a pre scroll event, that could be intercepted by DraggableHandlerImpl nestedScroll.onPreScroll( available = Offset(0f, secondScroll), source = NestedScrollSource.Drag @@ -801,18 +800,6 @@ class SceneGestureHandlerTest { } @Test - fun beforeDraggableStart_drag_shouldBeIgnored() = runGestureTest { - onDelta(pixels = down(fractionOfScreen = 0.1f)) - assertIdle(currentScene = SceneA) - } - - @Test - fun beforeDraggableStart_stop_shouldBeIgnored() = runGestureTest { - onDragStopped(velocity = velocityThreshold) - assertIdle(currentScene = SceneA) - } - - @Test fun beforeNestedScrollStart_stop_shouldBeIgnored() = runGestureTest { val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeWithPreview) nestedScroll.preFling(available = Velocity(0f, velocityThreshold)) @@ -826,7 +813,7 @@ class SceneGestureHandlerTest { val offsetY10 = downOffset(fractionOfScreen = 0.1f) // Start a drag and then stop it, given that - onDragStarted(overSlop = up(0.1f)) + val dragController = onDragStarted(overSlop = up(0.1f)) assertTransition(currentScene = SceneA) assertThat(progress).isEqualTo(0.1f) @@ -836,7 +823,7 @@ class SceneGestureHandlerTest { assertThat(progress).isEqualTo(0.2f) // this should be ignored, we are scrolling now! - onDragStopped(-velocityThreshold) + dragController.onDragStopped(-velocityThreshold) assertTransition(currentScene = SceneA) nestedScroll.scroll(available = -offsetY10) @@ -865,6 +852,7 @@ class SceneGestureHandlerTest { currentScene = SceneC, fromScene = SceneC, toScene = SceneB, + progress = 0.1f, isUserInputOngoing = true, ) @@ -873,18 +861,25 @@ class SceneGestureHandlerTest { // During the current gesture, start a new gesture, still in the middle of the screen. We // should intercept it. Because it is intercepted, the overSlop passed to onDragStarted() // should be 0f. - assertThat(sceneGestureHandler.shouldImmediatelyIntercept(middle)).isTrue() + assertThat(draggableHandler.shouldImmediatelyIntercept(middle)).isTrue() onDragStartedImmediately(startedPosition = middle) // We should have intercepted the transition, so the transition should be the same object. - assertTransition(currentScene = SceneC, fromScene = SceneC, toScene = SceneB) - assertThat(transitionState).isSameInstanceAs(firstTransition) + assertTransition( + currentScene = SceneC, + fromScene = SceneC, + toScene = SceneB, + progress = 0.1f, + isUserInputOngoing = true, + ) + // We should have a new transition + assertThat(transitionState).isNotSameInstanceAs(firstTransition) // Start a new gesture from the bottom of the screen. Because swiping up from the bottom of // C leads to scene A (and not B), the previous transitions is *not* intercepted and we // instead animate from C to A. val bottom = Offset(SCREEN_SIZE / 2, SCREEN_SIZE) - assertThat(sceneGestureHandler.shouldImmediatelyIntercept(bottom)).isFalse() + assertThat(draggableHandler.shouldImmediatelyIntercept(bottom)).isFalse() onDragStarted(startedPosition = bottom, overSlop = up(0.1f)) assertTransition( @@ -901,12 +896,12 @@ class SceneGestureHandlerTest { assertIdle(SceneA) // Swipe up to scene B. - onDragStarted(overSlop = up(0.1f)) + val dragController = onDragStarted(overSlop = up(0.1f)) assertTransition(currentScene = SceneA, fromScene = SceneA, toScene = SceneB) // Block the transition when the user release their finger. canChangeScene = { false } - onDragStopped(velocity = -velocityThreshold) + dragController.onDragStopped(velocity = -velocityThreshold) advanceUntilIdle() assertIdle(SceneA) } @@ -916,18 +911,18 @@ class SceneGestureHandlerTest { assertIdle(SceneA) // Swipe up to B. - onDragStarted(overSlop = up(0.1f)) + val dragController1 = onDragStarted(overSlop = up(0.1f)) assertTransition(currentScene = SceneA, fromScene = SceneA, toScene = SceneB) - onDragStopped(velocity = -velocityThreshold) + dragController1.onDragStopped(velocity = -velocityThreshold) assertTransition(currentScene = SceneB, fromScene = SceneA, toScene = SceneB) // Intercept the transition and swipe down back to scene A. - assertThat(sceneGestureHandler.shouldImmediatelyIntercept(startedPosition = null)).isTrue() - onDragStartedImmediately() + assertThat(draggableHandler.shouldImmediatelyIntercept(startedPosition = null)).isTrue() + val dragController2 = onDragStartedImmediately() // Block the transition when the user release their finger. canChangeScene = { false } - onDragStopped(velocity = velocityThreshold) + dragController2.onDragStopped(velocity = velocityThreshold) advanceUntilIdle() assertIdle(SceneB) diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt index cd99d05158cd..d8cf1c12989b 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt @@ -59,9 +59,18 @@ class MultiPointerDraggableTest { orientation = Orientation.Vertical, enabled = { enabled }, startDragImmediately = { false }, - onDragStarted = { _, _, _ -> started = true }, - onDragDelta = { _ -> dragged = true }, - onDragStopped = { stopped = true }, + onDragStarted = { _, _, _ -> + started = true + object : DragController { + override fun onDrag(delta: Float) { + dragged = true + } + + override fun onStop(velocity: Float, canChangeScene: Boolean) { + stopped = true + } + } + }, ) ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt index 503463171ae7..92eb8f8c36c2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt @@ -30,6 +30,7 @@ import android.view.MotionEvent import android.view.Surface import android.view.Surface.Rotation import android.view.View +import android.view.ViewGroup import android.view.WindowManager import android.view.accessibility.AccessibilityManager import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -46,7 +47,12 @@ import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor import com.android.systemui.classifier.FalsingCollector import com.android.systemui.dump.DumpManager +import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository +import com.android.systemui.keyguard.domain.interactor.FromAodTransitionInteractor +import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor +import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.power.data.repository.FakePowerRepository import com.android.systemui.power.domain.interactor.PowerInteractor @@ -127,7 +133,8 @@ class UdfpsControllerOverlayTest : SysuiTestCase() { @Mock private lateinit var defaultUdfpsTouchOverlayViewModel: DefaultUdfpsTouchOverlayViewModel @Mock private lateinit var udfpsKeyguardAccessibilityDelegate: UdfpsKeyguardAccessibilityDelegate - @Mock private lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor + private lateinit var keyguardTransitionRepository: FakeKeyguardTransitionRepository + private lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor @Mock private lateinit var shadeInteractor: ShadeInteractor @Captor private lateinit var layoutParamsCaptor: ArgumentCaptor<WindowManager.LayoutParams> @Mock private lateinit var udfpsOverlayInteractor: UdfpsOverlayInteractor @@ -150,6 +157,19 @@ class UdfpsControllerOverlayTest : SysuiTestCase() { mock(ScreenOffAnimationController::class.java), statusBarStateController, ) + keyguardTransitionRepository = FakeKeyguardTransitionRepository() + keyguardTransitionInteractor = + KeyguardTransitionInteractor( + scope = testScope.backgroundScope, + repository = keyguardTransitionRepository, + fromLockscreenTransitionInteractor = { + mock(FromLockscreenTransitionInteractor::class.java) + }, + fromPrimaryBouncerTransitionInteractor = { + mock(FromPrimaryBouncerTransitionInteractor::class.java) + }, + fromAodTransitionInteractor = { mock(FromAodTransitionInteractor::class.java) }, + ) whenever(inflater.inflate(R.layout.udfps_view, null, false)).thenReturn(udfpsView) whenever(inflater.inflate(R.layout.udfps_bp_view, null)) .thenReturn(mock(UdfpsBpView::class.java)) @@ -159,11 +179,25 @@ class UdfpsControllerOverlayTest : SysuiTestCase() { .thenReturn(mock(UdfpsFpmEmptyView::class.java)) } + private suspend fun withReasonSuspend( + @RequestReason reason: Int, + isDebuggable: Boolean = false, + enableDeviceEntryUdfpsRefactor: Boolean = false, + block: suspend () -> Unit, + ) { + withReason( + reason, + isDebuggable, + enableDeviceEntryUdfpsRefactor, + ) + block() + } + private fun withReason( @RequestReason reason: Int, isDebuggable: Boolean = false, enableDeviceEntryUdfpsRefactor: Boolean = false, - block: () -> Unit, + block: () -> Unit = {}, ) { if (enableDeviceEntryUdfpsRefactor) { mSetFlagsRule.enableFlags(Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR) @@ -312,6 +346,7 @@ class UdfpsControllerOverlayTest : SysuiTestCase() { lastWakeReason = WakeSleepReason.POWER_BUTTON, lastSleepReason = WakeSleepReason.OTHER, ) + runCurrent() controllerOverlay.show(udfpsController, overlayParams) runCurrent() verify(windowManager).addView(any(), any()) @@ -321,15 +356,25 @@ class UdfpsControllerOverlayTest : SysuiTestCase() { @Test fun showUdfpsOverlay_whileGoingToSleep() = testScope.runTest { - withReason(REASON_AUTH_KEYGUARD) { + withReasonSuspend(REASON_AUTH_KEYGUARD) { mSetFlagsRule.enableFlags(Flags.FLAG_UDFPS_VIEW_PERFORMANCE) + keyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.OFF, + to = KeyguardState.GONE, + testScope = this, + ) powerRepository.updateWakefulness( rawState = WakefulnessState.STARTING_TO_SLEEP, lastWakeReason = WakeSleepReason.POWER_BUTTON, lastSleepReason = WakeSleepReason.OTHER, ) + runCurrent() + + // WHEN a request comes to show the view controllerOverlay.show(udfpsController, overlayParams) runCurrent() + + // THEN the view does not get added immediately verify(windowManager, never()).addView(any(), any()) // we hide to end the job that listens for the finishedGoingToSleep signal @@ -338,25 +383,82 @@ class UdfpsControllerOverlayTest : SysuiTestCase() { } @Test - fun showUdfpsOverlay_afterFinishedGoingToSleep() = + fun showUdfpsOverlay_whileAsleep() = testScope.runTest { - withReason(REASON_AUTH_KEYGUARD) { + withReasonSuspend(REASON_AUTH_KEYGUARD) { mSetFlagsRule.enableFlags(Flags.FLAG_UDFPS_VIEW_PERFORMANCE) + keyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.OFF, + to = KeyguardState.GONE, + testScope = this, + ) powerRepository.updateWakefulness( - rawState = WakefulnessState.STARTING_TO_SLEEP, + rawState = WakefulnessState.ASLEEP, lastWakeReason = WakeSleepReason.POWER_BUTTON, lastSleepReason = WakeSleepReason.OTHER, ) + runCurrent() + + // WHEN a request comes to show the view controllerOverlay.show(udfpsController, overlayParams) runCurrent() + + // THEN view isn't added yet verify(windowManager, never()).addView(any(), any()) + // we hide to end the job that listens for the finishedGoingToSleep signal + controllerOverlay.hide() + } + } + + @Test + fun neverRemoveViewThatHasNotBeenAdded() = + testScope.runTest { + withReasonSuspend(REASON_AUTH_KEYGUARD) { + mSetFlagsRule.enableFlags(Flags.FLAG_UDFPS_VIEW_PERFORMANCE) + controllerOverlay.show(udfpsController, overlayParams) + val view = controllerOverlay.getTouchOverlay() + view?.let { + // parent is null, signalling that the view was never added + whenever(view.parent).thenReturn(null) + } + verify(windowManager, never()).removeView(eq(view)) + } + } + + @Test + fun showUdfpsOverlay_afterFinishedTransitioningToAOD() = + testScope.runTest { + withReasonSuspend(REASON_AUTH_KEYGUARD) { + mSetFlagsRule.enableFlags(Flags.FLAG_UDFPS_VIEW_PERFORMANCE) + keyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.OFF, + to = KeyguardState.GONE, + testScope = this, + ) powerRepository.updateWakefulness( - rawState = WakefulnessState.ASLEEP, + rawState = WakefulnessState.STARTING_TO_SLEEP, lastWakeReason = WakeSleepReason.POWER_BUTTON, lastSleepReason = WakeSleepReason.OTHER, ) runCurrent() + + // WHEN a request comes to show the view + controllerOverlay.show(udfpsController, overlayParams) + runCurrent() + + // THEN the view does not get added immediately + verify(windowManager, never()).addView(any(), any()) + + // WHEN the device finishes transitioning to AOD + keyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.GONE, + to = KeyguardState.AOD, + testScope = this, + ) + runCurrent() + + // THEN the view gets added verify(windowManager) .addView(eq(controllerOverlay.getTouchOverlay()), layoutParamsCaptor.capture()) } @@ -387,6 +489,7 @@ class UdfpsControllerOverlayTest : SysuiTestCase() { private fun hideUdfpsOverlay() { val didShow = controllerOverlay.show(udfpsController, overlayParams) val view = controllerOverlay.getTouchOverlay() + view?.let { whenever(view.parent).thenReturn(mock(ViewGroup::class.java)) } val didHide = controllerOverlay.hide() verify(windowManager).removeView(eq(view)) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java index 561cdbbf66ce..9b0b5dea0ad7 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java @@ -63,6 +63,7 @@ import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.Surface; import android.view.View; +import android.view.ViewGroup; import android.view.ViewRootImpl; import android.view.WindowManager; import android.view.accessibility.AccessibilityManager; @@ -234,6 +235,8 @@ public class UdfpsControllerTest extends SysuiTestCase { private ArgumentCaptor<IUdfpsOverlayController> mOverlayCaptor; private IUdfpsOverlayController mOverlayController; @Captor + private ArgumentCaptor<View> mViewCaptor; + @Captor private ArgumentCaptor<UdfpsView.OnTouchListener> mTouchListenerCaptor; @Captor private ArgumentCaptor<View.OnHoverListener> mHoverListenerCaptor; @@ -550,8 +553,11 @@ public class UdfpsControllerTest extends SysuiTestCase { mOpticalProps.sensorId, BiometricRequestConstants.REASON_ENROLL_ENROLLING, mUdfpsOverlayControllerCallback); + mFgExecutor.runAllReady(); - verify(mWindowManager).addView(any(), any()); + verify(mWindowManager).addView(mViewCaptor.capture(), any()); + when(mViewCaptor.getValue().getParent()) + .thenReturn(mock(ViewGroup.class)); // Update overlay parameters. reset(mWindowManager); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt index 47e1ee9c1b71..db1d5d91eb65 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt @@ -149,6 +149,42 @@ class PrimaryBouncerToGoneTransitionViewModelTest : SysuiTestCase() { values.forEach { assertThat(it).isEqualTo(1f) } } + @Test + fun notificationAlpha() = + testScope.runTest { + val values by collectValues(underTest.notificationAlpha) + runCurrent() + + keyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.PRIMARY_BOUNCER, + to = KeyguardState.GONE, + testScope, + ) + + assertThat(values[0]).isEqualTo(1f) + // Should fade to zero between here + assertThat(values[1]).isEqualTo(0f) + } + + @Test + fun notificationAlpha_leaveShadeOpen() = + testScope.runTest { + val values by collectValues(underTest.notificationAlpha) + runCurrent() + + sysuiStatusBarStateController.setLeaveOpenOnKeyguardHide(true) + + keyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.PRIMARY_BOUNCER, + to = KeyguardState.GONE, + testScope, + ) + + assertThat(values.size).isEqualTo(2) + // Shade stays open, and alpha should remain visible + values.forEach { assertThat(it).isEqualTo(1f) } + } + private fun step( value: Float, state: TransitionState = TransitionState.RUNNING diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/domain/interactor/SpatializerInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/domain/interactor/SpatializerInteractorTest.kt new file mode 100644 index 000000000000..a932dd6d106d --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/domain/interactor/SpatializerInteractorTest.kt @@ -0,0 +1,92 @@ +/* + * 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.domain.interactor + +import android.media.AudioDeviceAttributes +import android.media.AudioDeviceInfo +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.settingslib.media.domain.interactor.SpatializerInteractor +import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.testScope +import com.android.systemui.media.spatializerRepository +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class SpatializerInteractorTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val underTest = SpatializerInteractor(kosmos.spatializerRepository) + + @Test + fun setSpatialAudioEnabledFalse_isEnabled_false() { + with(kosmos) { + testScope.runTest { + underTest.setSpatialAudioEnabled(deviceAttributes, false) + + assertThat(underTest.isSpatialAudioEnabled(deviceAttributes)).isFalse() + } + } + } + + @Test + fun setSpatialAudioEnabledTrue_isEnabled_true() { + with(kosmos) { + testScope.runTest { + underTest.setSpatialAudioEnabled(deviceAttributes, true) + + assertThat(underTest.isSpatialAudioEnabled(deviceAttributes)).isTrue() + } + } + } + + @Test + fun setHeadTrackingEnabledFalse_isEnabled_false() { + with(kosmos) { + testScope.runTest { + underTest.setHeadTrackingEnabled(deviceAttributes, false) + + assertThat(underTest.isHeadTrackingEnabled(deviceAttributes)).isFalse() + } + } + } + + @Test + fun setHeadTrackingEnabledTrue_isEnabled_true() { + with(kosmos) { + testScope.runTest { + underTest.setHeadTrackingEnabled(deviceAttributes, true) + + assertThat(underTest.isHeadTrackingEnabled(deviceAttributes)).isTrue() + } + } + } + + private companion object { + val deviceAttributes = + AudioDeviceAttributes( + AudioDeviceAttributes.ROLE_OUTPUT, + AudioDeviceInfo.TYPE_BLE_HEADSET, + "test_address", + ) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt index 4e72843922e1..fff0a316cbf4 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt @@ -40,6 +40,7 @@ import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel import com.android.systemui.bouncer.ui.viewmodel.bouncerViewModel import com.android.systemui.classifier.domain.interactor.falsingInteractor import com.android.systemui.classifier.falsingCollector +import com.android.systemui.classifier.falsingManager import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.coroutines.collectLastValue import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository @@ -265,6 +266,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { displayId = displayTracker.defaultDisplayId, sceneLogger = mock(), falsingCollector = kosmos.falsingCollector, + falsingManager = kosmos.falsingManager, powerInteractor = powerInteractor, bouncerInteractor = bouncerInteractor, simBouncerInteractor = dagger.Lazy { kosmos.simBouncerInteractor }, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/PanelExpansionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/PanelExpansionInteractorTest.kt new file mode 100644 index 000000000000..9b0adb172e8d --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/PanelExpansionInteractorTest.kt @@ -0,0 +1,191 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.android.systemui.scene.domain.interactor + +import android.platform.test.annotations.DisableFlags +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.Flags.FLAG_SCENE_CONTAINER +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository +import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor +import com.android.systemui.flags.EnableSceneContainer +import com.android.systemui.kosmos.testScope +import com.android.systemui.scene.shared.model.ObservableTransitionState +import com.android.systemui.scene.shared.model.SceneKey +import com.android.systemui.scene.shared.model.fakeSceneDataSource +import com.android.systemui.shade.data.repository.fakeShadeRepository +import com.android.systemui.statusbar.notification.stack.ui.viewmodel.panelExpansionInteractor +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class PanelExpansionInteractorTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val deviceEntryRepository = kosmos.fakeDeviceEntryRepository + private val deviceUnlockedInteractor = kosmos.deviceUnlockedInteractor + private val sceneInteractor = kosmos.sceneInteractor + private val transitionState = + MutableStateFlow<ObservableTransitionState>( + ObservableTransitionState.Idle(SceneKey.Lockscreen) + ) + private val fakeSceneDataSource = kosmos.fakeSceneDataSource + private val fakeShadeRepository = kosmos.fakeShadeRepository + + private lateinit var underTest: PanelExpansionInteractor + + @Before + fun setUp() { + sceneInteractor.setTransitionState(transitionState) + } + + @Test + @EnableSceneContainer + fun legacyPanelExpansion_whenIdle_whenLocked() = + testScope.runTest { + underTest = kosmos.panelExpansionInteractor + setUnlocked(false) + val panelExpansion by collectLastValue(underTest.legacyPanelExpansion) + + changeScene(SceneKey.Lockscreen) { assertThat(panelExpansion).isEqualTo(1f) } + assertThat(panelExpansion).isEqualTo(1f) + + changeScene(SceneKey.Bouncer) { assertThat(panelExpansion).isEqualTo(1f) } + assertThat(panelExpansion).isEqualTo(1f) + + changeScene(SceneKey.Shade) { assertThat(panelExpansion).isEqualTo(1f) } + assertThat(panelExpansion).isEqualTo(1f) + + changeScene(SceneKey.QuickSettings) { assertThat(panelExpansion).isEqualTo(1f) } + assertThat(panelExpansion).isEqualTo(1f) + + changeScene(SceneKey.Communal) { assertThat(panelExpansion).isEqualTo(1f) } + assertThat(panelExpansion).isEqualTo(1f) + } + + @Test + @EnableSceneContainer + fun legacyPanelExpansion_whenIdle_whenUnlocked() = + testScope.runTest { + underTest = kosmos.panelExpansionInteractor + setUnlocked(true) + val panelExpansion by collectLastValue(underTest.legacyPanelExpansion) + + changeScene(SceneKey.Gone) { assertThat(panelExpansion).isEqualTo(0f) } + assertThat(panelExpansion).isEqualTo(0f) + + changeScene(SceneKey.Shade) { progress -> + assertThat(panelExpansion).isEqualTo(progress) + } + assertThat(panelExpansion).isEqualTo(1f) + + changeScene(SceneKey.QuickSettings) { + // Shade's already expanded, so moving to QS should also be 1f. + assertThat(panelExpansion).isEqualTo(1f) + } + assertThat(panelExpansion).isEqualTo(1f) + + changeScene(SceneKey.Communal) { assertThat(panelExpansion).isEqualTo(1f) } + assertThat(panelExpansion).isEqualTo(1f) + } + + @Test + @DisableFlags(FLAG_SCENE_CONTAINER) + fun legacyPanelExpansion_whenInLegacyMode() = + testScope.runTest { + underTest = kosmos.panelExpansionInteractor + val leet = 0.1337f + fakeShadeRepository.setLegacyShadeExpansion(leet) + setUnlocked(false) + val panelExpansion by collectLastValue(underTest.legacyPanelExpansion) + + changeScene(SceneKey.Lockscreen) + assertThat(panelExpansion).isEqualTo(leet) + + changeScene(SceneKey.Bouncer) + assertThat(panelExpansion).isEqualTo(leet) + + changeScene(SceneKey.Shade) + assertThat(panelExpansion).isEqualTo(leet) + + changeScene(SceneKey.QuickSettings) + assertThat(panelExpansion).isEqualTo(leet) + + changeScene(SceneKey.Communal) + assertThat(panelExpansion).isEqualTo(leet) + } + + private fun TestScope.setUnlocked(isUnlocked: Boolean) { + val isDeviceUnlocked by collectLastValue(deviceUnlockedInteractor.isDeviceUnlocked) + deviceEntryRepository.setUnlocked(isUnlocked) + runCurrent() + + assertThat(isDeviceUnlocked).isEqualTo(isUnlocked) + } + + private fun TestScope.changeScene( + toScene: SceneKey, + assertDuringProgress: ((progress: Float) -> Unit) = {}, + ) { + val currentScene by collectLastValue(sceneInteractor.currentScene) + val progressFlow = MutableStateFlow(0f) + transitionState.value = + ObservableTransitionState.Transition( + fromScene = checkNotNull(currentScene), + toScene = toScene, + progress = progressFlow, + isInitiatedByUserInput = true, + isUserInputOngoing = flowOf(true), + ) + runCurrent() + assertDuringProgress(progressFlow.value) + + progressFlow.value = 0.2f + runCurrent() + assertDuringProgress(progressFlow.value) + + progressFlow.value = 0.6f + runCurrent() + assertDuringProgress(progressFlow.value) + + progressFlow.value = 1f + runCurrent() + assertDuringProgress(progressFlow.value) + + transitionState.value = ObservableTransitionState.Idle(toScene) + fakeSceneDataSource.changeScene(toScene) + runCurrent() + assertDuringProgress(progressFlow.value) + + assertThat(currentScene).isEqualTo(toScene) + } +} 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 f49b4777cf14..4e1623661a58 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 @@ -32,6 +32,7 @@ import com.android.systemui.authentication.shared.model.AuthenticationMethodMode import com.android.systemui.bouncer.domain.interactor.bouncerInteractor import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor import com.android.systemui.classifier.FalsingCollector +import com.android.systemui.classifier.falsingManager import com.android.systemui.coroutines.collectLastValue import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor @@ -115,6 +116,7 @@ class SceneContainerStartableTest : SysuiTestCase() { displayId = Display.DEFAULT_DISPLAY, sceneLogger = mock(), falsingCollector = falsingCollector, + falsingManager = kosmos.falsingManager, powerInteractor = powerInteractor, bouncerInteractor = bouncerInteractor, simBouncerInteractor = { kosmos.simBouncerInteractor }, @@ -970,6 +972,20 @@ class SceneContainerStartableTest : SysuiTestCase() { ) } + @Test + fun respondToFalsingDetections() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene) + val transitionStateFlow = prepareState() + underTest.start() + emulateSceneTransition(transitionStateFlow, toScene = SceneKey.Bouncer) + assertThat(currentScene).isNotEqualTo(SceneKey.Lockscreen) + + kosmos.falsingManager.sendFalsingBelief() + + assertThat(currentScene).isEqualTo(SceneKey.Lockscreen) + } + private fun TestScope.emulateSceneTransition( transitionStateFlow: MutableStateFlow<ObservableTransitionState>, toScene: SceneKey, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/wakelock/ClientTrackingWakeLockTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/wakelock/ClientTrackingWakeLockTest.kt new file mode 100644 index 000000000000..fdfcdc486c02 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/wakelock/ClientTrackingWakeLockTest.kt @@ -0,0 +1,130 @@ +/* + * 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.util.wakelock + +import android.os.Build +import android.os.PowerManager +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import org.junit.After +import org.junit.Assert +import org.junit.Assume +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class ClientTrackingWakeLockTest : SysuiTestCase() { + + private val WHY = "test" + private val WHY_2 = "test2" + + lateinit var mWakeLock: ClientTrackingWakeLock + lateinit var mInner: PowerManager.WakeLock + + @Before + fun setUp() { + mInner = + WakeLock.createWakeLockInner(mContext, "WakeLockTest", PowerManager.PARTIAL_WAKE_LOCK) + mWakeLock = ClientTrackingWakeLock(mInner, null, 20000) + } + + @After + fun tearDown() { + mInner.setReferenceCounted(false) + mInner.release() + } + + @Test + fun createPartialInner_notHeldYet() { + Assert.assertFalse(mInner.isHeld) + } + + @Test + fun wakeLock_acquire() { + mWakeLock.acquire(WHY) + Assert.assertTrue(mInner.isHeld) + } + + @Test + fun wakeLock_release() { + mWakeLock.acquire(WHY) + mWakeLock.release(WHY) + Assert.assertFalse(mInner.isHeld) + } + + @Test + fun wakeLock_acquiredReleasedMultipleSources_stillHeld() { + mWakeLock.acquire(WHY) + mWakeLock.acquire(WHY_2) + mWakeLock.release(WHY) + + Assert.assertTrue(mInner.isHeld) + mWakeLock.release(WHY_2) + Assert.assertFalse(mInner.isHeld) + } + + @Test + fun wakeLock_releasedTooManyTimes_stillReleased_noThrow() { + Assume.assumeFalse(Build.IS_ENG) + mWakeLock.acquire(WHY) + mWakeLock.acquire(WHY_2) + mWakeLock.release(WHY) + mWakeLock.release(WHY_2) + mWakeLock.release(WHY) + Assert.assertFalse(mInner.isHeld) + } + + @Test + fun wakeLock_wrap() { + val ran = BooleanArray(1) + val wrapped = mWakeLock.wrap { ran[0] = true } + Assert.assertTrue(mInner.isHeld) + Assert.assertFalse(ran[0]) + wrapped.run() + Assert.assertTrue(ran[0]) + Assert.assertFalse(mInner.isHeld) + } + + @Test + fun prodBuild_wakeLock_releaseWithoutAcquire_noThrow() { + Assume.assumeFalse(Build.IS_ENG) + // shouldn't throw an exception on production builds + mWakeLock.release(WHY) + } + + @Test + fun acquireSeveralLocks_stringReportsCorrectCount() { + mWakeLock.acquire(WHY) + mWakeLock.acquire(WHY_2) + mWakeLock.acquire(WHY) + mWakeLock.acquire(WHY) + mWakeLock.acquire(WHY_2) + Assert.assertEquals(5, mWakeLock.activeClients()) + + mWakeLock.release(WHY_2) + mWakeLock.release(WHY_2) + Assert.assertEquals(3, mWakeLock.activeClients()) + + mWakeLock.release(WHY) + mWakeLock.release(WHY) + mWakeLock.release(WHY) + Assert.assertEquals(0, mWakeLock.activeClients()) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/SpatialAudioComponentKosmos.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/SpatialAudioComponentKosmos.kt new file mode 100644 index 000000000000..737b7f3e0af0 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/SpatialAudioComponentKosmos.kt @@ -0,0 +1,32 @@ +/* + * 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.component.spatial + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testScope +import com.android.systemui.media.spatializerInteractor +import com.android.systemui.volume.mediaOutputInteractor +import com.android.systemui.volume.panel.component.spatial.domain.interactor.SpatialAudioComponentInteractor + +val Kosmos.spatialAudioComponentInteractor by + Kosmos.Fixture { + SpatialAudioComponentInteractor( + mediaOutputInteractor, + spatializerInteractor, + testScope.backgroundScope + ) + } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/SpatialAudioAvailabilityCriteriaTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/SpatialAudioAvailabilityCriteriaTest.kt new file mode 100644 index 000000000000..36be90ecbf7e --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/SpatialAudioAvailabilityCriteriaTest.kt @@ -0,0 +1,138 @@ +/* + * 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.component.spatial.domain + +import android.media.session.MediaSession +import android.media.session.PlaybackState +import android.testing.TestableLooper.RunWithLooper +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.settingslib.bluetooth.CachedBluetoothDevice +import com.android.settingslib.media.BluetoothMediaDevice +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testScope +import com.android.systemui.media.spatializerRepository +import com.android.systemui.testKosmos +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.android.systemui.volume.localMediaRepository +import com.android.systemui.volume.mediaController +import com.android.systemui.volume.mediaControllerRepository +import com.android.systemui.volume.panel.component.spatial.spatialAudioComponentInteractor +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.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +@RunWithLooper(setAsMainLooper = true) +class SpatialAudioAvailabilityCriteriaTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val cachedBluetoothDevice: CachedBluetoothDevice = mock { + whenever(address).thenReturn("test_address") + } + private val bluetoothMediaDevice: BluetoothMediaDevice = mock { + whenever(cachedDevice).thenReturn(cachedBluetoothDevice) + } + + private lateinit var underTest: SpatialAudioAvailabilityCriteria + + @Before + fun setup() { + with(kosmos) { + mediaControllerRepository.setActiveLocalMediaController( + mediaController.apply { + whenever(packageName).thenReturn("test.pkg") + whenever(sessionToken).thenReturn(MediaSession.Token(0, mock {})) + whenever(playbackState).thenReturn(PlaybackState.Builder().build()) + } + ) + + underTest = SpatialAudioAvailabilityCriteria(spatialAudioComponentInteractor) + } + } + + @Test + fun noSpatialAudio_noHeadTracking_unavailable() { + with(kosmos) { + testScope.runTest { + localMediaRepository.updateCurrentConnectedDevice(bluetoothMediaDevice) + spatializerRepository.setIsHeadTrackingAvailable(false) + spatializerRepository.defaultSpatialAudioAvailable = false + + val isAvailable by collectLastValue(underTest.isAvailable()) + runCurrent() + + assertThat(isAvailable).isFalse() + } + } + } + + @Test + fun spatialAudio_noHeadTracking_available() { + with(kosmos) { + testScope.runTest { + localMediaRepository.updateCurrentConnectedDevice(bluetoothMediaDevice) + spatializerRepository.setIsHeadTrackingAvailable(false) + spatializerRepository.defaultSpatialAudioAvailable = true + + val isAvailable by collectLastValue(underTest.isAvailable()) + runCurrent() + + assertThat(isAvailable).isTrue() + } + } + } + + @Test + fun spatialAudio_headTracking_available() { + with(kosmos) { + testScope.runTest { + localMediaRepository.updateCurrentConnectedDevice(bluetoothMediaDevice) + spatializerRepository.setIsHeadTrackingAvailable(true) + spatializerRepository.defaultSpatialAudioAvailable = true + + val isAvailable by collectLastValue(underTest.isAvailable()) + runCurrent() + + assertThat(isAvailable).isTrue() + } + } + } + + @Test + fun spatialAudio_headTracking_noDevice_unavailable() { + with(kosmos) { + testScope.runTest { + spatializerRepository.setIsHeadTrackingAvailable(true) + spatializerRepository.defaultSpatialAudioAvailable = true + + val isAvailable by collectLastValue(underTest.isAvailable()) + runCurrent() + + assertThat(isAvailable).isFalse() + } + } + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/interactor/SpatialAudioComponentInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/interactor/SpatialAudioComponentInteractorTest.kt new file mode 100644 index 000000000000..eb6f0b2e32b3 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/interactor/SpatialAudioComponentInteractorTest.kt @@ -0,0 +1,119 @@ +/* + * 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.component.spatial.domain.interactor + +import android.media.AudioDeviceAttributes +import android.media.AudioDeviceInfo +import android.media.session.MediaSession +import android.media.session.PlaybackState +import android.testing.TestableLooper +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.settingslib.bluetooth.CachedBluetoothDevice +import com.android.settingslib.media.BluetoothMediaDevice +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectValues +import com.android.systemui.kosmos.testScope +import com.android.systemui.media.spatializerInteractor +import com.android.systemui.media.spatializerRepository +import com.android.systemui.testKosmos +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.android.systemui.volume.localMediaRepository +import com.android.systemui.volume.mediaController +import com.android.systemui.volume.mediaControllerRepository +import com.android.systemui.volume.mediaOutputInteractor +import com.android.systemui.volume.panel.component.spatial.domain.model.SpatialAudioEnabledModel +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.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) +class SpatialAudioComponentInteractorTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private lateinit var underTest: SpatialAudioComponentInteractor + + @Before + fun setup() { + with(kosmos) { + val cachedBluetoothDevice: CachedBluetoothDevice = mock { + whenever(address).thenReturn("test_address") + } + localMediaRepository.updateCurrentConnectedDevice( + mock<BluetoothMediaDevice> { + whenever(name).thenReturn("test_device") + whenever(cachedDevice).thenReturn(cachedBluetoothDevice) + } + ) + + whenever(mediaController.packageName).thenReturn("test.pkg") + whenever(mediaController.sessionToken).thenReturn(MediaSession.Token(0, mock {})) + whenever(mediaController.playbackState).thenReturn(PlaybackState.Builder().build()) + + mediaControllerRepository.setActiveLocalMediaController(mediaController) + + spatializerRepository.setIsSpatialAudioAvailable( + AudioDeviceAttributes( + AudioDeviceAttributes.ROLE_OUTPUT, + AudioDeviceInfo.TYPE_BLE_HEADSET, + "test_address" + ), + true + ) + spatializerRepository.setIsHeadTrackingAvailable(true) + + underTest = + SpatialAudioComponentInteractor( + mediaOutputInteractor, + spatializerInteractor, + testScope.backgroundScope, + ) + } + } + + @Test + fun setEnabled_changesIsEnabled() { + with(kosmos) { + testScope.runTest { + val values by collectValues(underTest.isEnabled) + + underTest.setEnabled(SpatialAudioEnabledModel.Disabled) + runCurrent() + underTest.setEnabled(SpatialAudioEnabledModel.HeadTrackingEnabled) + runCurrent() + underTest.setEnabled(SpatialAudioEnabledModel.SpatialAudioEnabled) + runCurrent() + + assertThat(values) + .containsExactly( + SpatialAudioEnabledModel.Disabled, + SpatialAudioEnabledModel.HeadTrackingEnabled, + SpatialAudioEnabledModel.SpatialAudioEnabled, + ) + .inOrder() + } + } + } +} diff --git a/packages/SystemUI/res/drawable/bg_shutdown_finder_message.xml b/packages/SystemUI/res/drawable/bg_shutdown_finder_message.xml new file mode 100644 index 000000000000..324ae0c5c1d4 --- /dev/null +++ b/packages/SystemUI/res/drawable/bg_shutdown_finder_message.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <corners android:radius="28dp" /> + <solid android:color="@color/global_actions_lite_button_background" /> +</shape>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/ic_finder_active.xml b/packages/SystemUI/res/drawable/ic_finder_active.xml new file mode 100644 index 000000000000..8ca221ab7392 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_finder_active.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8"?> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M12,0L12,0A12,12 0,0 1,24 12L24,12A12,12 0,0 1,12 24L12,24A12,12 0,0 1,0 12L0,12A12,12 0,0 1,12 0z" + android:fillColor="#00677D"/> + <path + android:pathData="M12.797,4.005C11.949,3.936 11.203,4.597 11.203,5.467V6.659C8.855,7.001 6.998,8.856 6.653,11.203H5.467C4.597,11.203 3.936,11.948 4.005,12.796L4.006,12.802L4.006,12.809C4.38,16.605 7.399,19.625 11.195,20C12.051,20.087 12.803,19.404 12.803,18.547V17.355C15.154,17.012 17.013,15.154 17.355,12.803H18.54C19.406,12.803 20.079,12.058 19.992,11.196C19.618,7.4 16.606,4.388 12.812,4.006L12.804,4.006L12.797,4.005ZM11.203,9.344V8.283C9.741,8.591 8.588,9.741 8.278,11.203H9.344C9.585,10.4 10.179,9.754 10.942,9.437C11.027,9.402 11.114,9.371 11.203,9.344ZM11.998,13.171C11.358,13.175 10.828,12.651 10.827,12.004H10.827C10.827,11.959 10.83,11.915 10.835,11.871C10.885,11.427 11.185,11.056 11.59,10.902C11.694,10.863 11.806,10.838 11.921,10.83C11.948,10.833 11.976,10.834 12.003,10.834C12.65,10.834 13.177,11.356 13.179,12.007C13.177,12.622 12.695,13.13 12.091,13.175C12.06,13.172 12.029,13.17 11.998,13.171ZM17.353,11.203H18.383C18.028,8.289 15.72,5.979 12.804,5.616V6.658C15.153,7 17.004,8.852 17.353,11.203ZM14.663,11.203C14.395,10.311 13.692,9.611 12.804,9.344V8.283C14.265,8.59 15.414,9.736 15.727,11.203H14.663ZM5.615,12.803H6.654C7.001,15.15 8.855,17.002 11.203,17.346V18.391C8.287,18.034 5.972,15.719 5.615,12.803ZM11.203,14.666C10.316,14.394 9.613,13.692 9.345,12.803H8.279C8.591,14.264 9.741,15.412 11.203,15.721V14.666ZM14.661,12.811H15.729C15.418,14.272 14.266,15.422 12.804,15.73V14.662C13.689,14.396 14.391,13.699 14.661,12.811Z" + android:fillColor="#ffffff" + android:fillType="evenOdd"/> +</vector> diff --git a/packages/SystemUI/res/layout/shutdown_dialog_finder_active.xml b/packages/SystemUI/res/layout/shutdown_dialog_finder_active.xml new file mode 100644 index 000000000000..b6db7fc8007f --- /dev/null +++ b/packages/SystemUI/res/layout/shutdown_dialog_finder_active.xml @@ -0,0 +1,72 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <TextView + android:id="@android:id/text1" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="24dp" + android:fontFamily="google-sans" + android:gravity="center" + android:text="@string/shutdown_progress" + android:textAppearance="?android:attr/textAppearanceMedium" + android:textDirection="locale" + android:textSize="18sp" + android:visibility="gone" + app:layout_constraintBottom_toTopOf="@android:id/text2" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_bias="0.375" + app:layout_constraintVertical_chainStyle="packed" /> + + <TextView + android:id="@android:id/text2" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="24dp" + android:fontFamily="google-sans" + android:gravity="center" + android:text="@string/shutdown_progress" + android:textAppearance="?android:attr/textAppearanceLarge" + android:textDirection="locale" + android:textSize="24sp" + app:layout_constraintBottom_toTopOf="@android:id/progress" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@android:id/text1" /> + + <ProgressBar + android:id="@android:id/progress" + style="?android:attr/progressBarStyleLarge" + android:layout_width="30dp" + android:layout_height="30dp" + android:importantForAccessibility="no" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@android:id/text2" /> + + <TextView + android:id="@+id/finer_hint" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_margin="32dp" + android:background="@drawable/bg_shutdown_finder_message" + android:drawablePadding="16dp" + android:drawableStart="@drawable/ic_finder_active" + android:fontFamily="google-sans" + android:gravity="start" + android:padding="20dp" + android:text="@string/finder_active" + android:textAppearance="?android:attr/textAppearanceSmall" + android:textColor="@android:color/secondary_text_dark" + android:textDirection="locale" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintTop_toBottomOf="@android:id/progress" + app:layout_constraintVertical_bias="1" /> +</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 4be1deb3de1c..32b1cadd1c6c 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -898,8 +898,8 @@ <dimen name="communal_enforced_rounded_corner_max_radius">16dp</dimen> <!-- Width and height used to filter widgets displayed in the communal widget picker --> - <dimen name="communal_widget_picker_desired_width">464dp</dimen> - <dimen name="communal_widget_picker_desired_height">307dp</dimen> + <dimen name="communal_widget_picker_desired_width">424dp</dimen> + <dimen name="communal_widget_picker_desired_height">282dp</dimen> <!-- The width/height of the unlock icon view on keyguard. --> <dimen name="keyguard_lock_height">42dp</dimen> @@ -1742,12 +1742,18 @@ <dimen name="communal_grid_height">630dp</dimen> <!-- Number of columns for each communal card --> <integer name="communal_grid_columns_per_card">6</integer> - <!-- Width of area on right edge of screen in which swipes will open the communal hub --> - <dimen name="communal_right_edge_swipe_region_width">16dp</dimen> + + <!-- The width of the swipe target to initiate opening or closing communal hub. --> + <dimen name="communal_gesture_initiation_width">68dp</dimen> + + <!-- TODO(b/322549765): unify with communal_gesture_initiation_width --> + <!-- Width of area on right edge of screen in which swipes will open the communal hub when on + the lockscreen --> + <dimen name="communal_right_edge_swipe_region_width">40dp</dimen> <!-- Height of area at top of communal hub where swipes should open the notification shade --> - <dimen name="communal_top_edge_swipe_region_height">32dp</dimen> + <dimen name="communal_top_edge_swipe_region_height">68dp</dimen> <!-- Height of area at bottom of communal hub where swipes should open the bouncer --> - <dimen name="communal_bottom_edge_swipe_region_height">32dp</dimen> + <dimen name="communal_bottom_edge_swipe_region_height">68dp</dimen> <dimen name="drag_and_drop_icon_size">70dp</dimen> @@ -1819,9 +1825,6 @@ <dimen name="dream_overlay_complication_smartspace_padding">24dp</dimen> <dimen name="dream_overlay_complication_smartspace_max_width">408dp</dimen> - <!-- The width of the swipe target to initiate opening communal hub over dreams. --> - <dimen name="communal_gesture_initiation_width">48dp</dimen> - <!-- The position of the end guide, which dream overlay complications can align their start with if their end is aligned with the parent end. Represented as the percentage over from the start of the parent container. --> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 4263d9402d66..ea0e3092a781 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2236,6 +2236,11 @@ <!-- Tuner string --> <!-- Tuner string --> + <!-- Message shown during shutdown when Find My Device with Dead Battery Finder is active [CHAR LIMIT=300] --> + <string name="finder_active">You can locate this phone with Find My Device even when powered off</string> + <!-- Shutdown Progress Dialog. This is shown if the user chooses to power off the phone. [CHAR LIMIT=60] --> + <string name="shutdown_progress">Shutting down\u2026</string> + <!-- Text help link for care instructions for overheating devices [CHAR LIMIT=40] --> <string name="thermal_shutdown_dialog_help_text">See care steps</string> <!-- URL for care instructions for overheating devices --> diff --git a/packages/SystemUI/src/com/android/keyguard/CarrierTextManager.java b/packages/SystemUI/src/com/android/keyguard/CarrierTextManager.java index 1cfa816f4612..75cace424e8b 100644 --- a/packages/SystemUI/src/com/android/keyguard/CarrierTextManager.java +++ b/packages/SystemUI/src/com/android/keyguard/CarrierTextManager.java @@ -17,9 +17,9 @@ package com.android.keyguard; import static com.android.keyguard.logging.CarrierTextManagerLogger.REASON_ACTIVE_DATA_SUB_CHANGED; -import static com.android.keyguard.logging.CarrierTextManagerLogger.REASON_ON_SIM_STATE_CHANGED; import static com.android.keyguard.logging.CarrierTextManagerLogger.REASON_ON_TELEPHONY_CAPABLE; import static com.android.keyguard.logging.CarrierTextManagerLogger.REASON_REFRESH_CARRIER_INFO; +import static com.android.keyguard.logging.CarrierTextManagerLogger.REASON_SIM_ERROR_STATE_CHANGED; import android.content.Context; import android.content.Intent; @@ -123,12 +123,15 @@ public class CarrierTextManager { return; } - mLogger.logUpdateCarrierTextForReason(REASON_ON_SIM_STATE_CHANGED); + + mLogger.logSimStateChangedCallback(subId, slotId, simState); if (getStatusForIccState(simState) == CarrierTextManager.StatusMode.SimIoError) { mSimErrorState[slotId] = true; + mLogger.logUpdateCarrierTextForReason(REASON_SIM_ERROR_STATE_CHANGED); updateCarrierText(); } else if (mSimErrorState[slotId]) { mSimErrorState[slotId] = false; + mLogger.logUpdateCarrierTextForReason(REASON_SIM_ERROR_STATE_CHANGED); updateCarrierText(); } } diff --git a/packages/SystemUI/src/com/android/keyguard/logging/CarrierTextManagerLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/CarrierTextManagerLogger.kt index d02b72f37795..cb474d3d7a92 100644 --- a/packages/SystemUI/src/com/android/keyguard/logging/CarrierTextManagerLogger.kt +++ b/packages/SystemUI/src/com/android/keyguard/logging/CarrierTextManagerLogger.kt @@ -94,6 +94,20 @@ class CarrierTextManagerLogger @Inject constructor(@CarrierTextManagerLog val bu ) } + fun logSimStateChangedCallback(subId: Int, slotId: Int, simState: Int) { + buffer.log( + TAG, + LogLevel.VERBOSE, + { + // subId is always a very small int, and we've run out of integers for log buffer + long1 = subId.toLong() + int1 = slotId + int2 = simState + }, + { "onSimStateChangedCallback: subId=$long1 slotId=$int1 simState=$int2" } + ) + } + /** * Used to log the starting point for _why_ the carrier text is updating. In order to keep us * from holding on to too many objects, we'll just use simple ints for reasons here @@ -113,7 +127,7 @@ class CarrierTextManagerLogger @Inject constructor(@CarrierTextManagerLog val bu companion object { const val REASON_REFRESH_CARRIER_INFO = 1 const val REASON_ON_TELEPHONY_CAPABLE = 2 - const val REASON_ON_SIM_STATE_CHANGED = 3 + const val REASON_SIM_ERROR_STATE_CHANGED = 3 const val REASON_ACTIVE_DATA_SUB_CHANGED = 4 @Retention(AnnotationRetention.SOURCE) @@ -122,7 +136,7 @@ class CarrierTextManagerLogger @Inject constructor(@CarrierTextManagerLog val bu [ REASON_REFRESH_CARRIER_INFO, REASON_ON_TELEPHONY_CAPABLE, - REASON_ON_SIM_STATE_CHANGED, + REASON_SIM_ERROR_STATE_CHANGED, REASON_ACTIVE_DATA_SUB_CHANGED, ] ) @@ -132,7 +146,7 @@ class CarrierTextManagerLogger @Inject constructor(@CarrierTextManagerLog val bu when (this) { REASON_REFRESH_CARRIER_INFO -> "REFRESH_CARRIER_INFO" REASON_ON_TELEPHONY_CAPABLE -> "ON_TELEPHONY_CAPABLE" - REASON_ON_SIM_STATE_CHANGED -> "SIM_STATE_CHANGED" + REASON_SIM_ERROR_STATE_CHANGED -> "SIM_ERROR_STATE_CHANGED" REASON_ACTIVE_DATA_SUB_CHANGED -> "ACTIVE_DATA_SUB_CHANGED" else -> "unknown" } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt index 921e39532f58..16865ca809d9 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt @@ -31,6 +31,7 @@ import android.hardware.biometrics.BiometricRequestConstants.RequestReason import android.hardware.fingerprint.IUdfpsOverlayControllerCallback import android.os.Build import android.os.RemoteException +import android.os.Trace import android.provider.Settings import android.util.Log import android.util.RotationUtils @@ -58,9 +59,9 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor 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.plugins.statusbar.StatusBarStateController import com.android.systemui.power.domain.interactor.PowerInteractor -import com.android.systemui.power.shared.model.WakefulnessState import com.android.systemui.res.R import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.LockscreenShadeTransitionController @@ -125,11 +126,15 @@ class UdfpsControllerOverlay @JvmOverloads constructor( private val powerInteractor: PowerInteractor, @Application private val scope: CoroutineScope, ) { - private val isFinishedGoingToSleep: Flow<Unit> = - powerInteractor.detailedWakefulness - .filter { it.internalWakefulnessState == WakefulnessState.ASLEEP } + private val currentStateUpdatedToOffAodOrDozing: Flow<Unit> = + transitionInteractor.currentKeyguardState + .filter { + it == KeyguardState.OFF || + it == KeyguardState.AOD || + it == KeyguardState.DOZING + } .map { } // map to Unit - private var listenForAsleepJob: Job? = null + private var listenForCurrentKeyguardState: Job? = null private var addViewRunnable: Runnable? = null private var overlayViewLegacy: UdfpsView? = null private set @@ -280,18 +285,19 @@ class UdfpsControllerOverlay @JvmOverloads constructor( private fun addViewNowOrLater(view: View, animation: UdfpsAnimationViewController<*>?) { if (udfpsViewPerformance()) { addViewRunnable = kotlinx.coroutines.Runnable { + Trace.setCounter("UdfpsAddView", 1) windowManager.addView( view, coreLayoutParams.updateDimensions(animation) ) } - if (powerInteractor.detailedWakefulness.value.internalWakefulnessState - != WakefulnessState.STARTING_TO_SLEEP) { + if (powerInteractor.detailedWakefulness.value.isAwake()) { + // Device is awake, so we add the view immediately. addViewIfPending() } else { - listenForAsleepJob?.cancel() - listenForAsleepJob = scope.launch { - isFinishedGoingToSleep.collect { + listenForCurrentKeyguardState?.cancel() + listenForCurrentKeyguardState = scope.launch { + currentStateUpdatedToOffAodOrDozing.collect { addViewIfPending() } } @@ -306,7 +312,7 @@ class UdfpsControllerOverlay @JvmOverloads constructor( private fun addViewIfPending() { addViewRunnable?.let { - listenForAsleepJob?.cancel() + listenForCurrentKeyguardState?.cancel() it.run() } addViewRunnable = null @@ -412,7 +418,14 @@ class UdfpsControllerOverlay @JvmOverloads constructor( udfpsDisplayModeProvider.disable(null) } getTouchOverlay()?.apply { - windowManager.removeView(this) + if (udfpsViewPerformance()) { + if (this.parent != null) { + windowManager.removeView(this) + } + Trace.setCounter("UdfpsAddView", 0) + } else { + windowManager.removeView(this) + } setOnTouchListener(null) setOnHoverListener(null) overlayTouchListener?.let { @@ -423,7 +436,7 @@ class UdfpsControllerOverlay @JvmOverloads constructor( overlayViewLegacy = null overlayTouchView = null overlayTouchListener = null - listenForAsleepJob?.cancel() + listenForCurrentKeyguardState?.cancel() return wasShowing } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java index 6d9994fb2205..ce24259bbc1e 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java @@ -71,6 +71,7 @@ import android.media.MediaRouter2Manager; import android.media.projection.IMediaProjectionManager; import android.media.projection.MediaProjectionManager; import android.media.session.MediaSessionManager; +import android.nearby.NearbyManager; import android.net.ConnectivityManager; import android.net.NetworkScoreManager; import android.net.wifi.WifiManager; @@ -441,6 +442,12 @@ public class FrameworkServicesModule { @Provides @Singleton + static NearbyManager provideNearbyManager(Context context) { + return context.getSystemService(NearbyManager.class); + } + + @Provides + @Singleton static NetworkScoreManager provideNetworkScoreManager(Context context) { return context.getSystemService(NetworkScoreManager.class); } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSysUIComponent.java index a90980fddfb0..a431a59fcef6 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSysUIComponent.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSysUIComponent.java @@ -16,7 +16,6 @@ package com.android.systemui.dagger; -import com.android.systemui.globalactions.ShutdownUiModule; import com.android.systemui.keyguard.CustomizationProvider; import com.android.systemui.statusbar.NotificationInsetsModule; import com.android.systemui.statusbar.QsFrameTranslateModule; @@ -32,7 +31,6 @@ import dagger.Subcomponent; DependencyProvider.class, NotificationInsetsModule.class, QsFrameTranslateModule.class, - ShutdownUiModule.class, SystemUIBinder.class, SystemUIModule.class, SystemUICoreStartableModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 33a69bf0d774..6bb846491224 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -369,12 +369,6 @@ object Flags { @Keep val WM_BUBBLE_BAR = sysPropBooleanFlag("persist.wm.debug.bubble_bar", default = false) - // TODO(b/260271148): Tracking bug - @Keep - @JvmField - val WM_DESKTOP_WINDOWING_2 = - sysPropBooleanFlag("persist.wm.debug.desktop_mode_2", default = false) - // TODO(b/254513207): Tracking Bug to delete @Keep @JvmField diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/ShutdownUi.java b/packages/SystemUI/src/com/android/systemui/globalactions/ShutdownUi.java index 51978ece14db..ccd69ca55f0c 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/ShutdownUi.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/ShutdownUi.java @@ -22,7 +22,10 @@ import android.annotation.Nullable; import android.annotation.StringRes; import android.app.Dialog; import android.content.Context; +import android.nearby.NearbyManager; +import android.net.platform.flags.Flags; import android.os.PowerManager; +import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.view.Window; @@ -38,6 +41,8 @@ import com.android.systemui.scrim.ScrimDrawable; import com.android.systemui.statusbar.BlurUtils; import com.android.systemui.statusbar.phone.ScrimController; +import javax.inject.Inject; + /** * Provides the UI shown during system shutdown. */ @@ -45,9 +50,13 @@ public class ShutdownUi { private Context mContext; private BlurUtils mBlurUtils; - public ShutdownUi(Context context, BlurUtils blurUtils) { + private NearbyManager mNearbyManager; + + @Inject + public ShutdownUi(Context context, BlurUtils blurUtils, NearbyManager nearbyManager) { mContext = context; mBlurUtils = blurUtils; + mNearbyManager = nearbyManager; } /** @@ -132,12 +141,28 @@ public class ShutdownUi { /** * Returns the layout resource to use for UI while shutting down. * @param isReboot Whether this is a reboot or a shutdown. - * @return */ - public int getShutdownDialogContent(boolean isReboot) { - return R.layout.shutdown_dialog; + @VisibleForTesting int getShutdownDialogContent(boolean isReboot) { + if (!Flags.poweredOffFindingPlatform()) { + return R.layout.shutdown_dialog; + } + int finderActive = mNearbyManager.getPoweredOffFindingMode(); + if (finderActive == NearbyManager.POWERED_OFF_FINDING_MODE_DISABLED + || finderActive == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) { + // inactive or unsupported, use regular shutdown dialog + return R.layout.shutdown_dialog; + } else if (finderActive == NearbyManager.POWERED_OFF_FINDING_MODE_ENABLED) { + // active, use dialog with finder info if shutting down + return isReboot ? R.layout.shutdown_dialog : + com.android.systemui.res.R.layout.shutdown_dialog_finder_active; + } else { + // that's weird? default to regular dialog + Log.w("ShutdownUi", "Unexpected value for finder active: " + finderActive); + return R.layout.shutdown_dialog; + } } + @StringRes @VisibleForTesting int getRebootMessage(boolean isReboot, @Nullable String reason) { if (reason != null && reason.startsWith(PowerManager.REBOOT_RECOVERY_UPDATE)) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt index 378ce52b4331..53f448826e80 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt @@ -60,6 +60,22 @@ constructor( private var leaveShadeOpen: Boolean = false private var willRunDismissFromKeyguard: Boolean = false + val notificationAlpha: Flow<Float> = + transitionAnimation.sharedFlow( + duration = 200.milliseconds, + onStart = { + leaveShadeOpen = statusBarStateController.leaveOpenOnKeyguardHide() + willRunDismissFromKeyguard = primaryBouncerInteractor.willRunDismissFromKeyguard() + }, + onStep = { + if (willRunDismissFromKeyguard || leaveShadeOpen) { + 1f + } else { + 1f - it + } + }, + ) + /** Bouncer container alpha */ val bouncerAlpha: Flow<Float> = if (featureFlags.isEnabled(Flags.REFACTOR_KEYGUARD_DISMISS_INTENT)) { @@ -94,6 +110,7 @@ constructor( } else { createLockscreenAlpha(primaryBouncerInteractor::willRunDismissFromKeyguard) } + private fun createLockscreenAlpha(willRunAnimationOnKeyguard: () -> Boolean): Flow<Float> { return transitionAnimation.sharedFlow( duration = 50.milliseconds, diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialog.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogDelegate.kt index 6b53c7ac0a14..7ece6e0defc1 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialog.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogDelegate.kt @@ -37,11 +37,13 @@ import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.android.internal.logging.UiEventLogger -import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.res.R import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.util.time.SystemClock +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow @@ -52,19 +54,24 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.withContext /** Dialog for showing active, connected and saved bluetooth devices. */ -@SysUISingleton -internal class BluetoothTileDialog -constructor( - private val bluetoothToggleInitialValue: Boolean, - private val initialUiProperties: BluetoothTileDialogViewModel.UiProperties, - private val cachedContentHeight: Int, - private val bluetoothTileDialogCallback: BluetoothTileDialogCallback, +class BluetoothTileDialogDelegate +@AssistedInject +internal constructor( + @Assisted private val context: Context, + @Assisted private val initialUiProperties: BluetoothTileDialogViewModel.UiProperties, + @Assisted private val cachedContentHeight: Int, + @Assisted private val bluetoothToggleInitialValue: Boolean, + @Assisted private val bluetoothTileDialogCallback: BluetoothTileDialogCallback, + @Assisted private val dismissListener: Runnable, @Main private val mainDispatcher: CoroutineDispatcher, private val systemClock: SystemClock, private val uiEventLogger: UiEventLogger, private val logger: BluetoothTileDialogLogger, - context: Context, -) : SystemUIDialog(context, DEFAULT_THEME, DEFAULT_DISMISS_ON_DEVICE_LOCK) { + private val systemuiDialogFactory: SystemUIDialog.Factory, + mainLayoutInflater: LayoutInflater, +) : SystemUIDialog.Delegate { + + private val layoutInflater = mainLayoutInflater.cloneInContext(context) private val mutableBluetoothStateToggle: MutableStateFlow<Boolean> = MutableStateFlow(bluetoothToggleInitialValue) @@ -91,78 +98,72 @@ constructor( private var lastItemRow: Int = -1 - private lateinit var toggleView: Switch - private lateinit var subtitleTextView: TextView - private lateinit var autoOnToggle: Switch - private lateinit var autoOnToggleView: View - private lateinit var doneButton: View - private lateinit var seeAllButton: View - private lateinit var pairNewDeviceButton: View - private lateinit var deviceListView: RecyclerView - private lateinit var scrollViewContent: View - private lateinit var progressBarAnimation: ProgressBar - private lateinit var progressBarBackground: View - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) + @AssistedFactory + internal interface Factory { + fun create( + context: Context, + initialUiProperties: BluetoothTileDialogViewModel.UiProperties, + cachedContentHeight: Int, + bluetoothEnabled: Boolean, + dialogCallback: BluetoothTileDialogCallback, + dimissListener: Runnable + ): BluetoothTileDialogDelegate + } + + override fun createDialog(): SystemUIDialog { + val dialog = systemuiDialogFactory.create(this, context) + + return dialog + } + + override fun onCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) { + SystemUIDialog.registerDismissListener(dialog, dismissListener) uiEventLogger.log(BluetoothTileDialogUiEvent.BLUETOOTH_TILE_DIALOG_SHOWN) - LayoutInflater.from(context).inflate(R.layout.bluetooth_tile_dialog, null).apply { + layoutInflater.inflate(R.layout.bluetooth_tile_dialog, null).apply { accessibilityPaneTitle = context.getText(R.string.accessibility_desc_quick_settings) - setContentView(this) + dialog.setContentView(this) } - toggleView = requireViewById(R.id.bluetooth_toggle) - subtitleTextView = requireViewById(R.id.bluetooth_tile_dialog_subtitle) as TextView - autoOnToggle = requireViewById(R.id.bluetooth_auto_on_toggle) - autoOnToggleView = requireViewById(R.id.bluetooth_auto_on_toggle_layout) - doneButton = requireViewById(R.id.done_button) - seeAllButton = requireViewById(R.id.see_all_button) - pairNewDeviceButton = requireViewById(R.id.pair_new_device_button) - deviceListView = requireViewById<RecyclerView>(R.id.device_list) - - setupToggle() - setupRecyclerView() - - subtitleTextView.text = context.getString(initialUiProperties.subTitleResId) - doneButton.setOnClickListener { dismiss() } - seeAllButton.setOnClickListener { bluetoothTileDialogCallback.onSeeAllClicked(it) } - pairNewDeviceButton.setOnClickListener { + setupToggle(dialog) + setupRecyclerView(dialog) + + getSubtitleTextView(dialog).text = context.getString(initialUiProperties.subTitleResId) + dialog.requireViewById<View>(R.id.done_button).setOnClickListener { dialog.dismiss() } + getSeeAllButton(dialog).setOnClickListener { + bluetoothTileDialogCallback.onSeeAllClicked(it) + } + getPairNewDeviceButton(dialog).setOnClickListener { bluetoothTileDialogCallback.onPairNewDeviceClicked(it) } - requireViewById<View>(R.id.scroll_view).apply { - scrollViewContent = this + getScrollViewContent(dialog).apply { minimumHeight = resources.getDimensionPixelSize(initialUiProperties.scrollViewMinHeightResId) layoutParams.height = maxOf(cachedContentHeight, minimumHeight) } - progressBarAnimation = requireViewById(R.id.bluetooth_tile_dialog_progress_animation) - progressBarBackground = requireViewById(R.id.bluetooth_tile_dialog_progress_background) } - override fun start() { + override fun onStart(dialog: SystemUIDialog) { lastUiUpdateMs = systemClock.elapsedRealtime() } - override fun dismiss() { - if (::scrollViewContent.isInitialized) { - mutableContentHeight.tryEmit(scrollViewContent.measuredHeight) - } - super.dismiss() + override fun onStop(dialog: SystemUIDialog) { + mutableContentHeight.tryEmit(getScrollViewContent(dialog).measuredHeight) } - internal suspend fun animateProgressBar(animate: Boolean) { + internal suspend fun animateProgressBar(dialog: SystemUIDialog, animate: Boolean) { withContext(mainDispatcher) { if (animate) { - showProgressBar() + showProgressBar(dialog) } else { delay(PROGRESS_BAR_ANIMATION_DURATION_MS) - hideProgressBar() + hideProgressBar(dialog) } } } internal suspend fun onDeviceItemUpdated( + dialog: SystemUIDialog, deviceItem: List<DeviceItem>, showSeeAll: Boolean, showPairNewDevice: Boolean @@ -176,10 +177,11 @@ constructor( } if (isActive) { deviceItemAdapter.refreshDeviceItemList(deviceItem) { - seeAllButton.visibility = if (showSeeAll) VISIBLE else GONE - pairNewDeviceButton.visibility = if (showPairNewDevice) VISIBLE else GONE + getSeeAllButton(dialog).visibility = if (showSeeAll) VISIBLE else GONE + getPairNewDeviceButton(dialog).visibility = + if (showPairNewDevice) VISIBLE else GONE // Update the height after data is updated - scrollViewContent.layoutParams.height = WRAP_CONTENT + getScrollViewContent(dialog).layoutParams.height = WRAP_CONTENT lastUiUpdateMs = systemClock.elapsedRealtime() lastItemRow = itemRow logger.logDeviceUiUpdate(lastUiUpdateMs - start) @@ -189,29 +191,29 @@ constructor( } internal fun onBluetoothStateUpdated( + dialog: SystemUIDialog, isEnabled: Boolean, uiProperties: BluetoothTileDialogViewModel.UiProperties ) { - toggleView.apply { + getToggleView(dialog).apply { isChecked = isEnabled setEnabled(true) alpha = ENABLED_ALPHA } - subtitleTextView.text = context.getString(uiProperties.subTitleResId) - autoOnToggleView.visibility = uiProperties.autoOnToggleVisibility + getSubtitleTextView(dialog).text = context.getString(uiProperties.subTitleResId) + getAutoOnToggleView(dialog).visibility = uiProperties.autoOnToggleVisibility } - internal fun onBluetoothAutoOnUpdated(isEnabled: Boolean) { - if (::autoOnToggle.isInitialized) { - autoOnToggle.apply { - isChecked = isEnabled - setEnabled(true) - alpha = ENABLED_ALPHA - } + internal fun onBluetoothAutoOnUpdated(dialog: SystemUIDialog, isEnabled: Boolean) { + getAutoOnToggle(dialog).apply { + isChecked = isEnabled + setEnabled(true) + alpha = ENABLED_ALPHA } } - private fun setupToggle() { + private fun setupToggle(dialog: SystemUIDialog) { + val toggleView = getToggleView(dialog) toggleView.isChecked = bluetoothToggleInitialValue toggleView.setOnCheckedChangeListener { view, isChecked -> mutableBluetoothStateToggle.value = isChecked @@ -223,8 +225,8 @@ constructor( uiEventLogger.log(BluetoothTileDialogUiEvent.BLUETOOTH_TOGGLE_CLICKED) } - autoOnToggleView.visibility = initialUiProperties.autoOnToggleVisibility - autoOnToggle.setOnCheckedChangeListener { view, isChecked -> + getAutoOnToggleView(dialog).visibility = initialUiProperties.autoOnToggleVisibility + getAutoOnToggle(dialog).setOnCheckedChangeListener { view, isChecked -> mutableBluetoothAutoOnToggle.value = isChecked view.apply { isEnabled = false @@ -234,30 +236,66 @@ constructor( } } - private fun setupRecyclerView() { - deviceListView.apply { + private fun getToggleView(dialog: SystemUIDialog): Switch { + return dialog.requireViewById(R.id.bluetooth_toggle) + } + + private fun getSubtitleTextView(dialog: SystemUIDialog): TextView { + return dialog.requireViewById(R.id.bluetooth_tile_dialog_subtitle) + } + + private fun getSeeAllButton(dialog: SystemUIDialog): View { + return dialog.requireViewById(R.id.see_all_button) + } + + private fun getPairNewDeviceButton(dialog: SystemUIDialog): View { + return dialog.requireViewById(R.id.pair_new_device_button) + } + + private fun getDeviceListView(dialog: SystemUIDialog): RecyclerView { + return dialog.requireViewById(R.id.device_list) + } + + private fun getAutoOnToggle(dialog: SystemUIDialog): Switch { + return dialog.requireViewById(R.id.bluetooth_auto_on_toggle) + } + + private fun getAutoOnToggleView(dialog: SystemUIDialog): View { + return dialog.requireViewById(R.id.bluetooth_auto_on_toggle_layout) + } + + private fun getProgressBarAnimation(dialog: SystemUIDialog): ProgressBar { + return dialog.requireViewById(R.id.bluetooth_tile_dialog_progress_animation) + } + + private fun getProgressBarBackground(dialog: SystemUIDialog): View { + return dialog.requireViewById(R.id.bluetooth_tile_dialog_progress_animation) + } + + private fun getScrollViewContent(dialog: SystemUIDialog): View { + return dialog.requireViewById(R.id.scroll_view) + } + + private fun setupRecyclerView(dialog: SystemUIDialog) { + getDeviceListView(dialog).apply { layoutManager = LinearLayoutManager(context) adapter = deviceItemAdapter } } - private fun showProgressBar() { - if ( - ::progressBarAnimation.isInitialized && - ::progressBarBackground.isInitialized && - progressBarAnimation.visibility != VISIBLE - ) { + private fun showProgressBar(dialog: SystemUIDialog) { + val progressBarAnimation = getProgressBarAnimation(dialog) + val progressBarBackground = getProgressBarBackground(dialog) + if (progressBarAnimation.visibility != VISIBLE) { progressBarAnimation.visibility = VISIBLE progressBarBackground.visibility = INVISIBLE } } - private fun hideProgressBar() { - if ( - ::progressBarAnimation.isInitialized && - ::progressBarBackground.isInitialized && - progressBarAnimation.visibility != INVISIBLE - ) { + private fun hideProgressBar(dialog: SystemUIDialog) { + val progressBarAnimation = getProgressBarAnimation(dialog) + val progressBarBackground = getProgressBarBackground(dialog) + if (progressBarAnimation.visibility != INVISIBLE) { progressBarAnimation.visibility = INVISIBLE progressBarBackground.visibility = VISIBLE } @@ -295,9 +333,7 @@ constructor( private val asyncListDiffer = AsyncListDiffer(this, diffUtilCallback) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DeviceItemViewHolder { - val view = - LayoutInflater.from(parent.context) - .inflate(R.layout.bluetooth_device_item, parent, false) + val view = layoutInflater.inflate(R.layout.bluetooth_device_item, parent, false) return DeviceItemViewHolder(view) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt index 5a14e5f11d38..04862077969d 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModel.kt @@ -38,13 +38,11 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.plugins.ActivityStarter -import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothTileDialog.Companion.ACTION_BLUETOOTH_DEVICE_DETAILS -import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothTileDialog.Companion.ACTION_PAIR_NEW_DEVICE -import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothTileDialog.Companion.ACTION_PREVIOUSLY_CONNECTED_DEVICE -import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothTileDialog.Companion.MAX_DEVICE_ITEM_ENTRY +import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothTileDialogDelegate.Companion.ACTION_BLUETOOTH_DEVICE_DETAILS +import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothTileDialogDelegate.Companion.ACTION_PAIR_NEW_DEVICE +import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothTileDialogDelegate.Companion.ACTION_PREVIOUSLY_CONNECTED_DEVICE +import com.android.systemui.qs.tiles.dialog.bluetooth.BluetoothTileDialogDelegate.Companion.MAX_DEVICE_ITEM_ENTRY import com.android.systemui.res.R -import com.android.systemui.statusbar.phone.SystemUIDialog -import com.android.systemui.util.time.SystemClock import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope @@ -67,13 +65,12 @@ constructor( private val bluetoothAutoOnInteractor: BluetoothAutoOnInteractor, private val dialogTransitionAnimator: DialogTransitionAnimator, private val activityStarter: ActivityStarter, - private val systemClock: SystemClock, private val uiEventLogger: UiEventLogger, - private val logger: BluetoothTileDialogLogger, @Application private val coroutineScope: CoroutineScope, @Main private val mainDispatcher: CoroutineDispatcher, @Background private val backgroundDispatcher: CoroutineDispatcher, @Main private val sharedPreferences: SharedPreferences, + private val bluetoothDialogDelegateFactory: BluetoothTileDialogDelegate.Factory, ) : BluetoothTileDialogCallback { private var job: Job? = null @@ -92,7 +89,8 @@ constructor( coroutineScope.launch(mainDispatcher) { var updateDeviceItemJob: Job? var updateDialogUiJob: Job? = null - val dialog = createBluetoothTileDialog(context) + val dialogDelegate = createBluetoothTileDialog(context) + val dialog = dialogDelegate.createDialog() view?.let { dialogTransitionAnimator.showFromView( @@ -118,13 +116,14 @@ constructor( .onEach { updateDialogUiJob?.cancel() updateDialogUiJob = launch { - dialog.apply { + dialogDelegate.apply { onDeviceItemUpdated( + dialog, it.take(MAX_DEVICE_ITEM_ENTRY), showSeeAll = it.size > MAX_DEVICE_ITEM_ENTRY, showPairNewDevice = bluetoothStateInteractor.isBluetoothEnabled ) - animateProgressBar(false) + animateProgressBar(dialog, false) } } } @@ -134,7 +133,7 @@ constructor( // the device item list and animiate the progress bar. deviceItemInteractor.deviceItemUpdateRequest .onEach { - dialog.animateProgressBar(true) + dialogDelegate.animateProgressBar(dialog, true) updateDeviceItemJob?.cancel() updateDeviceItemJob = launch { deviceItemInteractor.updateDeviceItems( @@ -150,7 +149,8 @@ constructor( bluetoothStateInteractor.bluetoothStateUpdate .filterNotNull() .onEach { - dialog.onBluetoothStateUpdated( + dialogDelegate.onBluetoothStateUpdated( + dialog, it, UiProperties.build(it, isAutoOnToggleFeatureAvailable()) ) @@ -166,20 +166,20 @@ constructor( // bluetoothStateToggle is emitted when user toggles the bluetooth state switch, // send the new value to the bluetoothStateInteractor and animate the progress bar. - dialog.bluetoothStateToggle + dialogDelegate.bluetoothStateToggle .onEach { - dialog.animateProgressBar(true) + dialogDelegate.animateProgressBar(dialog, true) bluetoothStateInteractor.isBluetoothEnabled = it } .launchIn(this) // deviceItemClick is emitted when user clicked on a device item. - dialog.deviceItemClick + dialogDelegate.deviceItemClick .onEach { deviceItemInteractor.updateDeviceItemOnClick(it) } .launchIn(this) // contentHeight is emitted when the dialog is dismissed. - dialog.contentHeight + dialogDelegate.contentHeight .onEach { withContext(backgroundDispatcher) { sharedPreferences.edit().putInt(CONTENT_HEIGHT_PREF_KEY, it).apply() @@ -191,12 +191,12 @@ constructor( // bluetoothAutoOnUpdate is emitted when bluetooth auto on on/off state is // changed. bluetoothAutoOnInteractor.isEnabled - .onEach { dialog.onBluetoothAutoOnUpdated(it) } + .onEach { dialogDelegate.onBluetoothAutoOnUpdated(dialog, it) } .launchIn(this) // bluetoothAutoOnToggle is emitted when user toggles the bluetooth auto on // switch, send the new value to the bluetoothAutoOnInteractor. - dialog.bluetoothAutoOnToggle + dialogDelegate.bluetoothAutoOnToggle .filterNotNull() .onEach { bluetoothAutoOnInteractor.setEnabled(it) } .launchIn(this) @@ -206,7 +206,7 @@ constructor( } } - private suspend fun createBluetoothTileDialog(context: Context): BluetoothTileDialog { + private suspend fun createBluetoothTileDialog(context: Context): BluetoothTileDialogDelegate { val cachedContentHeight = withContext(backgroundDispatcher) { sharedPreferences.getInt( @@ -215,21 +215,17 @@ constructor( ) } - return BluetoothTileDialog( + return bluetoothDialogDelegateFactory.create( + context, + UiProperties.build( bluetoothStateInteractor.isBluetoothEnabled, - UiProperties.build( - bluetoothStateInteractor.isBluetoothEnabled, - isAutoOnToggleFeatureAvailable() - ), - cachedContentHeight, - this@BluetoothTileDialogViewModel, - mainDispatcher, - systemClock, - uiEventLogger, - logger, - context - ) - .apply { SystemUIDialog.registerDismissListener(this) { cancelJob() } } + isAutoOnToggleFeatureAvailable() + ), + cachedContentHeight, + bluetoothStateInteractor.isBluetoothEnabled, + this@BluetoothTileDialogViewModel, + { cancelJob() } + ) } override fun onDeviceItemGearClicked(deviceItem: DeviceItem, view: View) { @@ -308,7 +304,7 @@ constructor( } } -internal interface BluetoothTileDialogCallback { +interface BluetoothTileDialogCallback { fun onDeviceItemGearClicked(deviceItem: DeviceItem, view: View) fun onSeeAllClicked(view: View) fun onPairNewDeviceClicked(view: View) diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/PanelExpansionInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/PanelExpansionInteractor.kt new file mode 100644 index 000000000000..36350f8af455 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/PanelExpansionInteractor.kt @@ -0,0 +1,104 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.android.systemui.scene.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.scene.shared.flag.SceneContainerFlag +import com.android.systemui.scene.shared.model.ObservableTransitionState +import com.android.systemui.scene.shared.model.SceneKey +import com.android.systemui.shade.data.repository.ShadeRepository +import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map + +@SysUISingleton +class PanelExpansionInteractor +@Inject +constructor( + sceneInteractor: SceneInteractor, + shadeRepository: ShadeRepository, +) { + + /** + * The amount by which the "panel" has been expanded (`0` when fully collapsed, `1` when fully + * expanded). + * + * This is a legacy concept from the time when the "panel" included the notification/QS shades + * as well as the keyguard (lockscreen and bouncer). This value is meant only for + * backwards-compatibility and should not be consumed by newer code. + */ + @Deprecated("Use SceneInteractor.currentScene instead.") + val legacyPanelExpansion: Flow<Float> = + if (SceneContainerFlag.isEnabled) { + sceneInteractor.transitionState.flatMapLatest { state -> + when (state) { + is ObservableTransitionState.Idle -> + flowOf( + if (state.scene != SceneKey.Gone) { + // When resting on a non-Gone scene, the panel is fully expanded. + 1f + } else { + // When resting on the Gone scene, the panel is considered fully + // collapsed. + 0f + } + ) + is ObservableTransitionState.Transition -> + when { + state.fromScene == SceneKey.Gone -> + if (state.toScene.isExpandable()) { + // Moving from Gone to a scene that can animate-expand has a + // panel + // expansion + // that tracks with the transition. + state.progress + } else { + // Moving from Gone to a scene that doesn't animate-expand + // immediately makes + // the panel fully expanded. + flowOf(1f) + } + state.toScene == SceneKey.Gone -> + if (state.fromScene.isExpandable()) { + // Moving to Gone from a scene that can animate-expand has a + // panel + // expansion + // that tracks with the transition. + state.progress.map { 1 - it } + } else { + // Moving to Gone from a scene that doesn't animate-expand + // immediately makes + // the panel fully collapsed. + flowOf(0f) + } + else -> flowOf(1f) + } + } + } + } else { + shadeRepository.legacyShadeExpansion + } + + private fun SceneKey.isExpandable(): Boolean { + return this == SceneKey.Shade || this == SceneKey.QuickSettings + } +} 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 b642d38289fe..034f87f4c72f 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 @@ -26,6 +26,7 @@ import com.android.systemui.bouncer.domain.interactor.BouncerInteractor import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor import com.android.systemui.classifier.FalsingCollector import com.android.systemui.classifier.FalsingCollectorActual +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.DisplayId @@ -34,6 +35,8 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.model.SceneContainerPlugin import com.android.systemui.model.SysUiState import com.android.systemui.model.updateFlags +import com.android.systemui.plugins.FalsingManager +import com.android.systemui.plugins.FalsingManager.FalsingBeliefListener import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.flag.SceneContainerFlags @@ -53,6 +56,7 @@ import java.io.PrintWriter import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged @@ -82,6 +86,7 @@ constructor( @DisplayId private val displayId: Int, private val sceneLogger: SceneLogger, @FalsingCollectorActual private val falsingCollector: FalsingCollector, + private val falsingManager: FalsingManager, private val powerInteractor: PowerInteractor, private val simBouncerInteractor: Lazy<SimBouncerInteractor>, private val authenticationInteractor: Lazy<AuthenticationInteractor>, @@ -98,6 +103,7 @@ constructor( automaticallySwitchScenes() hydrateSystemUiState() collectFalsingSignals() + respondToFalsingDetections() hydrateWindowFocus() hydrateInteractionState() } else { @@ -376,6 +382,18 @@ constructor( } } + /** Switches to the lockscreen when falsing is detected. */ + private fun respondToFalsingDetections() { + applicationScope.launch { + conflatedCallbackFlow { + val listener = FalsingBeliefListener { trySend(Unit) } + falsingManager.addFalsingBeliefListener(listener) + awaitClose { falsingManager.removeFalsingBeliefListener(listener) } + } + .collect { switchToScene(SceneKey.Lockscreen, "Falsing detected.") } + } + } + /** Keeps the focus state of the window view up-to-date. */ private fun hydrateWindowFocus() { applicationScope.launch { diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt index 45b6f65d5d67..ee76c0582b9d 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt @@ -16,11 +16,9 @@ package com.android.systemui.scene.ui.view -import android.view.Gravity import android.view.View import android.view.ViewGroup import android.view.WindowInsets -import android.widget.FrameLayout import androidx.activity.OnBackPressedDispatcher import androidx.activity.OnBackPressedDispatcherOwner import androidx.activity.setViewTreeOnBackPressedDispatcherOwner @@ -39,7 +37,6 @@ import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel import com.android.systemui.statusbar.notification.stack.shared.flexiNotifsEnabled import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer -import java.time.Instant import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch @@ -98,7 +95,7 @@ object SceneWindowRootViewBinder { ) val legacyView = view.requireViewById<View>(R.id.legacy_window_root) - view.addView(createVisibilityToggleView(legacyView)) + legacyView.isVisible = false // This moves the SharedNotificationContainer to the WindowRootView just after // the SceneContainerView. This SharedNotificationContainer should contain NSSL @@ -123,29 +120,4 @@ object SceneWindowRootViewBinder { } } } - - private var clickCount = 0 - private var lastClick = Instant.now() - - /** - * A temporary UI to toggle on/off the visibility of the given [otherView]. It is toggled by - * tapping 5 times in quick succession on the device camera (top center). - */ - // TODO(b/291321285): Remove this when the Flexiglass UI is mature enough to turn off legacy - // SysUI altogether. - private fun createVisibilityToggleView(otherView: View): View { - val toggleView = View(otherView.context) - otherView.isVisible = false - toggleView.layoutParams = FrameLayout.LayoutParams(200, 200, Gravity.CENTER_HORIZONTAL) - toggleView.setOnClickListener { - val now = Instant.now() - clickCount = if (now.minusSeconds(2) > lastClick) 1 else clickCount + 1 - if (clickCount == 5) { - otherView.isVisible = !otherView.isVisible - clickCount = 0 - } - lastClick = now - } - return toggleView - } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index d5bbaa5be53c..7b330b0f3803 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -1201,7 +1201,12 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump // Primary bouncer->Gone (ensures lockscreen content is not visible on successful auth) if (!migrateClocksToBlueprint()) { collectFlow(mView, mPrimaryBouncerToGoneTransitionViewModel.getLockscreenAlpha(), - setTransitionAlpha(mNotificationStackScrollLayoutController), mMainDispatcher); + setTransitionAlpha(mNotificationStackScrollLayoutController, + /* excludeNotifications=*/ true), mMainDispatcher); + collectFlow(mView, mPrimaryBouncerToGoneTransitionViewModel.getNotificationAlpha(), + (Float alpha) -> { + mNotificationStackScrollLayoutController.setMaxAlphaForExpansion(alpha); + }, mMainDispatcher); } } @@ -4725,9 +4730,17 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump private Consumer<Float> setTransitionAlpha( NotificationStackScrollLayoutController stackScroller) { + return setTransitionAlpha(stackScroller, /* excludeNotifications= */ false); + } + + private Consumer<Float> setTransitionAlpha( + NotificationStackScrollLayoutController stackScroller, + boolean excludeNotifications) { return (Float alpha) -> { mKeyguardStatusViewController.setAlpha(alpha); - stackScroller.setMaxAlphaForExpansion(alpha); + if (!excludeNotifications) { + stackScroller.setMaxAlphaForExpansion(alpha); + } if (keyguardBottomAreaRefactor()) { mKeyguardInteractor.setAlpha(alpha); diff --git a/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeTransitionController.kt index 971507055873..84afbed51faa 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeTransitionController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeTransitionController.kt @@ -19,8 +19,11 @@ package com.android.systemui.shade.transition import android.content.Context import android.content.res.Configuration import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dump.DumpManager import com.android.systemui.plugins.qs.QS +import com.android.systemui.scene.domain.interactor.PanelExpansionInteractor +import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.PanelState import com.android.systemui.shade.ShadeExpansionChangeEvent import com.android.systemui.shade.ShadeExpansionStateManager @@ -31,21 +34,26 @@ import com.android.systemui.statusbar.SysuiStatusBarStateController import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.SplitShadeStateController +import dagger.Lazy import java.io.PrintWriter import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch /** Controls the shade expansion transition on non-lockscreen. */ @SysUISingleton class ShadeTransitionController @Inject constructor( + @Application private val applicationScope: CoroutineScope, configurationController: ConfigurationController, shadeExpansionStateManager: ShadeExpansionStateManager, dumpManager: DumpManager, private val context: Context, private val scrimShadeTransitionController: ScrimShadeTransitionController, private val statusBarStateController: SysuiStatusBarStateController, - private val splitShadeStateController: SplitShadeStateController + private val splitShadeStateController: SplitShadeStateController, + private val panelExpansionInteractor: Lazy<PanelExpansionInteractor>, ) { lateinit var shadeViewController: ShadeViewController @@ -63,11 +71,27 @@ constructor( override fun onConfigChanged(newConfig: Configuration?) { updateResources() } - }) - val currentState = - shadeExpansionStateManager.addExpansionListener(this::onPanelExpansionChanged) - onPanelExpansionChanged(currentState) - shadeExpansionStateManager.addStateListener(this::onPanelStateChanged) + } + ) + if (SceneContainerFlag.isEnabled) { + applicationScope.launch { + panelExpansionInteractor.get().legacyPanelExpansion.collect { panelExpansion -> + onPanelExpansionChanged( + ShadeExpansionChangeEvent( + fraction = panelExpansion, + expanded = panelExpansion > 0f, + tracking = true, + dragDownPxAmount = 0f, + ) + ) + } + } + } else { + val currentState = + shadeExpansionStateManager.addExpansionListener(this::onPanelExpansionChanged) + onPanelExpansionChanged(currentState) + shadeExpansionStateManager.addStateListener(this::onPanelStateChanged) + } dumpManager.registerCriticalDumpable("ShadeTransitionController") { printWriter, _ -> dump(printWriter) } @@ -98,7 +122,9 @@ constructor( qs.isInitialized: ${this::qs.isInitialized} npvc.isInitialized: ${this::shadeViewController.isInitialized} nssl.isInitialized: ${this::notificationStackScrollLayoutController.isInitialized} - """.trimIndent()) + """ + .trimIndent() + ) } private fun isScreenUnlocked() = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java index ca19f71bd391..bb8168335b60 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java @@ -168,7 +168,7 @@ public class CommandQueue extends IStatusBar.Stub implements private static final int MSG_UNREGISTER_NEARBY_MEDIA_DEVICE_PROVIDER = 67 << MSG_SHIFT; private static final int MSG_TILE_SERVICE_REQUEST_LISTENING_STATE = 68 << MSG_SHIFT; private static final int MSG_SHOW_REAR_DISPLAY_DIALOG = 69 << MSG_SHIFT; - private static final int MSG_GO_TO_FULLSCREEN_FROM_SPLIT = 70 << MSG_SHIFT; + private static final int MSG_MOVE_FOCUSED_TASK_TO_FULLSCREEN = 70 << MSG_SHIFT; private static final int MSG_ENTER_STAGE_SPLIT_FROM_RUNNING_APP = 71 << MSG_SHIFT; private static final int MSG_SHOW_MEDIA_OUTPUT_SWITCHER = 72 << MSG_SHIFT; private static final int MSG_TOGGLE_TASKBAR = 73 << MSG_SHIFT; @@ -498,9 +498,9 @@ public class CommandQueue extends IStatusBar.Stub implements default void showRearDisplayDialog(int currentBaseState) {} /** - * @see IStatusBar#goToFullscreenFromSplit + * @see IStatusBar#moveFocusedTaskToFullscreen */ - default void goToFullscreenFromSplit() {} + default void moveFocusedTaskToFullscreen(int displayId) {} /** * @see IStatusBar#enterStageSplitFromRunningApp @@ -1422,8 +1422,10 @@ public class CommandQueue extends IStatusBar.Stub implements } @Override - public void goToFullscreenFromSplit() { - mHandler.obtainMessage(MSG_GO_TO_FULLSCREEN_FROM_SPLIT).sendToTarget(); + public void moveFocusedTaskToFullscreen(int displayId) { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = displayId; + mHandler.obtainMessage(MSG_MOVE_FOCUSED_TASK_TO_FULLSCREEN, args).sendToTarget(); } @Override @@ -1897,11 +1899,14 @@ public class CommandQueue extends IStatusBar.Stub implements mCallbacks.get(i).showRearDisplayDialog((Integer) msg.obj); } break; - case MSG_GO_TO_FULLSCREEN_FROM_SPLIT: + case MSG_MOVE_FOCUSED_TASK_TO_FULLSCREEN: { + args = (SomeArgs) msg.obj; + int displayId = args.argi1; for (int i = 0; i < mCallbacks.size(); i++) { - mCallbacks.get(i).goToFullscreenFromSplit(); + mCallbacks.get(i).moveFocusedTaskToFullscreen(displayId); } break; + } case MSG_ENTER_STAGE_SPLIT_FROM_RUNNING_APP: for (int i = 0; i < mCallbacks.size(); i++) { mCallbacks.get(i).enterStageSplitFromRunningApp((Boolean) msg.obj); @@ -1927,13 +1932,14 @@ public class CommandQueue extends IStatusBar.Stub implements mCallbacks.get(i).immersiveModeChanged(rootDisplayAreaId, isImmersiveMode); } break; - case MSG_ENTER_DESKTOP: + case MSG_ENTER_DESKTOP: { args = (SomeArgs) msg.obj; int displayId = args.argi1; for (int i = 0; i < mCallbacks.size(); i++) { mCallbacks.get(i).enterDesktop(displayId); } break; + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RowAlertTimeCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RowAlertTimeCoordinator.kt index 12de339871bb..4a7b7ca51ba2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RowAlertTimeCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/RowAlertTimeCoordinator.kt @@ -54,7 +54,7 @@ class RowAlertTimeCoordinator @Inject constructor() : Coordinator { } private fun GroupEntry.calculateLatestAlertTime(): Long { - val lastChildAlertedTime = children.maxOf { it.lastAudiblyAlertedMs } + val lastChildAlertedTime = children.maxOfOrNull { it.lastAudiblyAlertedMs } ?: 0 val summaryAlertedTime = checkNotNull(summary).lastAudiblyAlertedMs return max(lastChildAlertedTime, summaryAlertedTime) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java index 510086d4892b..dc9eeb35565a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java @@ -191,11 +191,11 @@ public class NotificationInterruptStateProviderImpl implements NotificationInter public boolean shouldBubbleUp(NotificationEntry entry) { final StatusBarNotification sbn = entry.getSbn(); - if (!canAlertCommon(entry, true)) { + if (!canAlertCommon(entry, false)) { return false; } - if (!canAlertAwakeCommon(entry, true)) { + if (!canAlertAwakeCommon(entry, false)) { return false; } 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 052e35c44bbe..a15d829ade07 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 @@ -333,7 +333,7 @@ constructor( lockscreenToPrimaryBouncerTransitionViewModel.lockscreenAlpha, occludedToAodTransitionViewModel.lockscreenAlpha, occludedToLockscreenTransitionViewModel.lockscreenAlpha, - primaryBouncerToGoneTransitionViewModel.lockscreenAlpha, + primaryBouncerToGoneTransitionViewModel.notificationAlpha, primaryBouncerToLockscreenTransitionViewModel.lockscreenAlpha, ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt index 6b303263d4b0..5e38715fefa2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt @@ -69,6 +69,9 @@ interface MobileIconInteractor { /** True if we consider this connection to be in service, i.e. can make calls */ val isInService: StateFlow<Boolean> + /** True if this connection is emergency only */ + val isEmergencyOnly: StateFlow<Boolean> + /** Observable for the data enabled state of this connection */ val isDataEnabled: StateFlow<Boolean> @@ -306,6 +309,8 @@ class MobileIconInteractorImpl( override val isInService = connectionRepository.isInService + override val isEmergencyOnly: StateFlow<Boolean> = connectionRepository.isEmergencyOnly + override val isAllowedDuringAirplaneMode = connectionRepository.isAllowedDuringAirplaneMode /** Whether or not to show the error state of [SignalDrawable] */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt index 6e1114c57e87..3f89d04bf492 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractor.kt @@ -72,9 +72,16 @@ constructor( /** When all connections are considered OOS, satellite connectivity is potentially valid */ val areAllConnectionsOutOfService = if (Flags.oemEnabledSatelliteFlag()) { - iconsInteractor.icons.aggregateOver(selector = { intr -> intr.isInService }) { - isInServiceList -> - isInServiceList.all { !it } + iconsInteractor.icons.aggregateOver( + selector = { intr -> + combine(intr.isInService, intr.isEmergencyOnly) { + isInService, + isEmergencyOnly -> + !isInService && !isEmergencyOnly + } + } + ) { isOosAndIsNotEmergencyOnly -> + isOosAndIsNotEmergencyOnly.all { it } } } else { flowOf(false) diff --git a/packages/SystemUI/src/com/android/systemui/unfold/FoldLightRevealOverlayAnimation.kt b/packages/SystemUI/src/com/android/systemui/unfold/FoldLightRevealOverlayAnimation.kt index 5c53ff98b777..d19a3364d502 100644 --- a/packages/SystemUI/src/com/android/systemui/unfold/FoldLightRevealOverlayAnimation.kt +++ b/packages/SystemUI/src/com/android/systemui/unfold/FoldLightRevealOverlayAnimation.kt @@ -16,6 +16,8 @@ package com.android.systemui.unfold +import android.animation.Animator +import android.animation.AnimatorListenerAdapter import android.animation.ValueAnimator import android.annotation.BinderThread import android.content.Context @@ -23,7 +25,6 @@ import android.os.Handler import android.os.SystemProperties import android.util.Log import android.view.animation.DecelerateInterpolator -import androidx.core.animation.addListener import com.android.internal.foldables.FoldLockSettingAvailabilityProvider import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.display.data.repository.DeviceStateRepository @@ -36,17 +37,25 @@ import com.android.systemui.unfold.FullscreenLightRevealAnimationController.Comp import com.android.systemui.unfold.dagger.UnfoldBg import com.android.systemui.util.animation.data.repository.AnimationStatusRepository import javax.inject.Inject +import kotlin.coroutines.resume import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.android.asCoroutineDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withTimeout +@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) class FoldLightRevealOverlayAnimation @Inject constructor( @@ -61,6 +70,9 @@ constructor( private val revealProgressValueAnimator: ValueAnimator = ValueAnimator.ofFloat(ALPHA_OPAQUE, ALPHA_TRANSPARENT) + private val areAnimationEnabled: Flow<Boolean> + get() = animationStatusRepository.areAnimationsEnabled() + private lateinit var controller: FullscreenLightRevealAnimationController @Volatile private var readyCallback: CompletableDeferred<Runnable>? = null @@ -89,33 +101,30 @@ constructor( applicationScope.launch(bgHandler.asCoroutineDispatcher()) { deviceStateRepository.state - .map { it != DeviceStateRepository.DeviceState.FOLDED } + .map { it == DeviceStateRepository.DeviceState.FOLDED } .distinctUntilChanged() - .filter { isUnfolded -> isUnfolded } - .collect { controller.ensureOverlayRemoved() } - } - - applicationScope.launch(bgHandler.asCoroutineDispatcher()) { - deviceStateRepository.state - .filter { - animationStatusRepository.areAnimationsEnabled().first() && - it == DeviceStateRepository.DeviceState.FOLDED - } - .collect { - try { - withTimeout(WAIT_FOR_ANIMATION_TIMEOUT_MS) { - readyCallback = CompletableDeferred() - val onReady = readyCallback?.await() - readyCallback = null - controller.addOverlay(ALPHA_OPAQUE, onReady) - waitForScreenTurnedOn() + .flatMapLatest { isFolded -> + flow<Nothing> { + if (!areAnimationEnabled.first() || !isFolded) { + return@flow + } + withTimeout(WAIT_FOR_ANIMATION_TIMEOUT_MS) { + readyCallback = CompletableDeferred() + val onReady = readyCallback?.await() + readyCallback = null + controller.addOverlay(ALPHA_OPAQUE, onReady) + waitForScreenTurnedOn() + } playFoldLightRevealOverlayAnimation() } - } catch (e: TimeoutCancellationException) { - Log.e(TAG, "Fold light reveal animation timed out") - ensureOverlayRemovedInternal() - } + .catchTimeoutAndLog() + .onCompletion { + val onReady = readyCallback?.takeIf { it.isCompleted }?.getCompleted() + onReady?.run() + readyCallback = null + } } + .collect {} } } @@ -128,19 +137,34 @@ constructor( powerInteractor.screenPowerState.filter { it == ScreenPowerState.SCREEN_ON }.first() } - private fun ensureOverlayRemovedInternal() { - revealProgressValueAnimator.cancel() - controller.ensureOverlayRemoved() - } - - private fun playFoldLightRevealOverlayAnimation() { + private suspend fun playFoldLightRevealOverlayAnimation() { revealProgressValueAnimator.duration = ANIMATION_DURATION revealProgressValueAnimator.interpolator = DecelerateInterpolator() revealProgressValueAnimator.addUpdateListener { animation -> controller.updateRevealAmount(animation.animatedFraction) } - revealProgressValueAnimator.addListener(onEnd = { controller.ensureOverlayRemoved() }) - revealProgressValueAnimator.start() + revealProgressValueAnimator.startAndAwaitCompletion() + } + + private suspend fun ValueAnimator.startAndAwaitCompletion(): Unit = + suspendCancellableCoroutine { continuation -> + val listener = + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + continuation.resume(Unit) + removeListener(this) + } + } + addListener(listener) + continuation.invokeOnCancellation { removeListener(listener) } + start() + } + + private fun <T> Flow<T>.catchTimeoutAndLog() = catch { exception -> + when (exception) { + is TimeoutCancellationException -> Log.e(TAG, "Fold light reveal animation timed out") + else -> throw exception + } } private companion object { diff --git a/packages/SystemUI/src/com/android/systemui/util/wakelock/ClientTrackingWakeLock.kt b/packages/SystemUI/src/com/android/systemui/util/wakelock/ClientTrackingWakeLock.kt new file mode 100644 index 000000000000..db300ebe6cae --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/wakelock/ClientTrackingWakeLock.kt @@ -0,0 +1,63 @@ +/* + * 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.util.wakelock + +import android.os.PowerManager +import android.util.Log +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicInteger + +/** + * [PowerManager.WakeLock] wrapper that tracks acquire/release reasons and logs them if owning + * logger is enabled. + */ +class ClientTrackingWakeLock( + private val pmWakeLock: PowerManager.WakeLock, + private val logger: WakeLockLogger?, + private val maxTimeout: Long +) : WakeLock { + + private val activeClients = ConcurrentHashMap<String, AtomicInteger>() + + override fun acquire(why: String) { + val count = activeClients.computeIfAbsent(why) { _ -> AtomicInteger(0) }.incrementAndGet() + logger?.logAcquire(pmWakeLock, why, count) + pmWakeLock.acquire(maxTimeout) + } + + override fun release(why: String) { + val count = activeClients[why]?.decrementAndGet() ?: -1 + if (count < 0) { + Log.wtf(WakeLock.TAG, "Releasing WakeLock with invalid reason: $why") + // Restore count just in case. + activeClients[why]?.incrementAndGet() + return + } + + logger?.logRelease(pmWakeLock, why, count) + pmWakeLock.release() + } + + override fun wrap(r: Runnable): Runnable = WakeLock.wrapImpl(this, r) + + fun activeClients(): Int = + activeClients.reduceValuesToInt(Long.MAX_VALUE, AtomicInteger::get, 0, Integer::sum) + + override fun toString(): String { + return "active clients=${activeClients()}" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/util/wakelock/DelayedWakeLock.java b/packages/SystemUI/src/com/android/systemui/util/wakelock/DelayedWakeLock.java index 039109e9ddc6..d2ed71cc3af1 100644 --- a/packages/SystemUI/src/com/android/systemui/util/wakelock/DelayedWakeLock.java +++ b/packages/SystemUI/src/com/android/systemui/util/wakelock/DelayedWakeLock.java @@ -19,8 +19,11 @@ package com.android.systemui.util.wakelock; import android.content.Context; import android.os.Handler; +import com.android.systemui.Flags; import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.dagger.qualifiers.Main; +import dagger.Lazy; import dagger.assisted.Assisted; import dagger.assisted.AssistedFactory; import dagger.assisted.AssistedInject; @@ -37,10 +40,13 @@ public class DelayedWakeLock implements WakeLock { private final WakeLock mInner; @AssistedInject - public DelayedWakeLock(@Background Handler handler, Context context, WakeLockLogger logger, + public DelayedWakeLock(@Background Lazy<Handler> bgHandler, + @Main Lazy<Handler> mainHandler, + Context context, WakeLockLogger logger, @Assisted String tag) { mInner = WakeLock.createPartial(context, logger, tag); - mHandler = handler; + mHandler = Flags.delayedWakelockReleaseOnBackgroundThread() ? bgHandler.get() + : mainHandler.get(); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/util/wakelock/WakeLock.java b/packages/SystemUI/src/com/android/systemui/util/wakelock/WakeLock.java index 6128feee8116..707751a58d84 100644 --- a/packages/SystemUI/src/com/android/systemui/util/wakelock/WakeLock.java +++ b/packages/SystemUI/src/com/android/systemui/util/wakelock/WakeLock.java @@ -22,6 +22,8 @@ import android.util.Log; import androidx.annotation.VisibleForTesting; +import com.android.systemui.Flags; + import java.util.HashMap; import javax.inject.Inject; @@ -112,6 +114,11 @@ public interface WakeLock { @VisibleForTesting static WakeLock wrap( final PowerManager.WakeLock inner, WakeLockLogger logger, long maxTimeout) { + if (Flags.delayedWakelockReleaseOnBackgroundThread()) { + return new ClientTrackingWakeLock(inner, logger, maxTimeout); + } + + // Non-thread safe implementation, remove when flag above is removed. return new WakeLock() { private final HashMap<String, Integer> mActiveClients = new HashMap<>(); diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/SpatializerModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/SpatializerModule.kt index 18a9161ac0e3..593b90aa3c68 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dagger/SpatializerModule.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/SpatializerModule.kt @@ -21,15 +21,19 @@ import android.media.Spatializer import com.android.settingslib.media.data.repository.SpatializerRepository import com.android.settingslib.media.data.repository.SpatializerRepositoryImpl import com.android.settingslib.media.domain.interactor.SpatializerInteractor +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import dagger.Module import dagger.Provides import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.CoroutineScope /** Spatializer module. */ @Module interface SpatializerModule { + companion object { + @Provides fun provideSpatializer( audioManager: AudioManager, @@ -38,8 +42,9 @@ interface SpatializerModule { @Provides fun provdieSpatializerRepository( spatializer: Spatializer, + @Application scope: CoroutineScope, @Background backgroundContext: CoroutineContext, - ): SpatializerRepository = SpatializerRepositoryImpl(spatializer, backgroundContext) + ): SpatializerRepository = SpatializerRepositoryImpl(spatializer, scope, backgroundContext) @Provides fun provideSpatializerInetractor(repository: SpatializerRepository): SpatializerInteractor = diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/domain/SpatialAudioAvailabilityCriteria.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/domain/SpatialAudioAvailabilityCriteria.kt new file mode 100644 index 000000000000..71bce5e470f4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/domain/SpatialAudioAvailabilityCriteria.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.volume.panel.component.spatial.domain + +import com.android.systemui.volume.panel.component.spatial.domain.interactor.SpatialAudioComponentInteractor +import com.android.systemui.volume.panel.component.spatial.domain.model.SpatialAudioAvailabilityModel +import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope +import com.android.systemui.volume.panel.domain.ComponentAvailabilityCriteria +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +@VolumePanelScope +class SpatialAudioAvailabilityCriteria +@Inject +constructor(private val interactor: SpatialAudioComponentInteractor) : + ComponentAvailabilityCriteria { + + override fun isAvailable(): Flow<Boolean> = + interactor.isAvailable.map { it is SpatialAudioAvailabilityModel.SpatialAudio } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/domain/interactor/SpatialAudioComponentInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/domain/interactor/SpatialAudioComponentInteractor.kt new file mode 100644 index 000000000000..4358611694b2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/domain/interactor/SpatialAudioComponentInteractor.kt @@ -0,0 +1,170 @@ +/* + * 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.component.spatial.domain.interactor + +import android.media.AudioDeviceAttributes +import android.media.AudioDeviceInfo +import com.android.settingslib.bluetooth.CachedBluetoothDevice +import com.android.settingslib.media.BluetoothMediaDevice +import com.android.settingslib.media.domain.interactor.SpatializerInteractor +import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputInteractor +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 javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn + +/** + * Provides an ability to access and update spatial audio and head tracking state. + * + * Head tracking is a sub-feature of spatial audio. This means that it requires spatial audio to be + * available for it to be available. And spatial audio to be enabled for it to be enabled. + */ +@VolumePanelScope +class SpatialAudioComponentInteractor +@Inject +constructor( + mediaOutputInteractor: MediaOutputInteractor, + private val spatializerInteractor: SpatializerInteractor, + @VolumePanelScope private val coroutineScope: CoroutineScope, +) { + + private val changes = MutableSharedFlow<Unit>() + private val currentAudioDeviceAttributes: StateFlow<AudioDeviceAttributes?> = + mediaOutputInteractor.currentConnectedDevice + .map { mediaDevice -> + mediaDevice ?: return@map null + val btDevice: CachedBluetoothDevice = + (mediaDevice as? BluetoothMediaDevice)?.cachedDevice ?: return@map null + btDevice.getAudioDeviceAttributes() + } + .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), null) + + /** + * Returns spatial audio availability model. It can be: + * - unavailable + * - only spatial audio is available + * - spatial audio and head tracking are available + */ + val isAvailable: StateFlow<SpatialAudioAvailabilityModel> = + combine( + currentAudioDeviceAttributes, + changes.onStart { emit(Unit) }, + spatializerInteractor.isHeadTrackingAvailable, + ) { attributes, _, isHeadTrackingAvailable -> + attributes ?: return@combine SpatialAudioAvailabilityModel.Unavailable + if (isHeadTrackingAvailable) { + return@combine SpatialAudioAvailabilityModel.HeadTracking + } + if (spatializerInteractor.isSpatialAudioAvailable(attributes)) { + return@combine SpatialAudioAvailabilityModel.SpatialAudio + } + SpatialAudioAvailabilityModel.Unavailable + } + .stateIn( + coroutineScope, + SharingStarted.Eagerly, + SpatialAudioAvailabilityModel.Unavailable, + ) + + /** + * Returns spatial audio enabled/disabled model. It can be + * - disabled + * - only spatial audio is enabled + * - spatial audio and head tracking are enabled + */ + val isEnabled: StateFlow<SpatialAudioEnabledModel> = + combine( + changes.onStart { emit(Unit) }, + currentAudioDeviceAttributes, + isAvailable, + ) { _, attributes, isAvailable -> + if (isAvailable is SpatialAudioAvailabilityModel.Unavailable) { + return@combine SpatialAudioEnabledModel.Disabled + } + attributes ?: return@combine SpatialAudioEnabledModel.Disabled + if (spatializerInteractor.isHeadTrackingEnabled(attributes)) { + return@combine SpatialAudioEnabledModel.HeadTrackingEnabled + } + if (spatializerInteractor.isSpatialAudioEnabled(attributes)) { + return@combine SpatialAudioEnabledModel.SpatialAudioEnabled + } + SpatialAudioEnabledModel.Disabled + } + .stateIn( + coroutineScope, + SharingStarted.Eagerly, + SpatialAudioEnabledModel.Disabled, + ) + + /** + * Sets current [isEnabled] to a specific [SpatialAudioEnabledModel]. It + * - disables both spatial audio and head tracking + * - enables only spatial audio + * - enables both spatial audio and head tracking + */ + suspend fun setEnabled(model: SpatialAudioEnabledModel) { + val attributes = currentAudioDeviceAttributes.value ?: return + spatializerInteractor.setSpatialAudioEnabled( + attributes, + model is SpatialAudioEnabledModel.SpatialAudioEnabled, + ) + spatializerInteractor.setHeadTrackingEnabled( + attributes, + model is SpatialAudioEnabledModel.HeadTrackingEnabled, + ) + changes.emit(Unit) + } + + private suspend fun CachedBluetoothDevice.getAudioDeviceAttributes(): AudioDeviceAttributes? { + return listOf( + AudioDeviceAttributes( + AudioDeviceAttributes.ROLE_OUTPUT, + AudioDeviceInfo.TYPE_BLE_HEADSET, + address + ), + AudioDeviceAttributes( + AudioDeviceAttributes.ROLE_OUTPUT, + AudioDeviceInfo.TYPE_BLE_SPEAKER, + address + ), + AudioDeviceAttributes( + AudioDeviceAttributes.ROLE_OUTPUT, + AudioDeviceInfo.TYPE_BLE_BROADCAST, + address + ), + AudioDeviceAttributes( + AudioDeviceAttributes.ROLE_OUTPUT, + AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, + address + ), + AudioDeviceAttributes( + AudioDeviceAttributes.ROLE_OUTPUT, + AudioDeviceInfo.TYPE_HEARING_AID, + address + ) + ) + .firstOrNull { spatializerInteractor.isSpatialAudioAvailable(it) } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/domain/model/SpatialAudioAvailabilityModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/domain/model/SpatialAudioAvailabilityModel.kt new file mode 100644 index 000000000000..cf1454618367 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/domain/model/SpatialAudioAvailabilityModel.kt @@ -0,0 +1,32 @@ +/* + * 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.component.spatial.domain.model + +/** Models spatial audio and head tracking availability. */ +interface SpatialAudioAvailabilityModel { + + /** Spatial audio is unavailable. */ + data object Unavailable : SpatialAudioAvailabilityModel + + /** Spatial audio is available. */ + interface SpatialAudio : SpatialAudioAvailabilityModel { + companion object : SpatialAudio + } + + /** Head tracking is available. This also means that [SpatialAudio] is available. */ + data object HeadTracking : SpatialAudio +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/domain/model/SpatialAudioEnabledModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/domain/model/SpatialAudioEnabledModel.kt new file mode 100644 index 000000000000..4e65f60aa0e1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/domain/model/SpatialAudioEnabledModel.kt @@ -0,0 +1,32 @@ +/* + * 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.component.spatial.domain.model + +/** Models spatial audio and head tracking enabled/disabled state. */ +interface SpatialAudioEnabledModel { + + /** Spatial audio is disabled. */ + data object Disabled : SpatialAudioEnabledModel + + /** Spatial audio is enabled. */ + interface SpatialAudioEnabled : SpatialAudioEnabledModel { + companion object : SpatialAudioEnabled + } + + /** Head tracking is enabled. This also means that [SpatialAudioEnabled]. */ + data object HeadTrackingEnabled : SpatialAudioEnabled +} diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java index 15e0965c16fe..324d723207cd 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java @@ -256,7 +256,7 @@ public final class WMShell implements }); mCommandQueue.addCallback(new CommandQueue.Callbacks() { @Override - public void goToFullscreenFromSplit() { + public void moveFocusedTaskToFullscreen(int displayId) { splitScreen.goToFullscreenFromSplit(); } }); @@ -362,6 +362,12 @@ public final class WMShell implements desktopMode.enterDesktop(displayId); } }); + mCommandQueue.addCallback(new CommandQueue.Callbacks() { + @Override + public void moveFocusedTaskToFullscreen(int displayId) { + desktopMode.moveFocusedTaskToFullscreen(displayId); + } + }); } @VisibleForTesting diff --git a/packages/SystemUI/tests/src/com/android/systemui/globalactions/ShutdownUiTest.java b/packages/SystemUI/tests/src/com/android/systemui/globalactions/ShutdownUiTest.java index 9d9b263c5df5..2d3ca6095835 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/globalactions/ShutdownUiTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/globalactions/ShutdownUiTest.java @@ -19,7 +19,13 @@ package com.android.systemui.globalactions; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertNull; +import static org.mockito.Mockito.when; + +import android.nearby.NearbyManager; +import android.net.platform.flags.Flags; import android.os.PowerManager; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.testing.AndroidTestingRunner; import androidx.test.filters.SmallTest; @@ -32,6 +38,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.MockitoAnnotations; @SmallTest @@ -41,10 +48,13 @@ public class ShutdownUiTest extends SysuiTestCase { ShutdownUi mShutdownUi; @Mock BlurUtils mBlurUtils; + @Mock + NearbyManager mNearbyManager; @Before public void setUp() throws Exception { - mShutdownUi = new ShutdownUi(getContext(), mBlurUtils); + MockitoAnnotations.initMocks(this); + mShutdownUi = new ShutdownUi(getContext(), mBlurUtils, mNearbyManager); } @Test @@ -82,4 +92,53 @@ public class ShutdownUiTest extends SysuiTestCase { String message = mShutdownUi.getReasonMessage("anything-else"); assertNull(message); } + + @EnableFlags(Flags.FLAG_POWERED_OFF_FINDING_PLATFORM) + @Test + public void getDialog_whenPowerOffFindingModeEnabled_returnsFinderDialog() { + when(mNearbyManager.getPoweredOffFindingMode()).thenReturn( + NearbyManager.POWERED_OFF_FINDING_MODE_ENABLED); + + int actualLayout = mShutdownUi.getShutdownDialogContent(false); + + int expectedLayout = com.android.systemui.res.R.layout.shutdown_dialog_finder_active; + assertEquals(actualLayout, expectedLayout); + } + + @DisableFlags(Flags.FLAG_POWERED_OFF_FINDING_PLATFORM) + @Test + public void getDialog_whenPowerOffFindingModeEnabledFlagDisabled_returnsFinderDialog() { + when(mNearbyManager.getPoweredOffFindingMode()).thenReturn( + NearbyManager.POWERED_OFF_FINDING_MODE_ENABLED); + + int actualLayout = mShutdownUi.getShutdownDialogContent(false); + + int expectedLayout = R.layout.shutdown_dialog; + assertEquals(actualLayout, expectedLayout); + } + + @EnableFlags(Flags.FLAG_POWERED_OFF_FINDING_PLATFORM) + @Test + public void getDialog_whenPowerOffFindingModeDisabled_returnsDefaultDialog() { + when(mNearbyManager.getPoweredOffFindingMode()).thenReturn( + NearbyManager.POWERED_OFF_FINDING_MODE_DISABLED); + + int actualLayout = mShutdownUi.getShutdownDialogContent(false); + + int expectedLayout = R.layout.shutdown_dialog; + assertEquals(actualLayout, expectedLayout); + } + + @EnableFlags(Flags.FLAG_POWERED_OFF_FINDING_PLATFORM) + @Test + public void getDialog_whenPowerOffFindingModeEnabledAndIsReboot_returnsDefaultDialog() { + when(mNearbyManager.getPoweredOffFindingMode()).thenReturn( + NearbyManager.POWERED_OFF_FINDING_MODE_ENABLED); + + int actualLayout = mShutdownUi.getShutdownDialogContent(true); + + int expectedLayout = R.layout.shutdown_dialog; + assertEquals(actualLayout, expectedLayout); + } + } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogDelegateTest.kt index 70b04175da91..8ecb95334bc4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogDelegateTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.qs.tiles.dialog.bluetooth +import android.content.Context import android.graphics.drawable.Drawable import android.testing.AndroidTestingRunner import android.testing.TestableLooper @@ -31,7 +32,13 @@ import androidx.test.filters.SmallTest import com.android.internal.logging.UiEventLogger import com.android.settingslib.bluetooth.CachedBluetoothDevice import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.DialogTransitionAnimator +import com.android.systemui.model.SysUiState import com.android.systemui.res.R +import com.android.systemui.statusbar.phone.SystemUIDialog +import com.android.systemui.statusbar.phone.SystemUIDialogManager +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineDispatcher @@ -43,6 +50,8 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock import org.mockito.Mockito.`when` import org.mockito.junit.MockitoJUnit @@ -51,7 +60,7 @@ import org.mockito.junit.MockitoRule @SmallTest @RunWith(AndroidTestingRunner::class) @TestableLooper.RunWithLooper(setAsMainLooper = true) -class BluetoothTileDialogTest : SysuiTestCase() { +class BluetoothTileDialogDelegateTest : SysuiTestCase() { companion object { const val DEVICE_NAME = "device" const val DEVICE_CONNECTION_SUMMARY = "active" @@ -76,6 +85,10 @@ class BluetoothTileDialogTest : SysuiTestCase() { isBluetoothEnabled = ENABLED, isAutoOnToggleFeatureAvailable = ENABLED ) + @Mock private lateinit var sysuiDialogFactory: SystemUIDialog.Factory + @Mock private lateinit var dialogManager: SystemUIDialogManager + @Mock private lateinit var sysuiState: SysUiState + @Mock private lateinit var dialogTransitionAnimator: DialogTransitionAnimator private val fakeSystemClock = FakeSystemClock() @@ -83,7 +96,7 @@ class BluetoothTileDialogTest : SysuiTestCase() { private lateinit var dispatcher: CoroutineDispatcher private lateinit var testScope: TestScope private lateinit var icon: Pair<Drawable, String> - private lateinit var bluetoothTileDialog: BluetoothTileDialog + private lateinit var mBluetoothTileDialogDelegate: BluetoothTileDialogDelegate private lateinit var deviceItem: DeviceItem @Before @@ -91,18 +104,44 @@ class BluetoothTileDialogTest : SysuiTestCase() { scheduler = TestCoroutineScheduler() dispatcher = UnconfinedTestDispatcher(scheduler) testScope = TestScope(dispatcher) - bluetoothTileDialog = - BluetoothTileDialog( - ENABLED, + + whenever(sysuiState.setFlag(anyInt(), anyBoolean())).thenReturn(sysuiState) + + mBluetoothTileDialogDelegate = + BluetoothTileDialogDelegate( + mContext, uiProperties, CONTENT_HEIGHT, + ENABLED, bluetoothTileDialogCallback, + {}, dispatcher, fakeSystemClock, uiEventLogger, logger, - mContext + sysuiDialogFactory, + LayoutInflater.from(mContext) ) + + whenever( + sysuiDialogFactory.create( + any(SystemUIDialog.Delegate::class.java), + any(Context::class.java) + ) + ) + .thenAnswer { + SystemUIDialog( + mContext, + 0, + SystemUIDialog.DEFAULT_DISMISS_ON_DEVICE_LOCK, + dialogManager, + sysuiState, + fakeBroadcastDispatcher, + dialogTransitionAnimator, + it.getArgument(0) + ) + } + icon = Pair(drawable, DEVICE_NAME) deviceItem = DeviceItem( @@ -118,11 +157,11 @@ class BluetoothTileDialogTest : SysuiTestCase() { @Test fun testShowDialog_createRecyclerViewWithAdapter() { - bluetoothTileDialog.show() + val dialog = mBluetoothTileDialogDelegate.createDialog() + dialog.show() - val recyclerView = bluetoothTileDialog.requireViewById<RecyclerView>(R.id.device_list) + val recyclerView = dialog.requireViewById<RecyclerView>(R.id.device_list) - assertThat(bluetoothTileDialog.isShowing).isTrue() assertThat(recyclerView).isNotNull() assertThat(recyclerView.visibility).isEqualTo(VISIBLE) assertThat(recyclerView.adapter).isNotNull() @@ -132,28 +171,18 @@ class BluetoothTileDialogTest : SysuiTestCase() { @Test fun testShowDialog_displayBluetoothDevice() { testScope.runTest { - bluetoothTileDialog = - BluetoothTileDialog( - ENABLED, - uiProperties, - CONTENT_HEIGHT, - bluetoothTileDialogCallback, - dispatcher, - fakeSystemClock, - uiEventLogger, - logger, - mContext - ) - bluetoothTileDialog.show() + val dialog = mBluetoothTileDialogDelegate.createDialog() + dialog.show() fakeSystemClock.setElapsedRealtime(Long.MAX_VALUE) - bluetoothTileDialog.onDeviceItemUpdated( + mBluetoothTileDialogDelegate.onDeviceItemUpdated( + dialog, listOf(deviceItem), showSeeAll = false, showPairNewDevice = false ) - val recyclerView = bluetoothTileDialog.requireViewById<RecyclerView>(R.id.device_list) - val adapter = recyclerView.adapter as BluetoothTileDialog.Adapter + val recyclerView = dialog.requireViewById<RecyclerView>(R.id.device_list) + val adapter = recyclerView?.adapter as BluetoothTileDialogDelegate.Adapter assertThat(adapter.itemCount).isEqualTo(1) assertThat(adapter.getItem(0).deviceName).isEqualTo(DEVICE_NAME) assertThat(adapter.getItem(0).connectionSummary).isEqualTo(DEVICE_CONNECTION_SUMMARY) @@ -168,17 +197,7 @@ class BluetoothTileDialogTest : SysuiTestCase() { val view = LayoutInflater.from(mContext).inflate(R.layout.bluetooth_device_item, null, false) val viewHolder = - BluetoothTileDialog( - ENABLED, - uiProperties, - CONTENT_HEIGHT, - bluetoothTileDialogCallback, - dispatcher, - fakeSystemClock, - uiEventLogger, - logger, - mContext - ) + mBluetoothTileDialogDelegate .Adapter(bluetoothTileDialogCallback) .DeviceItemViewHolder(view) viewHolder.bind(deviceItem, bluetoothTileDialogCallback) @@ -196,16 +215,19 @@ class BluetoothTileDialogTest : SysuiTestCase() { val view = LayoutInflater.from(mContext).inflate(R.layout.bluetooth_device_item, null, false) val viewHolder = - BluetoothTileDialog( - ENABLED, + BluetoothTileDialogDelegate( + mContext, uiProperties, CONTENT_HEIGHT, + ENABLED, bluetoothTileDialogCallback, + {}, dispatcher, fakeSystemClock, uiEventLogger, logger, - mContext + sysuiDialogFactory, + LayoutInflater.from(mContext) ) .Adapter(bluetoothTileDialogCallback) .DeviceItemViewHolder(view) @@ -220,32 +242,21 @@ class BluetoothTileDialogTest : SysuiTestCase() { @Test fun testOnDeviceUpdated_hideSeeAll_showPairNew() { testScope.runTest { - bluetoothTileDialog = - BluetoothTileDialog( - ENABLED, - uiProperties, - CONTENT_HEIGHT, - bluetoothTileDialogCallback, - dispatcher, - fakeSystemClock, - uiEventLogger, - logger, - mContext - ) - bluetoothTileDialog.show() + val dialog = mBluetoothTileDialogDelegate.createDialog() + dialog.show() fakeSystemClock.setElapsedRealtime(Long.MAX_VALUE) - bluetoothTileDialog.onDeviceItemUpdated( + mBluetoothTileDialogDelegate.onDeviceItemUpdated( + dialog, listOf(deviceItem), showSeeAll = false, showPairNewDevice = true ) - val seeAllButton = bluetoothTileDialog.requireViewById<View>(R.id.see_all_button) - val pairNewButton = - bluetoothTileDialog.requireViewById<View>(R.id.pair_new_device_button) - val recyclerView = bluetoothTileDialog.requireViewById<RecyclerView>(R.id.device_list) - val adapter = recyclerView.adapter as BluetoothTileDialog.Adapter - val scrollViewContent = bluetoothTileDialog.requireViewById<View>(R.id.scroll_view) + val seeAllButton = dialog.requireViewById<View>(R.id.see_all_button) + val pairNewButton = dialog.requireViewById<View>(R.id.pair_new_device_button) + val recyclerView = dialog.requireViewById<RecyclerView>(R.id.device_list) + val adapter = recyclerView?.adapter as BluetoothTileDialogDelegate.Adapter + val scrollViewContent = dialog.requireViewById<View>(R.id.scroll_view) assertThat(seeAllButton).isNotNull() assertThat(seeAllButton.visibility).isEqualTo(GONE) @@ -260,22 +271,24 @@ class BluetoothTileDialogTest : SysuiTestCase() { fun testShowDialog_cachedHeightLargerThanMinHeight_displayFromCachedHeight() { testScope.runTest { val cachedHeight = Int.MAX_VALUE - bluetoothTileDialog = - BluetoothTileDialog( - ENABLED, - uiProperties, - cachedHeight, - bluetoothTileDialogCallback, - dispatcher, - fakeSystemClock, - uiEventLogger, - logger, - mContext - ) - bluetoothTileDialog.show() - assertThat( - bluetoothTileDialog.requireViewById<View>(R.id.scroll_view).layoutParams.height - ) + val dialog = + BluetoothTileDialogDelegate( + mContext, + BluetoothTileDialogViewModel.UiProperties.build(ENABLED, ENABLED), + cachedHeight, + ENABLED, + bluetoothTileDialogCallback, + {}, + dispatcher, + fakeSystemClock, + uiEventLogger, + logger, + sysuiDialogFactory, + LayoutInflater.from(mContext) + ) + .createDialog() + dialog.show() + assertThat(dialog.requireViewById<View>(R.id.scroll_view).layoutParams.height) .isEqualTo(cachedHeight) } } @@ -283,22 +296,24 @@ class BluetoothTileDialogTest : SysuiTestCase() { @Test fun testShowDialog_cachedHeightLessThanMinHeight_displayFromUiProperties() { testScope.runTest { - bluetoothTileDialog = - BluetoothTileDialog( - ENABLED, - uiProperties, - MATCH_PARENT, - bluetoothTileDialogCallback, - dispatcher, - fakeSystemClock, - uiEventLogger, - logger, - mContext - ) - bluetoothTileDialog.show() - assertThat( - bluetoothTileDialog.requireViewById<View>(R.id.scroll_view).layoutParams.height - ) + val dialog = + BluetoothTileDialogDelegate( + mContext, + BluetoothTileDialogViewModel.UiProperties.build(ENABLED, ENABLED), + MATCH_PARENT, + ENABLED, + bluetoothTileDialogCallback, + {}, + dispatcher, + fakeSystemClock, + uiEventLogger, + logger, + sysuiDialogFactory, + LayoutInflater.from(mContext) + ) + .createDialog() + dialog.show() + assertThat(dialog.requireViewById<View>(R.id.scroll_view).layoutParams.height) .isGreaterThan(MATCH_PARENT) } } @@ -306,23 +321,25 @@ class BluetoothTileDialogTest : SysuiTestCase() { @Test fun testShowDialog_bluetoothEnabled_autoOnToggleGone() { testScope.runTest { - bluetoothTileDialog = - BluetoothTileDialog( - ENABLED, - BluetoothTileDialogViewModel.UiProperties.build(ENABLED, ENABLED), - MATCH_PARENT, - bluetoothTileDialogCallback, - dispatcher, - fakeSystemClock, - uiEventLogger, - logger, - mContext - ) - bluetoothTileDialog.show() + val dialog = + BluetoothTileDialogDelegate( + mContext, + BluetoothTileDialogViewModel.UiProperties.build(ENABLED, ENABLED), + MATCH_PARENT, + ENABLED, + bluetoothTileDialogCallback, + {}, + dispatcher, + fakeSystemClock, + uiEventLogger, + logger, + sysuiDialogFactory, + LayoutInflater.from(mContext) + ) + .createDialog() + dialog.show() assertThat( - bluetoothTileDialog - .requireViewById<View>(R.id.bluetooth_auto_on_toggle_layout) - .visibility + dialog.requireViewById<View>(R.id.bluetooth_auto_on_toggle_layout).visibility ) .isEqualTo(GONE) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt index cb9f4b4e4810..39e2413be40e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/BluetoothTileDialogViewModelTest.kt @@ -16,7 +16,6 @@ package com.android.systemui.qs.tiles.dialog.bluetooth -import android.content.SharedPreferences import android.content.pm.UserInfo import android.testing.AndroidTestingRunner import android.testing.TestableLooper @@ -31,10 +30,14 @@ import com.android.settingslib.flags.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.user.data.repository.FakeUserRepository +import com.android.systemui.util.FakeSharedPreferences import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.kotlin.getMutableStateFlow import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.nullable +import com.android.systemui.util.mockito.whenever import com.android.systemui.util.settings.FakeSettings import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat @@ -50,12 +53,12 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock import org.mockito.Mockito.anyBoolean import org.mockito.Mockito.never import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoRule @@ -84,9 +87,15 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { @Mock private lateinit var uiEventLogger: UiEventLogger - @Mock private lateinit var logger: BluetoothTileDialogLogger + @Mock + private lateinit var mBluetoothTileDialogDelegateDelegateFactory: + BluetoothTileDialogDelegate.Factory - @Mock private lateinit var sharedPreferences: SharedPreferences + @Mock private lateinit var bluetoothTileDialogDelegate: BluetoothTileDialogDelegate + + @Mock private lateinit var sysuiDialog: SystemUIDialog + + private val sharedPreferences = FakeSharedPreferences() private lateinit var scheduler: TestCoroutineScheduler private lateinit var dispatcher: CoroutineDispatcher @@ -123,20 +132,38 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { ), mDialogTransitionAnimator, activityStarter, - fakeSystemClock, uiEventLogger, - logger, testScope.backgroundScope, dispatcher, dispatcher, sharedPreferences, + mBluetoothTileDialogDelegateDelegateFactory ) - `when`(deviceItemInteractor.deviceItemUpdate).thenReturn(MutableSharedFlow()) - `when`(bluetoothStateInteractor.bluetoothStateUpdate) + whenever(deviceItemInteractor.deviceItemUpdate).thenReturn(MutableSharedFlow()) + whenever(bluetoothStateInteractor.bluetoothStateUpdate) .thenReturn(MutableStateFlow(null).asStateFlow()) - `when`(deviceItemInteractor.deviceItemUpdateRequest) + whenever(deviceItemInteractor.deviceItemUpdateRequest) .thenReturn(MutableStateFlow(Unit).asStateFlow()) - `when`(bluetoothStateInteractor.isBluetoothEnabled).thenReturn(true) + whenever(bluetoothStateInteractor.isBluetoothEnabled).thenReturn(true) + whenever( + mBluetoothTileDialogDelegateDelegateFactory.create( + any(), + any(), + anyInt(), + ArgumentMatchers.anyBoolean(), + any(), + any() + ) + ) + .thenReturn(bluetoothTileDialogDelegate) + whenever(bluetoothTileDialogDelegate.createDialog()).thenReturn(sysuiDialog) + whenever(bluetoothTileDialogDelegate.bluetoothStateToggle) + .thenReturn(getMutableStateFlow(false)) + whenever(bluetoothTileDialogDelegate.deviceItemClick) + .thenReturn(getMutableStateFlow(deviceItem)) + whenever(bluetoothTileDialogDelegate.contentHeight).thenReturn(getMutableStateFlow(0)) + whenever(bluetoothTileDialogDelegate.bluetoothAutoOnToggle) + .thenReturn(getMutableStateFlow(false)) } @Test @@ -145,7 +172,6 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { bluetoothTileDialogViewModel.showDialog(context, null) verify(mDialogTransitionAnimator, never()).showFromView(any(), any(), any(), any()) - verify(uiEventLogger).log(BluetoothTileDialogUiEvent.BLUETOOTH_TILE_DIALOG_SHOWN) } } @@ -191,7 +217,7 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { @Test fun testStartSettingsActivity_activityLaunched_dialogDismissed() { testScope.runTest { - `when`(deviceItem.cachedBluetoothDevice).thenReturn(cachedBluetoothDevice) + whenever(deviceItem.cachedBluetoothDevice).thenReturn(cachedBluetoothDevice) bluetoothTileDialogViewModel.showDialog(context, null) val clickedView = View(context) 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 950a9dbc2ff3..fd7b1399d03f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java @@ -599,6 +599,8 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { // Primary Bouncer->Gone when(mPrimaryBouncerToGoneTransitionViewModel.getLockscreenAlpha()) .thenReturn(emptyFlow()); + when(mPrimaryBouncerToGoneTransitionViewModel.getNotificationAlpha()) + .thenReturn(emptyFlow()); NotificationsKeyguardViewStateRepository notifsKeyguardViewStateRepository = new NotificationsKeyguardViewStateRepository(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ShadeTransitionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ShadeTransitionControllerTest.kt index 7737b4313e3c..651006dfc953 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ShadeTransitionControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ShadeTransitionControllerTest.kt @@ -1,15 +1,46 @@ +@file:OptIn(ExperimentalCoroutinesApi::class) + package com.android.systemui.shade.transition +import android.platform.test.annotations.DisableFlags import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest +import com.android.systemui.Flags.FLAG_SCENE_CONTAINER import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.deviceentry.data.repository.FakeDeviceEntryRepository +import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository +import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor +import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor import com.android.systemui.dump.DumpManager +import com.android.systemui.flags.EnableSceneContainer +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testScope +import com.android.systemui.scene.domain.interactor.PanelExpansionInteractor +import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.scene.shared.model.FakeSceneDataSource +import com.android.systemui.scene.shared.model.ObservableTransitionState +import com.android.systemui.scene.shared.model.SceneKey +import com.android.systemui.scene.shared.model.fakeSceneDataSource import com.android.systemui.shade.STATE_OPENING import com.android.systemui.shade.ShadeExpansionChangeEvent import com.android.systemui.shade.ShadeExpansionStateManager import com.android.systemui.statusbar.SysuiStatusBarStateController +import com.android.systemui.statusbar.notification.stack.ui.viewmodel.panelExpansionInteractor import com.android.systemui.statusbar.policy.FakeConfigurationController import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController +import com.android.systemui.testKosmos +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -29,32 +60,95 @@ class ShadeTransitionControllerTest : SysuiTestCase() { private val configurationController = FakeConfigurationController() private val shadeExpansionStateManager = ShadeExpansionStateManager() + private val kosmos = testKosmos() + private lateinit var testScope: TestScope + private lateinit var applicationScope: CoroutineScope + private lateinit var panelExpansionInteractor: PanelExpansionInteractor + private lateinit var deviceEntryRepository: FakeDeviceEntryRepository + private lateinit var deviceUnlockedInteractor: DeviceUnlockedInteractor + private lateinit var sceneInteractor: SceneInteractor + private lateinit var fakeSceneDataSource: FakeSceneDataSource @Before fun setUp() { MockitoAnnotations.initMocks(this) + testScope = kosmos.testScope + applicationScope = kosmos.applicationCoroutineScope + panelExpansionInteractor = kosmos.panelExpansionInteractor + deviceEntryRepository = kosmos.fakeDeviceEntryRepository + deviceUnlockedInteractor = kosmos.deviceUnlockedInteractor + sceneInteractor = kosmos.sceneInteractor + fakeSceneDataSource = kosmos.fakeSceneDataSource + controller = ShadeTransitionController( + applicationScope, configurationController, shadeExpansionStateManager, dumpManager, context, scrimShadeTransitionController, statusBarStateController, - ResourcesSplitShadeStateController() - ) + ResourcesSplitShadeStateController(), + ) { + panelExpansionInteractor + } } @Test + @DisableFlags(FLAG_SCENE_CONTAINER) fun onPanelStateChanged_forwardsToScrimTransitionController() { - startPanelExpansion() + startLegacyPanelExpansion() verify(scrimShadeTransitionController).onPanelStateChanged(STATE_OPENING) verify(scrimShadeTransitionController).onPanelExpansionChanged(DEFAULT_EXPANSION_EVENT) } - private fun startPanelExpansion() { + @Test + @EnableSceneContainer + fun sceneChanges_forwardsToScrimTransitionController() = + testScope.runTest { + var latestChangeEvent: ShadeExpansionChangeEvent? = null + whenever(scrimShadeTransitionController.onPanelExpansionChanged(any())).thenAnswer { + latestChangeEvent = it.arguments[0] as ShadeExpansionChangeEvent + Unit + } + setUnlocked(true) + val transitionState = + MutableStateFlow<ObservableTransitionState>( + ObservableTransitionState.Idle(SceneKey.Gone) + ) + sceneInteractor.setTransitionState(transitionState) + + changeScene(SceneKey.Gone, transitionState) + val currentScene by collectLastValue(sceneInteractor.currentScene) + assertThat(currentScene).isEqualTo(SceneKey.Gone) + + assertThat(latestChangeEvent) + .isEqualTo( + ShadeExpansionChangeEvent( + fraction = 0f, + expanded = false, + tracking = true, + dragDownPxAmount = 0f, + ) + ) + + changeScene(SceneKey.Shade, transitionState) { progress -> + assertThat(latestChangeEvent) + .isEqualTo( + ShadeExpansionChangeEvent( + fraction = progress, + expanded = progress > 0, + tracking = true, + dragDownPxAmount = 0f, + ) + ) + } + } + + private fun startLegacyPanelExpansion() { shadeExpansionStateManager.onPanelExpansionChanged( DEFAULT_EXPANSION_EVENT.fraction, DEFAULT_EXPANSION_EVENT.expanded, @@ -63,6 +157,52 @@ class ShadeTransitionControllerTest : SysuiTestCase() { ) } + private fun TestScope.setUnlocked(isUnlocked: Boolean) { + val isDeviceUnlocked by collectLastValue(deviceUnlockedInteractor.isDeviceUnlocked) + deviceEntryRepository.setUnlocked(isUnlocked) + runCurrent() + + assertThat(isDeviceUnlocked).isEqualTo(isUnlocked) + } + + private fun TestScope.changeScene( + toScene: SceneKey, + transitionState: MutableStateFlow<ObservableTransitionState>, + assertDuringProgress: ((progress: Float) -> Unit) = {}, + ) { + val currentScene by collectLastValue(sceneInteractor.currentScene) + val progressFlow = MutableStateFlow(0f) + transitionState.value = + ObservableTransitionState.Transition( + fromScene = checkNotNull(currentScene), + toScene = toScene, + progress = progressFlow, + isInitiatedByUserInput = true, + isUserInputOngoing = flowOf(true), + ) + runCurrent() + assertDuringProgress(progressFlow.value) + + progressFlow.value = 0.2f + runCurrent() + assertDuringProgress(progressFlow.value) + + progressFlow.value = 0.6f + runCurrent() + assertDuringProgress(progressFlow.value) + + progressFlow.value = 1f + runCurrent() + assertDuringProgress(progressFlow.value) + + transitionState.value = ObservableTransitionState.Idle(toScene) + fakeSceneDataSource.changeScene(toScene) + runCurrent() + assertDuringProgress(progressFlow.value) + + assertThat(currentScene).isEqualTo(toScene) + } + companion object { private const val DEFAULT_DRAG_DOWN_AMOUNT = 123f private val DEFAULT_EXPANSION_EVENT = @@ -70,6 +210,7 @@ class ShadeTransitionControllerTest : SysuiTestCase() { fraction = 0.5f, expanded = true, tracking = true, - dragDownPxAmount = DEFAULT_DRAG_DOWN_AMOUNT) + dragDownPxAmount = DEFAULT_DRAG_DOWN_AMOUNT + ) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt index d465b47e93b9..c07f289dd4fd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/domain/interactor/DeviceBasedSatelliteInteractorTest.kt @@ -239,7 +239,9 @@ class DeviceBasedSatelliteInteractorTest : SysuiTestCase() { // WHEN all of the connections are OOS i1.isInService.value = false + i1.isEmergencyOnly.value = false i2.isInService.value = false + i2.isEmergencyOnly.value = false // THEN the value is propagated to this interactor assertThat(latest).isTrue() @@ -256,6 +258,7 @@ class DeviceBasedSatelliteInteractorTest : SysuiTestCase() { // WHEN all of the connections are OOS i1.isInService.value = false + i1.isEmergencyOnly.value = false // THEN the value is propagated to this interactor assertThat(latest).isTrue() diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt index cd0652e53657..ec6642de839d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt @@ -80,6 +80,7 @@ class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { // GIVEN all icons are OOS val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1) i1.isInService.value = false + i1.isEmergencyOnly.value = false // GIVEN apm is disabled airplaneModeRepository.setIsAirplaneMode(false) @@ -99,6 +100,7 @@ class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { // GIVEN all icons are not OOS val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1) i1.isInService.value = true + i1.isEmergencyOnly.value = false // GIVEN apm is disabled airplaneModeRepository.setIsAirplaneMode(false) @@ -108,6 +110,35 @@ class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { } @Test + fun icon_nullWhenShouldNotShow_isEmergencyOnly() = + testScope.runTest { + val latest by collectLastValue(underTest.icon) + + // GIVEN satellite is allowed + repo.isSatelliteAllowedForCurrentLocation.value = true + + // GIVEN all icons are OOS + val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1) + i1.isInService.value = false + i1.isEmergencyOnly.value = false + + // GIVEN apm is disabled + airplaneModeRepository.setIsAirplaneMode(false) + + // Wait for delay to be completed + advanceTimeBy(10.seconds) + + // THEN icon is set because we don't have service + assertThat(latest).isInstanceOf(Icon::class.java) + + // GIVEN the connection is emergency only + i1.isEmergencyOnly.value = true + + // THEN icon is null because we have emergency connection + assertThat(latest).isNull() + } + + @Test fun icon_nullWhenShouldNotShow_apmIsEnabled() = testScope.runTest { val latest by collectLastValue(underTest.icon) @@ -118,6 +149,7 @@ class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { // GIVEN all icons are OOS val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1) i1.isInService.value = false + i1.isEmergencyOnly.value = false // GIVEN apm is enabled airplaneModeRepository.setIsAirplaneMode(true) @@ -138,6 +170,7 @@ class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { // GIVEN all icons are OOS val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1) i1.isInService.value = false + i1.isEmergencyOnly.value = false // GIVEN apm is disabled airplaneModeRepository.setIsAirplaneMode(false) @@ -161,6 +194,7 @@ class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { // GIVEN all icons are OOS val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1) i1.isInService.value = false + i1.isEmergencyOnly.value = false // GIVEN apm is disabled airplaneModeRepository.setIsAirplaneMode(false) diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/wakelock/WakeLockTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/wakelock/WakeLockTest.java index 23a9207ec643..ed07ad2a0976 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/util/wakelock/WakeLockTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/util/wakelock/WakeLockTest.java @@ -21,21 +21,40 @@ import static org.junit.Assert.assertTrue; import android.os.Build; import android.os.PowerManager; +import android.platform.test.flag.junit.FlagsParameterization; +import android.platform.test.flag.junit.SetFlagsRule; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; +import com.android.systemui.Flags; import com.android.systemui.SysuiTestCase; import org.junit.After; +import org.junit.Assume; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.List; @SmallTest -@RunWith(AndroidJUnit4.class) +@RunWith(Parameterized.class) public class WakeLockTest extends SysuiTestCase { + @Parameterized.Parameters(name = "{0}") + public static List<FlagsParameterization> getFlags() { + return FlagsParameterization.allCombinationsOf( + Flags.FLAG_DELAYED_WAKELOCK_RELEASE_ON_BACKGROUND_THREAD); + } + + @Rule public final SetFlagsRule mSetFlagsRule; + + public WakeLockTest(FlagsParameterization flags) { + mSetFlagsRule = new SetFlagsRule(SetFlagsRule.DefaultInitValueType.NULL_DEFAULT, flags); + } + private static final String WHY = "test"; WakeLock mWakeLock; PowerManager.WakeLock mInner; @@ -91,10 +110,7 @@ public class WakeLockTest extends SysuiTestCase { @Test public void prodBuild_wakeLock_releaseWithoutAcquire_noThrow() { - if (Build.IS_ENG) { - return; - } - + Assume.assumeFalse(Build.IS_ENG); // shouldn't throw an exception on production builds mWakeLock.release(WHY); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java index d2e03861b022..3a6324d3de53 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java @@ -61,7 +61,6 @@ import android.widget.ImageButton; import android.widget.SeekBar; import androidx.test.core.view.MotionEventBuilder; -import androidx.test.filters.FlakyTest; import androidx.test.filters.SmallTest; import com.android.internal.jank.InteractionJankMonitor; @@ -502,7 +501,6 @@ public class VolumeDialogImplTest extends SysuiTestCase { } @Test - @FlakyTest(bugId = 326204750) public void dialogDestroy_removesPostureControllerCallback() { verify(mPostureController, never()).removeCallback(any()); mDialog.destroy(); diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/classifier/FalsingManagerFake.java b/packages/SystemUI/tests/utils/src/com/android/systemui/classifier/FalsingManagerFake.java index 5038285aef00..974a11cfaf77 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/classifier/FalsingManagerFake.java +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/classifier/FalsingManagerFake.java @@ -201,4 +201,13 @@ public class FalsingManagerFake implements FalsingManager { public List<FalsingTapListener> getTapListeners() { return mTapListeners; } + + /** + * Calls every registered {@link FalsingBeliefListener} as if false touch occurred. + */ + public void sendFalsingBelief() { + for (FalsingBeliefListener listener : mFalsingBeliefListeners) { + listener.onFalse(); + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/ShutdownUiModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/SpatializerKosmos.kt index b7285da49bb7..7001ea8b6427 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/ShutdownUiModule.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/SpatializerKosmos.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * 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. @@ -13,19 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.systemui.globalactions -import android.content.Context -import com.android.systemui.statusbar.BlurUtils -import dagger.Module -import dagger.Provides +package com.android.systemui.media -/** Provides the UI shown during system shutdown. */ -@Module -class ShutdownUiModule { - /** Shutdown UI provider. */ - @Provides - fun provideShutdownUi(context: Context?, blurUtils: BlurUtils?): ShutdownUi { - return ShutdownUi(context, blurUtils) - } -} +import com.android.settingslib.media.domain.interactor.SpatializerInteractor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.media.data.repository.FakeSpatializerRepository + +val Kosmos.spatializerRepository by Kosmos.Fixture { FakeSpatializerRepository() } +val Kosmos.spatializerInteractor by Kosmos.Fixture { SpatializerInteractor(spatializerRepository) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/data/repository/FakeSpatializerRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/data/repository/FakeSpatializerRepository.kt new file mode 100644 index 000000000000..0183b97090dc --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/data/repository/FakeSpatializerRepository.kt @@ -0,0 +1,83 @@ +/* + * 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.data.repository + +import android.media.AudioDeviceAttributes +import com.android.settingslib.media.data.repository.SpatializerRepository +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +class FakeSpatializerRepository : SpatializerRepository { + + var defaultSpatialAudioAvailable: Boolean = false + + private val spatialAudioAvailabilityByDevice: MutableMap<AudioDeviceAttributes, Boolean> = + mutableMapOf() + private val spatialAudioCompatibleDevices: MutableList<AudioDeviceAttributes> = mutableListOf() + + private val mutableHeadTrackingAvailable = MutableStateFlow(false) + private val headTrackingEnabledByDevice = mutableMapOf<AudioDeviceAttributes, Boolean>() + + override val isHeadTrackingAvailable: StateFlow<Boolean> = + mutableHeadTrackingAvailable.asStateFlow() + + override suspend fun isSpatialAudioAvailableForDevice( + audioDeviceAttributes: AudioDeviceAttributes + ): Boolean = + spatialAudioAvailabilityByDevice.getOrDefault( + audioDeviceAttributes, + defaultSpatialAudioAvailable + ) + + override suspend fun getSpatialAudioCompatibleDevices(): Collection<AudioDeviceAttributes> = + spatialAudioCompatibleDevices + + override suspend fun addSpatialAudioCompatibleDevice( + audioDeviceAttributes: AudioDeviceAttributes + ) { + spatialAudioCompatibleDevices.add(audioDeviceAttributes) + } + + override suspend fun removeSpatialAudioCompatibleDevice( + audioDeviceAttributes: AudioDeviceAttributes + ) { + spatialAudioCompatibleDevices.remove(audioDeviceAttributes) + } + + override suspend fun isHeadTrackingEnabled( + audioDeviceAttributes: AudioDeviceAttributes + ): Boolean = headTrackingEnabledByDevice.getOrDefault(audioDeviceAttributes, false) + + override suspend fun setHeadTrackingEnabled( + audioDeviceAttributes: AudioDeviceAttributes, + isEnabled: Boolean + ) { + headTrackingEnabledByDevice[audioDeviceAttributes] = isEnabled + } + + fun setIsSpatialAudioAvailable( + audioDeviceAttributes: AudioDeviceAttributes, + isAvailable: Boolean, + ) { + spatialAudioAvailabilityByDevice[audioDeviceAttributes] = isAvailable + } + + fun setIsHeadTrackingAvailable(isAvailable: Boolean) { + mutableHeadTrackingAvailable.value = isAvailable + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/PanelExpansionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/PanelExpansionInteractorKosmos.kt new file mode 100644 index 000000000000..a025846f74a3 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/PanelExpansionInteractorKosmos.kt @@ -0,0 +1,30 @@ +/* + * 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.statusbar.notification.stack.ui.viewmodel + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.scene.domain.interactor.PanelExpansionInteractor +import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.shade.data.repository.shadeRepository + +val Kosmos.panelExpansionInteractor by Fixture { + PanelExpansionInteractor( + sceneInteractor = sceneInteractor, + shadeRepository = shadeRepository, + ) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt index c01032757bb0..11acf1c9ff64 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt @@ -60,6 +60,8 @@ class FakeMobileIconInteractor( override val isInService = MutableStateFlow(true) + override val isEmergencyOnly = MutableStateFlow(true) + override val isNonTerrestrial = MutableStateFlow(false) private val _isDataEnabled = MutableStateFlow(true) diff --git a/services/core/java/com/android/server/display/brightness/DisplayBrightnessStrategySelector.java b/services/core/java/com/android/server/display/brightness/DisplayBrightnessStrategySelector.java index 055f94a23363..babc36efcca8 100644 --- a/services/core/java/com/android/server/display/brightness/DisplayBrightnessStrategySelector.java +++ b/services/core/java/com/android/server/display/brightness/DisplayBrightnessStrategySelector.java @@ -71,6 +71,8 @@ public class DisplayBrightnessStrategySelector { @Nullable private final OffloadBrightnessStrategy mOffloadBrightnessStrategy; + private final DisplayBrightnessStrategy[] mDisplayBrightnessStrategies; + // We take note of the old brightness strategy so that we can know when the strategy changes. private String mOldBrightnessStrategyName; @@ -98,6 +100,10 @@ public class DisplayBrightnessStrategySelector { } else { mOffloadBrightnessStrategy = null; } + mDisplayBrightnessStrategies = new DisplayBrightnessStrategy[]{mInvalidBrightnessStrategy, + mScreenOffBrightnessStrategy, mDozeBrightnessStrategy, mFollowerBrightnessStrategy, + mBoostBrightnessStrategy, mOverrideBrightnessStrategy, mTemporaryBrightnessStrategy, + mOffloadBrightnessStrategy}; mAllowAutoBrightnessWhileDozingConfig = context.getResources().getBoolean( R.bool.config_allowAutoBrightnessWhileDozing); mOldBrightnessStrategyName = mInvalidBrightnessStrategy.getName(); @@ -179,9 +185,10 @@ public class DisplayBrightnessStrategySelector { " mAllowAutoBrightnessWhileDozingConfig= " + mAllowAutoBrightnessWhileDozingConfig); IndentingPrintWriter ipw = new IndentingPrintWriter(writer, " "); - mTemporaryBrightnessStrategy.dump(ipw); - if (mOffloadBrightnessStrategy != null) { - mOffloadBrightnessStrategy.dump(ipw); + for (DisplayBrightnessStrategy displayBrightnessStrategy: mDisplayBrightnessStrategies) { + if (displayBrightnessStrategy != null) { + displayBrightnessStrategy.dump(ipw); + } } } diff --git a/services/core/java/com/android/server/display/brightness/strategy/BoostBrightnessStrategy.java b/services/core/java/com/android/server/display/brightness/strategy/BoostBrightnessStrategy.java index dd400d998eb4..9ee1d73726bd 100644 --- a/services/core/java/com/android/server/display/brightness/strategy/BoostBrightnessStrategy.java +++ b/services/core/java/com/android/server/display/brightness/strategy/BoostBrightnessStrategy.java @@ -23,6 +23,8 @@ import com.android.server.display.DisplayBrightnessState; import com.android.server.display.brightness.BrightnessReason; import com.android.server.display.brightness.BrightnessUtils; +import java.io.PrintWriter; + /** * Manages the brightness of the display when the system brightness boost is requested. */ @@ -48,4 +50,7 @@ public class BoostBrightnessStrategy implements DisplayBrightnessStrategy { public String getName() { return "BoostBrightnessStrategy"; } + + @Override + public void dump(PrintWriter writer) {} } diff --git a/services/core/java/com/android/server/display/brightness/strategy/DisplayBrightnessStrategy.java b/services/core/java/com/android/server/display/brightness/strategy/DisplayBrightnessStrategy.java index 27d04fd7f743..1f28eb4fa113 100644 --- a/services/core/java/com/android/server/display/brightness/strategy/DisplayBrightnessStrategy.java +++ b/services/core/java/com/android/server/display/brightness/strategy/DisplayBrightnessStrategy.java @@ -21,6 +21,8 @@ import android.hardware.display.DisplayManagerInternal; import com.android.server.display.DisplayBrightnessState; +import java.io.PrintWriter; + /** * Decides the DisplayBrighntessState that the display should change to based on strategy-specific * logic within each implementation. Clamping should be done outside of DisplayBrightnessStrategy if @@ -40,4 +42,10 @@ public interface DisplayBrightnessStrategy { */ @NonNull String getName(); + + /** + * Dumps the state of the Strategy + * @param writer + */ + void dump(PrintWriter writer); } diff --git a/services/core/java/com/android/server/display/brightness/strategy/DozeBrightnessStrategy.java b/services/core/java/com/android/server/display/brightness/strategy/DozeBrightnessStrategy.java index 8299586e1cac..2be74438f87a 100644 --- a/services/core/java/com/android/server/display/brightness/strategy/DozeBrightnessStrategy.java +++ b/services/core/java/com/android/server/display/brightness/strategy/DozeBrightnessStrategy.java @@ -22,6 +22,8 @@ import com.android.server.display.DisplayBrightnessState; import com.android.server.display.brightness.BrightnessReason; import com.android.server.display.brightness.BrightnessUtils; +import java.io.PrintWriter; + /** * Manages the brightness of the display when the system is in the doze state. */ @@ -42,4 +44,6 @@ public class DozeBrightnessStrategy implements DisplayBrightnessStrategy { return "DozeBrightnessStrategy"; } + @Override + public void dump(PrintWriter writer) {} } diff --git a/services/core/java/com/android/server/display/brightness/strategy/FollowerBrightnessStrategy.java b/services/core/java/com/android/server/display/brightness/strategy/FollowerBrightnessStrategy.java index 585f576c25c3..54f9afcbdd94 100644 --- a/services/core/java/com/android/server/display/brightness/strategy/FollowerBrightnessStrategy.java +++ b/services/core/java/com/android/server/display/brightness/strategy/FollowerBrightnessStrategy.java @@ -75,6 +75,7 @@ public class FollowerBrightnessStrategy implements DisplayBrightnessStrategy { /** * Dumps the state of this class. */ + @Override public void dump(PrintWriter writer) { writer.println("FollowerBrightnessStrategy:"); writer.println(" mDisplayId=" + mDisplayId); diff --git a/services/core/java/com/android/server/display/brightness/strategy/InvalidBrightnessStrategy.java b/services/core/java/com/android/server/display/brightness/strategy/InvalidBrightnessStrategy.java index bc241964ff86..49c3e03c8742 100644 --- a/services/core/java/com/android/server/display/brightness/strategy/InvalidBrightnessStrategy.java +++ b/services/core/java/com/android/server/display/brightness/strategy/InvalidBrightnessStrategy.java @@ -23,6 +23,8 @@ import com.android.server.display.DisplayBrightnessState; import com.android.server.display.brightness.BrightnessReason; import com.android.server.display.brightness.BrightnessUtils; +import java.io.PrintWriter; + /** * Manages the brightness of the display when the system is in the invalid state. */ @@ -39,4 +41,7 @@ public class InvalidBrightnessStrategy implements DisplayBrightnessStrategy { public String getName() { return "InvalidBrightnessStrategy"; } + + @Override + public void dump(PrintWriter writer) {} } diff --git a/services/core/java/com/android/server/display/brightness/strategy/OffloadBrightnessStrategy.java b/services/core/java/com/android/server/display/brightness/strategy/OffloadBrightnessStrategy.java index 55f8914e26f6..4ffb16be5789 100644 --- a/services/core/java/com/android/server/display/brightness/strategy/OffloadBrightnessStrategy.java +++ b/services/core/java/com/android/server/display/brightness/strategy/OffloadBrightnessStrategy.java @@ -67,6 +67,7 @@ public class OffloadBrightnessStrategy implements DisplayBrightnessStrategy { /** * Dumps the state of this class. */ + @Override public void dump(PrintWriter writer) { writer.println("OffloadBrightnessStrategy:"); writer.println(" mOffloadScreenBrightness:" + mOffloadScreenBrightness); diff --git a/services/core/java/com/android/server/display/brightness/strategy/OverrideBrightnessStrategy.java b/services/core/java/com/android/server/display/brightness/strategy/OverrideBrightnessStrategy.java index 13327cb4dd2f..7b651d8ced8e 100644 --- a/services/core/java/com/android/server/display/brightness/strategy/OverrideBrightnessStrategy.java +++ b/services/core/java/com/android/server/display/brightness/strategy/OverrideBrightnessStrategy.java @@ -22,6 +22,8 @@ import com.android.server.display.DisplayBrightnessState; import com.android.server.display.brightness.BrightnessReason; import com.android.server.display.brightness.BrightnessUtils; +import java.io.PrintWriter; + /** * Manages the brightness of the display when the system brightness is overridden */ @@ -40,4 +42,7 @@ public class OverrideBrightnessStrategy implements DisplayBrightnessStrategy { public String getName() { return "OverrideBrightnessStrategy"; } + + @Override + public void dump(PrintWriter writer) {} } diff --git a/services/core/java/com/android/server/display/brightness/strategy/ScreenOffBrightnessStrategy.java b/services/core/java/com/android/server/display/brightness/strategy/ScreenOffBrightnessStrategy.java index 3d411d3db658..201ef4118854 100644 --- a/services/core/java/com/android/server/display/brightness/strategy/ScreenOffBrightnessStrategy.java +++ b/services/core/java/com/android/server/display/brightness/strategy/ScreenOffBrightnessStrategy.java @@ -23,6 +23,8 @@ import com.android.server.display.DisplayBrightnessState; import com.android.server.display.brightness.BrightnessReason; import com.android.server.display.brightness.BrightnessUtils; +import java.io.PrintWriter; + /** * Manages the brightness of the display when the system is in the ScreenOff state. */ @@ -41,4 +43,7 @@ public class ScreenOffBrightnessStrategy implements DisplayBrightnessStrategy { public String getName() { return "ScreenOffBrightnessStrategy"; } + + @Override + public void dump(PrintWriter writer) {} } diff --git a/services/core/java/com/android/server/display/brightness/strategy/TemporaryBrightnessStrategy.java b/services/core/java/com/android/server/display/brightness/strategy/TemporaryBrightnessStrategy.java index 35f7dd0a524d..bbd0c009debb 100644 --- a/services/core/java/com/android/server/display/brightness/strategy/TemporaryBrightnessStrategy.java +++ b/services/core/java/com/android/server/display/brightness/strategy/TemporaryBrightnessStrategy.java @@ -68,6 +68,7 @@ public class TemporaryBrightnessStrategy implements DisplayBrightnessStrategy { /** * Dumps the state of this class. */ + @Override public void dump(PrintWriter writer) { writer.println("TemporaryBrightnessStrategy:"); writer.println(" mTemporaryScreenBrightness:" + mTemporaryScreenBrightness); diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java index 70993ca3e21b..1e90ab279d32 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java @@ -317,6 +317,10 @@ public class HdmiCecLocalDeviceAudioSystem extends HdmiCecLocalDeviceSource { if ((systemAudioOnPowerOnProp == ALWAYS_SYSTEM_AUDIO_CONTROL_ON_POWER_ON) || ((systemAudioOnPowerOnProp == USE_LAST_STATE_SYSTEM_AUDIO_CONTROL_ON_POWER_ON) && lastSystemAudioControlStatus && isSystemAudioControlFeatureEnabled())) { + if (hasAction(SystemAudioInitiationActionFromAvr.class)) { + Slog.i(TAG, "SystemAudioInitiationActionFromAvr is in progress. Restarting."); + removeAction(SystemAudioInitiationActionFromAvr.class); + } addAndStartAction(new SystemAudioInitiationActionFromAvr(this)); } } @@ -1032,6 +1036,10 @@ public class HdmiCecLocalDeviceAudioSystem extends HdmiCecLocalDeviceSource { void onSystemAudioControlFeatureSupportChanged(boolean enabled) { setSystemAudioControlFeatureEnabled(enabled); if (enabled) { + if (hasAction(SystemAudioInitiationActionFromAvr.class)) { + Slog.i(TAG, "SystemAudioInitiationActionFromAvr is in progress. Restarting."); + removeAction(SystemAudioInitiationActionFromAvr.class); + } addAndStartAction(new SystemAudioInitiationActionFromAvr(this)); } } diff --git a/services/core/java/com/android/server/input/KeyboardMetricsCollector.java b/services/core/java/com/android/server/input/KeyboardMetricsCollector.java index 4b9f2cf9d0c0..277a4d40c7e4 100644 --- a/services/core/java/com/android/server/input/KeyboardMetricsCollector.java +++ b/services/core/java/com/android/server/input/KeyboardMetricsCollector.java @@ -231,7 +231,10 @@ public final class KeyboardMetricsCollector { DESKTOP_MODE( FrameworkStatsLog .KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__DESKTOP_MODE, - "DESKTOP_MODE"); + "DESKTOP_MODE"), + MULTI_WINDOW_NAVIGATION(FrameworkStatsLog + .KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__MULTI_WINDOW_NAVIGATION, + "MULTIWINDOW_NAVIGATION"); private final int mValue; diff --git a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java index 582058d21256..31bfc6954416 100644 --- a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java +++ b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java @@ -340,8 +340,6 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { static final String TAG = NetworkPolicyLogger.TAG; private static final boolean LOGD = NetworkPolicyLogger.LOGD; private static final boolean LOGV = NetworkPolicyLogger.LOGV; - // TODO: b/304347838 - Remove once the feature is in staging. - private static final boolean ALWAYS_RESTRICT_BACKGROUND_NETWORK = false; /** * No opportunistic quota could be calculated from user data plan or data settings. @@ -1070,8 +1068,7 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { } // The flag is boot-stable. - mBackgroundNetworkRestricted = ALWAYS_RESTRICT_BACKGROUND_NETWORK - && Flags.networkBlockedForTopSleepingAndAbove(); + mBackgroundNetworkRestricted = Flags.networkBlockedForTopSleepingAndAbove(); if (mBackgroundNetworkRestricted) { // Firewall rules and UidBlockedState will get updated in // updateRulesForGlobalChangeAL below. diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 8781bf19565e..428fca082f75 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -3504,8 +3504,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { if (firstDown && event.isMetaPressed() && event.isCtrlPressed()) { StatusBarManagerInternal statusbar = getStatusBarManagerInternal(); if (statusbar != null) { - statusbar.goToFullscreenFromSplit(); - logKeyboardSystemsEvent(event, KeyboardLogEvent.SPLIT_SCREEN_NAVIGATION); + statusbar.moveFocusedTaskToFullscreen(event.getDisplayId()); + logKeyboardSystemsEvent(event, KeyboardLogEvent.MULTI_WINDOW_NAVIGATION); return true; } } diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java index 3c6baa873eca..14e0ce1704c8 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java @@ -224,9 +224,11 @@ public interface StatusBarManagerInternal { void showRearDisplayDialog(int currentBaseState); /** - * Called when requested to go to fullscreen from the active split app. + * Called when requested to go to fullscreen from the focused app. + * + * @param displayId of the current display. */ - void goToFullscreenFromSplit(); + void moveFocusedTaskToFullscreen(int displayId); /** * Enters stage split from a current running app. diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java index 14c38bde6621..0b48a75298a4 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java @@ -810,11 +810,11 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D } @Override - public void goToFullscreenFromSplit() { + public void moveFocusedTaskToFullscreen(int displayId) { IStatusBar bar = mBar; if (bar != null) { try { - bar.goToFullscreenFromSplit(); + bar.moveFocusedTaskToFullscreen(displayId); } catch (RemoteException ex) { } } } diff --git a/services/core/java/com/android/server/utils/AnrTimer.java b/services/core/java/com/android/server/utils/AnrTimer.java index 743005a2d844..b7d8cfce34ed 100644 --- a/services/core/java/com/android/server/utils/AnrTimer.java +++ b/services/core/java/com/android/server/utils/AnrTimer.java @@ -767,7 +767,7 @@ public class AnrTimer<V> implements AutoCloseable { * Return true if the native timers are supported. Native timers are supported if the method * nativeAnrTimerSupported() can be executed and it returns true. */ - private static boolean nativeTimersSupported() { + public static boolean nativeTimersSupported() { try { return nativeAnrTimerSupported(); } catch (java.lang.UnsatisfiedLinkError e) { diff --git a/services/core/java/com/android/server/wm/ActivityAssistInfo.java b/services/core/java/com/android/server/wm/ActivityAssistInfo.java index 3b91780431cb..2dc00460eeb9 100644 --- a/services/core/java/com/android/server/wm/ActivityAssistInfo.java +++ b/services/core/java/com/android/server/wm/ActivityAssistInfo.java @@ -30,12 +30,14 @@ public class ActivityAssistInfo { private final IBinder mAssistToken; private final int mTaskId; private final ComponentName mComponentName; + private final int mUserId; public ActivityAssistInfo(ActivityRecord activityRecord) { this.mActivityToken = activityRecord.token; this.mAssistToken = activityRecord.assistToken; this.mTaskId = activityRecord.getTask().mTaskId; this.mComponentName = activityRecord.mActivityComponent; + this.mUserId = activityRecord.mUserId; } /** @hide */ @@ -57,4 +59,9 @@ public class ActivityAssistInfo { public ComponentName getComponentName() { return mComponentName; } + + /** @hide */ + public int getUserId() { + return mUserId; + } } diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 5036fc646327..90cff3950047 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -1456,7 +1456,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A mSizeConfigurations = sizeConfigurations; } - private void scheduleActivityMovedToDisplay(int displayId, Configuration config) { + private void scheduleActivityMovedToDisplay(int displayId, @NonNull Configuration config, + @NonNull ActivityWindowInfo activityWindowInfo) { if (!attachedToProcess()) { ProtoLog.w(WM_DEBUG_SWITCH, "Can't report activity moved " + "to display - client not running, activityRecord=%s, displayId=%d", @@ -1469,7 +1470,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A config); mAtmService.getLifecycleManager().scheduleTransactionItem(app.getThread(), - MoveToDisplayItem.obtain(token, displayId, config)); + MoveToDisplayItem.obtain(token, displayId, config, activityWindowInfo)); } catch (RemoteException e) { // If process died, whatever. } @@ -9799,8 +9800,10 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // Update last reported values. final Configuration newMergedOverrideConfig = getMergedOverrideConfiguration(); + final ActivityWindowInfo newActivityWindowInfo = getActivityWindowInfo(); setLastReportedConfiguration(getProcessGlobalConfiguration(), newMergedOverrideConfig); + setLastReportedActivityWindowInfo(newActivityWindowInfo); if (mState == INITIALIZING) { // No need to relaunch or schedule new config for activity that hasn't been launched @@ -9817,7 +9820,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // There are no significant differences, so we won't relaunch but should still deliver // the new configuration to the client process. if (displayChanged) { - scheduleActivityMovedToDisplay(newDisplayId, newMergedOverrideConfig); + scheduleActivityMovedToDisplay(newDisplayId, newMergedOverrideConfig, + newActivityWindowInfo); } else { scheduleConfigurationChanged(newMergedOverrideConfig); } @@ -9884,7 +9888,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // changes is always sent to all processes when they happen so it can just use whatever // system level configuration it last got. if (displayChanged) { - scheduleActivityMovedToDisplay(newDisplayId, newMergedOverrideConfig); + scheduleActivityMovedToDisplay(newDisplayId, newMergedOverrideConfig, + newActivityWindowInfo); } else { scheduleConfigurationChanged(newMergedOverrideConfig); } diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java index 4a5b2211800c..c0881180af94 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java @@ -822,4 +822,7 @@ public abstract class ActivityTaskManagerInternal { */ public abstract void unregisterCompatScaleProvider( @CompatScaleProvider.CompatScaleModeOrderId int id); + + /** Returns whether assist data is allowed. */ + public abstract boolean isAssistDataAllowed(); } diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index 514a3d87ecf0..218b7512b861 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -6023,6 +6023,10 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { public int startActivityWithScreenshot(@NonNull Intent intent, @NonNull String callingPackage, int callingUid, int callingPid, @Nullable IBinder resultTo, @Nullable Bundle options, int userId) { + userId = getActivityStartController().checkTargetUser(userId, + false /* validateIncomingUser */, Binder.getCallingPid(), + Binder.getCallingUid(), "startActivityWithScreenshot"); + return getActivityStartController() .obtainStarter(intent, "startActivityWithScreenshot") .setCallingUid(callingUid) @@ -7347,6 +7351,11 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { @CompatScaleProvider.CompatScaleModeOrderId int id) { ActivityTaskManagerService.this.unregisterCompatScaleProvider(id); } + + @Override + public boolean isAssistDataAllowed() { + return ActivityTaskManagerService.this.isAssistDataAllowed(); + } } static boolean isPip2ExperimentEnabled() { diff --git a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java index f11d6ec0828c..925a0776bf1e 100644 --- a/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java +++ b/services/core/java/com/android/server/wm/DesktopModeLaunchParamsModifier.java @@ -27,7 +27,7 @@ import android.os.SystemProperties; import android.util.Slog; import com.android.server.wm.LaunchParamsController.LaunchParamsModifier; -import com.android.wm.shell.Flags; +import com.android.window.flags.Flags; /** * The class that defines default launch params for tasks in desktop mode */ @@ -37,8 +37,6 @@ public class DesktopModeLaunchParamsModifier implements LaunchParamsModifier { TAG_WITH_CLASS_NAME ? "DesktopModeLaunchParamsModifier" : TAG_ATM; private static final boolean DEBUG = false; - // Desktop mode feature flags. - private static final boolean ENABLE_DESKTOP_WINDOWING = Flags.enableDesktopWindowing(); private static final boolean DESKTOP_MODE_PROTO2_SUPPORTED = SystemProperties.getBoolean("persist.wm.debug.desktop_mode_2", false); public static final float DESKTOP_MODE_INITIAL_BOUNDS_SCALE = @@ -67,6 +65,11 @@ public class DesktopModeLaunchParamsModifier implements LaunchParamsModifier { LaunchParamsController.LaunchParams currentParams, LaunchParamsController.LaunchParams outParams) { + if (!isDesktopModeEnabled()) { + appendLog("desktop mode is not enabled, continuing"); + return RESULT_CONTINUE; + } + if (task == null) { appendLog("task null, skipping"); return RESULT_SKIP; @@ -87,7 +90,7 @@ public class DesktopModeLaunchParamsModifier implements LaunchParamsModifier { // previous windowing mode to be restored even if the desktop mode state has changed. // Let task launches inherit the windowing mode from the source task if available, which // should have the desired windowing mode set by WM Shell. See b/286929122. - if (isDesktopModeSupported() && source != null && source.getTask() != null) { + if (isDesktopModeEnabled() && source != null && source.getTask() != null) { final Task sourceTask = source.getTask(); outParams.mWindowingMode = sourceTask.getWindowingMode(); appendLog("inherit-from-source=" + outParams.mWindowingMode); @@ -140,14 +143,8 @@ public class DesktopModeLaunchParamsModifier implements LaunchParamsModifier { if (DEBUG) Slog.d(TAG, mLogBuilder.toString()); } - /** Whether desktop mode is supported. */ - static boolean isDesktopModeSupported() { - // Check for aconfig flag first - if (ENABLE_DESKTOP_WINDOWING) { - return true; - } - // Fall back to sysprop flag - // TODO(b/304778354): remove sysprop once desktop aconfig flag supports dynamic overriding - return DESKTOP_MODE_PROTO2_SUPPORTED; + /** Whether desktop mode is enabled. */ + static boolean isDesktopModeEnabled() { + return Flags.enableDesktopWindowingMode(); } } diff --git a/services/core/java/com/android/server/wm/LaunchParamsController.java b/services/core/java/com/android/server/wm/LaunchParamsController.java index 91bb8d007948..97b5925893ae 100644 --- a/services/core/java/com/android/server/wm/LaunchParamsController.java +++ b/services/core/java/com/android/server/wm/LaunchParamsController.java @@ -64,10 +64,7 @@ class LaunchParamsController { void registerDefaultModifiers(ActivityTaskSupervisor supervisor) { // {@link TaskLaunchParamsModifier} handles window layout preferences. registerModifier(new TaskLaunchParamsModifier(supervisor)); - if (DesktopModeLaunchParamsModifier.isDesktopModeSupported()) { - // {@link DesktopModeLaunchParamsModifier} handles default task size changes - registerModifier(new DesktopModeLaunchParamsModifier()); - } + registerModifier(new DesktopModeLaunchParamsModifier()); } /** diff --git a/services/core/java/com/android/server/wm/OWNERS b/services/core/java/com/android/server/wm/OWNERS index e06f2158dc14..79eb0dc620a5 100644 --- a/services/core/java/com/android/server/wm/OWNERS +++ b/services/core/java/com/android/server/wm/OWNERS @@ -24,3 +24,5 @@ per-file Background*Start* = set noparent per-file Background*Start* = file:/BAL_OWNERS per-file Background*Start* = ogunwale@google.com, louischang@google.com +# File related to activity callers +per-file ActivityCallerState.java = file:/core/java/android/app/COMPONENT_CALLER_OWNERS diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java index 4698b6b2925c..5df2edc808f6 100644 --- a/services/core/java/com/android/server/wm/WindowManagerInternal.java +++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java @@ -1079,4 +1079,10 @@ public abstract class WindowManagerInternal { * Moves the current focus to the top activity window if the top activity is embedded. */ public abstract boolean moveFocusToTopEmbeddedWindowIfNeeded(); + + /** + * Returns an instance of {@link ScreenCapture.ScreenshotHardwareBuffer} containing the current + * screenshot. + */ + public abstract ScreenCapture.ScreenshotHardwareBuffer takeAssistScreenshot(); } diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 08d43ae6f4c9..c93cc074aa3d 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -4081,13 +4081,8 @@ public class WindowManagerService extends IWindowManager.Stub } } - /** - * Takes a snapshot of the screen. In landscape mode this grabs the whole screen. - * In portrait mode, it grabs the upper region of the screen based on the vertical dimension - * of the target image. - */ - @Override - public boolean requestAssistScreenshot(final IAssistDataReceiver receiver) { + @Nullable + private ScreenCapture.ScreenshotHardwareBuffer takeAssistScreenshot() { if (!checkCallingPermission(READ_FRAME_BUFFER, "requestAssistScreenshot()")) { throw new SecurityException("Requires READ_FRAME_BUFFER permission"); } @@ -4106,24 +4101,34 @@ public class WindowManagerService extends IWindowManager.Stub } } - final Bitmap bm; + final ScreenCapture.ScreenshotHardwareBuffer screenshotBuffer; if (captureArgs != null) { ScreenCapture.SynchronousScreenCaptureListener syncScreenCapture = ScreenCapture.createSyncCaptureListener(); ScreenCapture.captureLayers(captureArgs, syncScreenCapture); - final ScreenCapture.ScreenshotHardwareBuffer screenshotBuffer = - syncScreenCapture.getBuffer(); - bm = screenshotBuffer == null ? null : screenshotBuffer.asBitmap(); + screenshotBuffer = syncScreenCapture.getBuffer(); } else { - bm = null; + screenshotBuffer = null; } - if (bm == null) { + if (screenshotBuffer == null) { Slog.w(TAG_WM, "Failed to take screenshot"); } + return screenshotBuffer; + } + + /** + * Takes a snapshot of the screen. In landscape mode this grabs the whole screen. + * In portrait mode, it grabs the upper region of the screen based on the vertical dimension + * of the target image. + */ + @Override + public boolean requestAssistScreenshot(final IAssistDataReceiver receiver) { + final ScreenCapture.ScreenshotHardwareBuffer shb = takeAssistScreenshot(); + final Bitmap bm = shb != null ? shb.asBitmap() : null; FgThread.getHandler().post(() -> { try { receiver.onHandleAssistScreenshot(bm); @@ -8688,6 +8693,12 @@ public class WindowManagerService extends IWindowManager.Stub return false; } } + + @Override + public ScreenCapture.ScreenshotHardwareBuffer takeAssistScreenshot() { + // WMS.takeAssistScreenshot takes care of the locking. + return WindowManagerService.this.takeAssistScreenshot(); + } } private final class ImeTargetVisibilityPolicyImpl extends ImeTargetVisibilityPolicy { diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java index a6e05ddc792c..55208972895d 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java @@ -364,6 +364,30 @@ public class HdmiCecLocalDeviceAudioSystemTest { } @Test + public void systemAudioControlOnPowerOn_singleActionStarted() throws Exception { + mHdmiCecLocalDeviceAudioSystem.removeAction(SystemAudioInitiationActionFromAvr.class); + mHdmiCecLocalDeviceAudioSystem.systemAudioControlOnPowerOn( + Constants.ALWAYS_SYSTEM_AUDIO_CONTROL_ON_POWER_ON, true); + mHdmiCecLocalDeviceAudioSystem.systemAudioControlOnPowerOn( + Constants.ALWAYS_SYSTEM_AUDIO_CONTROL_ON_POWER_ON, true); + assertThat( + mHdmiCecLocalDeviceAudioSystem.getActions( + SystemAudioInitiationActionFromAvr.class)) + .hasSize(1); + } + + @Test + public void onSystemAudioControlFeatureSupportChanged_singleActionStarted() throws Exception { + mHdmiCecLocalDeviceAudioSystem.removeAction(SystemAudioInitiationActionFromAvr.class); + mHdmiCecLocalDeviceAudioSystem.onSystemAudioControlFeatureSupportChanged(true); + mHdmiCecLocalDeviceAudioSystem.onSystemAudioControlFeatureSupportChanged(true); + assertThat( + mHdmiCecLocalDeviceAudioSystem.getActions( + SystemAudioInitiationActionFromAvr.class)) + .hasSize(1); + } + + @Test public void handleActiveSource_updateActiveSource() throws Exception { HdmiCecMessage message = HdmiCecMessageBuilder.buildActiveSource(ADDR_TV, 0x0000); ActiveSource expectedActiveSource = new ActiveSource(ADDR_TV, 0x0000); diff --git a/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java index b1e62f9c9a82..15cd5115a49e 100644 --- a/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java @@ -206,7 +206,6 @@ import libcore.io.Streams; import org.junit.After; import org.junit.Assume; import org.junit.Before; -import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.MethodRule; @@ -2151,14 +2150,12 @@ public class NetworkPolicyManagerServiceTest { assertFalse(mService.isUidNetworkingBlocked(UID_E, false)); } - @Ignore("Temporarily disabled until the feature is enabled") @Test @RequiresFlagsEnabled(Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE) public void testBackgroundChainEnabled() throws Exception { verify(mNetworkManager).setFirewallChainEnabled(FIREWALL_CHAIN_BACKGROUND, true); } - @Ignore("Temporarily disabled until the feature is enabled") @Test @RequiresFlagsEnabled(Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE) public void testBackgroundChainOnProcStateChange() throws Exception { @@ -2188,7 +2185,6 @@ public class NetworkPolicyManagerServiceTest { assertTrue(mService.isUidNetworkingBlocked(UID_A, false)); } - @Ignore("Temporarily disabled until the feature is enabled") @Test @RequiresFlagsEnabled(Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE) public void testBackgroundChainOnAllowlistChange() throws Exception { @@ -2227,7 +2223,6 @@ public class NetworkPolicyManagerServiceTest { assertFalse(mService.isUidNetworkingBlocked(UID_B, false)); } - @Ignore("Temporarily disabled until the feature is enabled") @Test @RequiresFlagsEnabled(Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE) public void testBackgroundChainOnTempAllowlistChange() throws Exception { @@ -2266,7 +2261,6 @@ public class NetworkPolicyManagerServiceTest { && uidState.procState == procState && uidState.capability == capability; } - @Ignore("Temporarily disabled until the feature is enabled") @Test @RequiresFlagsEnabled(Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE) public void testUidObserverFiltersProcStateChanges() throws Exception { @@ -2329,7 +2323,6 @@ public class NetworkPolicyManagerServiceTest { waitForUidEventHandlerIdle(); } - @Ignore("Temporarily disabled until the feature is enabled") @Test @RequiresFlagsEnabled(Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE) public void testUidObserverFiltersStaleChanges() throws Exception { @@ -2350,7 +2343,6 @@ public class NetworkPolicyManagerServiceTest { waitForUidEventHandlerIdle(); } - @Ignore("Temporarily disabled until the feature is enabled") @Test @RequiresFlagsEnabled(Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE) public void testUidObserverFiltersCapabilityChanges() throws Exception { diff --git a/services/tests/servicestests/src/com/android/server/utils/AnrTimerTest.java b/services/tests/servicestests/src/com/android/server/utils/AnrTimerTest.java index c8bef45af839..076d5caf5954 100644 --- a/services/tests/servicestests/src/com/android/server/utils/AnrTimerTest.java +++ b/services/tests/servicestests/src/com/android/server/utils/AnrTimerTest.java @@ -312,10 +312,12 @@ public class AnrTimerTest { } /** - * Verify the dump output. + * Verify the dump output. This only applies when native timers are supported. */ @Test public void testDumpOutput() throws Exception { + if (!AnrTimer.nativeTimersSupported()) return; + String r1 = getDumpOutput(); assertThat(r1).doesNotContain("timer:"); diff --git a/services/tests/wmtests/src/com/android/server/policy/ShortcutLoggingTests.java b/services/tests/wmtests/src/com/android/server/policy/ShortcutLoggingTests.java index e904eae00802..0a29dfbd7db7 100644 --- a/services/tests/wmtests/src/com/android/server/policy/ShortcutLoggingTests.java +++ b/services/tests/wmtests/src/com/android/server/policy/ShortcutLoggingTests.java @@ -145,7 +145,7 @@ public class ShortcutLoggingTests extends ShortcutKeyTestBase { KeyboardLogEvent.SYSTEM_MUTE, KeyEvent.KEYCODE_MUTE, 0}, {"Meta + Ctrl + DPAD_UP -> Split screen navigation", new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_DPAD_UP}, - KeyboardLogEvent.SPLIT_SCREEN_NAVIGATION, KeyEvent.KEYCODE_DPAD_UP, + KeyboardLogEvent.MULTI_WINDOW_NAVIGATION, KeyEvent.KEYCODE_DPAD_UP, META_ON | CTRL_ON}, {"Meta + Ctrl + DPAD_LEFT -> Split screen navigation", new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_DPAD_LEFT}, diff --git a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java index ef36bff91a67..4060d40865d5 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DesktopModeLaunchParamsModifierTests.java @@ -24,6 +24,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static com.android.server.wm.DesktopModeLaunchParamsModifier.DESKTOP_MODE_INITIAL_BOUNDS_SCALE; import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.PHASE_BOUNDS; import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.PHASE_DISPLAY; +import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.RESULT_CONTINUE; import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.RESULT_DONE; import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.RESULT_SKIP; @@ -31,11 +32,14 @@ import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.mock; import android.graphics.Rect; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import androidx.test.filters.SmallTest; import com.android.server.wm.LaunchParamsController.LaunchParamsModifier.Result; +import com.android.window.flags.Flags; import org.junit.Before; import org.junit.Test; @@ -70,11 +74,19 @@ public class DesktopModeLaunchParamsModifierTests extends WindowTestsBase { } @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + public void testReturnsContinueIfDesktopWindowingIsDisabled() { + assertEquals(RESULT_CONTINUE, new CalculateRequestBuilder().setTask(null).calculate()); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) public void testReturnsSkipIfTaskIsNull() { assertEquals(RESULT_SKIP, new CalculateRequestBuilder().setTask(null).calculate()); } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) public void testReturnsSkipIfNotBoundsPhase() { final Task task = new TaskBuilder(mSupervisor).build(); assertEquals(RESULT_SKIP, new CalculateRequestBuilder().setTask(task).setPhase( @@ -82,6 +94,7 @@ public class DesktopModeLaunchParamsModifierTests extends WindowTestsBase { } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) public void testReturnsSkipIfTaskNotUsingActivityTypeStandardOrUndefined() { final Task task = new TaskBuilder(mSupervisor).setActivityType( ACTIVITY_TYPE_ASSISTANT).build(); @@ -89,6 +102,7 @@ public class DesktopModeLaunchParamsModifierTests extends WindowTestsBase { } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) public void testReturnsDoneIfTaskUsingActivityTypeStandard() { final Task task = new TaskBuilder(mSupervisor).setActivityType( ACTIVITY_TYPE_STANDARD).build(); @@ -96,6 +110,7 @@ public class DesktopModeLaunchParamsModifierTests extends WindowTestsBase { } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) public void testReturnsDoneIfTaskUsingActivityTypeUndefined() { final Task task = new TaskBuilder(mSupervisor).setActivityType( ACTIVITY_TYPE_UNDEFINED).build(); @@ -103,6 +118,7 @@ public class DesktopModeLaunchParamsModifierTests extends WindowTestsBase { } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) public void testReturnsSkipIfCurrentParamsHasBounds() { final Task task = new TaskBuilder(mSupervisor).setActivityType( ACTIVITY_TYPE_STANDARD).build(); @@ -111,6 +127,7 @@ public class DesktopModeLaunchParamsModifierTests extends WindowTestsBase { } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) public void testUsesDefaultBounds() { final Task task = new TaskBuilder(mSupervisor).setActivityType( ACTIVITY_TYPE_STANDARD).build(); @@ -125,6 +142,7 @@ public class DesktopModeLaunchParamsModifierTests extends WindowTestsBase { } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) public void testUsesDisplayAreaAndWindowingModeFromSource() { final Task task = new TaskBuilder(mSupervisor).setActivityType( ACTIVITY_TYPE_STANDARD).build(); diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java index c217780d90d6..aef7158fd613 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java @@ -16,15 +16,21 @@ package com.android.server.voiceinteraction; +import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; +import static android.content.Intent.FLAG_ACTIVITY_NO_ANIMATION; +import static android.content.Intent.FLAG_ACTIVITY_NO_USER_ACTION; import android.Manifest; import android.annotation.CallbackExecutor; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.RequiresPermission; import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.ActivityManagerInternal; +import android.app.ActivityOptions; import android.app.AppGlobals; +import android.app.admin.DevicePolicyManagerInternal; import android.app.role.OnRoleHoldersChangedListener; import android.app.role.RoleManager; import android.content.ComponentName; @@ -41,6 +47,7 @@ import android.content.pm.ShortcutServiceInternal; import android.content.pm.UserInfo; import android.content.res.Resources; import android.database.ContentObserver; +import android.graphics.Bitmap; import android.hardware.soundtrigger.IRecognitionStatusCallback; import android.hardware.soundtrigger.KeyphraseMetadata; import android.hardware.soundtrigger.ModelParams; @@ -84,6 +91,7 @@ import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; import android.util.Slog; +import android.window.ScreenCapture; import com.android.internal.R; import com.android.internal.annotations.GuardedBy; @@ -110,7 +118,9 @@ import com.android.server.pm.permission.LegacyPermissionManagerInternal; import com.android.server.policy.AppOpsPolicy; import com.android.server.utils.Slogf; import com.android.server.utils.TimingsTraceAndSlog; +import com.android.server.wm.ActivityAssistInfo; import com.android.server.wm.ActivityTaskManagerInternal; +import com.android.server.wm.WindowManagerInternal; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -127,6 +137,19 @@ public class VoiceInteractionManagerService extends SystemService { static final String TAG = "VoiceInteractionManager"; static final boolean DEBUG = false; + /** Static constants used by Contextual Search helper. */ + private static final String CS_KEY_FLAG_SECURE_FOUND = + "com.android.contextualsearch.flag_secure_found"; + private static final String CS_KEY_FLAG_SCREENSHOT = + "com.android.contextualsearch.screenshot"; + private static final String CS_KEY_FLAG_IS_MANAGED_PROFILE_VISIBLE = + "com.android.contextualsearch.is_managed_profile_visible"; + private static final String CS_KEY_FLAG_VISIBLE_PACKAGE_NAMES = + "com.android.contextualsearch.visible_package_names"; + private static final String CS_INTENT_FILTER = + "com.android.contextualsearch.LAUNCH"; + + final Context mContext; final ContentResolver mResolver; // Can be overridden for testing purposes @@ -135,6 +158,8 @@ public class VoiceInteractionManagerService extends SystemService { final ActivityManagerInternal mAmInternal; final ActivityTaskManagerInternal mAtmInternal; final UserManagerInternal mUserManagerInternal; + final WindowManagerInternal mWmInternal; + final DevicePolicyManagerInternal mDpmInternal; final ArrayMap<Integer, VoiceInteractionManagerServiceStub.SoundTriggerSession> mLoadedKeyphraseIds = new ArrayMap<>(); ShortcutServiceInternal mShortcutServiceInternal; @@ -156,7 +181,10 @@ public class VoiceInteractionManagerService extends SystemService { LocalServices.getService(ActivityManagerInternal.class)); mAtmInternal = Objects.requireNonNull( LocalServices.getService(ActivityTaskManagerInternal.class)); - + mWmInternal = Objects.requireNonNull( + LocalServices.getService(WindowManagerInternal.class)); + mDpmInternal = Objects.requireNonNull( + LocalServices.getService(DevicePolicyManagerInternal.class)); LegacyPermissionManagerInternal permissionManagerInternal = LocalServices.getService( LegacyPermissionManagerInternal.class); permissionManagerInternal.setVoiceInteractionPackagesProvider( @@ -1019,6 +1047,56 @@ public class VoiceInteractionManagerService extends SystemService { public boolean showSessionFromSession(@NonNull IBinder token, @Nullable Bundle sessionArgs, int flags, @Nullable String attributionTag) { synchronized (this) { + final String csKey = mContext.getResources() + .getString(R.string.config_defaultContextualSearchKey); + final String csEnabledKey = mContext.getResources() + .getString(R.string.config_defaultContextualSearchEnabled); + + // If the request is for Contextual Search, process it differently + if (sessionArgs != null && sessionArgs.containsKey(csKey)) { + if (sessionArgs.getBoolean(csEnabledKey, true)) { + // If Contextual Search is enabled, try to follow that path. + Intent launchIntent = getContextualSearchIntent(sessionArgs); + if (launchIntent != null) { + // Hand over to contextual search helper. + Slog.d(TAG, "Handed over to contextual search helper."); + final long caller = Binder.clearCallingIdentity(); + try { + return startContextualSearch(launchIntent); + } finally { + Binder.restoreCallingIdentity(caller); + } + } + } + + // Since we are here, Contextual Search helper couldn't handle the request. + final String visEnabledKey = mContext.getResources() + .getString(R.string.config_defaultContextualSearchLegacyEnabled); + if (sessionArgs.getBoolean(visEnabledKey, true)) { + // If visEnabledKey is set to true (or absent), we try following VIS path. + String csPkgName = mContext.getResources() + .getString(R.string.config_defaultContextualSearchPackageName); + if (!csPkgName.equals(getCurInteractor( + Binder.getCallingUserHandle().getIdentifier()).getPackageName())) { + // Check if the interactor can handle Contextual Search. + // If not, return failure. + Slog.w(TAG, "Contextual Search not supported yet. Returning failure."); + return false; + } + } else { + // If visEnabledKey is set to false AND the request was for Contextual + // Search, return false. + return false; + } + // Given that we haven't returned yet, we can say that + // - Contextual Search Helper couldn't handle the request + // - VIS path for Contextual Search is enabled + // - The current interactor supports Contextual Search. + // Hence, we will proceed with the VIS path. + Slog.d(TAG, "Contextual search not supported yet. Proceeding with VIS."); + + } + if (mImpl == null) { Slog.w(TAG, "showSessionFromSession without running voice interaction service"); return false; @@ -2644,6 +2722,70 @@ public class VoiceInteractionManagerService extends SystemService { } } }; + + private Intent getContextualSearchIntent(Bundle args) { + String csPkgName = mContext.getResources() + .getString(R.string.config_defaultContextualSearchPackageName); + if (csPkgName.isEmpty()) { + // Return null if csPackageName is not specified. + return null; + } + Intent launchIntent = new Intent(CS_INTENT_FILTER); + launchIntent.setPackage(csPkgName); + ResolveInfo resolveInfo = mContext.getPackageManager().resolveActivity( + launchIntent, PackageManager.MATCH_FACTORY_ONLY); + if (resolveInfo == null) { + return null; + } + launchIntent.setComponent(resolveInfo.getComponentInfo().getComponentName()); + launchIntent.addFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_NO_ANIMATION + | FLAG_ACTIVITY_NO_USER_ACTION); + launchIntent.putExtras(args); + boolean isAssistDataAllowed = mAtmInternal.isAssistDataAllowed(); + final List<ActivityAssistInfo> records = mAtmInternal.getTopVisibleActivities(); + ArrayList<String> visiblePackageNames = new ArrayList<>(); + boolean isManagedProfileVisible = false; + for (ActivityAssistInfo record: records) { + // Add the package name to the list only if assist data is allowed. + if (isAssistDataAllowed) { + visiblePackageNames.add(record.getComponentName().getPackageName()); + } + if (mDpmInternal.isUserOrganizationManaged(record.getUserId())) { + isManagedProfileVisible = true; + } + } + final ScreenCapture.ScreenshotHardwareBuffer shb = mWmInternal.takeAssistScreenshot(); + final Bitmap bm = shb != null ? shb.asBitmap() : null; + // Now that everything is fetched, putting it in the launchIntent. + if (bm != null) { + launchIntent.putExtra(CS_KEY_FLAG_SECURE_FOUND, shb.containsSecureLayers()); + // Only put the screenshot if assist data is allowed + if (isAssistDataAllowed) { + launchIntent.putExtra(CS_KEY_FLAG_SCREENSHOT, bm.asShared()); + } + } + launchIntent.putExtra(CS_KEY_FLAG_IS_MANAGED_PROFILE_VISIBLE, isManagedProfileVisible); + // Only put the list of visible package names if assist data is allowed + if (isAssistDataAllowed) { + launchIntent.putExtra(CS_KEY_FLAG_VISIBLE_PACKAGE_NAMES, visiblePackageNames); + } + + return launchIntent; + } + + @RequiresPermission(android.Manifest.permission.START_TASKS_FROM_RECENTS) + private boolean startContextualSearch(Intent launchIntent) { + // Contextual search starts with a frozen screen - so we launch without + // any system animations or starting window. + final ActivityOptions opts = ActivityOptions.makeCustomTaskAnimation(mContext, + /* enterResId= */ 0, /* exitResId= */ 0, null, null, null); + opts.setDisableStartingWindow(true); + int resultCode = mAtmInternal.startActivityWithScreenshot(launchIntent, + mContext.getPackageName(), Binder.getCallingUid(), Binder.getCallingPid(), null, + opts.toBundle(), Binder.getCallingUserHandle().getIdentifier()); + return resultCode == ActivityManager.START_SUCCESS; + } + } /** diff --git a/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/GraphicsActivity.java b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/GraphicsActivity.java index 217659ee4345..700856c50bae 100644 --- a/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/GraphicsActivity.java +++ b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/GraphicsActivity.java @@ -726,7 +726,7 @@ public class GraphicsActivity extends Activity { .map(Object::toString) .collect(Collectors.joining(", "))); int initialNumEvents = mModeChangedEvents.size(); - surface.setFrameRate(30.f, compatibility); + surface.setFrameRate(70.f, compatibility); verifyFrameRates(expectedFrameRates, surface); verifyModeSwitchesDontChangeResolution(initialNumEvents, mModeChangedEvents.size()); }); @@ -824,7 +824,7 @@ public class GraphicsActivity extends Activity { Display display = getDisplay(); List<Float> expectedFrameRates = getRefreshRates(display.getMode(), display) .stream() - .filter(rate -> rate >= 30.f) + .filter(rate -> rate >= 70.f) .collect(Collectors.toList()); assumeTrue("**** testSurfaceControlFrameRateCompatibility SKIPPED because no refresh rate " |