diff options
243 files changed, 8640 insertions, 1947 deletions
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java index 13903acc0439..f429966e042a 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java @@ -56,6 +56,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; import com.android.internal.util.FrameworkStatsLog; +import com.android.modules.expresslog.Counter; import com.android.server.LocalServices; import com.android.server.job.GrantedUriPermissions; import com.android.server.job.JobSchedulerInternal; @@ -161,6 +162,9 @@ public final class JobStatus { /** If the job is going to be passed an unmetered network. */ private boolean mHasAccessToUnmetered; + /** If the effective bucket has been downgraded once due to being buggy. */ + private boolean mIsDowngradedDueToBuggyApp; + /** * The additional set of dynamic constraints that must be met if this is an expedited job that * had a long enough run while the device was Dozing or in battery saver. @@ -1173,18 +1177,32 @@ public final class JobStatus { // like other ACTIVE apps. return ACTIVE_INDEX; } + + final int bucketWithMediaExemption; + if (actualBucket != RESTRICTED_INDEX && actualBucket != NEVER_INDEX + && mHasMediaBackupExemption) { + // Treat it as if it's at most WORKING_INDEX (lower index grants higher quota) since + // media backup jobs are important to the user, and the source package may not have + // been used directly in a while. + bucketWithMediaExemption = Math.min(WORKING_INDEX, actualBucket); + } else { + bucketWithMediaExemption = actualBucket; + } + // If the app is considered buggy, but hasn't yet been put in the RESTRICTED bucket // (potentially because it's used frequently by the user), limit its effective bucket // so that it doesn't get to run as much as a normal ACTIVE app. - final int highestBucket = isBuggy ? WORKING_INDEX : ACTIVE_INDEX; - if (actualBucket != RESTRICTED_INDEX && actualBucket != NEVER_INDEX - && mHasMediaBackupExemption) { - // Treat it as if it's at least WORKING_INDEX since media backup jobs are important - // to the user, and the - // source package may not have been used directly in a while. - return Math.max(highestBucket, Math.min(WORKING_INDEX, actualBucket)); + if (isBuggy && bucketWithMediaExemption < WORKING_INDEX) { + if (!mIsDowngradedDueToBuggyApp) { + // Safety check to avoid logging multiple times for the same job. + Counter.logIncrementWithUid( + "job_scheduler.value_job_quota_reduced_due_to_buggy_uid", + getTimeoutBlameUid()); + mIsDowngradedDueToBuggyApp = true; + } + return WORKING_INDEX; } - return Math.max(highestBucket, actualBucket); + return bucketWithMediaExemption; } /** Returns the real standby bucket of the job. */ diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 6d82922484bc..2a6d84b1acc6 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -5298,6 +5298,13 @@ public abstract class Context { public static final String APP_PREDICTION_SERVICE = "app_prediction"; /** + * Used for reading system-wide, overridable flags. + * + * @hide + */ + public static final String FEATURE_FLAGS_SERVICE = "feature_flags"; + + /** * Official published name of the search ui service. * * <p><b>NOTE: </b> this service is optional; callers of diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index 2948bd9b8868..bef023e1feee 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -1432,7 +1432,6 @@ public abstract class PackageManager { INSTALL_ALLOW_DOWNGRADE, INSTALL_STAGED, INSTALL_REQUEST_UPDATE_OWNERSHIP, - INSTALL_DONT_EXTRACT_BASELINE_PROFILES, }) @Retention(RetentionPolicy.SOURCE) public @interface InstallFlags {} @@ -1647,13 +1646,6 @@ public abstract class PackageManager { */ public static final int INSTALL_FROM_MANAGED_USER_OR_PROFILE = 1 << 26; - /** - * Flag parameter for {@link PackageInstaller.SessionParams} to indicate that do not extract - * the baseline profiles when parsing the apk - * @hide - */ - public static final int INSTALL_DONT_EXTRACT_BASELINE_PROFILES = 1 << 27; - /** @hide */ @IntDef(flag = true, value = { DONT_KILL_APP, diff --git a/core/java/android/content/pm/dex/DexMetadataHelper.java b/core/java/android/content/pm/dex/DexMetadataHelper.java index 3b53c25d1d8b..e75aa065d3d3 100644 --- a/core/java/android/content/pm/dex/DexMetadataHelper.java +++ b/core/java/android/content/pm/dex/DexMetadataHelper.java @@ -23,9 +23,6 @@ import android.content.pm.parsing.ApkLiteParseUtils; import android.content.pm.parsing.PackageLite; import android.content.pm.parsing.result.ParseInput; import android.content.pm.parsing.result.ParseResult; -import android.content.res.AssetFileDescriptor; -import android.content.res.AssetManager; -import android.os.ParcelFileDescriptor.AutoCloseInputStream; import android.os.SystemProperties; import android.util.ArrayMap; import android.util.JsonReader; @@ -36,7 +33,6 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.security.VerityUtils; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -48,7 +44,6 @@ import java.util.Collection; import java.util.List; import java.util.Map; import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; /** * Helper class used to compute and validate the location of dex metadata files. @@ -67,11 +62,6 @@ public class DexMetadataHelper { private static final String DEX_METADATA_FILE_EXTENSION = ".dm"; - private static final String PROFILE_FILE_NAME = "primary.prof"; - private static final String PROFILE_METADATA_FILE_NAME = "primary.profm"; - private static final String BASELINE_PROFILE_SOURCE_RELATIVE_PATH = "dexopt/baseline.prof"; - private static final String BASELINE_PROFILE_METADATA_RELATIVE_PATH = "dexopt/baseline.profm"; - private DexMetadataHelper() {} /** Return true if the given file is a dex metadata file. */ @@ -323,76 +313,4 @@ public class DexMetadataHelper { } } - /** - * Extract the baseline profiles from the assets directory in the apk. Then create a - * ZIP archive with the (.dm) file extension (e.g. foo.dm from foo.apk) to include the - * baseline profiles and put the DexMetadata file in the same directory with the apk. - * - * @param assetManager The {@link AssetManager} to use. - * @param apkPath The path of the apk - * @return {@code true} if the extraction is successful. Otherwise, return {@code false}. - * - * @see #buildDexMetadataPathForApk(String) - */ - public static boolean extractBaselineProfilesToDexMetadataFileFromApk(AssetManager assetManager, - String apkPath) { - if (!ApkLiteParseUtils.isApkPath(apkPath)) { - if (DEBUG) { - Log.d(TAG, "It is not an apk file: " + apkPath); - } - return false; - } - - // get the name of the DexMetadata file from the path of the apk - final File dmFile = new File(buildDexMetadataPathForApk(apkPath)); - boolean success = false; - - // load profile and profile metadata from assets directory in the apk - try (InputStream profileIs = openStreamFromAssets(assetManager, - BASELINE_PROFILE_SOURCE_RELATIVE_PATH); - InputStream profileMetadataIs = openStreamFromAssets(assetManager, - BASELINE_PROFILE_METADATA_RELATIVE_PATH)) { - // Create the zip archive file and write the baseline profiles into it. - try (FileOutputStream fos = new FileOutputStream(dmFile)) { - try (ZipOutputStream zipOs = new ZipOutputStream(fos)) { - zipOs.putNextEntry(new ZipEntry(PROFILE_FILE_NAME)); - zipOs.write(profileIs.readAllBytes()); - zipOs.closeEntry(); - - zipOs.putNextEntry(new ZipEntry(PROFILE_METADATA_FILE_NAME)); - zipOs.write(profileMetadataIs.readAllBytes()); - zipOs.closeEntry(); - success = true; - } - } - } catch (IOException e) { - if (DEBUG) { - Log.e(TAG, "Extract baseline profiles from apk failed: " + e.getMessage()); - } - } finally { - if (!success) { - if (dmFile.exists()) { - dmFile.delete(); - } - } - } - return success; - } - - /** - * Loads an {@link AutoCloseInputStream} from assets with the path. - * - * @param assetManager The {@link AssetManager} to use. - * @param path The source file's relative path. - * @return An AutoCloseInputStream in case the file was successfully read. - * @throws IOException If anything goes wrong while opening or reading the file. - */ - private static AutoCloseInputStream openStreamFromAssets(AssetManager assetManager, String path) - throws IOException { - AssetFileDescriptor descriptor = assetManager.openFd(path); - // Based on the java doc of AssetFileDescriptor#createInputStream, it will always return - // an AutoCloseInputStream. It should be fine we cast it from FileInputStream to - // AutoCloseInputStream here. - return (AutoCloseInputStream) descriptor.createInputStream(); - } } diff --git a/core/java/android/database/sqlite/SQLiteConnection.java b/core/java/android/database/sqlite/SQLiteConnection.java index 382e4f8b04e9..b7489229a424 100644 --- a/core/java/android/database/sqlite/SQLiteConnection.java +++ b/core/java/android/database/sqlite/SQLiteConnection.java @@ -176,6 +176,8 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen private static native void nativeCancel(long connectionPtr); private static native void nativeResetCancel(long connectionPtr, boolean cancelable); private static native int nativeLastInsertRowId(long connectionPtr); + private static native long nativeChanges(long connectionPtr); + private static native long nativeTotalChanges(long connectionPtr); private SQLiteConnection(SQLiteConnectionPool pool, SQLiteDatabaseConfiguration configuration, @@ -1823,11 +1825,36 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen * @return The ROWID of the last row to be inserted under this connection. * @hide */ - long lastInsertRowId() { + long getLastInsertRowId() { try { return nativeLastInsertRowId(mConnectionPtr); } finally { Reference.reachabilityFence(this); } } + + /** + * Return the number of database changes on the current connection made by the last SQL + * statement + * @hide + */ + long getLastChangedRowsCount() { + try { + return nativeChanges(mConnectionPtr); + } finally { + Reference.reachabilityFence(this); + } + } + + /** + * Return the total number of database changes made on the current connection. + * @hide + */ + long getTotalChangedRowsCount() { + try { + return nativeTotalChanges(mConnectionPtr); + } finally { + Reference.reachabilityFence(this); + } + } } diff --git a/core/java/android/database/sqlite/SQLiteDatabase.java b/core/java/android/database/sqlite/SQLiteDatabase.java index eceec34ba0f4..a3f838347258 100644 --- a/core/java/android/database/sqlite/SQLiteDatabase.java +++ b/core/java/android/database/sqlite/SQLiteDatabase.java @@ -2197,7 +2197,49 @@ public final class SQLiteDatabase extends SQLiteClosable { * @throws IllegalStateException if there is no current transaction. */ public long getLastInsertRowId() { - return getThreadSession().lastInsertRowId(); + return getThreadSession().getLastInsertRowId(); + } + + /** + * Return the number of database rows that were inserted, updated, or deleted by the most recent + * SQL statement within the current transaction. + * + * @see <a href="https://sqlite.org/c3ref/changes.html">sqlite3_changes64</a> + * + * @return The number of rows changed by the most recent sql statement + * @throws IllegalStateException if there is no current transaction. + * @hide + */ + public long getLastChangedRowsCount() { + return getThreadSession().getLastChangedRowsCount(); + } + + /** + * Return the total number of database rows that have been inserted, updated, or deleted on + * the current connection since it was created. Due to Android's internal management of + * SQLite connections, the value may, or may not, include changes made in earlier + * transactions. Best practice is to compare values returned within a single transaction. + * + * <code><pre> + * database.beginTransaction(); + * try { + * long initialValue = database.getTotalChangedRowsCount(); + * // Execute SQL statements + * long changedRows = database.getTotalChangedRowsCount() - initialValue; + * // changedRows counts the total number of rows updated in the transaction. + * } finally { + * database.endTransaction(); + * } + * </pre></code> + * + * @see <a href="https://sqlite.org/c3ref/changes.html">sqlite3_total_changes64</a> + * + * @return The number of rows changed on the current connection. + * @throws IllegalStateException if there is no current transaction. + * @hide + */ + public long getTotalChangedRowsCount() { + return getThreadSession().getTotalChangedRowsCount(); } /** diff --git a/core/java/android/database/sqlite/SQLiteSession.java b/core/java/android/database/sqlite/SQLiteSession.java index 2379c849e534..ef1a9cbe0adf 100644 --- a/core/java/android/database/sqlite/SQLiteSession.java +++ b/core/java/android/database/sqlite/SQLiteSession.java @@ -988,9 +988,29 @@ public final class SQLiteSession { * necessary to acquire and release the connection: the connection has already been acquired. * @hide */ - long lastInsertRowId() { + long getLastInsertRowId() { throwIfNoTransaction(); - return mConnection.lastInsertRowId(); + return mConnection.getLastInsertRowId(); + } + + /** + * Return the number of database rows that were changed by the most recent SQL statement on + * this connection. + * @hide + */ + long getLastChangedRowsCount() { + throwIfNoTransaction(); + return mConnection.getLastChangedRowsCount(); + } + + /** + * Return the total number of database rows that were changed on the current connection, since + * it was created. + * @hide + */ + long getTotalChangedRowsCount() { + throwIfNoTransaction(); + return mConnection.getTotalChangedRowsCount(); } /** diff --git a/core/java/android/flags/BooleanFlag.java b/core/java/android/flags/BooleanFlag.java new file mode 100644 index 000000000000..d4a35b25f623 --- /dev/null +++ b/core/java/android/flags/BooleanFlag.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.flags; + +import android.annotation.NonNull; + +/** + * A flag representing a true or false value. + * + * The value will always be the same during the lifetime of the process it is read in. + * + * @hide + */ +public class BooleanFlag extends BooleanFlagBase { + private final boolean mDefault; + + /** + * @param namespace A namespace for this flag. See {@link android.provider.DeviceConfig}. + * @param name A name for this flag. + * @param defaultValue The value of this flag if no other override is present. + */ + BooleanFlag(String namespace, String name, boolean defaultValue) { + super(namespace, name); + mDefault = defaultValue; + } + + @Override + @NonNull + public Boolean getDefault() { + return mDefault; + } + + @Override + public BooleanFlag defineMetaData(String label, String description, String categoryName) { + super.defineMetaData(label, description, categoryName); + return this; + } +} diff --git a/core/java/android/flags/BooleanFlagBase.java b/core/java/android/flags/BooleanFlagBase.java new file mode 100644 index 000000000000..985dbe3f2f01 --- /dev/null +++ b/core/java/android/flags/BooleanFlagBase.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.flags; + +import android.annotation.NonNull; + +abstract class BooleanFlagBase implements Flag<Boolean> { + + private final String mNamespace; + private final String mName; + private String mLabel; + private String mDescription; + private String mCategoryName; + + /** + * @param namespace A namespace for this flag. See {@link android.provider.DeviceConfig}. + * @param name A name for this flag. + */ + BooleanFlagBase(String namespace, String name) { + mNamespace = namespace; + mName = name; + mLabel = name; + } + + public abstract Boolean getDefault(); + + @Override + @NonNull + public String getNamespace() { + return mNamespace; + } + + @Override + @NonNull + public String getName() { + return mName; + } + + @Override + public BooleanFlagBase defineMetaData(String label, String description, String categoryName) { + mLabel = label; + mDescription = description; + mCategoryName = categoryName; + return this; + } + + @Override + @NonNull + public String getLabel() { + return mLabel; + } + + @Override + public String getDescription() { + return mDescription; + } + + @Override + public String getCategoryName() { + return mCategoryName; + } + + @Override + @NonNull + public String toString() { + return getNamespace() + "." + getName() + "[" + getDefault() + "]"; + } +} diff --git a/core/java/android/flags/DynamicBooleanFlag.java b/core/java/android/flags/DynamicBooleanFlag.java new file mode 100644 index 000000000000..271a8c5f4d15 --- /dev/null +++ b/core/java/android/flags/DynamicBooleanFlag.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.flags; + +/** + * A flag representing a true or false value. + * + * The value may be different from one read to the next. + * + * @hide + */ +public class DynamicBooleanFlag extends BooleanFlagBase implements DynamicFlag<Boolean> { + + private final boolean mDefault; + + /** + * @param namespace A namespace for this flag. See {@link android.provider.DeviceConfig}. + * @param name A name for this flag. + * @param defaultValue The value of this flag if no other override is present. + */ + DynamicBooleanFlag(String namespace, String name, boolean defaultValue) { + super(namespace, name); + mDefault = defaultValue; + } + + @Override + public Boolean getDefault() { + return mDefault; + } + + @Override + public DynamicBooleanFlag defineMetaData(String label, String description, String categoryName) { + super.defineMetaData(label, description, categoryName); + return this; + } +} diff --git a/core/java/android/flags/DynamicFlag.java b/core/java/android/flags/DynamicFlag.java new file mode 100644 index 000000000000..68819c58c064 --- /dev/null +++ b/core/java/android/flags/DynamicFlag.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.flags; + +/** + * A flag for which the value may be different from one read to the next. + * + * @param <T> The type of value that this flag stores. E.g. Boolean or String. + * + * @hide + */ +public interface DynamicFlag<T> extends Flag<T> { + @Override + default boolean isDynamic() { + return true; + } +} diff --git a/core/java/android/flags/FeatureFlags.java b/core/java/android/flags/FeatureFlags.java new file mode 100644 index 000000000000..8d3112c35d51 --- /dev/null +++ b/core/java/android/flags/FeatureFlags.java @@ -0,0 +1,379 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.flags; + +import android.annotation.NonNull; +import android.content.Context; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.util.ArraySet; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * A class for querying constants from the system - primarily booleans. + * + * Clients using this class can define their flags and their default values in one place, + * can override those values on running devices for debugging and testing purposes, and can control + * what flags are available to be used on release builds. + * + * TODO(b/279054964): A lot. This is skeleton code right now. + * @hide + */ +public class FeatureFlags { + private static final String TAG = "FeatureFlags"; + private static FeatureFlags sInstance; + private static final Object sInstanceLock = new Object(); + + private final Set<Flag<?>> mKnownFlags = new ArraySet<>(); + private final Set<Flag<?>> mDirtyFlags = new ArraySet<>(); + + private IFeatureFlags mIFeatureFlags; + private final Map<String, Map<String, Boolean>> mBooleanOverrides = new HashMap<>(); + private final Set<ChangeListener> mListeners = new HashSet<>(); + + /** + * Obtain a per-process instance of FeatureFlags. + * @return A singleton instance of {@link FeatureFlags}. + */ + @NonNull + public static FeatureFlags getInstance() { + synchronized (sInstanceLock) { + if (sInstance == null) { + sInstance = new FeatureFlags(); + } + } + + return sInstance; + } + + /** See {@link FeatureFlagsFake}. */ + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) + public static void setInstance(FeatureFlags instance) { + synchronized (sInstanceLock) { + sInstance = instance; + } + } + + private final IFeatureFlagsCallback mIFeatureFlagsCallback = new IFeatureFlagsCallback.Stub() { + @Override + public void onFlagChange(SyncableFlag flag) { + for (Flag<?> f : mKnownFlags) { + if (flagEqualsSyncableFlag(f, flag)) { + if (f instanceof DynamicFlag<?>) { + if (f instanceof DynamicBooleanFlag) { + String value = flag.getValue(); + if (value == null) { // Null means any existing overrides were erased. + value = ((DynamicBooleanFlag) f).getDefault().toString(); + } + addBooleanOverride(flag.getNamespace(), flag.getName(), value); + } + FeatureFlags.this.onFlagChange((DynamicFlag<?>) f); + } + break; + } + } + } + }; + + private FeatureFlags() { + this(null); + } + + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) + public FeatureFlags(IFeatureFlags iFeatureFlags) { + mIFeatureFlags = iFeatureFlags; + + if (mIFeatureFlags != null) { + try { + mIFeatureFlags.registerCallback(mIFeatureFlagsCallback); + } catch (RemoteException e) { + // Shouldn't happen with things passed into tests. + Log.e(TAG, "Could not register callbacks!", e); + } + } + } + + /** + * Construct a new {@link BooleanFlag}. + * + * Use this instead of constructing a {@link BooleanFlag} directly, as it registers the flag + * with the internals of the flagging system. + */ + @NonNull + public static BooleanFlag booleanFlag( + @NonNull String namespace, @NonNull String name, boolean def) { + return getInstance().addFlag(new BooleanFlag(namespace, name, def)); + } + + /** + * Construct a new {@link FusedOffFlag}. + * + * Use this instead of constructing a {@link FusedOffFlag} directly, as it registers the + * flag with the internals of the flagging system. + */ + @NonNull + public static FusedOffFlag fusedOffFlag(@NonNull String namespace, @NonNull String name) { + return getInstance().addFlag(new FusedOffFlag(namespace, name)); + } + + /** + * Construct a new {@link FusedOnFlag}. + * + * Use this instead of constructing a {@link FusedOnFlag} directly, as it registers the flag + * with the internals of the flagging system. + */ + @NonNull + public static FusedOnFlag fusedOnFlag(@NonNull String namespace, @NonNull String name) { + return getInstance().addFlag(new FusedOnFlag(namespace, name)); + } + + /** + * Construct a new {@link DynamicBooleanFlag}. + * + * Use this instead of constructing a {@link DynamicBooleanFlag} directly, as it registers + * the flag with the internals of the flagging system. + */ + @NonNull + public static DynamicBooleanFlag dynamicBooleanFlag( + @NonNull String namespace, @NonNull String name, boolean def) { + return getInstance().addFlag(new DynamicBooleanFlag(namespace, name, def)); + } + + /** + * Add a listener to be alerted when a {@link DynamicFlag} changes. + * + * See also {@link #removeChangeListener(ChangeListener)}. + * + * @param listener The listener to add. + */ + public void addChangeListener(@NonNull ChangeListener listener) { + mListeners.add(listener); + } + + /** + * Remove a listener that was added earlier. + * + * See also {@link #addChangeListener(ChangeListener)}. + * + * @param listener The listener to remove. + */ + public void removeChangeListener(@NonNull ChangeListener listener) { + mListeners.remove(listener); + } + + protected void onFlagChange(@NonNull DynamicFlag<?> flag) { + for (ChangeListener l : mListeners) { + l.onFlagChanged(flag); + } + } + + /** + * Returns whether the supplied flag is true or not. + * + * {@link BooleanFlag} should only be used in debug builds. They do not get optimized out. + * + * The first time a flag is read, its value is cached for the lifetime of the process. + */ + public boolean isEnabled(@NonNull BooleanFlag flag) { + return getBooleanInternal(flag); + } + + /** + * Returns whether the supplied flag is true or not. + * + * Always returns false. + */ + public boolean isEnabled(@NonNull FusedOffFlag flag) { + return false; + } + + /** + * Returns whether the supplied flag is true or not. + * + * Always returns true; + */ + public boolean isEnabled(@NonNull FusedOnFlag flag) { + return true; + } + + /** + * Returns whether the supplied flag is true or not. + * + * Can return a different value for the flag each time it is called if an override comes in. + */ + public boolean isCurrentlyEnabled(@NonNull DynamicBooleanFlag flag) { + return getBooleanInternal(flag); + } + + private boolean getBooleanInternal(Flag<Boolean> flag) { + sync(); + Map<String, Boolean> ns = mBooleanOverrides.get(flag.getNamespace()); + Boolean value = null; + if (ns != null) { + value = ns.get(flag.getName()); + } + if (value == null) { + throw new IllegalStateException("Boolean flag being read but was not synced: " + flag); + } + + return value; + } + + private <T extends Flag<?>> T addFlag(T flag) { + synchronized (FeatureFlags.class) { + mDirtyFlags.add(flag); + mKnownFlags.add(flag); + } + return flag; + } + + /** + * Sync any known flags that have not yet been synced. + * + * This is called implicitly when any flag is read, and is not generally needed except in + * exceptional circumstances. + */ + public void sync() { + synchronized (FeatureFlags.class) { + if (mDirtyFlags.isEmpty()) { + return; + } + syncInternal(mDirtyFlags); + mDirtyFlags.clear(); + } + } + + /** + * Called when new flags have been declared. Gives the implementation a chance to act on them. + * + * Guaranteed to be called from a synchronized, thread-safe context. + */ + protected void syncInternal(Set<Flag<?>> dirtyFlags) { + IFeatureFlags iFeatureFlags = bind(); + List<SyncableFlag> syncableFlags = new ArrayList<>(); + for (Flag<?> f : dirtyFlags) { + syncableFlags.add(flagToSyncableFlag(f)); + } + + List<SyncableFlag> serverFlags = List.of(); // Need to initialize the list with something. + try { + // New values come back from the service. + serverFlags = iFeatureFlags.syncFlags(syncableFlags); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + + for (Flag<?> f : dirtyFlags) { + boolean found = false; + for (SyncableFlag sf : serverFlags) { + if (flagEqualsSyncableFlag(f, sf)) { + if (f instanceof BooleanFlag || f instanceof DynamicBooleanFlag) { + addBooleanOverride(sf.getNamespace(), sf.getName(), sf.getValue()); + } + found = true; + break; + } + } + if (!found) { + if (f instanceof BooleanFlag) { + addBooleanOverride( + f.getNamespace(), + f.getName(), + ((BooleanFlag) f).getDefault() ? "true" : "false"); + } + } + } + } + + private void addBooleanOverride(String namespace, String name, String override) { + Map<String, Boolean> nsOverrides = mBooleanOverrides.get(namespace); + if (nsOverrides == null) { + nsOverrides = new HashMap<>(); + mBooleanOverrides.put(namespace, nsOverrides); + } + nsOverrides.put(name, parseBoolean(override)); + } + + private SyncableFlag flagToSyncableFlag(Flag<?> f) { + return new SyncableFlag( + f.getNamespace(), + f.getName(), + f.getDefault().toString(), + f instanceof DynamicFlag<?>); + } + + private IFeatureFlags bind() { + if (mIFeatureFlags == null) { + mIFeatureFlags = IFeatureFlags.Stub.asInterface( + ServiceManager.getService(Context.FEATURE_FLAGS_SERVICE)); + try { + mIFeatureFlags.registerCallback(mIFeatureFlagsCallback); + } catch (RemoteException e) { + Log.e(TAG, "Failed to listen for flag changes!"); + } + } + + return mIFeatureFlags; + } + + static boolean parseBoolean(String value) { + // Check for a truish string. + boolean result = value.equalsIgnoreCase("true") + || value.equals("1") + || value.equalsIgnoreCase("t") + || value.equalsIgnoreCase("on"); + if (!result) { // Expect a falsish string, else log an error. + if (!(value.equalsIgnoreCase("false") + || value.equals("0") + || value.equalsIgnoreCase("f") + || value.equalsIgnoreCase("off"))) { + Log.e(TAG, + "Tried parsing " + value + " as boolean but it doesn't look like one. " + + "Value expected to be one of true|false, 1|0, t|f, on|off."); + } + } + return result; + } + + private static boolean flagEqualsSyncableFlag(Flag<?> f, SyncableFlag sf) { + return f.getName().equals(sf.getName()) && f.getNamespace().equals(sf.getNamespace()); + } + + + /** + * A simpler listener that is alerted when a {@link DynamicFlag} changes. + * + * See {@link #addChangeListener(ChangeListener)} + */ + public interface ChangeListener { + /** + * Called when a {@link DynamicFlag} changes. + * + * @param flag The flag that has changed. + */ + void onFlagChanged(DynamicFlag<?> flag); + } +} diff --git a/core/java/android/flags/FeatureFlagsFake.java b/core/java/android/flags/FeatureFlagsFake.java new file mode 100644 index 000000000000..daedcdae0b5f --- /dev/null +++ b/core/java/android/flags/FeatureFlagsFake.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.flags; + +import android.annotation.NonNull; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * An implementation of {@link FeatureFlags} for testing. + * + * Before you read a flag from using this Fake, you must set that flag using + * {@link #setFlagValue(BooleanFlagBase, boolean)}. This ensures that your tests are deterministic. + * + * If you are relying on {@link FeatureFlags#getInstance()} to access FeatureFlags in your code + * under test, (instead of dependency injection), you can pass an instance of this fake to + * {@link FeatureFlags#setInstance(FeatureFlags)}. Be sure to call that method again, passing null, + * to ensure hermetic testing - you don't want static state persisting between your test methods. + * + * @hide + */ +public class FeatureFlagsFake extends FeatureFlags { + private final Map<BooleanFlagBase, Boolean> mFlagValues = new HashMap<>(); + private final Set<BooleanFlagBase> mReadFlags = new HashSet<>(); + + public FeatureFlagsFake(IFeatureFlags iFeatureFlags) { + super(iFeatureFlags); + } + + @Override + public boolean isEnabled(@NonNull BooleanFlag flag) { + return requireFlag(flag); + } + + @Override + public boolean isEnabled(@NonNull FusedOffFlag flag) { + return requireFlag(flag); + } + + @Override + public boolean isEnabled(@NonNull FusedOnFlag flag) { + return requireFlag(flag); + } + + @Override + public boolean isCurrentlyEnabled(@NonNull DynamicBooleanFlag flag) { + return requireFlag(flag); + } + + @Override + protected void syncInternal(Set<Flag<?>> dirtyFlags) { + } + + /** + * Explicitly set a flag's value for reading in tests. + * + * You _must_ call this for every flag your code-under-test will read. Otherwise, an + * {@link IllegalStateException} will be thrown. + * + * You are able to set values for {@link FusedOffFlag} and {@link FusedOnFlag}, despite those + * flags having a fixed value at compile time, since unit tests should still test the state of + * those flags as both true and false. I.e. a flag that is off might be turned on in a future + * build or vice versa. + * + * You can not call this method _after_ a non-dynamic flag has been read. Non-dynamic flags + * are held stable in the system, so changing a value after reading would not match + * real-implementation behavior. + * + * Calling this method will trigger any {@link android.flags.FeatureFlags.ChangeListener}s that + * are registered for the supplied flag if the flag is a {@link DynamicFlag}. + * + * @param flag The BooleanFlag that you want to set a value for. + * @param value The value that the flag should return when accessed. + */ + public void setFlagValue(@NonNull BooleanFlagBase flag, boolean value) { + if (!(flag instanceof DynamicBooleanFlag) && mReadFlags.contains(flag)) { + throw new RuntimeException( + "You can not set the value of a flag after it has been read. Tried to set " + + flag + " to " + value + " but it already " + mFlagValues.get(flag)); + } + mFlagValues.put(flag, value); + if (flag instanceof DynamicBooleanFlag) { + onFlagChange((DynamicFlag<?>) flag); + } + } + + private boolean requireFlag(BooleanFlagBase flag) { + if (!mFlagValues.containsKey(flag)) { + throw new IllegalStateException( + "Tried to access " + flag + " in test but no overrided specified. You must " + + "call #setFlagValue for each flag read in a test."); + } + mReadFlags.add(flag); + + return mFlagValues.get(flag); + } + +} diff --git a/core/java/android/flags/Flag.java b/core/java/android/flags/Flag.java new file mode 100644 index 000000000000..b97a4c8a0fe7 --- /dev/null +++ b/core/java/android/flags/Flag.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.flags; + +import android.annotation.NonNull; + +/** + * Base class for constants read via {@link android.flags.FeatureFlags}. + * + * @param <T> The type of value that this flag stores. E.g. Boolean or String. + * + * @hide + */ +public interface Flag<T> { + /** The namespace for a flag. Should combine uniquely with its name. */ + @NonNull + String getNamespace(); + + /** The name of the flag. Should combine uniquely with its namespace. */ + @NonNull + String getName(); + + /** The value of this flag if no override has been set. Null values are not supported. */ + @NonNull + T getDefault(); + + /** Returns true if the value of this flag can change at runtime. */ + default boolean isDynamic() { + return false; + } + + /** + * Add human-readable details to the flag. Flag client's are not required to set this. + * + * See {@link #getLabel()}, {@link #getDescription()}, and {@link #getCategoryName()}. + * + * @return Returns `this`, to make a fluent api. + */ + Flag<T> defineMetaData(String label, String description, String categoryName); + + /** + * A human-readable name for the flag. Defaults to {@link #getName()} + * + * See {@link #defineMetaData(String, String, String)} + */ + @NonNull + default String getLabel() { + return getName(); + } + + /** + * A human-readable description for the flag. Defaults to null if unset. + * + * See {@link #defineMetaData(String, String, String)} + */ + default String getDescription() { + return null; + } + + /** + * A human-readable category name for the flag. Defaults to null if unset. + * + * See {@link #defineMetaData(String, String, String)} + */ + default String getCategoryName() { + return null; + } +} diff --git a/core/java/android/flags/FusedOffFlag.java b/core/java/android/flags/FusedOffFlag.java new file mode 100644 index 000000000000..6844b8faafef --- /dev/null +++ b/core/java/android/flags/FusedOffFlag.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.flags; + +import android.annotation.NonNull; +import android.provider.DeviceConfig; + +/** + * A flag representing a false value. + * + * The flag can never be changed or overridden. It is false at compile time. + * + * @hide + */ +public final class FusedOffFlag extends BooleanFlagBase { + /** + * @param namespace A namespace for this flag. See {@link DeviceConfig}. + * @param name A name for this flag. + */ + FusedOffFlag(String namespace, String name) { + super(namespace, name); + } + + @Override + @NonNull + public Boolean getDefault() { + return false; + } + + @Override + public FusedOffFlag defineMetaData(String label, String description, String categoryName) { + super.defineMetaData(label, description, categoryName); + return this; + } +} diff --git a/core/java/android/flags/FusedOnFlag.java b/core/java/android/flags/FusedOnFlag.java new file mode 100644 index 000000000000..e9adba7595c1 --- /dev/null +++ b/core/java/android/flags/FusedOnFlag.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.flags; + +import android.annotation.NonNull; +import android.provider.DeviceConfig; + +/** + * A flag representing a true value. + * + * The flag can never be changed or overridden. It is true at compile time. + * + * @hide + */ +public final class FusedOnFlag extends BooleanFlagBase { + /** + * @param namespace A namespace for this flag. See {@link DeviceConfig}. + * @param name A name for this flag. + */ + FusedOnFlag(String namespace, String name) { + super(namespace, name); + } + + @Override + @NonNull + public Boolean getDefault() { + return true; + } + + @Override + public FusedOnFlag defineMetaData(String label, String description, String categoryName) { + super.defineMetaData(label, description, categoryName); + return this; + } +} diff --git a/core/java/android/flags/IFeatureFlags.aidl b/core/java/android/flags/IFeatureFlags.aidl new file mode 100644 index 000000000000..3efcec97fe6d --- /dev/null +++ b/core/java/android/flags/IFeatureFlags.aidl @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + package android.flags; + +import android.flags.IFeatureFlagsCallback; +import android.flags.SyncableFlag; + +/** + * Binder interface for communicating with {@link com.android.server.flags.FeatureFlagsService}. + * + * This interface is used by {@link android.flags.FeatureFlags} and developers should use that to + * interface with the service. FeatureFlags is the "client" in this documentation. + * + * The methods allow client apps to communicate what flags they care about, and receive back + * current values for those flags. For stable flags, this is the finalized value until the device + * restarts. For {@link DynamicFlag}s, this is the last known value, though it may change in the + * future. Clients can listen for changes to flag values so that it can react accordingly. + * @hide + */ +interface IFeatureFlags { + /** + * Synchronize with the {@link com.android.server.flags.FeatureFlagsService} about flags of + * interest. + * + * The client should pass in a list of flags that it is using as {@link SyncableFlag}s, which + * includes what it thinks the default values of the flags are. + * + * The response will contain a list of matching SyncableFlags, whose values are set to what the + * value of the flags actually are. The client should update its internal state flag data to + * match. + * + * Generally speaking, if a flag that is passed in is new to the FeatureFlagsService, the + * service will cache the passed-in value, and return it back out. If, however, a different + * client has synced that flag with the service previously, FeatureFlagsService will return the + * existing cached value, which may or may not be what the current client passed in. This allows + * FeatureFlagsService to keep clients in agreement with one another. + */ + List<SyncableFlag> syncFlags(in List<SyncableFlag> flagList); + + /** + * Pass in an {@link IFeatureFlagsCallback} that will be called whenever a {@link DymamicFlag} + * changes. + */ + void registerCallback(IFeatureFlagsCallback callback); + + /** + * Remove a {@link IFeatureFlagsCallback} that was previously registered with + * {@link #registerCallback}. + */ + void unregisterCallback(IFeatureFlagsCallback callback); + + /** + * Query the {@link com.android.server.flags.FeatureFlagsService} for flags, but don't + * cache them. See {@link #syncFlags}. + * + * You almost certainly don't want this method. This is intended for the Flag Flipper + * application that needs to query the state of system but doesn't want to affect it by + * doing so. All other clients should use {@link syncFlags}. + */ + List<SyncableFlag> queryFlags(in List<SyncableFlag> flagList); + + /** + * Change a flags value in the system. + * + * This is intended for use by the Flag Flipper application. + */ + void overrideFlag(in SyncableFlag flag); + + /** + * Restore a flag to its default value. + * + * This is intended for use by the Flag Flipper application. + */ + void resetFlag(in SyncableFlag flag); +}
\ No newline at end of file diff --git a/core/java/android/flags/IFeatureFlagsCallback.aidl b/core/java/android/flags/IFeatureFlagsCallback.aidl new file mode 100644 index 000000000000..f708667ea4c4 --- /dev/null +++ b/core/java/android/flags/IFeatureFlagsCallback.aidl @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + package android.flags; + +import android.flags.SyncableFlag; + +/** + * Callback for {@link IFeatureFlags#registerCallback} to get alerts when a {@link DynamicFlag} + * changes. + * + * DynamicFlags can change at run time. Stable flags will never result in a call to this method. + * + * @hide + */ +oneway interface IFeatureFlagsCallback { + void onFlagChange(in SyncableFlag flag); +}
\ No newline at end of file diff --git a/core/java/android/flags/OWNERS b/core/java/android/flags/OWNERS new file mode 100644 index 000000000000..fa125c4a159c --- /dev/null +++ b/core/java/android/flags/OWNERS @@ -0,0 +1,7 @@ +# Bug component: 1306523 + +mankoff@google.com +pixel@google.com + +dsandler@android.com + diff --git a/core/java/android/flags/SyncableFlag.aidl b/core/java/android/flags/SyncableFlag.aidl new file mode 100644 index 000000000000..1526ec148967 --- /dev/null +++ b/core/java/android/flags/SyncableFlag.aidl @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.flags; + +/** + * A parcelable data class for serializing {@link Flag} across a Binder. + */ +parcelable SyncableFlag;
\ No newline at end of file diff --git a/core/java/android/flags/SyncableFlag.java b/core/java/android/flags/SyncableFlag.java new file mode 100644 index 000000000000..449bcc3c49f5 --- /dev/null +++ b/core/java/android/flags/SyncableFlag.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.flags; + +import android.annotation.NonNull; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * @hide + */ +public final class SyncableFlag implements Parcelable { + private final String mNamespace; + private final String mName; + private final String mValue; + private final boolean mDynamic; + private final boolean mOverridden; + + public SyncableFlag( + @NonNull String namespace, + @NonNull String name, + @NonNull String value, + boolean dynamic) { + this(namespace, name, value, dynamic, false); + } + + public SyncableFlag( + @NonNull String namespace, + @NonNull String name, + @NonNull String value, + boolean dynamic, + boolean overridden + ) { + mNamespace = namespace; + mName = name; + mValue = value; + mDynamic = dynamic; + mOverridden = overridden; + } + + @NonNull + public String getNamespace() { + return mNamespace; + } + + @NonNull + public String getName() { + return mName; + } + + @NonNull + public String getValue() { + return mValue; + } + + public boolean isDynamic() { + return mDynamic; + } + + public boolean isOverridden() { + return mOverridden; + } + + @NonNull + public static final Parcelable.Creator<SyncableFlag> CREATOR = new Parcelable.Creator<>() { + public SyncableFlag createFromParcel(Parcel in) { + return new SyncableFlag( + in.readString(), + in.readString(), + in.readString(), + in.readBoolean(), + in.readBoolean()); + } + + public SyncableFlag[] newArray(int size) { + return new SyncableFlag[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeString(mNamespace); + dest.writeString(mName); + dest.writeString(mValue); + dest.writeBoolean(mDynamic); + dest.writeBoolean(mOverridden); + } + + @Override + public String toString() { + return getNamespace() + "." + getName() + "[" + getValue() + "]"; + } +} diff --git a/core/java/android/hardware/biometrics/BiometricPrompt.java b/core/java/android/hardware/biometrics/BiometricPrompt.java index 2e40f6096ccb..912e8df6bdc7 100644 --- a/core/java/android/hardware/biometrics/BiometricPrompt.java +++ b/core/java/android/hardware/biometrics/BiometricPrompt.java @@ -144,6 +144,7 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan private Context mContext; private IAuthService mService; + // LINT.IfChange /** * Creates a builder for a {@link BiometricPrompt} dialog. * @param context The {@link Context} that will be used to build the prompt. @@ -417,6 +418,7 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan * @hide */ @NonNull + @RequiresPermission(anyOf = {USE_BIOMETRIC_INTERNAL}) public Builder setDisallowBiometricsIfPolicyExists(boolean checkDevicePolicyManager) { mPromptInfo.setDisallowBiometricsIfPolicyExists(checkDevicePolicyManager); return this; @@ -429,6 +431,7 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan * @hide */ @NonNull + @RequiresPermission(anyOf = {USE_BIOMETRIC_INTERNAL}) public Builder setReceiveSystemEvents(boolean set) { mPromptInfo.setReceiveSystemEvents(set); return this; @@ -442,6 +445,7 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan * @hide */ @NonNull + @RequiresPermission(anyOf = {TEST_BIOMETRIC, USE_BIOMETRIC_INTERNAL}) public Builder setIgnoreEnrollmentState(boolean ignoreEnrollmentState) { mPromptInfo.setIgnoreEnrollmentState(ignoreEnrollmentState); return this; @@ -454,10 +458,12 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan * @hide */ @NonNull + @RequiresPermission(anyOf = {TEST_BIOMETRIC, USE_BIOMETRIC_INTERNAL}) public Builder setIsForLegacyFingerprintManager(int sensorId) { mPromptInfo.setIsForLegacyFingerprintManager(sensorId); return this; } + // LINT.ThenChange(frameworks/base/core/java/android/hardware/biometrics/PromptInfo.java) /** * Creates a {@link BiometricPrompt}. diff --git a/core/java/android/hardware/biometrics/PromptInfo.java b/core/java/android/hardware/biometrics/PromptInfo.java index 02aad1dc4b4b..e27507874167 100644 --- a/core/java/android/hardware/biometrics/PromptInfo.java +++ b/core/java/android/hardware/biometrics/PromptInfo.java @@ -113,6 +113,7 @@ public class PromptInfo implements Parcelable { dest.writeBoolean(mIsForLegacyFingerprintManager); } + // LINT.IfChange public boolean containsTestConfigurations() { if (mIsForLegacyFingerprintManager && mAllowedSensorIds.size() == 1 @@ -122,6 +123,10 @@ public class PromptInfo implements Parcelable { return true; } else if (mAllowBackgroundAuthentication) { return true; + } else if (mIsForLegacyFingerprintManager) { + return true; + } else if (mIgnoreEnrollmentState) { + return true; } return false; } @@ -144,6 +149,7 @@ public class PromptInfo implements Parcelable { } return false; } + // LINT.ThenChange(frameworks/base/core/java/android/hardware/biometrics/BiometricPrompt.java) // Setters diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java index cc3a662870bb..e47fab41f9ff 100644 --- a/core/java/android/service/wallpaper/WallpaperService.java +++ b/core/java/android/service/wallpaper/WallpaperService.java @@ -868,6 +868,11 @@ public abstract class WallpaperService extends Service { * This will trigger a {@link #onComputeColors()} call. */ public void notifyColorsChanged() { + if (mDestroyed) { + Log.i(TAG, "Ignoring notifyColorsChanged(), Engine has already been destroyed."); + return; + } + final long now = mClockFunction.get(); if (now - mLastColorInvalidation < NOTIFY_COLORS_RATE_LIMIT_MS) { Log.w(TAG, "This call has been deferred. You should only call " @@ -2231,7 +2236,11 @@ public abstract class WallpaperService extends Service { } } - void detach() { + /** + * @hide + */ + @VisibleForTesting + public void detach() { if (mDestroyed) { return; } @@ -2449,6 +2458,14 @@ public abstract class WallpaperService extends Service { } public void reportShown() { + if (mEngine == null) { + Log.i(TAG, "Can't report null engine as shown."); + return; + } + if (mEngine.mDestroyed) { + Log.i(TAG, "Engine was destroyed before we could draw."); + return; + } if (!mShownReported) { mShownReported = true; Trace.beginSection("WPMS.mConnection.engineShown"); diff --git a/core/java/com/android/internal/flags/CoreFlags.java b/core/java/com/android/internal/flags/CoreFlags.java new file mode 100644 index 000000000000..f177ef88c38f --- /dev/null +++ b/core/java/com/android/internal/flags/CoreFlags.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2023 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.internal.flags; + +import android.flags.BooleanFlag; +import android.flags.DynamicBooleanFlag; +import android.flags.FeatureFlags; +import android.flags.FusedOffFlag; +import android.flags.FusedOnFlag; +import android.flags.SyncableFlag; + +import java.util.ArrayList; +import java.util.List; + +/** + * Flags defined here are can be read by code in core. + * + * Flags not defined here will throw a security exception if third-party processes attempts to read + * them. + * + * DO NOT define a flag here unless you explicitly intend for that flag to be readable by code that + * runs inside a third party process. + */ +public abstract class CoreFlags { + private static final List<SyncableFlag> sKnownFlags = new ArrayList<>(); + + public static BooleanFlag BOOL_FLAG = booleanFlag("core", "bool_flag", false); + public static FusedOffFlag OFF_FLAG = fusedOffFlag("core", "off_flag"); + public static FusedOnFlag ON_FLAG = fusedOnFlag("core", "on_flag"); + public static DynamicBooleanFlag DYN_FLAG = dynamicBooleanFlag("core", "dyn_flag", true); + + /** Returns true if the passed in flag matches a flag in this class. */ + public static boolean isCoreFlag(SyncableFlag flag) { + for (SyncableFlag knownFlag : sKnownFlags) { + if (knownFlag.getName().equals(flag.getName()) + && knownFlag.getNamespace().equals(flag.getNamespace())) { + return true; + } + } + return false; + } + + public static List<SyncableFlag> getCoreFlags() { + return sKnownFlags; + } + + private static BooleanFlag booleanFlag(String namespace, String name, boolean defaultValue) { + BooleanFlag f = FeatureFlags.booleanFlag(namespace, name, defaultValue); + + sKnownFlags.add(new SyncableFlag(namespace, name, Boolean.toString(defaultValue), false)); + + return f; + } + + private static FusedOffFlag fusedOffFlag(String namespace, String name) { + FusedOffFlag f = FeatureFlags.fusedOffFlag(namespace, name); + + sKnownFlags.add(new SyncableFlag(namespace, name, "false", false)); + + return f; + } + + private static FusedOnFlag fusedOnFlag(String namespace, String name) { + FusedOnFlag f = FeatureFlags.fusedOnFlag(namespace, name); + + sKnownFlags.add(new SyncableFlag(namespace, name, "true", false)); + + return f; + } + + private static DynamicBooleanFlag dynamicBooleanFlag( + String namespace, String name, boolean defaultValue) { + DynamicBooleanFlag f = FeatureFlags.dynamicBooleanFlag(namespace, name, defaultValue); + + sKnownFlags.add(new SyncableFlag(namespace, name, Boolean.toString(defaultValue), true)); + + return f; + } +} diff --git a/core/java/com/android/internal/os/TimeoutRecord.java b/core/java/com/android/internal/os/TimeoutRecord.java index 1d0ba3f4faa2..e9a8d4b75f16 100644 --- a/core/java/com/android/internal/os/TimeoutRecord.java +++ b/core/java/com/android/internal/os/TimeoutRecord.java @@ -58,6 +58,7 @@ public class TimeoutRecord { int APP_REGISTERED = 7; int SHORT_FGS_TIMEOUT = 8; int JOB_SERVICE = 9; + int APP_START = 10; } /** Kind of timeout, e.g. BROADCAST_RECEIVER, etc. */ @@ -190,4 +191,10 @@ public class TimeoutRecord { public static TimeoutRecord forJobService(String reason) { return TimeoutRecord.endingNow(TimeoutKind.JOB_SERVICE, reason); } + + /** Record for app startup timeout. */ + @NonNull + public static TimeoutRecord forAppStart(String reason) { + return TimeoutRecord.endingNow(TimeoutKind.APP_START, reason); + } } diff --git a/core/jni/android_database_SQLiteConnection.cpp b/core/jni/android_database_SQLiteConnection.cpp index 7e827a837dce..29520c24da75 100644 --- a/core/jni/android_database_SQLiteConnection.cpp +++ b/core/jni/android_database_SQLiteConnection.cpp @@ -885,6 +885,16 @@ static jint nativeLastInsertRowId(JNIEnv* env, jclass, jlong connectionPtr) { return sqlite3_last_insert_rowid(connection->db); } +static jlong nativeChanges(JNIEnv* env, jclass, jlong connectionPtr) { + SQLiteConnection* connection = reinterpret_cast<SQLiteConnection*>(connectionPtr); + return sqlite3_changes64(connection->db); +} + +static jlong nativeTotalChanges(JNIEnv* env, jclass, jlong connectionPtr) { + SQLiteConnection* connection = reinterpret_cast<SQLiteConnection*>(connectionPtr); + return sqlite3_total_changes64(connection->db); +} + static const JNINativeMethod sMethods[] = { /* name, signature, funcPtr */ @@ -943,7 +953,9 @@ static const JNINativeMethod sMethods[] = { "nativeResetCancel", "(JZ)V", (void*)nativeResetCancel }, - { "nativeLastInsertRowId", "(J)I", (void*) nativeLastInsertRowId } + { "nativeLastInsertRowId", "(J)I", (void*) nativeLastInsertRowId }, + { "nativeChanges", "(J)J", (void*) nativeChanges }, + { "nativeTotalChanges", "(J)J", (void*) nativeTotalChanges }, }; int register_android_database_SQLiteConnection(JNIEnv *env) diff --git a/core/proto/android/os/system_properties.proto b/core/proto/android/os/system_properties.proto index 3cedba0632aa..5a3539acae26 100644 --- a/core/proto/android/os/system_properties.proto +++ b/core/proto/android/os/system_properties.proto @@ -172,7 +172,7 @@ message SystemPropertiesProto { optional Status tombstoned = 29; optional Status ueventd = 30; optional Status update_engine = 31; - optional Status update_verifier_nonencrypted = 32; + optional Status update_verifier = 32; optional Status virtual_touchpad = 33; optional Status vndservicemanager = 34; optional Status vold = 35; diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 0c8707deb542..baa47daa1251 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -7663,6 +7663,24 @@ <permission android:name="android.permission.GET_ANY_PROVIDER_TYPE" android:protectionLevel="signature" /> + + <!-- @hide Allows internal applications to read and synchronize non-core flags. + Apps without this permission can only read a subset of flags specifically intended + for use in "core", (i.e. third party apps). Apps with this permission can define their + own flags, and federate those values with other system-level apps. + <p>Not for use by third-party applications. + <p>Protection level: signature + --> + <permission android:name="android.permission.SYNC_FLAGS" + android:protectionLevel="signature" /> + + <!-- @hide Allows internal applications to override flags in the FeatureFlags service. + <p>Not for use by third-party applications. + <p>Protection level: signature + --> + <permission android:name="android.permission.WRITE_FLAGS" + android:protectionLevel="signature" /> + <!-- Attribution for Geofencing service. --> <attribution android:tag="GeofencingService" android:label="@string/geofencing_service"/> <!-- Attribution for Country Detector. --> diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index d828f33ca514..a11eaa91b2a6 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -1786,6 +1786,10 @@ <string name="biometric_dialog_default_title">Verify it\u2019s you</string> <!-- Subtitle shown on the system-provided biometric dialog, asking the user to authenticate with a biometric (e.g. fingerprint or face). [CHAR LIMIT=70] --> <string name="biometric_dialog_default_subtitle">Use your biometric to continue</string> + <!-- Subtitle shown on the system-provided biometric dialog, asking the user to authenticate with fingerprint. [CHAR LIMIT=70] --> + <string name="biometric_dialog_fingerprint_subtitle">Use your fingerprint to continue</string> + <!-- Subtitle shown on the system-provided biometric dialog, asking the user to authenticate with face. [CHAR LIMIT=70] --> + <string name="biometric_dialog_face_subtitle">Use your face to continue</string> <!-- Subtitle shown on the system-provided biometric dialog, asking the user to authenticate with a biometric (e.g. fingerprint or face) or their screen lock credential (i.e. PIN, pattern, or password). [CHAR LIMIT=90] --> <string name="biometric_or_screen_lock_dialog_default_subtitle">Use your biometric or screen lock to continue</string> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 956c1f3c7a01..0951aecceb94 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -2581,6 +2581,8 @@ <java-symbol type="string" name="biometric_or_screen_lock_app_setting_name" /> <java-symbol type="string" name="biometric_dialog_default_title" /> <java-symbol type="string" name="biometric_dialog_default_subtitle" /> + <java-symbol type="string" name="biometric_dialog_face_subtitle" /> + <java-symbol type="string" name="biometric_dialog_fingerprint_subtitle" /> <java-symbol type="string" name="biometric_or_screen_lock_dialog_default_subtitle" /> <java-symbol type="string" name="biometric_error_hw_unavailable" /> <java-symbol type="string" name="biometric_error_user_canceled" /> diff --git a/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java b/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java index ea5e7b639731..fc72f611ccc8 100644 --- a/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java +++ b/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java @@ -137,4 +137,81 @@ public class SQLiteDatabaseTest { fail("Timed out"); } } + + /** + * Create a database with one table with three columns. + */ + private void createComplexDatabase() { + mDatabase.beginTransaction(); + try { + mDatabase.execSQL("CREATE TABLE t1 (i int, d double, t text);"); + mDatabase.setTransactionSuccessful(); + } finally { + mDatabase.endTransaction(); + } + } + + /** + * A three-value insert for the complex database. + */ + private String createComplexInsert() { + return "INSERT INTO t1 (i, d, t) VALUES (?1, ?2, ?3)"; + } + + @Test + public void testAutomaticCounters() { + final int size = 10; + + createComplexDatabase(); + + // Put 10 lines in the database. + mDatabase.beginTransaction(); + try { + try (SQLiteRawStatement s = mDatabase.createRawStatement(createComplexInsert())) { + for (int i = 0; i < size; i++) { + int vi = i * 3; + double vd = i * 2.5; + String vt = String.format("text%02dvalue", i); + s.bindInt(1, vi); + s.bindDouble(2, vd); + s.bindText(3, vt); + boolean r = s.step(); + // No row is returned by this query. + assertFalse(r); + s.reset(); + assertEquals(i + 1, mDatabase.getLastInsertRowId()); + assertEquals(1, mDatabase.getLastChangedRowsCount()); + assertEquals(i + 2, mDatabase.getTotalChangedRowsCount()); + } + } + mDatabase.setTransactionSuccessful(); + } finally { + mDatabase.endTransaction(); + } + + // Put a second 10 lines in the database. + mDatabase.beginTransaction(); + try { + try (SQLiteRawStatement s = mDatabase.createRawStatement(createComplexInsert())) { + for (int i = 0; i < size; i++) { + int vi = i * 3; + double vd = i * 2.5; + String vt = String.format("text%02dvalue", i); + s.bindInt(1, vi); + s.bindDouble(2, vd); + s.bindText(3, vt); + boolean r = s.step(); + // No row is returned by this query. + assertFalse(r); + s.reset(); + assertEquals(size + i + 1, mDatabase.getLastInsertRowId()); + assertEquals(1, mDatabase.getLastChangedRowsCount()); + assertEquals(size + i + 2, mDatabase.getTotalChangedRowsCount()); + } + } + mDatabase.setTransactionSuccessful(); + } finally { + mDatabase.endTransaction(); + } + } } diff --git a/core/tests/coretests/src/android/database/sqlite/SQLiteRawStatementTest.java b/core/tests/coretests/src/android/database/sqlite/SQLiteRawStatementTest.java index 59a1643c061f..36bb8e5496c9 100644 --- a/core/tests/coretests/src/android/database/sqlite/SQLiteRawStatementTest.java +++ b/core/tests/coretests/src/android/database/sqlite/SQLiteRawStatementTest.java @@ -846,59 +846,6 @@ public class SQLiteRawStatementTest { } @Test - public void testLastInsertRowId() { - final int size = 10; - - createComplexDatabase(); - - // Put 10 lines in the database. - mDatabase.beginTransaction(); - try { - try (SQLiteRawStatement s = mDatabase.createRawStatement(createComplexInsert())) { - for (int i = 0; i < size; i++) { - int vi = i * 3; - double vd = i * 2.5; - String vt = String.format("text%02dvalue", i); - s.bindInt(1, vi); - s.bindDouble(2, vd); - s.bindText(3, vt); - boolean r = s.step(); - // No row is returned by this query. - assertFalse(r); - s.reset(); - assertEquals(i + 1, mDatabase.getLastInsertRowId()); - } - } - mDatabase.setTransactionSuccessful(); - } finally { - mDatabase.endTransaction(); - } - - // Put a second 10 lines in the database. - mDatabase.beginTransaction(); - try { - try (SQLiteRawStatement s = mDatabase.createRawStatement(createComplexInsert())) { - for (int i = 0; i < size; i++) { - int vi = i * 3; - double vd = i * 2.5; - String vt = String.format("text%02dvalue", i); - s.bindInt(1, vi); - s.bindDouble(2, vd); - s.bindText(3, vt); - boolean r = s.step(); - // No row is returned by this query. - assertFalse(r); - s.reset(); - assertEquals(size + i + 1, mDatabase.getLastInsertRowId()); - } - } - mDatabase.setTransactionSuccessful(); - } finally { - mDatabase.endTransaction(); - } - } - - @Test public void testUnicode() { // Create the t1 table and put some data in it. mDatabase.beginTransaction(); diff --git a/core/tests/coretests/src/android/flags/FeatureFlagsTest.java b/core/tests/coretests/src/android/flags/FeatureFlagsTest.java new file mode 100644 index 000000000000..3fc94394d12c --- /dev/null +++ b/core/tests/coretests/src/android/flags/FeatureFlagsTest.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.flags; + +import static com.google.common.truth.Truth.assertThat; + +import android.platform.test.annotations.Presubmit; + +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; + +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + + +@SmallTest +@Presubmit +public class FeatureFlagsTest { + + IFeatureFlagsFake mIFeatureFlagsFake = new IFeatureFlagsFake(); + FeatureFlags mFeatureFlags = new FeatureFlags(mIFeatureFlagsFake); + + @Before + public void setup() { + FeatureFlags.setInstance(mFeatureFlags); + } + + @Test + public void testFusedOff_Disabled() { + FusedOffFlag flag = FeatureFlags.fusedOffFlag("test", "a"); + assertThat(mFeatureFlags.isEnabled(flag)).isFalse(); + } + + @Test + public void testFusedOn_Enabled() { + FusedOnFlag flag = FeatureFlags.fusedOnFlag("test", "a"); + assertThat(mFeatureFlags.isEnabled(flag)).isTrue(); + } + + @Test + public void testBooleanFlag_DefaultDisabled() { + BooleanFlag flag = FeatureFlags.booleanFlag("test", "a", false); + assertThat(mFeatureFlags.isEnabled(flag)).isFalse(); + } + + @Test + public void testBooleanFlag_DefaultEnabled() { + BooleanFlag flag = FeatureFlags.booleanFlag("test", "a", true); + assertThat(mFeatureFlags.isEnabled(flag)).isTrue(); + } + + @Test + public void testDynamicBooleanFlag_DefaultDisabled() { + DynamicBooleanFlag flag = FeatureFlags.dynamicBooleanFlag("test", "a", false); + assertThat(mFeatureFlags.isCurrentlyEnabled(flag)).isFalse(); + } + + @Test + public void testDynamicBooleanFlag_DefaultEnabled() { + DynamicBooleanFlag flag = FeatureFlags.dynamicBooleanFlag("test", "a", true); + assertThat(mFeatureFlags.isCurrentlyEnabled(flag)).isTrue(); + } + + @Test + public void testBooleanFlag_OverrideBeforeRead() { + BooleanFlag flag = FeatureFlags.booleanFlag("test", "a", false); + SyncableFlag syncableFlag = new SyncableFlag( + flag.getNamespace(), flag.getName(), "true", false); + + mIFeatureFlagsFake.setFlagOverrides(List.of(syncableFlag)); + + assertThat(mFeatureFlags.isEnabled(flag)).isTrue(); + } + + @Test + public void testFusedOffFlag_OverrideHasNoEffect() { + FusedOffFlag flag = FeatureFlags.fusedOffFlag("test", "a"); + SyncableFlag syncableFlag = new SyncableFlag( + flag.getNamespace(), flag.getName(), "true", false); + + mIFeatureFlagsFake.setFlagOverrides(List.of(syncableFlag)); + + assertThat(mFeatureFlags.isEnabled(flag)).isFalse(); + } + + @Test + public void testFusedOnFlag_OverrideHasNoEffect() { + FusedOnFlag flag = FeatureFlags.fusedOnFlag("test", "a"); + SyncableFlag syncableFlag = new SyncableFlag( + flag.getNamespace(), flag.getName(), "false", false); + + mIFeatureFlagsFake.setFlagOverrides(List.of(syncableFlag)); + + assertThat(mFeatureFlags.isEnabled(flag)).isTrue(); + } + + @Test + public void testDynamicFlag_OverrideBeforeRead() { + DynamicBooleanFlag flag = FeatureFlags.dynamicBooleanFlag("test", "a", false); + SyncableFlag syncableFlag = new SyncableFlag( + flag.getNamespace(), flag.getName(), "true", true); + + mIFeatureFlagsFake.setFlagOverrides(List.of(syncableFlag)); + + // Changes to true + assertThat(mFeatureFlags.isCurrentlyEnabled(flag)).isTrue(); + } + + @Test + public void testDynamicFlag_OverrideAfterRead() { + DynamicBooleanFlag flag = FeatureFlags.dynamicBooleanFlag("test", "a", false); + SyncableFlag syncableFlag = new SyncableFlag( + flag.getNamespace(), flag.getName(), "true", true); + + // Starts false + assertThat(mFeatureFlags.isCurrentlyEnabled(flag)).isFalse(); + + mIFeatureFlagsFake.setFlagOverrides(List.of(syncableFlag)); + + // Changes to true + assertThat(mFeatureFlags.isCurrentlyEnabled(flag)).isTrue(); + } + + @Test + public void testDynamicFlag_FiresListener() { + DynamicBooleanFlag flag = FeatureFlags.dynamicBooleanFlag("test", "a", false); + AtomicBoolean called = new AtomicBoolean(false); + FeatureFlags.ChangeListener listener = flag1 -> called.set(true); + + mFeatureFlags.addChangeListener(listener); + + SyncableFlag syncableFlag = new SyncableFlag( + flag.getNamespace(), flag.getName(), flag.getDefault().toString(), true); + + mIFeatureFlagsFake.setFlagOverrides(List.of(syncableFlag)); + + // Fires listener. + assertThat(called.get()).isTrue(); + } +} diff --git a/core/tests/coretests/src/android/flags/IFeatureFlagsFake.java b/core/tests/coretests/src/android/flags/IFeatureFlagsFake.java new file mode 100644 index 000000000000..bc5d8aa3ac73 --- /dev/null +++ b/core/tests/coretests/src/android/flags/IFeatureFlagsFake.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.flags; + +import android.os.IBinder; +import android.os.RemoteException; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +class IFeatureFlagsFake implements IFeatureFlags { + + private final Set<IFeatureFlagsCallback> mCallbacks = new HashSet<>(); + + List<SyncableFlag> mOverrides; + + @Override + public IBinder asBinder() { + return null; + } + + @Override + public List<SyncableFlag> syncFlags(List<SyncableFlag> flagList) { + return mOverrides == null ? flagList : mOverrides; + } + + @Override + public List<SyncableFlag> queryFlags(List<SyncableFlag> flagList) { + return mOverrides == null ? flagList : mOverrides; } + + @Override + public void overrideFlag(SyncableFlag syncableFlag) { + SyncableFlag match = findFlag(syncableFlag); + if (match != null) { + mOverrides.remove(match); + } + + mOverrides.add(syncableFlag); + + for (IFeatureFlagsCallback cb : mCallbacks) { + try { + cb.onFlagChange(syncableFlag); + } catch (RemoteException e) { + // does not happen in fakes. + } + } + } + + @Override + public void resetFlag(SyncableFlag syncableFlag) { + SyncableFlag match = findFlag(syncableFlag); + if (match != null) { + mOverrides.remove(match); + } + + for (IFeatureFlagsCallback cb : mCallbacks) { + try { + cb.onFlagChange(syncableFlag); + } catch (RemoteException e) { + // does not happen in fakes. + } + } + } + + private SyncableFlag findFlag(SyncableFlag syncableFlag) { + SyncableFlag match = null; + for (SyncableFlag sf : mOverrides) { + if (sf.getName().equals(syncableFlag.getName()) + && sf.getNamespace().equals(syncableFlag.getNamespace())) { + match = sf; + break; + } + } + + return match; + } + @Override + public void registerCallback(IFeatureFlagsCallback callback) { + mCallbacks.add(callback); + } + + @Override + public void unregisterCallback(IFeatureFlagsCallback callback) { + mCallbacks.remove(callback); + } + + public void setFlagOverrides(List<SyncableFlag> flagList) { + mOverrides = flagList; + for (SyncableFlag sf : flagList) { + for (IFeatureFlagsCallback cb : mCallbacks) { + try { + cb.onFlagChange(sf); + } catch (RemoteException e) { + // does not happen in fakes. + } + } + } + } +} diff --git a/data/etc/platform.xml b/data/etc/platform.xml index 2c85fe4e9206..c4530f64a82d 100644 --- a/data/etc/platform.xml +++ b/data/etc/platform.xml @@ -346,4 +346,8 @@ <!-- Allow IMS service entitlement app to schedule jobs to run when app in background. --> <allow-in-power-save-except-idle package="com.android.imsserviceentitlement" /> + + <!-- Allow device lock controller app to schedule jobs and alarms when app in background, + otherwise, it may not be able to enforce provision for managed devices. --> + <allow-in-power-save package="com.android.devicelockcontroller" /> </permissions> diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java index 0059577da39c..ccf95527efea 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java @@ -296,7 +296,7 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { * {@link IllegalArgumentException} since this can cause negative UI effects down stream. * * @param context a proxy for the {@link android.view.Window} that contains the - * {@link DisplayFeature}. + * {@link DisplayFeature}. * @return a {@link List} of {@link DisplayFeature}s that are within the * {@link android.view.Window} of the {@link Activity} */ @@ -328,10 +328,32 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { rotateRectToDisplayRotation(displayId, featureRect); transformToWindowSpaceRect(windowConfiguration, featureRect); - if (!isZero(featureRect)) { + if (isZero(featureRect)) { // TODO(b/228641877): Remove guarding when fixed. - features.add(new FoldingFeature(featureRect, baseFeature.getType(), state)); + continue; + } + if (featureRect.left != 0 && featureRect.top != 0) { + throw new IllegalArgumentException("Bounding rectangle must start at the top or " + + "left of the window. BaseFeatureRect: " + baseFeature.getRect() + + ", FeatureRect: " + featureRect + + ", WindowConfiguration: " + windowConfiguration); + + } + if (featureRect.left == 0 + && featureRect.width() != windowConfiguration.getBounds().width()) { + throw new IllegalArgumentException("Horizontal FoldingFeature must have full width." + + " BaseFeatureRect: " + baseFeature.getRect() + + ", FeatureRect: " + featureRect + + ", WindowConfiguration: " + windowConfiguration); + } + if (featureRect.top == 0 + && featureRect.height() != windowConfiguration.getBounds().height()) { + throw new IllegalArgumentException("Vertical FoldingFeature must have full height." + + " BaseFeatureRect: " + baseFeature.getRect() + + ", FeatureRect: " + featureRect + + ", WindowConfiguration: " + windowConfiguration); } + features.add(new FoldingFeature(featureRect, baseFeature.getType(), state)); } return features; } 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 6d14440c9b18..f8d7b6bc3aad 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 @@ -202,17 +202,18 @@ class DesktopTasksController( } /** - * Moves a single task to freeform and sets the taskBounds to the passed in bounds, - * startBounds + * The first part of the animated move to desktop transition. Applies the changes to move task + * to desktop mode and sets the taskBounds to the passed in bounds, startBounds. This is + * followed with a call to {@link finishMoveToDesktop} or {@link cancelMoveToDesktop}. */ - fun moveToFreeform( + fun startMoveToDesktop( taskInfo: RunningTaskInfo, startBounds: Rect, dragToDesktopValueAnimator: MoveToDesktopAnimator ) { KtProtoLog.v( WM_SHELL_DESKTOP_MODE, - "DesktopTasksController: moveToFreeform with bounds taskId=%d", + "DesktopTasksController: startMoveToDesktop taskId=%d", taskInfo.taskId ) val wct = WindowContainerTransaction() @@ -221,18 +222,21 @@ class DesktopTasksController( wct.setBounds(taskInfo.token, startBounds) if (Transitions.ENABLE_SHELL_TRANSITIONS) { - enterDesktopTaskTransitionHandler.startMoveToFreeformAnimation(wct, - dragToDesktopValueAnimator, mOnAnimationFinishedCallback) + enterDesktopTaskTransitionHandler.startMoveToDesktop(wct, dragToDesktopValueAnimator, + mOnAnimationFinishedCallback) } else { shellTaskOrganizer.applyTransaction(wct) } } - /** Brings apps to front and sets freeform task bounds */ - private fun moveToDesktopWithAnimation(taskInfo: RunningTaskInfo, freeformBounds: Rect) { + /** + * The second part of the animated move to desktop transition, called after + * {@link startMoveToDesktop}. Brings apps to front and sets freeform task bounds. + */ + private fun finalizeMoveToDesktop(taskInfo: RunningTaskInfo, freeformBounds: Rect) { KtProtoLog.v( WM_SHELL_DESKTOP_MODE, - "DesktopTasksController: moveToDesktop with animation taskId=%d", + "DesktopTasksController: finalizeMoveToDesktop taskId=%d", taskInfo.taskId ) val wct = WindowContainerTransaction() @@ -241,8 +245,8 @@ class DesktopTasksController( wct.setBounds(taskInfo.token, freeformBounds) if (Transitions.ENABLE_SHELL_TRANSITIONS) { - enterDesktopTaskTransitionHandler.startTransition( - Transitions.TRANSIT_ENTER_DESKTOP_MODE, wct, mOnAnimationFinishedCallback) + enterDesktopTaskTransitionHandler.finalizeMoveToDesktop(wct, + mOnAnimationFinishedCallback) } else { shellTaskOrganizer.applyTransaction(wct) releaseVisualIndicator() @@ -272,13 +276,14 @@ class DesktopTasksController( } /** - * Move a task to fullscreen after being dragged from fullscreen and released back into - * status bar area + * The second part of the animated move to desktop transition, called after + * {@link startMoveToDesktop}. Move a task to fullscreen after being dragged from fullscreen + * and released back into status bar area. */ - fun cancelMoveToFreeform(task: RunningTaskInfo, moveToDesktopAnimator: MoveToDesktopAnimator) { + fun cancelMoveToDesktop(task: RunningTaskInfo, moveToDesktopAnimator: MoveToDesktopAnimator) { KtProtoLog.v( WM_SHELL_DESKTOP_MODE, - "DesktopTasksController: cancelMoveToFreeform taskId=%d", + "DesktopTasksController: cancelMoveToDesktop taskId=%d", task.taskId ) val wct = WindowContainerTransaction() @@ -784,7 +789,7 @@ class DesktopTasksController( taskInfo: RunningTaskInfo, freeformBounds: Rect ) { - moveToDesktopWithAnimation(taskInfo, freeformBounds) + finalizeMoveToDesktop(taskInfo, freeformBounds) } private fun getStatusBarHeight(taskInfo: RunningTaskInfo): Int { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java index 650cac5cb999..1acf783257f2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java @@ -79,7 +79,7 @@ public class EnterDesktopTaskTransitionHandler implements Transitions.Transition * @param wct WindowContainerTransaction for transition * @param onAnimationEndCallback to be called after animation */ - public void startTransition(@WindowManager.TransitionType int type, + private void startTransition(@WindowManager.TransitionType int type, @NonNull WindowContainerTransaction wct, Consumer<SurfaceControl.Transaction> onAnimationEndCallback) { mOnAnimationFinishedCallback = onAnimationEndCallback; @@ -88,17 +88,29 @@ public class EnterDesktopTaskTransitionHandler implements Transitions.Transition } /** - * Starts Transition of type TRANSIT_ENTER_FREEFORM + * Starts Transition of type TRANSIT_START_MOVE_TO_DESKTOP_MODE * @param wct WindowContainerTransaction for transition * @param moveToDesktopAnimator Animator that shrinks and positions task during two part move * to desktop animation * @param onAnimationEndCallback to be called after animation */ - public void startMoveToFreeformAnimation(@NonNull WindowContainerTransaction wct, + public void startMoveToDesktop(@NonNull WindowContainerTransaction wct, @NonNull MoveToDesktopAnimator moveToDesktopAnimator, Consumer<SurfaceControl.Transaction> onAnimationEndCallback) { mMoveToDesktopAnimator = moveToDesktopAnimator; - startTransition(Transitions.TRANSIT_ENTER_FREEFORM, wct, onAnimationEndCallback); + startTransition(Transitions.TRANSIT_START_MOVE_TO_DESKTOP_MODE, wct, + onAnimationEndCallback); + } + + /** + * Starts Transition of type TRANSIT_FINALIZE_MOVE_TO_DESKTOP_MODE + * @param wct WindowContainerTransaction for transition + * @param onAnimationEndCallback to be called after animation + */ + public void finalizeMoveToDesktop(@NonNull WindowContainerTransaction wct, + Consumer<SurfaceControl.Transaction> onAnimationEndCallback) { + startTransition(Transitions.TRANSIT_FINALIZE_MOVE_TO_DESKTOP_MODE, wct, + onAnimationEndCallback); } /** @@ -155,7 +167,7 @@ public class EnterDesktopTaskTransitionHandler implements Transitions.Transition } final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); - if (type == Transitions.TRANSIT_ENTER_FREEFORM + if (type == Transitions.TRANSIT_START_MOVE_TO_DESKTOP_MODE && taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) { // Transitioning to freeform but keeping fullscreen bounds, so the crop is set // to null and we don't require an animation @@ -182,7 +194,7 @@ public class EnterDesktopTaskTransitionHandler implements Transitions.Transition } Rect endBounds = change.getEndAbsBounds(); - if (type == Transitions.TRANSIT_ENTER_DESKTOP_MODE + if (type == Transitions.TRANSIT_FINALIZE_MOVE_TO_DESKTOP_MODE && taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM && !endBounds.isEmpty()) { // This Transition animates a task to freeform bounds after being dragged into freeform diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java index 75659960bc32..4ca383f66267 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java @@ -149,11 +149,13 @@ public class Transitions implements RemoteCallable<Transitions>, /** Transition type for maximize to freeform transition. */ public static final int TRANSIT_RESTORE_FROM_MAXIMIZE = WindowManager.TRANSIT_FIRST_CUSTOM + 9; - /** Transition type to freeform in desktop mode. */ - public static final int TRANSIT_ENTER_FREEFORM = WindowManager.TRANSIT_FIRST_CUSTOM + 10; + /** Transition type for starting the move to desktop mode. */ + public static final int TRANSIT_START_MOVE_TO_DESKTOP_MODE = + WindowManager.TRANSIT_FIRST_CUSTOM + 10; - /** Transition type to freeform in desktop mode. */ - public static final int TRANSIT_ENTER_DESKTOP_MODE = WindowManager.TRANSIT_FIRST_CUSTOM + 11; + /** Transition type for finalizing the move to desktop mode. */ + public static final int TRANSIT_FINALIZE_MOVE_TO_DESKTOP_MODE = + WindowManager.TRANSIT_FIRST_CUSTOM + 11; /** Transition type to fullscreen from desktop mode. */ public static final int TRANSIT_EXIT_DESKTOP_MODE = WindowManager.TRANSIT_FIRST_CUSTOM + 12; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index 1a18fc2d7546..80cf96a93efa 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -197,7 +197,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { @NonNull TransitionInfo info, @NonNull TransitionInfo.Change change) { if (change.getMode() == WindowManager.TRANSIT_CHANGE - && (info.getType() == Transitions.TRANSIT_ENTER_DESKTOP_MODE + && (info.getType() == Transitions.TRANSIT_FINALIZE_MOVE_TO_DESKTOP_MODE || info.getType() == Transitions.TRANSIT_CANCEL_ENTERING_DESKTOP_MODE || info.getType() == Transitions.TRANSIT_EXIT_DESKTOP_MODE || info.getType() == Transitions.TRANSIT_DESKTOP_MODE_TOGGLE_RESIZE)) { @@ -616,7 +616,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { } else if (mMoveToDesktopAnimator != null) { relevantDecor.incrementRelayoutBlock(); mDesktopTasksController.ifPresent( - c -> c.cancelMoveToFreeform(relevantDecor.mTaskInfo, + c -> c.cancelMoveToDesktop(relevantDecor.mTaskInfo, mMoveToDesktopAnimator)); mMoveToDesktopAnimator = null; return; @@ -643,7 +643,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mDragToDesktopAnimationStartBounds, relevantDecor.mTaskInfo, relevantDecor.mTaskSurface); mDesktopTasksController.ifPresent( - c -> c.moveToFreeform(relevantDecor.mTaskInfo, + c -> c.startMoveToDesktop(relevantDecor.mTaskInfo, mDragToDesktopAnimationStartBounds, mMoveToDesktopAnimator)); mMoveToDesktopAnimator.startAnimation(); diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/BaseAppCompat.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/BaseAppCompat.kt index 0f9579d58929..69c8ecd5644d 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/BaseAppCompat.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/BaseAppCompat.kt @@ -17,7 +17,6 @@ package com.android.wm.shell.flicker.appcompat import android.content.Context -import android.system.helpers.CommandsHelper import android.tools.common.traces.component.ComponentNameMatcher import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.FlickerTestData @@ -29,15 +28,18 @@ import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd import com.android.wm.shell.flicker.appWindowIsVisibleAtStart import com.android.wm.shell.flicker.appWindowKeepVisible import com.android.wm.shell.flicker.layerKeepVisible -import org.junit.After + import org.junit.Assume import org.junit.Before +import org.junit.Rule abstract class BaseAppCompat(flicker: LegacyFlickerTest) : BaseTest(flicker) { protected val context: Context = instrumentation.context protected val letterboxApp = LetterboxAppHelper(instrumentation) - lateinit var cmdHelper: CommandsHelper - private lateinit var letterboxStyle: HashMap<String, String> + + @JvmField + @Rule + val letterboxRule: LetterboxRule = LetterboxRule() /** {@inheritDoc} */ override val transition: FlickerBuilder.() -> Unit @@ -52,50 +54,7 @@ abstract class BaseAppCompat(flicker: LegacyFlickerTest) : BaseTest(flicker) { @Before fun before() { - cmdHelper = CommandsHelper.getInstance(instrumentation) - Assume.assumeTrue(tapl.isTablet && isIgnoreOrientationRequest()) - letterboxStyle = mapLetterboxStyle() - resetLetterboxStyle() - setLetterboxEducationEnabled(false) - } - - @After - fun after() { - resetLetterboxStyle() - } - - private fun mapLetterboxStyle(): HashMap<String, String> { - val res = cmdHelper.executeShellCommand("wm get-letterbox-style") - val lines = res.lines() - val map = HashMap<String, String>() - for (line in lines) { - val keyValuePair = line.split(":") - if (keyValuePair.size == 2) { - val key = keyValuePair[0].trim() - map[key] = keyValuePair[1].trim() - } - } - return map - } - - private fun getLetterboxStyle(): HashMap<String, String> { - if (!::letterboxStyle.isInitialized) { - letterboxStyle = mapLetterboxStyle() - } - return letterboxStyle - } - - private fun resetLetterboxStyle() { - cmdHelper.executeShellCommand("wm reset-letterbox-style") - } - - private fun setLetterboxEducationEnabled(enabled: Boolean) { - cmdHelper.executeShellCommand("wm set-letterbox-style --isEducationEnabled $enabled") - } - - private fun isIgnoreOrientationRequest(): Boolean { - val res = cmdHelper.executeShellCommand("wm get-ignore-orientation-request") - return res != null && res.contains("true") + Assume.assumeTrue(tapl.isTablet && letterboxRule.isIgnoreOrientationRequest) } fun FlickerTestData.setStartRotation() = setRotation(flicker.scenario.startRotation) @@ -115,7 +74,7 @@ abstract class BaseAppCompat(flicker: LegacyFlickerTest) : BaseTest(flicker) { /** Only run on tests with config_letterboxActivityCornersRadius != 0 in devices */ private fun assumeLetterboxRoundedCornersEnabled() { - Assume.assumeTrue(getLetterboxStyle().getValue("Corner radius") != "0") + Assume.assumeTrue(letterboxRule.hasCornerRadius) } fun assertLetterboxAppVisibleAtStartAndEnd() { diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/LetterboxRule.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/LetterboxRule.kt new file mode 100644 index 000000000000..5a1136f97c6f --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/LetterboxRule.kt @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.appcompat + +import android.app.Instrumentation +import android.system.helpers.CommandsHelper +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement + +/** + * JUnit Rule to handle letterboxStyles and states + */ +class LetterboxRule( + private val withLetterboxEducationEnabled: Boolean = false, + private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation(), + private val cmdHelper: CommandsHelper = CommandsHelper.getInstance(instrumentation) +) : TestRule { + + private val execAdb: (String) -> String = {cmd -> cmdHelper.executeShellCommand(cmd)} + private lateinit var _letterboxStyle: MutableMap<String, String> + + val letterboxStyle: Map<String, String> + get() { + if (!::_letterboxStyle.isInitialized) { + _letterboxStyle = mapLetterboxStyle() + } + return _letterboxStyle + } + + val cornerRadius: Int? + get() = asInt(letterboxStyle["Corner radius"]) + + val hasCornerRadius: Boolean + get() { + val radius = cornerRadius + return radius != null && radius > 0 + } + + val isIgnoreOrientationRequest: Boolean + get() = execAdb("wm get-ignore-orientation-request")?.contains("true") ?: false + + override fun apply(base: Statement?, description: Description?): Statement { + resetLetterboxStyle() + _letterboxStyle = mapLetterboxStyle() + val isLetterboxEducationEnabled = _letterboxStyle.getValue("Is education enabled") + var hasLetterboxEducationStateChanged = false + if ("$withLetterboxEducationEnabled" != isLetterboxEducationEnabled) { + hasLetterboxEducationStateChanged = true + execAdb("wm set-letterbox-style --isEducationEnabled " + + withLetterboxEducationEnabled) + } + return try { + object : Statement() { + @Throws(Throwable::class) + override fun evaluate() { + base!!.evaluate() + } + } + } finally { + if (hasLetterboxEducationStateChanged) { + execAdb("wm set-letterbox-style --isEducationEnabled " + + isLetterboxEducationEnabled + ) + } + resetLetterboxStyle() + } + } + + private fun mapLetterboxStyle(): HashMap<String, String> { + val res = execAdb("wm get-letterbox-style") + val lines = res.lines() + val map = HashMap<String, String>() + for (line in lines) { + val keyValuePair = line.split(":") + if (keyValuePair.size == 2) { + val key = keyValuePair[0].trim() + map[key] = keyValuePair[1].trim() + } + } + return map + } + + private fun resetLetterboxStyle() { + execAdb("wm reset-letterbox-style") + } + + private fun asInt(str: String?): Int? = try { + str?.toInt() + } catch (e: NumberFormatException) { + null + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/OpenAppInSizeCompatModeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/OpenAppInSizeCompatModeTest.kt index a7bd2584ba23..67d5718e6c1f 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/OpenAppInSizeCompatModeTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/OpenAppInSizeCompatModeTest.kt @@ -31,7 +31,7 @@ import org.junit.runners.Parameterized /** * Test launching app in size compat mode. * - * To run this test: `atest WMShellFlickerTests:OpenAppInSizeCompatModeTest` + * To run this test: `atest WMShellFlickerTestsOther:OpenAppInSizeCompatModeTest` * * Actions: * ``` diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/OpenTransparentActivityTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/OpenTransparentActivityTest.kt new file mode 100644 index 000000000000..e6ca261a317f --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/OpenTransparentActivityTest.kt @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.appcompat + +import android.platform.test.annotations.Postsubmit +import android.tools.common.Rotation +import android.tools.common.flicker.assertions.FlickerTest +import android.tools.common.traces.component.ComponentNameMatcher +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.LegacyFlickerTest +import android.tools.device.flicker.legacy.LegacyFlickerTestFactory +import androidx.test.filters.RequiresDevice +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +/** + * Test launching app in size compat mode. + * + * To run this test: `atest WMShellFlickerTestsOther:OpenTransparentActivityTest` + * + * Actions: + * ``` + * Launch a letteboxed app and then a transparent activity from it. We test the bounds + * are the same. + * ``` + * + * Notes: + * ``` + * Some default assertions (e.g., nav bar, status bar and screen covered) + * are inherited [BaseTest] + * ``` + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +class OpenTransparentActivityTest(flicker: LegacyFlickerTest) : TransparentBaseAppCompat(flicker) { + + /** {@inheritDoc} */ + override val transition: FlickerBuilder.() -> Unit + get() = { + setup { + letterboxTranslucentLauncherApp.launchViaIntent(wmHelper) + } + transitions { + waitAndGetLaunchTransparent()?.click() ?: error("Launch Transparent not found") + } + teardown { + letterboxTranslucentApp.exit(wmHelper) + letterboxTranslucentLauncherApp.exit(wmHelper) + } + } + + /** + * Checks the transparent activity is launched on top of the opaque one + */ + @Postsubmit + @Test + fun translucentActivityIsLaunchedOnTopOfOpaqueActivity() { + flicker.assertWm { + this.isAppWindowOnTop(letterboxTranslucentLauncherApp) + .then() + .isAppWindowOnTop(letterboxTranslucentApp) + } + } + + /** + * Checks that the activity is letterboxed + */ + @Postsubmit + @Test + fun translucentActivityIsLetterboxed() { + flicker.assertLayers { isVisible(ComponentNameMatcher.LETTERBOX) } + } + + /** + * Checks that the translucent activity inherits bounds from the opaque one. + */ + @Postsubmit + @Test + fun translucentActivityInheritsBoundsFromOpaqueActivity() { + flicker.assertLayersEnd { + this.visibleRegion(letterboxTranslucentApp) + .coversExactly(visibleRegion(letterboxTranslucentLauncherApp).region) + } + } + + /** + * Checks that the translucent activity has rounded corners + */ + @Postsubmit + @Test + fun translucentActivityHasRoundedCorners() { + flicker.assertLayersEnd { + this.hasRoundedCorners(letterboxTranslucentApp) + } + } + + companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestFactory.rotationTests] for configuring screen orientation and + * navigation modes. + */ + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<FlickerTest> { + return LegacyFlickerTestFactory + .nonRotationTests(supportedRotations = listOf(Rotation.ROTATION_90)) + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/RepositionFixedPortraitAppTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/RepositionFixedPortraitAppTest.kt index e875aae431a1..68fa8d2fc2e8 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/RepositionFixedPortraitAppTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/RepositionFixedPortraitAppTest.kt @@ -32,7 +32,9 @@ import org.junit.runners.Parameterized /** * Test launching a fixed portrait letterboxed app in landscape and repositioning to the right. * - * To run this test: `atest WMShellFlickerTests:RepositionFixedPortraitAppTest` Actions: + * To run this test: `atest WMShellFlickerTestsOther:RepositionFixedPortraitAppTest` + * + * Actions: * * ``` * Launch a fixed portrait app in landscape to letterbox app diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/RestartAppInSizeCompatModeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/RestartAppInSizeCompatModeTest.kt index a18a144b4bf1..fcb6931af9a2 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/RestartAppInSizeCompatModeTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/RestartAppInSizeCompatModeTest.kt @@ -31,7 +31,7 @@ import org.junit.runners.Parameterized /** * Test restarting app in size compat mode. * - * To run this test: `atest WMShellFlickerTests:RestartAppInSizeCompatModeTest` + * To run this test: `atest WMShellFlickerTestsOther:RestartAppInSizeCompatModeTest` * * Actions: * ``` diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/TransparentBaseAppCompat.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/TransparentBaseAppCompat.kt new file mode 100644 index 000000000000..ea0392cee95a --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/TransparentBaseAppCompat.kt @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.appcompat + +import android.content.Context +import android.tools.device.flicker.legacy.FlickerTestData +import android.tools.device.flicker.legacy.LegacyFlickerTest +import android.tools.device.helpers.FIND_TIMEOUT +import android.tools.device.traces.parsers.toFlickerComponent +import androidx.test.uiautomator.By +import androidx.test.uiautomator.UiObject2 +import androidx.test.uiautomator.Until +import com.android.server.wm.flicker.helpers.LetterboxAppHelper +import com.android.server.wm.flicker.testapp.ActivityOptions +import com.android.wm.shell.flicker.BaseTest +import org.junit.Assume +import org.junit.Before +import org.junit.Rule + +abstract class TransparentBaseAppCompat(flicker: LegacyFlickerTest) : BaseTest(flicker) { + protected val context: Context = instrumentation.context + protected val letterboxTranslucentLauncherApp = LetterboxAppHelper( + instrumentation, + launcherName = ActivityOptions.LaunchTransparentActivity.LABEL, + component = ActivityOptions.LaunchTransparentActivity.COMPONENT.toFlickerComponent() + ) + protected val letterboxTranslucentApp = LetterboxAppHelper( + instrumentation, + launcherName = ActivityOptions.TransparentActivity.LABEL, + component = ActivityOptions.TransparentActivity.COMPONENT.toFlickerComponent() + ) + + @JvmField + @Rule + val letterboxRule: LetterboxRule = LetterboxRule() + + @Before + fun before() { + Assume.assumeTrue(tapl.isTablet && letterboxRule.isIgnoreOrientationRequest) + } + + protected fun FlickerTestData.waitAndGetLaunchTransparent(): UiObject2? = + device.wait( + Until.findObject(By.text("Launch Transparent")), + FIND_TIMEOUT + ) + + protected fun FlickerTestData.goBack() = device.pressBack() +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/CopyContentInSplitGesturalNavLandscapeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/CopyContentInSplitGesturalNavLandscapeBenchmark.kt index 566adec75615..5209af777641 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/CopyContentInSplitGesturalNavLandscapeBenchmark.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/CopyContentInSplitGesturalNavLandscapeBenchmark.kt @@ -19,11 +19,9 @@ package com.android.wm.shell.flicker.service.splitscreen.benchmark import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.common.Rotation -import androidx.test.filters.RequiresDevice import com.android.wm.shell.flicker.service.splitscreen.scenarios.CopyContentInSplit import org.junit.Test -@RequiresDevice class CopyContentInSplitGesturalNavLandscapeBenchmark : CopyContentInSplit(Rotation.ROTATION_90) { @PlatinumTest(focusArea = "sysui") @Presubmit diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/CopyContentInSplitGesturalNavPortraitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/CopyContentInSplitGesturalNavPortraitBenchmark.kt index 92b62273d8cb..97e5badb45ae 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/CopyContentInSplitGesturalNavPortraitBenchmark.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/CopyContentInSplitGesturalNavPortraitBenchmark.kt @@ -19,11 +19,9 @@ package com.android.wm.shell.flicker.service.splitscreen.benchmark import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.common.Rotation -import androidx.test.filters.RequiresDevice import com.android.wm.shell.flicker.service.splitscreen.scenarios.CopyContentInSplit import org.junit.Test -@RequiresDevice class CopyContentInSplitGesturalNavPortraitBenchmark : CopyContentInSplit(Rotation.ROTATION_0) { @PlatinumTest(focusArea = "sysui") @Presubmit diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DismissSplitScreenByDividerGesturalNavLandscapeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DismissSplitScreenByDividerGesturalNavLandscapeBenchmark.kt index e6d56b5c94d3..0c79155fc68f 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DismissSplitScreenByDividerGesturalNavLandscapeBenchmark.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DismissSplitScreenByDividerGesturalNavLandscapeBenchmark.kt @@ -19,11 +19,9 @@ package com.android.wm.shell.flicker.service.splitscreen.benchmark import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.common.Rotation -import androidx.test.filters.RequiresDevice import com.android.wm.shell.flicker.service.splitscreen.scenarios.DismissSplitScreenByDivider import org.junit.Test -@RequiresDevice class DismissSplitScreenByDividerGesturalNavLandscapeBenchmark : DismissSplitScreenByDivider(Rotation.ROTATION_90) { @PlatinumTest(focusArea = "sysui") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DismissSplitScreenByDividerGesturalNavPortraitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DismissSplitScreenByDividerGesturalNavPortraitBenchmark.kt index 6752c58bd568..fcd9111e2736 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DismissSplitScreenByDividerGesturalNavPortraitBenchmark.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DismissSplitScreenByDividerGesturalNavPortraitBenchmark.kt @@ -19,11 +19,9 @@ package com.android.wm.shell.flicker.service.splitscreen.benchmark import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.common.Rotation -import androidx.test.filters.RequiresDevice import com.android.wm.shell.flicker.service.splitscreen.scenarios.DismissSplitScreenByDivider import org.junit.Test -@RequiresDevice class DismissSplitScreenByDividerGesturalNavPortraitBenchmark : DismissSplitScreenByDivider(Rotation.ROTATION_0) { @PlatinumTest(focusArea = "sysui") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DismissSplitScreenByGoHomeGesturalNavLandscapeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DismissSplitScreenByGoHomeGesturalNavLandscapeBenchmark.kt index 7c9ab9939dd0..3be7f0355998 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DismissSplitScreenByGoHomeGesturalNavLandscapeBenchmark.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DismissSplitScreenByGoHomeGesturalNavLandscapeBenchmark.kt @@ -19,11 +19,9 @@ package com.android.wm.shell.flicker.service.splitscreen.benchmark import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.common.Rotation -import androidx.test.filters.RequiresDevice import com.android.wm.shell.flicker.service.splitscreen.scenarios.DismissSplitScreenByGoHome import org.junit.Test -@RequiresDevice class DismissSplitScreenByGoHomeGesturalNavLandscapeBenchmark : DismissSplitScreenByGoHome(Rotation.ROTATION_90) { @PlatinumTest(focusArea = "sysui") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DismissSplitScreenByGoHomeGesturalNavPortraitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DismissSplitScreenByGoHomeGesturalNavPortraitBenchmark.kt index 4b795713cb23..003ba64df0d0 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DismissSplitScreenByGoHomeGesturalNavPortraitBenchmark.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DismissSplitScreenByGoHomeGesturalNavPortraitBenchmark.kt @@ -19,11 +19,9 @@ package com.android.wm.shell.flicker.service.splitscreen.benchmark import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.common.Rotation -import androidx.test.filters.RequiresDevice import com.android.wm.shell.flicker.service.splitscreen.scenarios.DismissSplitScreenByGoHome import org.junit.Test -@RequiresDevice class DismissSplitScreenByGoHomeGesturalNavPortraitBenchmark : DismissSplitScreenByGoHome(Rotation.ROTATION_0) { @PlatinumTest(focusArea = "sysui") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DragDividerToResizeGesturalNavLandscapeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DragDividerToResizeGesturalNavLandscapeBenchmark.kt index 04950799732e..c3f41a641907 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DragDividerToResizeGesturalNavLandscapeBenchmark.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DragDividerToResizeGesturalNavLandscapeBenchmark.kt @@ -19,11 +19,9 @@ package com.android.wm.shell.flicker.service.splitscreen.benchmark import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.common.Rotation -import androidx.test.filters.RequiresDevice import com.android.wm.shell.flicker.service.splitscreen.scenarios.DragDividerToResize import org.junit.Test -@RequiresDevice class DragDividerToResizeGesturalNavLandscapeBenchmark : DragDividerToResize(Rotation.ROTATION_90) { @PlatinumTest(focusArea = "sysui") @Presubmit diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DragDividerToResizeGesturalNavPortraitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DragDividerToResizeGesturalNavPortraitBenchmark.kt index 71ef48bea686..1f2588f840aa 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DragDividerToResizeGesturalNavPortraitBenchmark.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/DragDividerToResizeGesturalNavPortraitBenchmark.kt @@ -19,11 +19,9 @@ package com.android.wm.shell.flicker.service.splitscreen.benchmark import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.common.Rotation -import androidx.test.filters.RequiresDevice import com.android.wm.shell.flicker.service.splitscreen.scenarios.DragDividerToResize import org.junit.Test -@RequiresDevice class DragDividerToResizeGesturalNavPortraitBenchmark : DragDividerToResize(Rotation.ROTATION_0) { @PlatinumTest(focusArea = "sysui") @Presubmit diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsGesturalNavLandscapeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsGesturalNavLandscapeBenchmark.kt index c78729c6dc92..4aaa3d8c263f 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsGesturalNavLandscapeBenchmark.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsGesturalNavLandscapeBenchmark.kt @@ -19,11 +19,9 @@ package com.android.wm.shell.flicker.service.splitscreen.benchmark import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.common.Rotation -import androidx.test.filters.RequiresDevice import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenByDragFromAllApps import org.junit.Test -@RequiresDevice class EnterSplitScreenByDragFromAllAppsGesturalNavLandscapeBenchmark : EnterSplitScreenByDragFromAllApps(Rotation.ROTATION_90) { @PlatinumTest(focusArea = "sysui") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsGesturalNavPortraitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsGesturalNavPortraitBenchmark.kt index 30bce2f657b1..773ae162ab1b 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsGesturalNavPortraitBenchmark.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsGesturalNavPortraitBenchmark.kt @@ -19,11 +19,9 @@ package com.android.wm.shell.flicker.service.splitscreen.benchmark import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.common.Rotation -import androidx.test.filters.RequiresDevice import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenByDragFromAllApps import org.junit.Test -@RequiresDevice class EnterSplitScreenByDragFromAllAppsGesturalNavPortraitBenchmark : EnterSplitScreenByDragFromAllApps(Rotation.ROTATION_0) { @PlatinumTest(focusArea = "sysui") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromNotificationGesturalNavLandscapeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromNotificationGesturalNavLandscapeBenchmark.kt index b33ea7c89158..2ddd0fb30114 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromNotificationGesturalNavLandscapeBenchmark.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromNotificationGesturalNavLandscapeBenchmark.kt @@ -19,11 +19,9 @@ package com.android.wm.shell.flicker.service.splitscreen.benchmark import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.common.Rotation -import androidx.test.filters.RequiresDevice import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenByDragFromNotification import org.junit.Test -@RequiresDevice class EnterSplitScreenByDragFromNotificationGesturalNavLandscapeBenchmark : EnterSplitScreenByDragFromNotification(Rotation.ROTATION_90) { @PlatinumTest(focusArea = "sysui") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromNotificationGesturalNavPortraitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromNotificationGesturalNavPortraitBenchmark.kt index 07a86a57117b..2927e90e2619 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromNotificationGesturalNavPortraitBenchmark.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromNotificationGesturalNavPortraitBenchmark.kt @@ -19,11 +19,9 @@ package com.android.wm.shell.flicker.service.splitscreen.benchmark import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.common.Rotation -import androidx.test.filters.RequiresDevice import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenByDragFromNotification import org.junit.Test -@RequiresDevice class EnterSplitScreenByDragFromNotificationGesturalNavPortraitBenchmark : EnterSplitScreenByDragFromNotification(Rotation.ROTATION_0) { @PlatinumTest(focusArea = "sysui") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutGesturalNavLandscapeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutGesturalNavLandscapeBenchmark.kt index 9a1d12787b9d..c5b1a228f74f 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutGesturalNavLandscapeBenchmark.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutGesturalNavLandscapeBenchmark.kt @@ -19,11 +19,9 @@ package com.android.wm.shell.flicker.service.splitscreen.benchmark import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.common.Rotation -import androidx.test.filters.RequiresDevice import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenByDragFromShortcut import org.junit.Test -@RequiresDevice class EnterSplitScreenByDragFromShortcutGesturalNavLandscapeBenchmark : EnterSplitScreenByDragFromShortcut(Rotation.ROTATION_90) { @PlatinumTest(focusArea = "sysui") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutGesturalNavPortraitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutGesturalNavPortraitBenchmark.kt index 266e268a3537..970d0d530a56 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutGesturalNavPortraitBenchmark.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutGesturalNavPortraitBenchmark.kt @@ -19,11 +19,9 @@ package com.android.wm.shell.flicker.service.splitscreen.benchmark import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.common.Rotation -import androidx.test.filters.RequiresDevice import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenByDragFromShortcut import org.junit.Test -@RequiresDevice class EnterSplitScreenByDragFromShortcutGesturalNavPortraitBenchmark : EnterSplitScreenByDragFromShortcut(Rotation.ROTATION_0) { @PlatinumTest(focusArea = "sysui") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarGesturalNavLandscapeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarGesturalNavLandscapeBenchmark.kt index 83fc30bceb7b..47e97483db82 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarGesturalNavLandscapeBenchmark.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarGesturalNavLandscapeBenchmark.kt @@ -19,11 +19,9 @@ package com.android.wm.shell.flicker.service.splitscreen.benchmark import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.common.Rotation -import androidx.test.filters.RequiresDevice import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenByDragFromTaskbar import org.junit.Test -@RequiresDevice class EnterSplitScreenByDragFromTaskbarGesturalNavLandscapeBenchmark : EnterSplitScreenByDragFromTaskbar(Rotation.ROTATION_90) { @PlatinumTest(focusArea = "sysui") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarGesturalNavPortraitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarGesturalNavPortraitBenchmark.kt index b2f19299c7f0..15fa1c8c4324 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarGesturalNavPortraitBenchmark.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarGesturalNavPortraitBenchmark.kt @@ -19,11 +19,9 @@ package com.android.wm.shell.flicker.service.splitscreen.benchmark import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.common.Rotation -import androidx.test.filters.RequiresDevice import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenByDragFromTaskbar import org.junit.Test -@RequiresDevice class EnterSplitScreenByDragFromTaskbarGesturalNavPortraitBenchmark : EnterSplitScreenByDragFromTaskbar(Rotation.ROTATION_0) { @PlatinumTest(focusArea = "sysui") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenFromOverviewGesturalNavLandscapeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenFromOverviewGesturalNavLandscapeBenchmark.kt index dae92dddbfec..c6f83cf9a181 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenFromOverviewGesturalNavLandscapeBenchmark.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenFromOverviewGesturalNavLandscapeBenchmark.kt @@ -19,11 +19,9 @@ package com.android.wm.shell.flicker.service.splitscreen.benchmark import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.common.Rotation -import androidx.test.filters.RequiresDevice import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenFromOverview import org.junit.Test -@RequiresDevice class EnterSplitScreenFromOverviewGesturalNavLandscapeBenchmark : EnterSplitScreenFromOverview(Rotation.ROTATION_90) { @PlatinumTest(focusArea = "sysui") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenFromOverviewGesturalNavPortraitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenFromOverviewGesturalNavPortraitBenchmark.kt index 732047ba38ad..15dbc0d0f6c0 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenFromOverviewGesturalNavPortraitBenchmark.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/EnterSplitScreenFromOverviewGesturalNavPortraitBenchmark.kt @@ -19,11 +19,9 @@ package com.android.wm.shell.flicker.service.splitscreen.benchmark import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.common.Rotation -import androidx.test.filters.RequiresDevice import com.android.wm.shell.flicker.service.splitscreen.scenarios.EnterSplitScreenFromOverview import org.junit.Test -@RequiresDevice class EnterSplitScreenFromOverviewGesturalNavPortraitBenchmark : EnterSplitScreenFromOverview(Rotation.ROTATION_0) { @PlatinumTest(focusArea = "sysui") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchAppByDoubleTapDividerGesturalNavLandscapeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchAppByDoubleTapDividerGesturalNavLandscapeBenchmark.kt index 1de7efd7970a..da8a992e165b 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchAppByDoubleTapDividerGesturalNavLandscapeBenchmark.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchAppByDoubleTapDividerGesturalNavLandscapeBenchmark.kt @@ -19,11 +19,9 @@ package com.android.wm.shell.flicker.service.splitscreen.benchmark import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.common.Rotation -import androidx.test.filters.RequiresDevice import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchAppByDoubleTapDivider import org.junit.Test -@RequiresDevice class SwitchAppByDoubleTapDividerGesturalNavLandscapeBenchmark : SwitchAppByDoubleTapDivider(Rotation.ROTATION_90) { @PlatinumTest(focusArea = "sysui") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchAppByDoubleTapDividerGesturalNavPortraitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchAppByDoubleTapDividerGesturalNavPortraitBenchmark.kt index 1a046aa5b09e..0f6f65a27a5c 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchAppByDoubleTapDividerGesturalNavPortraitBenchmark.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchAppByDoubleTapDividerGesturalNavPortraitBenchmark.kt @@ -19,11 +19,9 @@ package com.android.wm.shell.flicker.service.splitscreen.benchmark import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.common.Rotation -import androidx.test.filters.RequiresDevice import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchAppByDoubleTapDivider import org.junit.Test -@RequiresDevice class SwitchAppByDoubleTapDividerGesturalNavPortraitBenchmark : SwitchAppByDoubleTapDivider(Rotation.ROTATION_0) { @PlatinumTest(focusArea = "sysui") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromAnotherAppGesturalNavLandscapeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromAnotherAppGesturalNavLandscapeBenchmark.kt index 6e88f0eddee8..6cb9dd5401e6 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromAnotherAppGesturalNavLandscapeBenchmark.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromAnotherAppGesturalNavLandscapeBenchmark.kt @@ -19,11 +19,9 @@ package com.android.wm.shell.flicker.service.splitscreen.benchmark import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.common.Rotation -import androidx.test.filters.RequiresDevice import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchBackToSplitFromAnotherApp import org.junit.Test -@RequiresDevice class SwitchBackToSplitFromAnotherAppGesturalNavLandscapeBenchmark : SwitchBackToSplitFromAnotherApp(Rotation.ROTATION_90) { @PlatinumTest(focusArea = "sysui") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromAnotherAppGesturalNavPortraitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromAnotherAppGesturalNavPortraitBenchmark.kt index d26a29c80583..8467e888f219 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromAnotherAppGesturalNavPortraitBenchmark.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromAnotherAppGesturalNavPortraitBenchmark.kt @@ -19,11 +19,9 @@ package com.android.wm.shell.flicker.service.splitscreen.benchmark import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.common.Rotation -import androidx.test.filters.RequiresDevice import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchBackToSplitFromAnotherApp import org.junit.Test -@RequiresDevice class SwitchBackToSplitFromAnotherAppGesturalNavPortraitBenchmark : SwitchBackToSplitFromAnotherApp(Rotation.ROTATION_0) { @PlatinumTest(focusArea = "sysui") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromHomeGesturalNavLandscapeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromHomeGesturalNavLandscapeBenchmark.kt index 4a552b0aed6a..555d2779bb2c 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromHomeGesturalNavLandscapeBenchmark.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromHomeGesturalNavLandscapeBenchmark.kt @@ -19,11 +19,9 @@ package com.android.wm.shell.flicker.service.splitscreen.benchmark import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.common.Rotation -import androidx.test.filters.RequiresDevice import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchBackToSplitFromHome import org.junit.Test -@RequiresDevice class SwitchBackToSplitFromHomeGesturalNavLandscapeBenchmark : SwitchBackToSplitFromHome(Rotation.ROTATION_90) { @PlatinumTest(focusArea = "sysui") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromHomeGesturalNavPortraitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromHomeGesturalNavPortraitBenchmark.kt index b7376eaea66d..b7ef8a27af31 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromHomeGesturalNavPortraitBenchmark.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromHomeGesturalNavPortraitBenchmark.kt @@ -19,11 +19,9 @@ package com.android.wm.shell.flicker.service.splitscreen.benchmark import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.common.Rotation -import androidx.test.filters.RequiresDevice import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchBackToSplitFromHome import org.junit.Test -@RequiresDevice class SwitchBackToSplitFromHomeGesturalNavPortraitBenchmark : SwitchBackToSplitFromHome(Rotation.ROTATION_0) { @PlatinumTest(focusArea = "sysui") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromRecentGesturalNavLandscapeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromRecentGesturalNavLandscapeBenchmark.kt index b2d05e4a2632..3b726b213b5c 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromRecentGesturalNavLandscapeBenchmark.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromRecentGesturalNavLandscapeBenchmark.kt @@ -19,11 +19,9 @@ package com.android.wm.shell.flicker.service.splitscreen.benchmark import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.common.Rotation -import androidx.test.filters.RequiresDevice import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchBackToSplitFromRecent import org.junit.Test -@RequiresDevice class SwitchBackToSplitFromRecentGesturalNavLandscapeBenchmark : SwitchBackToSplitFromRecent(Rotation.ROTATION_90) { @PlatinumTest(focusArea = "sysui") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromRecentGesturalNavPortraitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromRecentGesturalNavPortraitBenchmark.kt index 6de31b1315e4..676171953386 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromRecentGesturalNavPortraitBenchmark.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBackToSplitFromRecentGesturalNavPortraitBenchmark.kt @@ -19,11 +19,9 @@ package com.android.wm.shell.flicker.service.splitscreen.benchmark import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.common.Rotation -import androidx.test.filters.RequiresDevice import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchBackToSplitFromRecent import org.junit.Test -@RequiresDevice class SwitchBackToSplitFromRecentGesturalNavPortraitBenchmark : SwitchBackToSplitFromRecent(Rotation.ROTATION_0) { @PlatinumTest(focusArea = "sysui") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBetweenSplitPairsGesturalNavLandscapeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBetweenSplitPairsGesturalNavLandscapeBenchmark.kt index aab18a6d27b9..ef5032ca2715 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBetweenSplitPairsGesturalNavLandscapeBenchmark.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBetweenSplitPairsGesturalNavLandscapeBenchmark.kt @@ -19,11 +19,9 @@ package com.android.wm.shell.flicker.service.splitscreen.benchmark import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.common.Rotation -import androidx.test.filters.RequiresDevice import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchBetweenSplitPairs import org.junit.Test -@RequiresDevice class SwitchBetweenSplitPairsGesturalNavLandscapeBenchmark : SwitchBetweenSplitPairs(Rotation.ROTATION_90) { @PlatinumTest(focusArea = "sysui") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBetweenSplitPairsGesturalNavPortraitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBetweenSplitPairsGesturalNavPortraitBenchmark.kt index b074f2c161c9..f92f4cb29e17 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBetweenSplitPairsGesturalNavPortraitBenchmark.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/SwitchBetweenSplitPairsGesturalNavPortraitBenchmark.kt @@ -19,11 +19,9 @@ package com.android.wm.shell.flicker.service.splitscreen.benchmark import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit import android.tools.common.Rotation -import androidx.test.filters.RequiresDevice import com.android.wm.shell.flicker.service.splitscreen.scenarios.SwitchBetweenSplitPairs import org.junit.Test -@RequiresDevice class SwitchBetweenSplitPairsGesturalNavPortraitBenchmark : SwitchBetweenSplitPairs(Rotation.ROTATION_0) { @PlatinumTest(focusArea = "sysui") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/UnlockKeyguardToSplitScreenGesturalNavLandscapeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/UnlockKeyguardToSplitScreenGesturalNavLandscapeBenchmark.kt index c402aa4444d8..a3f53518cf67 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/UnlockKeyguardToSplitScreenGesturalNavLandscapeBenchmark.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/UnlockKeyguardToSplitScreenGesturalNavLandscapeBenchmark.kt @@ -18,13 +18,11 @@ package com.android.wm.shell.flicker.service.splitscreen.benchmark import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit -import androidx.test.filters.RequiresDevice import com.android.wm.shell.flicker.service.splitscreen.scenarios.UnlockKeyguardToSplitScreen import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.BlockJUnit4ClassRunner -@RequiresDevice @RunWith(BlockJUnit4ClassRunner::class) class UnlockKeyguardToSplitScreenGesturalNavLandscapeBenchmark : UnlockKeyguardToSplitScreen() { @PlatinumTest(focusArea = "sysui") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/UnlockKeyguardToSplitScreenGesturalNavPortraitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/UnlockKeyguardToSplitScreenGesturalNavPortraitBenchmark.kt index 840401c23a91..65ec90f2e19e 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/UnlockKeyguardToSplitScreenGesturalNavPortraitBenchmark.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/benchmark/UnlockKeyguardToSplitScreenGesturalNavPortraitBenchmark.kt @@ -18,13 +18,11 @@ package com.android.wm.shell.flicker.service.splitscreen.benchmark import android.platform.test.annotations.PlatinumTest import android.platform.test.annotations.Presubmit -import androidx.test.filters.RequiresDevice import com.android.wm.shell.flicker.service.splitscreen.scenarios.UnlockKeyguardToSplitScreen import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.BlockJUnit4ClassRunner -@RequiresDevice @RunWith(BlockJUnit4ClassRunner::class) class UnlockKeyguardToSplitScreenGesturalNavPortraitBenchmark : UnlockKeyguardToSplitScreen() { @PlatinumTest(focusArea = "sysui") diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromRecent.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromRecent.kt index d44d1779a3f6..9f9d4bb1a2a4 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromRecent.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromRecent.kt @@ -47,6 +47,7 @@ constructor(val rotation: Rotation = Rotation.ROTATION_0) { fun setup() { tapl.setEnableRotation(true) tapl.setExpectedRotation(rotation.value) + tapl.workspace.switchToOverview().dismissAllTasks() SplitScreenUtils.enterSplit(wmHelper, tapl, device, primaryApp, secondaryApp) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandlerTest.java index c6642f3472f0..885ae3851c6d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandlerTest.java @@ -99,16 +99,17 @@ public class EnterDesktopTaskTransitionHandlerTest { final int taskId = 1; WindowContainerTransaction wct = new WindowContainerTransaction(); doReturn(mToken).when(mTransitions) - .startTransition(Transitions.TRANSIT_ENTER_FREEFORM, wct, + .startTransition(Transitions.TRANSIT_START_MOVE_TO_DESKTOP_MODE, wct, mEnterDesktopTaskTransitionHandler); doReturn(taskId).when(mMoveToDesktopAnimator).getTaskId(); - mEnterDesktopTaskTransitionHandler.startMoveToFreeformAnimation(wct, + mEnterDesktopTaskTransitionHandler.startMoveToDesktop(wct, mMoveToDesktopAnimator, null); TransitionInfo.Change change = createChange(WindowManager.TRANSIT_CHANGE, taskId, WINDOWING_MODE_FREEFORM); - TransitionInfo info = createTransitionInfo(Transitions.TRANSIT_ENTER_FREEFORM, change); + TransitionInfo info = createTransitionInfo(Transitions.TRANSIT_START_MOVE_TO_DESKTOP_MODE, + change); assertTrue(mEnterDesktopTaskTransitionHandler @@ -120,17 +121,18 @@ public class EnterDesktopTaskTransitionHandlerTest { @Test public void testTransitEnterDesktopModeAnimation() throws Throwable { - final int transitionType = Transitions.TRANSIT_ENTER_DESKTOP_MODE; + final int transitionType = Transitions.TRANSIT_FINALIZE_MOVE_TO_DESKTOP_MODE; final int taskId = 1; WindowContainerTransaction wct = new WindowContainerTransaction(); doReturn(mToken).when(mTransitions) .startTransition(transitionType, wct, mEnterDesktopTaskTransitionHandler); - mEnterDesktopTaskTransitionHandler.startTransition(transitionType, wct, null); + mEnterDesktopTaskTransitionHandler.finalizeMoveToDesktop(wct, null); TransitionInfo.Change change = createChange(WindowManager.TRANSIT_CHANGE, taskId, WINDOWING_MODE_FREEFORM); change.setEndAbsBounds(new Rect(0, 0, 1, 1)); - TransitionInfo info = createTransitionInfo(Transitions.TRANSIT_ENTER_DESKTOP_MODE, change); + TransitionInfo info = createTransitionInfo( + Transitions.TRANSIT_FINALIZE_MOVE_TO_DESKTOP_MODE, change); runOnUiThread(() -> { try { diff --git a/libs/hwui/pipeline/skia/ShaderCache.cpp b/libs/hwui/pipeline/skia/ShaderCache.cpp index f71e7289bd37..b87002371775 100644 --- a/libs/hwui/pipeline/skia/ShaderCache.cpp +++ b/libs/hwui/pipeline/skia/ShaderCache.cpp @@ -88,6 +88,9 @@ void ShaderCache::initShaderDiskCache(const void* identity, ssize_t size) { mBlobCache.reset(new FileBlobCache(maxKeySize, maxValueSize, maxTotalSize, mFilename)); validateCache(identity, size); mInitialized = true; + if (identity != nullptr && size > 0 && mIDHash.size()) { + set(&sIDKey, sizeof(sIDKey), mIDHash.data(), mIDHash.size()); + } } } @@ -96,11 +99,6 @@ void ShaderCache::setFilename(const char* filename) { mFilename = filename; } -BlobCache* ShaderCache::getBlobCacheLocked() { - LOG_ALWAYS_FATAL_IF(!mInitialized, "ShaderCache has not been initialized"); - return mBlobCache.get(); -} - sk_sp<SkData> ShaderCache::load(const SkData& key) { ATRACE_NAME("ShaderCache::load"); size_t keySize = key.size(); @@ -115,8 +113,7 @@ sk_sp<SkData> ShaderCache::load(const SkData& key) { if (!valueBuffer) { return nullptr; } - BlobCache* bc = getBlobCacheLocked(); - size_t valueSize = bc->get(key.data(), keySize, valueBuffer, mObservedBlobValueSize); + size_t valueSize = mBlobCache->get(key.data(), keySize, valueBuffer, mObservedBlobValueSize); int maxTries = 3; while (valueSize > mObservedBlobValueSize && maxTries > 0) { mObservedBlobValueSize = std::min(valueSize, maxValueSize); @@ -126,7 +123,7 @@ sk_sp<SkData> ShaderCache::load(const SkData& key) { return nullptr; } valueBuffer = newValueBuffer; - valueSize = bc->get(key.data(), keySize, valueBuffer, mObservedBlobValueSize); + valueSize = mBlobCache->get(key.data(), keySize, valueBuffer, mObservedBlobValueSize); maxTries--; } if (!valueSize) { @@ -143,16 +140,17 @@ sk_sp<SkData> ShaderCache::load(const SkData& key) { return SkData::MakeFromMalloc(valueBuffer, valueSize); } -namespace { -// Helper for BlobCache::set to trace the result. -void set(BlobCache* cache, const void* key, size_t keySize, const void* value, size_t valueSize) { - switch (cache->set(key, keySize, value, valueSize)) { +void ShaderCache::set(const void* key, size_t keySize, const void* value, size_t valueSize) { + switch (mBlobCache->set(key, keySize, value, valueSize)) { case BlobCache::InsertResult::kInserted: // This is what we expect/hope. It means the cache is large enough. return; case BlobCache::InsertResult::kDidClean: { ATRACE_FORMAT("ShaderCache: evicted an entry to fit {key: %lu value %lu}!", keySize, valueSize); + if (mIDHash.size()) { + set(&sIDKey, sizeof(sIDKey), mIDHash.data(), mIDHash.size()); + } return; } case BlobCache::InsertResult::kNotEnoughSpace: { @@ -172,15 +170,10 @@ void set(BlobCache* cache, const void* key, size_t keySize, const void* value, s } } } -} // namespace void ShaderCache::saveToDiskLocked() { ATRACE_NAME("ShaderCache::saveToDiskLocked"); if (mInitialized && mBlobCache) { - if (mIDHash.size()) { - auto key = sIDKey; - set(mBlobCache.get(), &key, sizeof(key), mIDHash.data(), mIDHash.size()); - } // The most straightforward way to make ownership shared mMutex.unlock(); mMutex.lock_shared(); @@ -209,11 +202,10 @@ void ShaderCache::store(const SkData& key, const SkData& data, const SkString& / const void* value = data.data(); - BlobCache* bc = getBlobCacheLocked(); if (mInStoreVkPipelineInProgress) { if (mOldPipelineCacheSize == -1) { // Record the initial pipeline cache size stored in the file. - mOldPipelineCacheSize = bc->get(key.data(), keySize, nullptr, 0); + mOldPipelineCacheSize = mBlobCache->get(key.data(), keySize, nullptr, 0); } if (mNewPipelineCacheSize != -1 && mNewPipelineCacheSize == valueSize) { // There has not been change in pipeline cache size. Stop trying to save. @@ -228,7 +220,7 @@ void ShaderCache::store(const SkData& key, const SkData& data, const SkString& / mNewPipelineCacheSize = -1; mTryToStorePipelineCache = true; } - set(bc, key.data(), keySize, value, valueSize); + set(key.data(), keySize, value, valueSize); if (!mSavePending && mDeferredSaveDelayMs > 0) { mSavePending = true; diff --git a/libs/hwui/pipeline/skia/ShaderCache.h b/libs/hwui/pipeline/skia/ShaderCache.h index c63a65ac1ed8..6ccb212fe6ca 100644 --- a/libs/hwui/pipeline/skia/ShaderCache.h +++ b/libs/hwui/pipeline/skia/ShaderCache.h @@ -97,20 +97,18 @@ private: void operator=(const ShaderCache&) = delete; /** - * "getBlobCacheLocked" returns the BlobCache object being used to store the - * key/value blob pairs. If the BlobCache object has not yet been created, - * this will do so, loading the serialized cache contents from disk if - * possible. - */ - BlobCache* getBlobCacheLocked() REQUIRES(mMutex); - - /** * "validateCache" updates the cache to match the given identity. If the * cache currently has the wrong identity, all entries in the cache are cleared. */ bool validateCache(const void* identity, ssize_t size) REQUIRES(mMutex); /** + * Helper for BlobCache::set to trace the result and ensure the identity hash + * does not get evicted. + */ + void set(const void* key, size_t keySize, const void* value, size_t valueSize) REQUIRES(mMutex); + + /** * "saveToDiskLocked" attempts to save the current contents of the cache to * disk. If the identity hash exists, we will insert the identity hash into * the cache for next validation. @@ -128,11 +126,9 @@ private: bool mInitialized GUARDED_BY(mMutex) = false; /** - * "mBlobCache" is the cache in which the key/value blob pairs are stored. It - * is initially NULL, and will be initialized by getBlobCacheLocked the - * first time it's needed. - * The blob cache contains the Android build number. We treat version mismatches as an empty - * cache (logic implemented in BlobCache::unflatten). + * "mBlobCache" is the cache in which the key/value blob pairs are stored. + * The blob cache contains the Android build number. We treat version mismatches + * as an empty cache (logic implemented in BlobCache::unflatten). */ std::unique_ptr<FileBlobCache> mBlobCache GUARDED_BY(mMutex); diff --git a/libs/hwui/renderthread/VulkanManager.cpp b/libs/hwui/renderthread/VulkanManager.cpp index eb4570705030..f3409455d401 100644 --- a/libs/hwui/renderthread/VulkanManager.cpp +++ b/libs/hwui/renderthread/VulkanManager.cpp @@ -41,7 +41,7 @@ namespace android { namespace uirenderer { namespace renderthread { -static std::array<std::string_view, 19> sEnableExtensions{ +static std::array<std::string_view, 20> sEnableExtensions{ VK_KHR_BIND_MEMORY_2_EXTENSION_NAME, VK_KHR_DEDICATED_ALLOCATION_EXTENSION_NAME, VK_KHR_EXTERNAL_MEMORY_CAPABILITIES_EXTENSION_NAME, @@ -61,6 +61,7 @@ static std::array<std::string_view, 19> sEnableExtensions{ VK_EXT_QUEUE_FAMILY_FOREIGN_EXTENSION_NAME, VK_KHR_EXTERNAL_SEMAPHORE_FD_EXTENSION_NAME, VK_KHR_ANDROID_SURFACE_EXTENSION_NAME, + VK_EXT_GLOBAL_PRIORITY_EXTENSION_NAME, }; static bool shouldEnableExtension(const std::string_view& extension) { diff --git a/media/java/android/media/MediaRoute2Info.java b/media/java/android/media/MediaRoute2Info.java index 13936603e604..91fa873078fc 100644 --- a/media/java/android/media/MediaRoute2Info.java +++ b/media/java/android/media/MediaRoute2Info.java @@ -450,27 +450,27 @@ public final class MediaRoute2Info implements Parcelable { public static final String FEATURE_REMOTE_GROUP_PLAYBACK = "android.media.route.feature.REMOTE_GROUP_PLAYBACK"; - final String mId; - final CharSequence mName; - final List<String> mFeatures; + private final String mId; + private final CharSequence mName; + private final List<String> mFeatures; @Type - final int mType; - final boolean mIsSystem; - final Uri mIconUri; - final CharSequence mDescription; + private final int mType; + private final boolean mIsSystem; + private final Uri mIconUri; + private final CharSequence mDescription; @ConnectionState - final int mConnectionState; - final String mClientPackageName; - final String mPackageName; - final int mVolumeHandling; - final int mVolumeMax; - final int mVolume; - final String mAddress; - final Set<String> mDeduplicationIds; - final Bundle mExtras; - final String mProviderId; - final boolean mIsVisibilityRestricted; - final Set<String> mAllowedPackages; + private final int mConnectionState; + private final String mClientPackageName; + private final String mPackageName; + private final int mVolumeHandling; + private final int mVolumeMax; + private final int mVolume; + private final String mAddress; + private final Set<String> mDeduplicationIds; + private final Bundle mExtras; + private final String mProviderId; + private final boolean mIsVisibilityRestricted; + private final Set<String> mAllowedPackages; MediaRoute2Info(@NonNull Builder builder) { mId = builder.mId; diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt index a0ff216875a2..ddb92b19b0cf 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt @@ -27,14 +27,17 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.material3.Divider +import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import com.android.settingslib.development.DevelopmentSettingsEnabler import com.android.settingslib.spa.framework.compose.rememberDrawablePainter import com.android.settingslib.spa.framework.theme.SettingsDimension import com.android.settingslib.spa.widget.ui.SettingsBody @@ -80,11 +83,22 @@ class AppInfoProvider(private val packageInfo: PackageInfo) { } @Composable - fun FooterAppVersion() { + fun FooterAppVersion(showPackageName: Boolean = rememberIsDevelopmentSettingsEnabled()) { if (packageInfo.versionName == null) return - Divider() - Box(modifier = Modifier.padding(SettingsDimension.itemPadding)) { + HorizontalDivider() + Column(modifier = Modifier.padding(SettingsDimension.itemPadding)) { SettingsBody(stringResource(R.string.version_text, packageInfo.versionNameBidiWrapped)) + if (showPackageName) { + SettingsBody(packageInfo.packageName) + } + } + } + + @Composable + private fun rememberIsDevelopmentSettingsEnabled(): Boolean { + val context = LocalContext.current + return remember { + DevelopmentSettingsEnabler.isDevelopmentSettingsEnabled(context) } } diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppInfoTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppInfoTest.kt index bb56c10b28e3..6831e56596cc 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppInfoTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppInfoTest.kt @@ -93,6 +93,7 @@ class AppInfoTest { val packageInfo = PackageInfo().apply { applicationInfo = APP versionName = VERSION_NAME + packageName = PACKAGE_NAME } val appInfoProvider = AppInfoProvider(packageInfo) @@ -105,9 +106,45 @@ class AppInfoTest { composeTestRule.onNodeWithText("version $VERSION_NAME").assertIsDisplayed() } + @Test + fun footerAppVersion_developmentEnabled_packageNameIsDisplayed() { + val packageInfo = PackageInfo().apply { + applicationInfo = APP + versionName = VERSION_NAME + packageName = PACKAGE_NAME + } + val appInfoProvider = AppInfoProvider(packageInfo) + + composeTestRule.setContent { + CompositionLocalProvider(LocalContext provides context) { + appInfoProvider.FooterAppVersion(true) + } + } + composeTestRule.onNodeWithText(PACKAGE_NAME).assertIsDisplayed() + } + + + @Test + fun footerAppVersion_developmentDisabled_packageNameDoesNotExist() { + val packageInfo = PackageInfo().apply { + applicationInfo = APP + versionName = VERSION_NAME + packageName = PACKAGE_NAME + } + val appInfoProvider = AppInfoProvider(packageInfo) + + composeTestRule.setContent { + CompositionLocalProvider(LocalContext provides context) { + appInfoProvider.FooterAppVersion(false) + } + } + composeTestRule.onNodeWithText(PACKAGE_NAME).assertDoesNotExist() + } + private companion object { const val LABEL = "Label" const val VERSION_NAME = "VersionName" + const val PACKAGE_NAME = "package.name" val APP = object : ApplicationInfo() { override fun loadLabel(pm: PackageManager) = LABEL } diff --git a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/Tile.java b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/Tile.java index 612a9282fece..1a938d6ec37e 100644 --- a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/Tile.java +++ b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/Tile.java @@ -179,7 +179,8 @@ public abstract class Tile implements Parcelable { * Check whether tile has order. */ public boolean hasOrder() { - return mMetaData.containsKey(META_DATA_KEY_ORDER) + return mMetaData != null + && mMetaData.containsKey(META_DATA_KEY_ORDER) && mMetaData.get(META_DATA_KEY_ORDER) instanceof Integer; } @@ -204,7 +205,7 @@ public abstract class Tile implements Parcelable { CharSequence title = null; ensureMetadataNotStale(context); final PackageManager packageManager = context.getPackageManager(); - if (mMetaData.containsKey(META_DATA_PREFERENCE_TITLE)) { + if (mMetaData != null && mMetaData.containsKey(META_DATA_PREFERENCE_TITLE)) { if (mMetaData.containsKey(META_DATA_PREFERENCE_TITLE_URI)) { // If has as uri to provide dynamic title, skip loading here. UI will later load // at tile binding time. diff --git a/packages/SystemUI/OWNERS b/packages/SystemUI/OWNERS index 5fdf354bc448..bba926d776db 100644 --- a/packages/SystemUI/OWNERS +++ b/packages/SystemUI/OWNERS @@ -23,6 +23,7 @@ bhnm@google.com brycelee@google.com brzezinski@google.com caitlinshk@google.com +cameronyee@google.com chandruis@google.com chrisgollner@google.com cinek@google.com @@ -44,6 +45,7 @@ jglazier@google.com jjaggi@google.com jonmiranda@google.com joshtrask@google.com +juansmartinez@google.com juliacr@google.com juliatuttle@google.com justinkoh@google.com diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt index 8e79e3ce1742..38b99cc5f5ee 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ViewHierarchyAnimator.kt @@ -70,6 +70,9 @@ class ViewHierarchyAnimator { * If a new layout change happens while an animation is already in progress, the animation * is updated to continue from the current values to the new end state. * + * A set of [excludedViews] can be passed. If any dependent view from [rootView] matches an + * entry in this set, changes to that view will not be animated. + * * The animator continues to respond to layout changes until [stopAnimating] is called. * * Successive calls to this method override the previous settings ([interpolator] and @@ -82,9 +85,16 @@ class ViewHierarchyAnimator { fun animate( rootView: View, interpolator: Interpolator = DEFAULT_INTERPOLATOR, - duration: Long = DEFAULT_DURATION + duration: Long = DEFAULT_DURATION, + excludedViews: Set<View> = emptySet() ): Boolean { - return animate(rootView, interpolator, duration, ephemeral = false) + return animate( + rootView, + interpolator, + duration, + ephemeral = false, + excludedViews = excludedViews + ) } /** @@ -95,16 +105,24 @@ class ViewHierarchyAnimator { fun animateNextUpdate( rootView: View, interpolator: Interpolator = DEFAULT_INTERPOLATOR, - duration: Long = DEFAULT_DURATION + duration: Long = DEFAULT_DURATION, + excludedViews: Set<View> = emptySet() ): Boolean { - return animate(rootView, interpolator, duration, ephemeral = true) + return animate( + rootView, + interpolator, + duration, + ephemeral = true, + excludedViews = excludedViews + ) } private fun animate( rootView: View, interpolator: Interpolator, duration: Long, - ephemeral: Boolean + ephemeral: Boolean, + excludedViews: Set<View> = emptySet() ): Boolean { if ( !occupiesSpace( @@ -119,7 +137,7 @@ class ViewHierarchyAnimator { } val listener = createUpdateListener(interpolator, duration, ephemeral) - addListener(rootView, listener, recursive = true) + addListener(rootView, listener, recursive = true, excludedViews = excludedViews) return true } @@ -921,8 +939,11 @@ class ViewHierarchyAnimator { private fun addListener( view: View, listener: View.OnLayoutChangeListener, - recursive: Boolean = false + recursive: Boolean = false, + excludedViews: Set<View> = emptySet() ) { + if (excludedViews.contains(view)) return + // Make sure that only one listener is active at a time. val previousListener = view.getTag(R.id.tag_layout_listener) if (previousListener != null && previousListener is View.OnLayoutChangeListener) { @@ -933,7 +954,12 @@ class ViewHierarchyAnimator { view.setTag(R.id.tag_layout_listener, listener) if (view is ViewGroup && recursive) { for (i in 0 until view.childCount) { - addListener(view.getChildAt(i), listener, recursive = true) + addListener( + view.getChildAt(i), + listener, + recursive = true, + excludedViews = excludedViews + ) } } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt index ca91b8a21a81..38b751c9445d 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt @@ -17,15 +17,19 @@ package com.android.systemui.notifications.ui.composable +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp @Composable @@ -34,10 +38,26 @@ fun Notifications( ) { // TODO(b/272779828): implement. Column( - modifier = modifier.fillMaxWidth().defaultMinSize(minHeight = 300.dp).padding(4.dp), + modifier = + modifier + .fillMaxWidth() + .defaultMinSize(minHeight = 300.dp) + .clip(RoundedCornerShape(32.dp)) + .background(MaterialTheme.colorScheme.surface) + .padding(16.dp), ) { - Text("Notifications", modifier = Modifier.align(Alignment.CenterHorizontally)) + Text( + text = "Notifications", + modifier = Modifier.align(Alignment.CenterHorizontally), + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onSurface, + ) Spacer(modifier = Modifier.weight(1f)) - Text("Shelf", modifier = Modifier.align(Alignment.CenterHorizontally)) + Text( + text = "Shelf", + modifier = Modifier.align(Alignment.CenterHorizontally), + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.onSurface, + ) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/QuickSettings.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/QuickSettings.kt index 665d6dd0cfa2..1bb341c76e69 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/QuickSettings.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/QuickSettings.kt @@ -17,15 +17,19 @@ package com.android.systemui.qs.footer.ui.compose +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp @Composable @@ -34,10 +38,26 @@ fun QuickSettings( ) { // TODO(b/272780058): implement. Column( - modifier = modifier.fillMaxWidth().defaultMinSize(minHeight = 300.dp).padding(4.dp), + modifier = + modifier + .fillMaxWidth() + .defaultMinSize(minHeight = 300.dp) + .clip(RoundedCornerShape(32.dp)) + .background(MaterialTheme.colorScheme.primary) + .padding(16.dp), ) { - Text("Quick settings", modifier = Modifier.align(Alignment.CenterHorizontally)) + Text( + text = "Quick settings", + modifier = Modifier.align(Alignment.CenterHorizontally), + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onPrimary, + ) Spacer(modifier = Modifier.weight(1f)) - Text("QS footer actions", modifier = Modifier.align(Alignment.CenterHorizontally)) + Text( + text = "QS footer actions", + modifier = Modifier.align(Alignment.CenterHorizontally), + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.onPrimary, + ) } } 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 20e175160aa6..27358f53aaf2 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 @@ -16,18 +16,19 @@ package com.android.systemui.shade.ui.composable +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.material3.Button -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.notifications.ui.composable.Notifications +import com.android.systemui.qs.footer.ui.compose.QuickSettings import com.android.systemui.scene.shared.model.Direction import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel @@ -62,12 +63,7 @@ class ShadeScene( override fun Content( containerName: String, modifier: Modifier, - ) { - ShadeScene( - viewModel = viewModel, - modifier = modifier, - ) - } + ) = ShadeScene(viewModel, modifier) private fun destinationScenes( up: SceneKey, @@ -84,23 +80,15 @@ private fun ShadeScene( viewModel: ShadeSceneViewModel, modifier: Modifier = Modifier, ) { - // TODO(b/280887022): implement the real UI. - - Box(modifier = modifier) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.align(Alignment.Center) - ) { - Text("Shade", style = MaterialTheme.typography.headlineMedium) - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - Button( - onClick = { viewModel.onContentClicked() }, - ) { - Text("Open some content") - } - } - } + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = + Modifier.fillMaxSize() + .clickable(onClick = { viewModel.onContentClicked() }) + .padding(horizontal = 16.dp, vertical = 48.dp) + ) { + QuickSettings(modifier = modifier.height(160.dp)) + Notifications(modifier = modifier.weight(1f)) } } diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_message_area.xml b/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_message_area.xml index 57b3acd6557a..66c54f2a668e 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_message_area.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_message_area.xml @@ -21,6 +21,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="@dimen/keyguard_lock_padding" + android:importantForAccessibility="no" android:ellipsize="marquee" android:focusable="true" android:gravity="center" diff --git a/packages/SystemUI/res/layout/media_smartspace_recommendations.xml b/packages/SystemUI/res/layout/media_smartspace_recommendations.xml deleted file mode 100644 index 9304ff72f054..000000000000 --- a/packages/SystemUI/res/layout/media_smartspace_recommendations.xml +++ /dev/null @@ -1,136 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ Copyright (C) 2019 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 - --> - -<!-- Layout for media recommendations inside QSPanel carousel --> -<!-- See media_recommendation_expanded.xml and media_recommendation_collapsed.xml for the - constraints. --> -<com.android.systemui.util.animation.TransitionLayout - xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" - android:id="@+id/media_recommendations" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:clipChildren="false" - android:clipToPadding="false" - android:forceHasOverlappingRendering="false" - android:background="@drawable/qs_media_background" - android:theme="@style/MediaPlayer"> - - <!-- This view just ensures the full media player is a certain height. --> - <View - android:id="@+id/sizing_view" - android:layout_width="match_parent" - android:layout_height="@dimen/qs_media_session_height_expanded" /> - - <com.android.internal.widget.CachingIconView - android:id="@+id/recommendation_card_icon" - android:layout_width="@dimen/qs_media_app_icon_size" - android:layout_height="@dimen/qs_media_app_icon_size" - android:minWidth="@dimen/qs_media_app_icon_size" - android:minHeight="@dimen/qs_media_app_icon_size" - android:layout_marginStart="@dimen/qs_media_padding" - android:layout_marginTop="@dimen/qs_media_rec_icon_top_margin" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> - - <FrameLayout - android:id="@+id/media_cover1_container" - style="@style/MediaPlayer.Recommendation.AlbumContainer" - > - <ImageView - android:id="@+id/media_cover1" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:minWidth="@dimen/qs_media_rec_album_size" - android:minHeight="@dimen/qs_media_rec_album_size" - app:layout_constraintTop_toTopOf="parent" - app:layout_constraintBottom_toBottomOf="parent" - android:adjustViewBounds="true" - android:background="@drawable/bg_smartspace_media_item" - style="@style/MediaPlayer.Recommendation.Album" - android:clipToOutline="true" - android:scaleType="centerCrop"/> - </FrameLayout> - - <TextView - android:id="@+id/media_title1" - style="@style/MediaPlayer.Recommendation.Text.Title" - /> - - <TextView - android:id="@+id/media_subtitle1" - style="@style/MediaPlayer.Recommendation.Text.Subtitle" - /> - - <FrameLayout - android:id="@+id/media_cover2_container" - style="@style/MediaPlayer.Recommendation.AlbumContainer" - > - <ImageView - android:id="@+id/media_cover2" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:minWidth="@dimen/qs_media_rec_album_size" - android:minHeight="@dimen/qs_media_rec_album_size" - android:adjustViewBounds="true" - android:background="@drawable/bg_smartspace_media_item" - style="@style/MediaPlayer.Recommendation.Album" - android:clipToOutline="true" - android:scaleType="centerCrop"/> - </FrameLayout> - - <TextView - android:id="@+id/media_title2" - style="@style/MediaPlayer.Recommendation.Text.Title" - /> - - <TextView - android:id="@+id/media_subtitle2" - style="@style/MediaPlayer.Recommendation.Text.Subtitle" - /> - - <FrameLayout - android:id="@+id/media_cover3_container" - style="@style/MediaPlayer.Recommendation.AlbumContainer" - > - <ImageView - android:id="@+id/media_cover3" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:minWidth="@dimen/qs_media_rec_album_size" - android:minHeight="@dimen/qs_media_rec_album_size" - android:adjustViewBounds="true" - android:background="@drawable/bg_smartspace_media_item" - style="@style/MediaPlayer.Recommendation.Album" - android:clipToOutline="true" - android:scaleType="centerCrop"/> - </FrameLayout> - - <TextView - android:id="@+id/media_title3" - style="@style/MediaPlayer.Recommendation.Text.Title" - /> - - <TextView - android:id="@+id/media_subtitle3" - style="@style/MediaPlayer.Recommendation.Text.Subtitle" - /> - - <include - layout="@layout/media_long_press_menu" /> - -</com.android.systemui.util.animation.TransitionLayout> diff --git a/packages/SystemUI/res/layout/super_notification_shade.xml b/packages/SystemUI/res/layout/super_notification_shade.xml index 2fde9479d42a..a33625212d34 100644 --- a/packages/SystemUI/res/layout/super_notification_shade.xml +++ b/packages/SystemUI/res/layout/super_notification_shade.xml @@ -76,6 +76,13 @@ android:layout_height="match_parent" android:visibility="invisible" /> + <!-- Shared container for the notification stack. Can be positioned by either + the keyguard_root_view or notification_panel --> + <com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer + android:id="@+id/shared_notification_container" + android:layout_width="match_parent" + android:layout_height="match_parent" /> + <include layout="@layout/brightness_mirror_container" /> <com.android.systemui.scrim.ScrimView diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 47cd1e707557..3366f4f6d443 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -663,9 +663,6 @@ <dimen name="restricted_padlock_pading">4dp</dimen> - <!-- How far the expanded QS panel peeks from the header in collapsed state. --> - <dimen name="qs_peek_height">0dp</dimen> - <!-- Padding between subtitles and the following text in the QSFooter dialog --> <dimen name="qs_footer_dialog_subtitle_padding">20dp</dimen> diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml index 134a7a96bbc3..3a2177a0045c 100644 --- a/packages/SystemUI/res/values/ids.xml +++ b/packages/SystemUI/res/values/ids.xml @@ -211,6 +211,7 @@ <item type="id" name="keyguard_indication_area" /> <item type="id" name="keyguard_indication_text" /> <item type="id" name="keyguard_indication_text_bottom" /> + <item type="id" name="nssl_guideline" /> <item type="id" name="lock_icon" /> <item type="id" name="lock_icon_bg" /> </resources> diff --git a/packages/SystemUI/res/xml/media_recommendation_collapsed.xml b/packages/SystemUI/res/xml/media_recommendation_collapsed.xml deleted file mode 100644 index b7d4b3aac079..000000000000 --- a/packages/SystemUI/res/xml/media_recommendation_collapsed.xml +++ /dev/null @@ -1,101 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ Copyright (C) 2020 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 - --> -<ConstraintSet - xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" > - - <Constraint - android:id="@+id/sizing_view" - android:layout_width="match_parent" - android:layout_height="@dimen/qs_media_session_height_collapsed" - /> - - <!-- Only the constraintBottom and marginBottom are different. The rest of the constraints are - the same as the constraints in media_recommendations_expanded.xml. But, due to how - ConstraintSets work, all the constraints need to be in the same place. So, the shared - constraints can't be put in the shared layout file media_smartspace_recommendations.xml and - the constraints are instead duplicated between here and media_recommendations_expanded.xml. - Ditto for the other cover containers. --> - <Constraint - android:id="@+id/media_cover1_container" - app:layout_constraintBottom_toBottomOf="parent" - android:layout_marginBottom="@dimen/qs_media_padding" - style="@style/MediaPlayer.Recommendation.AlbumContainer" - app:layout_constraintTop_toTopOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintEnd_toStartOf="@id/media_cover2_container" - android:layout_marginEnd="@dimen/qs_media_rec_album_side_margin" - app:layout_constraintHorizontal_chainStyle="packed" - app:layout_constraintHorizontal_bias="1.0" - app:layout_constraintVertical_bias="0.5" - /> - - <Constraint - android:id="@+id/media_title1" - android:visibility="gone" - /> - - <Constraint - android:id="@+id/media_subtitle1" - android:visibility="gone" - /> - - <Constraint - android:id="@+id/media_cover2_container" - app:layout_constraintBottom_toBottomOf="parent" - android:layout_marginBottom="@dimen/qs_media_padding" - style="@style/MediaPlayer.Recommendation.AlbumContainer" - app:layout_constraintTop_toTopOf="parent" - app:layout_constraintStart_toEndOf="@id/media_cover1_container" - app:layout_constraintEnd_toStartOf="@id/media_cover3_container" - android:layout_marginEnd="@dimen/qs_media_rec_album_side_margin" - app:layout_constraintVertical_bias="0.5" - /> - - <Constraint - android:id="@+id/media_title2" - android:visibility="gone" - /> - - <Constraint - android:id="@+id/media_subtitle2" - android:visibility="gone" - /> - - <Constraint - android:id="@+id/media_cover3_container" - app:layout_constraintBottom_toBottomOf="parent" - android:layout_marginBottom="@dimen/qs_media_padding" - style="@style/MediaPlayer.Recommendation.AlbumContainer" - app:layout_constraintTop_toTopOf="parent" - app:layout_constraintStart_toEndOf="@id/media_cover2_container" - app:layout_constraintEnd_toEndOf="parent" - android:layout_marginEnd="@dimen/qs_media_padding" - app:layout_constraintVertical_bias="0.5" - /> - - <Constraint - android:id="@+id/media_title3" - android:visibility="gone" - /> - - <Constraint - android:id="@+id/media_subtitle3" - android:visibility="gone" - /> - -</ConstraintSet> diff --git a/packages/SystemUI/res/xml/media_recommendation_expanded.xml b/packages/SystemUI/res/xml/media_recommendation_expanded.xml deleted file mode 100644 index ce25a7d01bf7..000000000000 --- a/packages/SystemUI/res/xml/media_recommendation_expanded.xml +++ /dev/null @@ -1,123 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ Copyright (C) 2020 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 - --> -<ConstraintSet - xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" - > - - <Constraint - android:id="@+id/sizing_view" - android:layout_width="match_parent" - android:layout_height="@dimen/qs_media_session_height_expanded" - /> - - <Constraint - android:id="@+id/media_cover1_container" - style="@style/MediaPlayer.Recommendation.AlbumContainer" - app:layout_constraintTop_toTopOf="parent" - app:layout_constraintBottom_toTopOf="@+id/media_title1" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintEnd_toStartOf="@id/media_cover2_container" - android:layout_marginEnd="@dimen/qs_media_rec_album_side_margin" - app:layout_constraintHorizontal_chainStyle="packed" - app:layout_constraintVertical_chainStyle="packed" - app:layout_constraintHorizontal_bias="1.0" - app:layout_constraintVertical_bias="0.4" - /> - - <Constraint - android:id="@+id/media_title1" - style="@style/MediaPlayer.Recommendation.Text.Title" - app:layout_constraintStart_toStartOf="@+id/media_cover1_container" - app:layout_constraintEnd_toEndOf="@+id/media_cover1_container" - app:layout_constraintTop_toBottomOf="@+id/media_cover1_container" - app:layout_constraintBottom_toTopOf="@+id/media_subtitle1" - /> - - <Constraint - android:id="@+id/media_subtitle1" - style="@style/MediaPlayer.Recommendation.Text.Subtitle" - app:layout_constraintStart_toStartOf="@+id/media_cover1_container" - app:layout_constraintEnd_toEndOf="@+id/media_cover1_container" - app:layout_constraintTop_toBottomOf="@+id/media_title1" - app:layout_constraintBottom_toBottomOf="parent" - android:layout_marginBottom="@dimen/qs_media_padding" - /> - - <Constraint - android:id="@+id/media_cover2_container" - style="@style/MediaPlayer.Recommendation.AlbumContainer" - app:layout_constraintTop_toTopOf="parent" - app:layout_constraintBottom_toTopOf="@id/media_title2" - app:layout_constraintStart_toEndOf="@id/media_cover1_container" - app:layout_constraintEnd_toStartOf="@id/media_cover3_container" - android:layout_marginEnd="@dimen/qs_media_rec_album_side_margin" - app:layout_constraintVertical_chainStyle="packed" - app:layout_constraintVertical_bias="0.4" - /> - - <Constraint - android:id="@+id/media_title2" - style="@style/MediaPlayer.Recommendation.Text.Title" - app:layout_constraintStart_toStartOf="@+id/media_cover2_container" - app:layout_constraintEnd_toEndOf="@+id/media_cover2_container" - app:layout_constraintTop_toBottomOf="@+id/media_cover2_container" - app:layout_constraintBottom_toTopOf="@+id/media_subtitle2" - /> - - <Constraint - android:id="@+id/media_subtitle2" - style="@style/MediaPlayer.Recommendation.Text.Subtitle" - app:layout_constraintStart_toStartOf="@+id/media_cover2_container" - app:layout_constraintEnd_toEndOf="@+id/media_cover2_container" - app:layout_constraintTop_toBottomOf="@+id/media_title2" - app:layout_constraintBottom_toBottomOf="parent" - android:layout_marginBottom="@dimen/qs_media_padding" - /> - - <Constraint - android:id="@+id/media_cover3_container" - style="@style/MediaPlayer.Recommendation.AlbumContainer" - app:layout_constraintTop_toTopOf="parent" - app:layout_constraintBottom_toTopOf="@id/media_title3" - app:layout_constraintStart_toEndOf="@id/media_cover2_container" - app:layout_constraintEnd_toEndOf="parent" - android:layout_marginEnd="@dimen/qs_media_padding" - app:layout_constraintVertical_chainStyle="packed" - app:layout_constraintVertical_bias="0.4" - /> - - <Constraint - android:id="@+id/media_title3" - style="@style/MediaPlayer.Recommendation.Text.Title" - app:layout_constraintStart_toStartOf="@+id/media_cover3_container" - app:layout_constraintEnd_toEndOf="@+id/media_cover3_container" - app:layout_constraintTop_toBottomOf="@+id/media_cover3_container" - app:layout_constraintBottom_toTopOf="@+id/media_subtitle3" - /> - - <Constraint - android:id="@+id/media_subtitle3" - style="@style/MediaPlayer.Recommendation.Text.Subtitle" - app:layout_constraintStart_toStartOf="@+id/media_cover3_container" - app:layout_constraintEnd_toEndOf="@+id/media_cover3_container" - app:layout_constraintTop_toBottomOf="@+id/media_title3" - app:layout_constraintBottom_toBottomOf="parent" - android:layout_marginBottom="@dimen/qs_media_padding" - /> - -</ConstraintSet> diff --git a/packages/SystemUI/res/xml/media_recommendations_view_collapsed.xml b/packages/SystemUI/res/xml/media_recommendations_collapsed.xml index d3be3c7de5ad..d3be3c7de5ad 100644 --- a/packages/SystemUI/res/xml/media_recommendations_view_collapsed.xml +++ b/packages/SystemUI/res/xml/media_recommendations_collapsed.xml diff --git a/packages/SystemUI/res/xml/media_recommendations_view_expanded.xml b/packages/SystemUI/res/xml/media_recommendations_expanded.xml index 88c70552e9e8..88c70552e9e8 100644 --- a/packages/SystemUI/res/xml/media_recommendations_view_expanded.xml +++ b/packages/SystemUI/res/xml/media_recommendations_expanded.xml diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java index 2377057f1fc5..d9b7bde66c67 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java @@ -69,7 +69,7 @@ public class KeyguardPINView extends KeyguardPinBasedInputView { (long) (125 * KeyguardPatternView.DISAPPEAR_MULTIPLIER_LOCKED), 0.6f /* translationScale */, 0.45f /* delayScale */, AnimationUtils.loadInterpolator( - mContext, android.R.interpolator.fast_out_linear_in)); + mContext, android.R.interpolator.fast_out_linear_in)); mDisappearYTranslation = getResources().getDimensionPixelSize( R.dimen.disappear_y_translation); mYTrans = getResources().getDimensionPixelSize(R.dimen.pin_view_trans_y_entry); @@ -82,8 +82,10 @@ public class KeyguardPINView extends KeyguardPinBasedInputView { } void onDevicePostureChanged(@DevicePostureInt int posture) { - mLastDevicePosture = posture; - updateMargins(); + if (mLastDevicePosture != posture) { + mLastDevicePosture = posture; + updateMargins(); + } } @Override diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java index 38c07dc98471..2bdf46e1309d 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java @@ -104,8 +104,10 @@ public class KeyguardPatternView extends KeyguardInputView } void onDevicePostureChanged(@DevicePostureInt int posture) { - mLastDevicePosture = posture; - updateMargins(); + if (mLastDevicePosture != posture) { + mLastDevicePosture = posture; + updateMargins(); + } } private void updateMargins() { diff --git a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt index 70b43713599b..a9779663cc7c 100644 --- a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt @@ -64,14 +64,6 @@ interface AuthenticationRepository { val isUnlocked: StateFlow<Boolean> /** - * Whether lock screen bypass is enabled. When enabled, the lock screen will be automatically - * dismisses once the authentication challenge is completed. For example, completing a biometric - * authentication challenge via face unlock or fingerprint sensor can automatically bypass the - * lock screen. - */ - val isBypassEnabled: StateFlow<Boolean> - - /** * Whether the auto confirm feature is enabled for the currently-selected user. * * Note that the length of the PIN is also important to take into consideration, please see @@ -113,9 +105,6 @@ interface AuthenticationRepository { */ suspend fun isLockscreenEnabled(): Boolean - /** See [isBypassEnabled]. */ - fun setBypassEnabled(isBypassEnabled: Boolean) - /** Reports an authentication attempt. */ suspend fun reportAuthenticationAttempt(isSuccessful: Boolean) @@ -157,7 +146,7 @@ constructor( private val lockPatternUtils: LockPatternUtils, ) : AuthenticationRepository { - override val isUnlocked: StateFlow<Boolean> = keyguardRepository.isKeyguardUnlocked + override val isUnlocked = keyguardRepository.isKeyguardUnlocked override suspend fun isLockscreenEnabled(): Boolean { return withContext(backgroundDispatcher) { @@ -166,9 +155,6 @@ constructor( } } - private val _isBypassEnabled = MutableStateFlow(false) - override val isBypassEnabled: StateFlow<Boolean> = _isBypassEnabled.asStateFlow() - override val isAutoConfirmEnabled: StateFlow<Boolean> = userRepository.selectedUserInfo .map { it.id } @@ -225,10 +211,6 @@ constructor( } } - override fun setBypassEnabled(isBypassEnabled: Boolean) { - _isBypassEnabled.value = isBypassEnabled - } - override suspend fun reportAuthenticationAttempt(isSuccessful: Boolean) { val selectedUserId = userRepository.selectedUserId withContext(backgroundDispatcher) { diff --git a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt index 3283e406ddb0..b482977bde67 100644 --- a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt @@ -24,6 +24,7 @@ import com.android.systemui.authentication.shared.model.AuthenticationThrottling import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.keyguard.data.repository.KeyguardRepository import com.android.systemui.user.data.repository.UserRepository import com.android.systemui.util.time.SystemClock import javax.inject.Inject @@ -51,6 +52,7 @@ constructor( private val repository: AuthenticationRepository, @Background private val backgroundDispatcher: CoroutineDispatcher, private val userRepository: UserRepository, + private val keyguardRepository: KeyguardRepository, private val clock: SystemClock, ) { /** @@ -76,14 +78,6 @@ constructor( initialValue = true, ) - /** - * Whether lock screen bypass is enabled. When enabled, the lock screen will be automatically - * dismisses once the authentication challenge is completed. For example, completing a biometric - * authentication challenge via face unlock or fingerprint sensor can automatically bypass the - * lock screen. - */ - val isBypassEnabled: StateFlow<Boolean> = repository.isBypassEnabled - /** The current authentication throttling state, only meaningful if [isThrottled] is `true`. */ val throttling: StateFlow<AuthenticationThrottlingModel> = repository.throttling @@ -156,6 +150,16 @@ constructor( } /** + * Whether lock screen bypass is enabled. When enabled, the lock screen will be automatically + * dismisses once the authentication challenge is completed. For example, completing a biometric + * authentication challenge via face unlock or fingerprint sensor can automatically bypass the + * lock screen. + */ + fun isBypassEnabled(): Boolean { + return keyguardRepository.isBypassEnabled() + } + + /** * Attempts to authenticate the user and unlock the device. * * If [tryAutoConfirm] is `true`, authentication is attempted if and only if the auth method @@ -218,11 +222,6 @@ constructor( return authenticationResult.isSuccessful } - /** See [isBypassEnabled]. */ - fun toggleBypassEnabled() { - repository.setBypassEnabled(!repository.isBypassEnabled.value) - } - /** Starts refreshing the throttling state every second. */ private suspend fun startThrottlingCountdown() { cancelCountdown() diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java index 7a2f2443dbd2..9df56fcce430 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java @@ -250,7 +250,7 @@ public class AuthContainerView extends LinearLayout .setMessage(messageBody) .setPositiveButton(android.R.string.ok, null) .create(); - alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL); + alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG); alertDialog.show(); } @@ -263,7 +263,7 @@ public class AuthContainerView extends LinearLayout .setOnDismissListener( dialog -> animateAway(AuthDialogCallback.DISMISSED_ERROR)) .create(); - alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL); + alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG); alertDialog.show(); } diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index add323983928..0670ec380861 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -438,10 +438,6 @@ object Flags { // TODO(b/266157412): Tracking Bug val MEDIA_RETAIN_SESSIONS = unreleasedFlag(913, "media_retain_sessions") - // TODO(b/266739309): Tracking Bug - @JvmField - val MEDIA_RECOMMENDATION_CARD_UPDATE = releasedFlag(914, "media_recommendation_card_update") - // TODO(b/267007629): Tracking Bug val MEDIA_RESUME_PROGRESS = releasedFlag(915, "media_resume_progress") diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt index f59ad90d86ff..23f6fa6bbf23 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt @@ -31,6 +31,9 @@ import com.android.systemui.keyguard.ui.view.layout.KeyguardLayoutManagerCommand import com.android.systemui.keyguard.ui.viewmodel.KeyguardIndicationAreaViewModel import com.android.systemui.shade.NotificationShadeWindowView import com.android.systemui.statusbar.KeyguardIndicationController +import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer +import com.android.systemui.statusbar.notification.stack.ui.viewbinder.SharedNotificationContainerBinder +import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel import javax.inject.Inject import kotlinx.coroutines.DisposableHandle @@ -40,7 +43,9 @@ class KeyguardViewConfigurator @Inject constructor( private val keyguardRootView: KeyguardRootView, + private val sharedNotificationContainer: SharedNotificationContainer, private val keyguardIndicationAreaViewModel: KeyguardIndicationAreaViewModel, + private val sharedNotificationContainerViewModel: SharedNotificationContainerViewModel, private val notificationShadeWindowView: NotificationShadeWindowView, private val featureFlags: FeatureFlags, private val indicationController: KeyguardIndicationController, @@ -55,10 +60,28 @@ constructor( notificationShadeWindowView.requireViewById(R.id.notification_panel) as ViewGroup bindIndicationArea(notificationPanel) bindLockIconView(notificationPanel) + setupNotificationStackScrollLayout(notificationPanel) + keyguardLayoutManager.layoutViews() keyguardLayoutManagerCommandListener.start() } + fun setupNotificationStackScrollLayout(legacyParent: ViewGroup) { + if (featureFlags.isEnabled(Flags.MIGRATE_NSSL)) { + // This moves the existing NSSL view to a different parent, as the controller is a + // singleton and recreating it has other bad side effects + val nssl = + legacyParent.requireViewById<View>(R.id.notification_stack_scroller).also { + (it.getParent() as ViewGroup).removeView(it) + } + sharedNotificationContainer.addNotificationStackScrollLayout(nssl) + SharedNotificationContainerBinder.bind( + sharedNotificationContainer, + sharedNotificationContainerViewModel + ) + } + } + fun bindIndicationArea(legacyParent: ViewGroup) { indicationAreaHandle?.dispose() diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt index d119920e1a42..42bee4a3bdcd 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt @@ -42,6 +42,7 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.phone.BiometricUnlockController import com.android.systemui.statusbar.phone.BiometricUnlockController.WakeAndUnlockMode import com.android.systemui.statusbar.phone.DozeParameters +import com.android.systemui.statusbar.phone.KeyguardBypassController import com.android.systemui.statusbar.policy.KeyguardStateController import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher @@ -171,6 +172,14 @@ interface KeyguardRepository { */ fun isKeyguardShowing(): Boolean + /** + * Whether lock screen bypass is enabled. When enabled, the lock screen will be automatically + * dismissed once the authentication challenge is completed. For example, completing a biometric + * authentication challenge via face unlock or fingerprint sensor can automatically bypass the + * lock screen. + */ + fun isBypassEnabled(): Boolean + /** Sets whether the bottom area UI should animate the transition out of doze state. */ fun setAnimateDozingTransitions(animate: Boolean) @@ -206,6 +215,7 @@ constructor( wakefulnessLifecycle: WakefulnessLifecycle, biometricUnlockController: BiometricUnlockController, private val keyguardStateController: KeyguardStateController, + private val keyguardBypassController: KeyguardBypassController, private val keyguardUpdateMonitor: KeyguardUpdateMonitor, private val dozeTransitionListener: DozeTransitionListener, private val dozeParameters: DozeParameters, @@ -252,23 +262,17 @@ constructor( override val isAodAvailable: Flow<Boolean> = conflatedCallbackFlow { val callback = - object : DozeParameters.Callback { - override fun onAlwaysOnChange() { - trySendWithFailureLogging( - dozeParameters.getAlwaysOn(), - TAG, - "updated isAodAvailable" - ) - } + DozeParameters.Callback { + trySendWithFailureLogging( + dozeParameters.alwaysOn, + TAG, + "updated isAodAvailable" + ) } dozeParameters.addCallback(callback) // Adding the callback does not send an initial update. - trySendWithFailureLogging( - dozeParameters.getAlwaysOn(), - TAG, - "initial isAodAvailable" - ) + trySendWithFailureLogging(dozeParameters.alwaysOn, TAG, "initial isAodAvailable") awaitClose { dozeParameters.removeCallback(callback) } } @@ -464,6 +468,10 @@ constructor( return keyguardStateController.isShowing } + override fun isBypassEnabled(): Boolean { + return keyguardBypassController.bypassEnabled + } + override val statusBarState: Flow<StatusBarState> = conflatedCallbackFlow { val callback = object : StatusBarStateController.StateListener { diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/RecommendationViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/RecommendationViewHolder.kt index 0b33904829d5..258284e8af51 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/RecommendationViewHolder.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/recommendation/RecommendationViewHolder.kt @@ -31,15 +31,12 @@ import com.android.systemui.util.animation.TransitionLayout private const val TAG = "RecommendationViewHolder" /** ViewHolder for a Smartspace media recommendation. */ -class RecommendationViewHolder private constructor(itemView: View, updatedView: Boolean) { +class RecommendationViewHolder private constructor(itemView: View) { val recommendations = itemView as TransitionLayout // Recommendation screen - lateinit var cardIcon: ImageView - lateinit var mediaAppIcons: List<CachingIconView> - lateinit var mediaProgressBars: List<SeekBar> - lateinit var cardTitle: TextView + val cardTitle: TextView = itemView.requireViewById(R.id.media_rec_title) val mediaCoverContainers = listOf<ViewGroup>( @@ -47,53 +44,25 @@ class RecommendationViewHolder private constructor(itemView: View, updatedView: itemView.requireViewById(R.id.media_cover2_container), itemView.requireViewById(R.id.media_cover3_container) ) + val mediaAppIcons: List<CachingIconView> = + mediaCoverContainers.map { it.requireViewById(R.id.media_rec_app_icon) } val mediaTitles: List<TextView> = - if (updatedView) { - mediaCoverContainers.map { it.requireViewById(R.id.media_title) } - } else { - listOf( - itemView.requireViewById(R.id.media_title1), - itemView.requireViewById(R.id.media_title2), - itemView.requireViewById(R.id.media_title3) - ) - } + mediaCoverContainers.map { it.requireViewById(R.id.media_title) } val mediaSubtitles: List<TextView> = - if (updatedView) { - mediaCoverContainers.map { it.requireViewById(R.id.media_subtitle) } - } else { - listOf( - itemView.requireViewById(R.id.media_subtitle1), - itemView.requireViewById(R.id.media_subtitle2), - itemView.requireViewById(R.id.media_subtitle3) - ) + mediaCoverContainers.map { it.requireViewById(R.id.media_subtitle) } + val mediaProgressBars: List<SeekBar> = + mediaCoverContainers.map { + it.requireViewById<SeekBar?>(R.id.media_progress_bar).apply { + // Media playback is in the direction of tape, not time, so it stays LTR + layoutDirection = View.LAYOUT_DIRECTION_LTR + } } val mediaCoverItems: List<ImageView> = - if (updatedView) { - mediaCoverContainers.map { it.requireViewById(R.id.media_cover) } - } else { - listOf( - itemView.requireViewById(R.id.media_cover1), - itemView.requireViewById(R.id.media_cover2), - itemView.requireViewById(R.id.media_cover3) - ) - } + mediaCoverContainers.map { it.requireViewById(R.id.media_cover) } val gutsViewHolder = GutsViewHolder(itemView) init { - if (updatedView) { - mediaAppIcons = mediaCoverContainers.map { it.requireViewById(R.id.media_rec_app_icon) } - cardTitle = itemView.requireViewById(R.id.media_rec_title) - mediaProgressBars = - mediaCoverContainers.map { - it.requireViewById<SeekBar?>(R.id.media_progress_bar).apply { - // Media playback is in the direction of tape, not time, so it stays LTR - layoutDirection = View.LAYOUT_DIRECTION_LTR - } - } - } else { - cardIcon = itemView.requireViewById<ImageView>(R.id.recommendation_card_icon) - } (recommendations.background as IlluminationDrawable).let { background -> mediaCoverContainers.forEach { background.registerLightSource(it) } background.registerLightSource(gutsViewHolder.cancel) @@ -114,63 +83,31 @@ class RecommendationViewHolder private constructor(itemView: View, updatedView: * @param parent Parent of inflated view. */ @JvmStatic - fun create( - inflater: LayoutInflater, - parent: ViewGroup, - updatedView: Boolean, - ): RecommendationViewHolder { + fun create(inflater: LayoutInflater, parent: ViewGroup): RecommendationViewHolder { val itemView = - if (updatedView) { - inflater.inflate( - R.layout.media_recommendations, - parent, - false /* attachToRoot */ - ) - } else { - inflater.inflate( - R.layout.media_smartspace_recommendations, - parent, - false /* attachToRoot */ - ) - } + inflater.inflate(R.layout.media_recommendations, parent, false /* attachToRoot */) // Because this media view (a TransitionLayout) is used to measure and layout the views // in various states before being attached to its parent, we can't depend on the default // LAYOUT_DIRECTION_INHERIT to correctly resolve the ltr direction. itemView.layoutDirection = View.LAYOUT_DIRECTION_LOCALE - return RecommendationViewHolder(itemView, updatedView) + return RecommendationViewHolder(itemView) } // Res Ids for the control components on the recommendation view. val controlsIds = setOf( - R.id.recommendation_card_icon, R.id.media_rec_title, - R.id.media_cover1, - R.id.media_cover2, - R.id.media_cover3, R.id.media_cover, R.id.media_cover1_container, R.id.media_cover2_container, R.id.media_cover3_container, - R.id.media_title1, - R.id.media_title2, - R.id.media_title3, R.id.media_title, - R.id.media_subtitle1, - R.id.media_subtitle2, - R.id.media_subtitle3, R.id.media_subtitle, ) val mediaTitlesAndSubtitlesIds = setOf( - R.id.media_title1, - R.id.media_title2, - R.id.media_title3, R.id.media_title, - R.id.media_subtitle1, - R.id.media_subtitle2, - R.id.media_subtitle3, R.id.media_subtitle, ) diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt index 70b5e75e6048..398dcf260dff 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt @@ -744,11 +744,7 @@ constructor( val newRecs = mediaControlPanelFactory.get() newRecs.attachRecommendation( - RecommendationViewHolder.create( - LayoutInflater.from(context), - mediaContent, - mediaFlags.isRecommendationCardUpdateEnabled() - ) + RecommendationViewHolder.create(LayoutInflater.from(context), mediaContent) ) newRecs.mediaViewController.sizeChangedListener = this::updateCarouselDimensions val lp = diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java index a978b92cb234..a12bc2c99d63 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java @@ -784,14 +784,7 @@ public class MediaControlPanel { contentDescription = mRecommendationViewHolder.getGutsViewHolder().getGutsText().getText(); } else if (data != null) { - if (mFeatureFlags.isEnabled(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE)) { - contentDescription = mContext.getString( - R.string.controls_media_smartspace_rec_header); - } else { - contentDescription = mContext.getString( - R.string.controls_media_smartspace_rec_description, - data.getAppName(mContext)); - } + contentDescription = mContext.getString(R.string.controls_media_smartspace_rec_header); } else { contentDescription = null; } @@ -1377,10 +1370,6 @@ public class MediaControlPanel { PackageManager packageManager = mContext.getPackageManager(); // Set up media source app's logo. Drawable icon = packageManager.getApplicationIcon(applicationInfo); - if (!mFeatureFlags.isEnabled(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE)) { - ImageView headerLogoImageView = mRecommendationViewHolder.getCardIcon(); - headerLogoImageView.setImageDrawable(icon); - } fetchAndUpdateRecommendationColors(icon); // Set up media rec card's tap action if applicable. @@ -1401,16 +1390,7 @@ public class MediaControlPanel { // Set up media item cover. ImageView mediaCoverImageView = mediaCoverItems.get(itemIndex); - if (mFeatureFlags.isEnabled(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE)) { - bindRecommendationArtwork( - recommendation, - data.getPackageName(), - itemIndex - ); - } else { - mediaCoverImageView.post( - () -> mediaCoverImageView.setImageIcon(recommendation.getIcon())); - } + bindRecommendationArtwork(recommendation, data.getPackageName(), itemIndex); // Set up the media item's click listener if applicable. ViewGroup mediaCoverContainer = mediaCoverContainers.get(itemIndex); @@ -1455,21 +1435,18 @@ public class MediaControlPanel { subtitleView.setText(subtitle); // Set up progress bar - if (mFeatureFlags.isEnabled(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE)) { - SeekBar mediaProgressBar = - mRecommendationViewHolder.getMediaProgressBars().get(itemIndex); - TextView mediaSubtitle = - mRecommendationViewHolder.getMediaSubtitles().get(itemIndex); - // show progress bar if the recommended album is played. - Double progress = MediaDataUtils.getDescriptionProgress(recommendation.getExtras()); - if (progress == null || progress <= 0.0) { - mediaProgressBar.setVisibility(View.GONE); - mediaSubtitle.setVisibility(View.VISIBLE); - } else { - mediaProgressBar.setProgress((int) (progress * 100)); - mediaProgressBar.setVisibility(View.VISIBLE); - mediaSubtitle.setVisibility(View.GONE); - } + SeekBar mediaProgressBar = + mRecommendationViewHolder.getMediaProgressBars().get(itemIndex); + TextView mediaSubtitle = mRecommendationViewHolder.getMediaSubtitles().get(itemIndex); + // show progress bar if the recommended album is played. + Double progress = MediaDataUtils.getDescriptionProgress(recommendation.getExtras()); + if (progress == null || progress <= 0.0) { + mediaProgressBar.setVisibility(View.GONE); + mediaSubtitle.setVisibility(View.VISIBLE); + } else { + mediaProgressBar.setProgress((int) (progress * 100)); + mediaProgressBar.setVisibility(View.VISIBLE); + mediaSubtitle.setVisibility(View.GONE); } } mSmartspaceMediaItemsCount = NUM_REQUIRED_RECOMMENDATIONS; @@ -1588,9 +1565,7 @@ public class MediaControlPanel { int textPrimaryColor = MediaColorSchemesKt.textPrimaryFromScheme(colorScheme); int textSecondaryColor = MediaColorSchemesKt.textSecondaryFromScheme(colorScheme); - if (mFeatureFlags.isEnabled(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE)) { - mRecommendationViewHolder.getCardTitle().setTextColor(textPrimaryColor); - } + mRecommendationViewHolder.getCardTitle().setTextColor(textPrimaryColor); mRecommendationViewHolder.getRecommendations() .setBackgroundTintList(ColorStateList.valueOf(backgroundColor)); @@ -1598,12 +1573,9 @@ public class MediaControlPanel { (title) -> title.setTextColor(textPrimaryColor)); mRecommendationViewHolder.getMediaSubtitles().forEach( (subtitle) -> subtitle.setTextColor(textSecondaryColor)); - if (mFeatureFlags.isEnabled(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE)) { - mRecommendationViewHolder.getMediaProgressBars().forEach( - (progressBar) -> progressBar.setProgressTintList( - ColorStateList.valueOf(textPrimaryColor)) - ); - } + mRecommendationViewHolder.getMediaProgressBars().forEach( + (progressBar) -> progressBar.setProgressTintList( + ColorStateList.valueOf(textPrimaryColor))); mRecommendationViewHolder.getGutsViewHolder().setColors(colorScheme); } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt index 4bca778b77c5..1dd969f9bea5 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt @@ -655,13 +655,8 @@ constructor( expandedLayout.load(context, R.xml.media_session_expanded) } TYPE.RECOMMENDATION -> { - if (mediaFlags.isRecommendationCardUpdateEnabled()) { - collapsedLayout.load(context, R.xml.media_recommendations_view_collapsed) - expandedLayout.load(context, R.xml.media_recommendations_view_expanded) - } else { - collapsedLayout.load(context, R.xml.media_recommendation_collapsed) - expandedLayout.load(context, R.xml.media_recommendation_expanded) - } + collapsedLayout.load(context, R.xml.media_recommendations_collapsed) + expandedLayout.load(context, R.xml.media_recommendations_expanded) } } refreshState() diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt index 01f047ccd4f5..09aef8826200 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt @@ -49,10 +49,6 @@ class MediaFlags @Inject constructor(private val featureFlags: FeatureFlags) { */ fun isRetainingPlayersEnabled() = featureFlags.isEnabled(Flags.MEDIA_RETAIN_SESSIONS) - /** Check whether we show the updated recommendation card. */ - fun isRecommendationCardUpdateEnabled() = - featureFlags.isEnabled(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE) - /** Check whether to get progress information for resume players */ fun isResumeProgressEnabled() = featureFlags.isEnabled(Flags.MEDIA_RESUME_PROGRESS) diff --git a/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt b/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt index eb1ca66f6ca8..809edc09070a 100644 --- a/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt @@ -70,6 +70,19 @@ constructor( } } + /** + * Wakes up the device if dreaming with a screensaver. + * + * @param why a string explaining why we're waking the device for debugging purposes. Should be + * in SCREAMING_SNAKE_CASE. + * @param wakeReason the PowerManager-based reason why we're waking the device. + */ + fun wakeUpIfDreaming(why: String, @PowerManager.WakeReason wakeReason: Int) { + if (statusBarStateController.isDreaming) { + repository.wakeUp(why, wakeReason) + } + } + companion object { private const val FSI_WAKE_WHY = "full_screen_intent" } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java index 83b373d5e626..856a92e85ad7 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java @@ -58,6 +58,7 @@ public class QSPanelController extends QSPanelControllerBase<QSPanel> { private final BrightnessSliderController mBrightnessSliderController; private final BrightnessMirrorHandler mBrightnessMirrorHandler; private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; + private boolean mListening; private View.OnTouchListener mTileLayoutTouchListener = new View.OnTouchListener() { @Override @@ -159,12 +160,15 @@ public class QSPanelController extends QSPanelControllerBase<QSPanel> { public void setListening(boolean listening, boolean expanded) { setListening(listening && expanded); - // Set the listening as soon as the QS fragment starts listening regardless of the - //expansion, so it will update the current brightness before the slider is visible. - if (listening) { - mBrightnessController.registerCallbacks(); - } else { - mBrightnessController.unregisterCallbacks(); + if (listening != mListening) { + mListening = listening; + // Set the listening as soon as the QS fragment starts listening regardless of the + //expansion, so it will update the current brightness before the slider is visible. + if (listening) { + mBrightnessController.registerCallbacks(); + } else { + mBrightnessController.unregisterCallbacks(); + } } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartable.kt index 285ff74f8b2e..b23c4ec9aa8b 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartable.kt @@ -77,7 +77,7 @@ constructor( authenticationInteractor.isUnlocked .map { isUnlocked -> val currentSceneKey = sceneInteractor.currentScene(CONTAINER_NAME).value.key - val isBypassEnabled = authenticationInteractor.isBypassEnabled.value + val isBypassEnabled = authenticationInteractor.isBypassEnabled() when { isUnlocked -> when (currentSceneKey) { diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java index 9594ba374fc3..6af9b739da52 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessController.java @@ -22,7 +22,6 @@ import static com.android.settingslib.display.BrightnessUtils.convertLinearToGam import android.animation.ValueAnimator; import android.annotation.NonNull; -import android.content.ContentResolver; import android.content.Context; import android.database.ContentObserver; import android.hardware.display.BrightnessInfo; @@ -31,10 +30,10 @@ import android.net.Uri; import android.os.AsyncTask; import android.os.Handler; import android.os.HandlerExecutor; +import android.os.Looper; import android.os.Message; import android.os.PowerManager; import android.os.RemoteException; -import android.os.ServiceManager; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; @@ -43,6 +42,8 @@ import android.service.vr.IVrStateCallbacks; import android.util.Log; import android.util.MathUtils; +import androidx.annotation.Nullable; + import com.android.internal.display.BrightnessSynchronizer; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; @@ -52,10 +53,13 @@ import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.settings.DisplayTracker; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.policy.BrightnessMirrorController; +import com.android.systemui.util.settings.SecureSettings; import java.util.concurrent.Executor; -import javax.inject.Inject; +import dagger.assisted.Assisted; +import dagger.assisted.AssistedFactory; +import dagger.assisted.AssistedInject; public class BrightnessController implements ToggleSlider.Listener, MirroredBrightnessController { private static final String TAG = "CentralSurfaces.BrightnessController"; @@ -75,8 +79,11 @@ public class BrightnessController implements ToggleSlider.Listener, MirroredBrig private final DisplayManager mDisplayManager; private final UserTracker mUserTracker; private final DisplayTracker mDisplayTracker; + @Nullable private final IVrManager mVrManager; + private final SecureSettings mSecureSettings; + private final Executor mMainExecutor; private final Handler mBackgroundHandler; private final BrightnessObserver mBrightnessObserver; @@ -106,6 +113,8 @@ public class BrightnessController implements ToggleSlider.Listener, MirroredBrig /** ContentObserver to watch brightness */ private class BrightnessObserver extends ContentObserver { + private boolean mObserving = false; + BrightnessObserver(Handler handler) { super(handler); } @@ -124,19 +133,17 @@ public class BrightnessController implements ToggleSlider.Listener, MirroredBrig } public void startObserving() { - final ContentResolver cr = mContext.getContentResolver(); - cr.unregisterContentObserver(this); - cr.registerContentObserver( - BRIGHTNESS_MODE_URI, - false, this, UserHandle.USER_ALL); - mDisplayTracker.addBrightnessChangeCallback(mBrightnessListener, - new HandlerExecutor(mHandler)); + if (!mObserving) { + mObserving = true; + mSecureSettings.registerContentObserverForUser( + BRIGHTNESS_MODE_URI, + false, this, UserHandle.USER_ALL); + } } public void stopObserving() { - final ContentResolver cr = mContext.getContentResolver(); - cr.unregisterContentObserver(this); - mDisplayTracker.removeCallback(mBrightnessListener); + mSecureSettings.unregisterContentObserver(this); + mObserving = false; } } @@ -159,6 +166,8 @@ public class BrightnessController implements ToggleSlider.Listener, MirroredBrig } mBrightnessObserver.startObserving(); + mDisplayTracker.addBrightnessChangeCallback(mBrightnessListener, + new HandlerExecutor(mMainHandler)); mUserTracker.addCallback(mUserChangedCallback, mMainExecutor); // Update the slider and mode before attaching the listener so we don't @@ -166,7 +175,7 @@ public class BrightnessController implements ToggleSlider.Listener, MirroredBrig mUpdateModeRunnable.run(); mUpdateSliderRunnable.run(); - mHandler.sendEmptyMessage(MSG_ATTACH_LISTENER); + mMainHandler.sendEmptyMessage(MSG_ATTACH_LISTENER); } }; @@ -187,9 +196,10 @@ public class BrightnessController implements ToggleSlider.Listener, MirroredBrig } mBrightnessObserver.stopObserving(); + mDisplayTracker.removeCallback(mBrightnessListener); mUserTracker.removeCallback(mUserChangedCallback); - mHandler.sendEmptyMessage(MSG_DETACH_LISTENER); + mMainHandler.sendEmptyMessage(MSG_DETACH_LISTENER); } }; @@ -225,7 +235,7 @@ public class BrightnessController implements ToggleSlider.Listener, MirroredBrig mBrightnessMin = info.brightnessMinimum; // Value is passed as intbits, since this is what the message takes. final int valueAsIntBits = Float.floatToIntBits(info.brightness); - mHandler.obtainMessage(MSG_UPDATE_SLIDER, valueAsIntBits, + mMainHandler.obtainMessage(MSG_UPDATE_SLIDER, valueAsIntBits, inVrMode ? 1 : 0).sendToTarget(); } }; @@ -233,14 +243,14 @@ public class BrightnessController implements ToggleSlider.Listener, MirroredBrig private final IVrStateCallbacks mVrStateCallbacks = new IVrStateCallbacks.Stub() { @Override public void onVrStateChanged(boolean enabled) { - mHandler.obtainMessage(MSG_VR_MODE_CHANGED, enabled ? 1 : 0, 0) + mMainHandler.obtainMessage(MSG_VR_MODE_CHANGED, enabled ? 1 : 0, 0) .sendToTarget(); } }; - private final Handler mHandler = new Handler() { + private final Handler.Callback mHandlerCallback = new Handler.Callback() { @Override - public void handleMessage(Message msg) { + public boolean handleMessage(Message msg) { mExternalChange = true; try { switch (msg.what) { @@ -257,14 +267,18 @@ public class BrightnessController implements ToggleSlider.Listener, MirroredBrig updateVrMode(msg.arg1 != 0); break; default: - super.handleMessage(msg); + return false; + } } finally { mExternalChange = false; } + return true; } }; + private final Handler mMainHandler; + private final UserTracker.Callback mUserChangedCallback = new UserTracker.Callback() { @Override @@ -274,12 +288,17 @@ public class BrightnessController implements ToggleSlider.Listener, MirroredBrig } }; + @AssistedInject public BrightnessController( Context context, - ToggleSlider control, + @Assisted ToggleSlider control, UserTracker userTracker, DisplayTracker displayTracker, + DisplayManager displayManager, + SecureSettings secureSettings, + @Nullable IVrManager iVrManager, @Main Executor mainExecutor, + @Main Looper mainLooper, @Background Handler bgHandler) { mContext = context; mControl = control; @@ -288,22 +307,23 @@ public class BrightnessController implements ToggleSlider.Listener, MirroredBrig mBackgroundHandler = bgHandler; mUserTracker = userTracker; mDisplayTracker = displayTracker; - mBrightnessObserver = new BrightnessObserver(mHandler); - + mSecureSettings = secureSettings; mDisplayId = mContext.getDisplayId(); - PowerManager pm = context.getSystemService(PowerManager.class); + mDisplayManager = displayManager; + mVrManager = iVrManager; - mDisplayManager = context.getSystemService(DisplayManager.class); - mVrManager = IVrManager.Stub.asInterface(ServiceManager.getService( - Context.VR_SERVICE)); + mMainHandler = new Handler(mainLooper, mHandlerCallback); + mBrightnessObserver = new BrightnessObserver(mMainHandler); } public void registerCallbacks() { + mBackgroundHandler.removeCallbacks(mStartListeningRunnable); mBackgroundHandler.post(mStartListeningRunnable); } /** Unregister all call backs, both to and from the controller */ public void unregisterCallbacks() { + mBackgroundHandler.removeCallbacks(mStopListeningRunnable); mBackgroundHandler.post(mStopListeningRunnable); mControlValueInitialized = false; } @@ -418,38 +438,12 @@ public class BrightnessController implements ToggleSlider.Listener, MirroredBrig mSliderAnimator.start(); } - /** Factory for creating a {@link BrightnessController}. */ - public static class Factory { - private final Context mContext; - private final UserTracker mUserTracker; - private final DisplayTracker mDisplayTracker; - private final Executor mMainExecutor; - private final Handler mBackgroundHandler; - - @Inject - public Factory( - Context context, - UserTracker userTracker, - DisplayTracker displayTracker, - @Main Executor mainExecutor, - @Background Handler bgHandler) { - mContext = context; - mUserTracker = userTracker; - mDisplayTracker = displayTracker; - mMainExecutor = mainExecutor; - mBackgroundHandler = bgHandler; - } + + /** Factory for creating a {@link BrightnessController}. */ + @AssistedFactory + public interface Factory { /** Create a {@link BrightnessController} */ - public BrightnessController create(ToggleSlider toggleSlider) { - return new BrightnessController( - mContext, - toggleSlider, - mUserTracker, - mDisplayTracker, - mMainExecutor, - mBackgroundHandler); - } + BrightnessController create(ToggleSlider toggleSlider); } - } diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java index 182e4569b549..38b1f14e45de 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java @@ -23,7 +23,6 @@ import static android.view.WindowManagerPolicyConstants.EXTRA_FROM_BRIGHTNESS_KE import android.app.Activity; import android.graphics.Rect; import android.os.Bundle; -import android.os.Handler; import android.view.Gravity; import android.view.KeyEvent; import android.view.View; @@ -37,10 +36,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.systemui.R; -import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.settings.DisplayTracker; -import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper; import com.android.systemui.util.concurrency.DelayableExecutor; @@ -56,26 +52,21 @@ public class BrightnessDialog extends Activity { private BrightnessController mBrightnessController; private final BrightnessSliderController.Factory mToggleSliderFactory; - private final UserTracker mUserTracker; - private final DisplayTracker mDisplayTracker; + private final BrightnessController.Factory mBrightnessControllerFactory; private final DelayableExecutor mMainExecutor; - private final Handler mBackgroundHandler; private final AccessibilityManagerWrapper mAccessibilityMgr; private Runnable mCancelTimeoutRunnable; @Inject public BrightnessDialog( - UserTracker userTracker, - DisplayTracker displayTracker, - BrightnessSliderController.Factory factory, + BrightnessSliderController.Factory brightnessSliderfactory, + BrightnessController.Factory brightnessControllerFactory, @Main DelayableExecutor mainExecutor, - @Background Handler bgHandler, - AccessibilityManagerWrapper accessibilityMgr) { - mUserTracker = userTracker; - mDisplayTracker = displayTracker; - mToggleSliderFactory = factory; + AccessibilityManagerWrapper accessibilityMgr + ) { + mToggleSliderFactory = brightnessSliderfactory; + mBrightnessControllerFactory = brightnessControllerFactory; mMainExecutor = mainExecutor; - mBackgroundHandler = bgHandler; mAccessibilityMgr = accessibilityMgr; } @@ -121,8 +112,7 @@ public class BrightnessDialog extends Activity { controller.init(); frame.addView(controller.getRootView(), MATCH_PARENT, WRAP_CONTENT); - mBrightnessController = new BrightnessController( - this, controller, mUserTracker, mDisplayTracker, mMainExecutor, mBackgroundHandler); + mBrightnessController = mBrightnessControllerFactory.create(controller); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/shade/LockscreenHostedDreamGestureListener.kt b/packages/SystemUI/src/com/android/systemui/shade/LockscreenHostedDreamGestureListener.kt new file mode 100644 index 000000000000..45fc68a793bd --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/LockscreenHostedDreamGestureListener.kt @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shade + +import android.os.PowerManager +import android.view.GestureDetector +import android.view.MotionEvent +import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.data.repository.KeyguardRepository +import com.android.systemui.plugins.FalsingManager +import com.android.systemui.plugins.FalsingManager.LOW_PENALTY +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.statusbar.StatusBarState +import javax.inject.Inject + +/** + * This gestureListener will wake up by tap when the device is dreaming but not dozing, and the + * selected screensaver is hosted in lockscreen. Tap is gated by the falsing manager. + * + * Touches go through the [NotificationShadeWindowViewController]. + */ +@SysUISingleton +class LockscreenHostedDreamGestureListener +@Inject +constructor( + private val falsingManager: FalsingManager, + private val powerInteractor: PowerInteractor, + private val statusBarStateController: StatusBarStateController, + private val primaryBouncerInteractor: PrimaryBouncerInteractor, + private val keyguardRepository: KeyguardRepository, + private val shadeLogger: ShadeLogger, +) : GestureDetector.SimpleOnGestureListener() { + private val TAG = this::class.simpleName + + override fun onSingleTapUp(e: MotionEvent): Boolean { + if (shouldHandleMotionEvent()) { + if (!falsingManager.isFalseTap(LOW_PENALTY)) { + shadeLogger.d("$TAG#onSingleTapUp tap handled, requesting wakeUpIfDreaming") + powerInteractor.wakeUpIfDreaming( + "DREAMING_SINGLE_TAP", + PowerManager.WAKE_REASON_TAP + ) + } else { + shadeLogger.d("$TAG#onSingleTapUp false tap ignored") + } + return true + } + return false + } + + private fun shouldHandleMotionEvent(): Boolean { + return keyguardRepository.isActiveDreamLockscreenHosted.value && + statusBarStateController.state == StatusBarState.KEYGUARD && + !primaryBouncerInteractor.isBouncerShowing() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index ea15035a6c6f..9399d48be5f3 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -168,7 +168,6 @@ import com.android.systemui.statusbar.NotificationShadeDepthController; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.NotificationShelfController; import com.android.systemui.statusbar.PulseExpansionHandler; -import com.android.systemui.statusbar.RemoteInputController; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.VibratorHelper; @@ -224,6 +223,8 @@ import com.android.systemui.util.Utils; import com.android.systemui.util.time.SystemClock; import com.android.wm.shell.animation.FlingAnimationUtils; +import kotlin.Unit; + import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collections; @@ -234,8 +235,6 @@ import java.util.function.Consumer; import javax.inject.Inject; import javax.inject.Provider; -import kotlin.Unit; - import kotlinx.coroutines.CoroutineDispatcher; @SysUISingleton @@ -440,8 +439,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump private final FalsingCollector mFalsingCollector; private final ShadeHeadsUpTrackerImpl mShadeHeadsUpTracker = new ShadeHeadsUpTrackerImpl(); private final ShadeFoldAnimator mShadeFoldAnimator = new ShadeFoldAnimatorImpl(); - private final ShadeNotificationPresenterImpl mShadeNotificationPresenter = - new ShadeNotificationPresenterImpl(); private boolean mShowIconsWhenExpanded; private int mIndicationBottomPadding; @@ -1473,7 +1470,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump // so we should not add a padding for them stackScrollerPadding = 0; } else { - stackScrollerPadding = mQsController.getUnlockedStackScrollerPadding(); + stackScrollerPadding = mQsController.getHeaderHeight(); } } else { stackScrollerPadding = mClockPositionResult.stackScrollerPaddingExpanded; @@ -1520,7 +1517,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump userSwitcherPreferredY, darkAmount, mOverStretchAmount, bypassEnabled, - mQsController.getUnlockedStackScrollerPadding(), + mQsController.getHeaderHeight(), mQsController.computeExpansionFraction(), mDisplayTopInset, mSplitShadeEnabled, @@ -3323,23 +3320,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump ).printTableData(ipw); } - private final class ShadeNotificationPresenterImpl implements ShadeNotificationPresenter{ - @Override - public RemoteInputController.Delegate createRemoteInputDelegate() { - return mNotificationStackScrollLayoutController.createDelegate(); - } - - @Override - public boolean hasPulsingNotifications() { - return mNotificationListContainer.hasPulsingNotifications(); - } - } - - @Override - public ShadeNotificationPresenter getShadeNotificationPresenter() { - return mShadeNotificationPresenter; - } - @Override public void initDependencies( CentralSurfaces centralSurfaces, diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java index 5c41d572149c..108ea68ae8e0 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java @@ -16,6 +16,7 @@ package com.android.systemui.shade; +import static com.android.systemui.flags.Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED; import static com.android.systemui.flags.Flags.TRACKPAD_GESTURE_COMMON; import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow; @@ -46,6 +47,7 @@ import com.android.systemui.bouncer.ui.binder.KeyguardBouncerViewBinder; import com.android.systemui.bouncer.ui.viewmodel.KeyguardBouncerViewModel; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.compose.ComposeFacade; +import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dock.DockManager; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; @@ -72,7 +74,6 @@ import com.android.systemui.statusbar.notification.stack.NotificationStackScroll import com.android.systemui.statusbar.phone.CentralSurfaces; import com.android.systemui.statusbar.phone.PhoneStatusBarViewController; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; -import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent; import com.android.systemui.statusbar.window.StatusBarWindowStateController; import com.android.systemui.unfold.UnfoldTransitionProgressProvider; import com.android.systemui.util.time.SystemClock; @@ -87,7 +88,7 @@ import javax.inject.Provider; /** * Controller for {@link NotificationShadeWindowView}. */ -@CentralSurfacesComponent.CentralSurfacesScope +@SysUISingleton public class NotificationShadeWindowViewController { private static final String TAG = "NotifShadeWindowVC"; private final FalsingCollector mFalsingCollector; @@ -102,9 +103,12 @@ public class NotificationShadeWindowViewController { private final KeyguardUnlockAnimationController mKeyguardUnlockAnimationController; private final AmbientState mAmbientState; private final PulsingGestureListener mPulsingGestureListener; + private final LockscreenHostedDreamGestureListener mLockscreenHostedDreamGestureListener; private final NotificationInsetsController mNotificationInsetsController; private final boolean mIsTrackpadCommonEnabled; + private final FeatureFlags mFeatureFlags; private GestureDetector mPulsingWakeupGestureHandler; + private GestureDetector mDreamingWakeupGestureHandler; private View mBrightnessMirror; private boolean mTouchActive; private boolean mTouchCancelled; @@ -156,6 +160,7 @@ public class NotificationShadeWindowViewController { NotificationInsetsController notificationInsetsController, AmbientState ambientState, PulsingGestureListener pulsingGestureListener, + LockscreenHostedDreamGestureListener lockscreenHostedDreamGestureListener, KeyguardBouncerViewModel keyguardBouncerViewModel, KeyguardBouncerComponent.Factory keyguardBouncerComponentFactory, KeyguardMessageAreaController.Factory messageAreaControllerFactory, @@ -187,8 +192,10 @@ public class NotificationShadeWindowViewController { mKeyguardUnlockAnimationController = keyguardUnlockAnimationController; mAmbientState = ambientState; mPulsingGestureListener = pulsingGestureListener; + mLockscreenHostedDreamGestureListener = lockscreenHostedDreamGestureListener; mNotificationInsetsController = notificationInsetsController; mIsTrackpadCommonEnabled = featureFlags.isEnabled(TRACKPAD_GESTURE_COMMON); + mFeatureFlags = featureFlags; // This view is not part of the newly inflated expanded status bar. mBrightnessMirror = mView.findViewById(R.id.brightness_mirror_container); @@ -237,7 +244,10 @@ public class NotificationShadeWindowViewController { mStackScrollLayout = mView.findViewById(R.id.notification_stack_scroller); mPulsingWakeupGestureHandler = new GestureDetector(mView.getContext(), mPulsingGestureListener); - + if (mFeatureFlags.isEnabled(LOCKSCREEN_WALLPAPER_DREAM_ENABLED)) { + mDreamingWakeupGestureHandler = new GestureDetector(mView.getContext(), + mLockscreenHostedDreamGestureListener); + } mView.setLayoutInsetsController(mNotificationInsetsController); mView.setInteractionEventHandler(new NotificationShadeWindowView.InteractionEventHandler() { @Override @@ -291,6 +301,10 @@ public class NotificationShadeWindowViewController { mFalsingCollector.onTouchEvent(ev); mPulsingWakeupGestureHandler.onTouchEvent(ev); + if (mDreamingWakeupGestureHandler != null + && mDreamingWakeupGestureHandler.onTouchEvent(ev)) { + return true; + } if (mStatusBarKeyguardViewManager.dispatchTouchEvent(ev)) { return true; } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt index fba01201190e..5c1dd5670d8a 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt @@ -29,6 +29,8 @@ import androidx.constraintlayout.widget.ConstraintSet.TOP import com.android.systemui.R import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.fragments.FragmentService import com.android.systemui.navigationbar.NavigationModeController import com.android.systemui.plugins.qs.QS @@ -36,6 +38,7 @@ import com.android.systemui.plugins.qs.QSContainerController import com.android.systemui.recents.OverviewProxyService import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener import com.android.systemui.shared.system.QuickStepContract +import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController import com.android.systemui.util.LargeScreenUtils import com.android.systemui.util.ViewController import com.android.systemui.util.concurrency.DelayableExecutor @@ -54,7 +57,10 @@ class NotificationsQSContainerController @Inject constructor( private val shadeHeaderController: ShadeHeaderController, private val shadeExpansionStateManager: ShadeExpansionStateManager, private val fragmentService: FragmentService, - @Main private val delayableExecutor: DelayableExecutor + @Main private val delayableExecutor: DelayableExecutor, + private val featureFlags: FeatureFlags, + private val + notificationStackScrollLayoutController: NotificationStackScrollLayoutController, ) : ViewController<NotificationsQuickSettingsContainer>(view), QSContainerController { private var qsExpanded = false @@ -118,6 +124,9 @@ class NotificationsQSContainerController @Inject constructor( isGestureNavigation = QuickStepContract.isGesturalMode(mode) } isGestureNavigation = QuickStepContract.isGesturalMode(currentMode) + + mView.setStackScroller(notificationStackScrollLayoutController.getView()) + mView.setMigratingNSSL(featureFlags.isEnabled(Flags.MIGRATE_NSSL)) } public override fun onViewAttached() { @@ -254,14 +263,17 @@ class NotificationsQSContainerController @Inject constructor( } private fun setNotificationsConstraints(constraintSet: ConstraintSet) { + if (featureFlags.isEnabled(Flags.MIGRATE_NSSL)) { + return + } val startConstraintId = if (splitShadeEnabled) R.id.qs_edge_guideline else PARENT_ID + val nsslId = R.id.notification_stack_scroller constraintSet.apply { - connect(R.id.notification_stack_scroller, START, startConstraintId, START) - setMargin(R.id.notification_stack_scroller, START, - if (splitShadeEnabled) 0 else panelMarginHorizontal) - setMargin(R.id.notification_stack_scroller, END, panelMarginHorizontal) - setMargin(R.id.notification_stack_scroller, TOP, topMargin) - setMargin(R.id.notification_stack_scroller, BOTTOM, notificationsBottomMargin) + connect(nsslId, START, startConstraintId, START) + setMargin(nsslId, START, if (splitShadeEnabled) 0 else panelMarginHorizontal) + setMargin(nsslId, END, panelMarginHorizontal) + setMargin(nsslId, TOP, topMargin) + setMargin(nsslId, BOTTOM, notificationsBottomMargin) } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java index e5b84bd86514..3b3df50f1520 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java @@ -23,6 +23,7 @@ import android.graphics.Canvas; import android.graphics.Rect; import android.util.AttributeSet; import android.view.View; +import android.view.ViewGroup.MarginLayoutParams; import android.view.WindowInsets; import androidx.annotation.Nullable; @@ -56,6 +57,7 @@ public class NotificationsQuickSettingsContainer extends ConstraintLayout private QS mQs; private View mQSContainer; private int mLastQSPaddingBottom; + private boolean mIsMigratingNSSL; /** * These are used to compute the bounding box containing the shade and the notification scrim, @@ -75,10 +77,13 @@ public class NotificationsQuickSettingsContainer extends ConstraintLayout protected void onFinishInflate() { super.onFinishInflate(); mQsFrame = findViewById(R.id.qs_frame); - mStackScroller = findViewById(R.id.notification_stack_scroller); mKeyguardStatusBar = findViewById(R.id.keyguard_header); } + void setStackScroller(View stackScroller) { + mStackScroller = stackScroller; + } + @Override public void onFragmentViewCreated(String tag, Fragment fragment) { mQs = (QS) fragment; @@ -108,7 +113,7 @@ public class NotificationsQuickSettingsContainer extends ConstraintLayout } public void setNotificationsMarginBottom(int margin) { - LayoutParams params = (LayoutParams) mStackScroller.getLayoutParams(); + MarginLayoutParams params = (MarginLayoutParams) mStackScroller.getLayoutParams(); params.bottomMargin = margin; mStackScroller.setLayoutParams(params); } @@ -173,8 +178,15 @@ public class NotificationsQuickSettingsContainer extends ConstraintLayout super.dispatchDraw(canvas); } + void setMigratingNSSL(boolean isMigrating) { + mIsMigratingNSSL = isMigrating; + } + @Override protected boolean drawChild(Canvas canvas, View child, long drawingTime) { + if (mIsMigratingNSSL) { + return super.drawChild(canvas, child, drawingTime); + } int layoutIndex = mLayoutDrawingOrder.indexOf(child); if (layoutIndex >= 0) { return super.drawChild(canvas, mDrawingOrderedChildren.get(layoutIndex), drawingTime); diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java index 025c461110ef..baac57ca44ba 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsController.java @@ -233,7 +233,6 @@ public class QuickSettingsController implements Dumpable { private int mMaxExpansionHeight; /** Expansion fraction of the notification shade */ private float mShadeExpandedFraction; - private int mPeekHeight; private float mLastOverscroll; private boolean mExpansionFromOverscroll; private boolean mExpansionEnabledPolicy = true; @@ -429,7 +428,6 @@ public class QuickSettingsController implements Dumpable { final ViewConfiguration configuration = ViewConfiguration.get(this.mPanelView.getContext()); mTouchSlop = configuration.getScaledTouchSlop(); mSlopMultiplier = configuration.getScaledAmbiguousGestureMultiplier(); - mPeekHeight = mResources.getDimensionPixelSize(R.dimen.qs_peek_height); mStatusBarMinHeight = SystemBarUtils.getStatusBarHeight(mPanelView.getContext()); mScrimCornerRadius = mResources.getDimensionPixelSize( R.dimen.notification_scrim_corner_radius); @@ -500,12 +498,7 @@ public class QuickSettingsController implements Dumpable { } int getHeaderHeight() { - return mQs.getHeader().getHeight(); - } - - /** Returns the padding of the stackscroller when unlocked */ - int getUnlockedStackScrollerPadding() { - return (mQs != null ? mQs.getHeader().getHeight() : 0) + mPeekHeight; + return isQsFragmentCreated() ? mQs.getHeader().getHeight() : 0; } private boolean isRemoteInputActiveWithKeyboardUp() { @@ -2090,8 +2083,6 @@ public class QuickSettingsController implements Dumpable { ipw.println(mMaxExpansionHeight); ipw.print("mShadeExpandedFraction="); ipw.println(mShadeExpandedFraction); - ipw.print("mPeekHeight="); - ipw.println(mPeekHeight); ipw.print("mLastOverscroll="); ipw.println(mLastOverscroll); ipw.print("mExpansionFromOverscroll="); diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt index 6e78357b63f6..be21df1139ad 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt @@ -44,6 +44,7 @@ import com.android.systemui.statusbar.NotificationShelfController import com.android.systemui.statusbar.notification.row.dagger.NotificationShelfComponent import com.android.systemui.statusbar.notification.shelf.ui.viewbinder.NotificationShelfViewBinderWrapperControllerImpl import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout +import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer import com.android.systemui.statusbar.phone.KeyguardBottomAreaView import com.android.systemui.statusbar.phone.StatusBarLocation import com.android.systemui.statusbar.phone.StatusIconContainer @@ -185,6 +186,14 @@ abstract class ShadeModule { return notificationShadeWindowView.findViewById(R.id.keyguard_root_view) } + @Provides + @SysUISingleton + fun providesSharedNotificationContainer( + notificationShadeWindowView: NotificationShadeWindowView, + ): SharedNotificationContainer { + return notificationShadeWindowView.findViewById(R.id.shared_notification_container) + } + // TODO(b/277762009): Only allow this view's controller to inject the view. See above. @Provides @SysUISingleton diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeSurface.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeSurface.kt index 8d5c30b51677..2532bad1d7a7 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeSurface.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeSurface.kt @@ -18,6 +18,7 @@ package com.android.systemui.shade import android.view.ViewPropertyAnimator import com.android.systemui.statusbar.GestureRecorder import com.android.systemui.statusbar.NotificationShelfController +import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController import com.android.systemui.statusbar.phone.CentralSurfaces import com.android.systemui.statusbar.phone.HeadsUpManagerPhone @@ -63,6 +64,9 @@ interface ShadeSurface : ShadeViewController { /** Animates the view from its current alpha to zero then runs the runnable. */ fun fadeOut(startDelayMs: Long, durationMs: Long, endAction: Runnable): ViewPropertyAnimator + /** Returns the NSSL controller. */ + val notificationStackScrollLayoutController: NotificationStackScrollLayoutController + /** Set whether the bouncer is showing. */ fun setBouncerShowing(bouncerShowing: Boolean) diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt index 9aa5eb0cd68b..d5b5c87ec781 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt @@ -19,9 +19,7 @@ import android.view.MotionEvent import android.view.ViewGroup import android.view.ViewTreeObserver import com.android.systemui.keyguard.shared.model.WakefulnessModel -import com.android.systemui.statusbar.RemoteInputController import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow -import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController import com.android.systemui.statusbar.phone.HeadsUpAppearanceController import com.android.systemui.statusbar.phone.KeyguardStatusBarView import com.android.systemui.statusbar.phone.KeyguardStatusBarViewController @@ -141,9 +139,6 @@ interface ShadeViewController { /** Returns the StatusBarState. */ val barState: Int - /** Returns the NSSL controller. */ - val notificationStackScrollLayoutController: NotificationStackScrollLayoutController - /** Sets the amount of progress in the status bar launch animation. */ fun applyLaunchAnimationProgress(linearProgress: Float) @@ -261,9 +256,6 @@ interface ShadeViewController { /** Returns the ShadeFoldAnimator. */ val shadeFoldAnimator: ShadeFoldAnimator - /** Returns the ShadeNotificationPresenter. */ - val shadeNotificationPresenter: ShadeNotificationPresenter - companion object { /** * Returns a multiplicative factor to use when determining the falsing threshold for touches @@ -325,16 +317,7 @@ interface ShadeFoldAnimator { fun cancelFoldToAodAnimation() /** Returns the main view of the shade. */ - val view: ViewGroup -} - -/** Handles the shade's interactions with StatusBarNotificationPresenter. */ -interface ShadeNotificationPresenter { - /** Returns a new delegate for some view controller pieces of the remote input process. */ - fun createRemoteInputDelegate(): RemoteInputController.Delegate - - /** Returns whether the screen has temporarily woken up to display notifications. */ - fun hasPulsingNotifications(): Boolean + val view: ViewGroup? } /** diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt new file mode 100644 index 000000000000..287ac528385f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shade + +import android.view.MotionEvent +import android.view.ViewGroup +import android.view.ViewTreeObserver +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow +import com.android.systemui.statusbar.phone.HeadsUpAppearanceController +import java.util.function.Consumer +import javax.inject.Inject + +/** Empty implementation of ShadeViewController for variants with no shade. */ +class ShadeViewControllerEmptyImpl @Inject constructor() : ShadeViewController { + override fun expand(animate: Boolean) {} + override fun expandToQs() {} + override fun expandToNotifications() {} + override val isExpandingOrCollapsing: Boolean = false + override val isExpanded: Boolean = false + override val isPanelExpanded: Boolean = false + override val isShadeFullyExpanded: Boolean = false + override fun collapse(delayed: Boolean, speedUpFactor: Float) {} + override fun collapse(animate: Boolean, delayed: Boolean, speedUpFactor: Float) {} + override fun collapseWithDuration(animationDuration: Int) {} + override fun instantCollapse() {} + override fun animateCollapseQs(fullyCollapse: Boolean) {} + override fun canBeCollapsed(): Boolean = false + override val isCollapsing: Boolean = false + override val isFullyCollapsed: Boolean = false + override val isTracking: Boolean = false + override val isViewEnabled: Boolean = false + override fun setOpenCloseListener(openCloseListener: OpenCloseListener) {} + override fun shouldHideStatusBarIconsWhenExpanded() = false + override fun blockExpansionForCurrentTouch() {} + override fun setTrackingStartedListener(trackingStartedListener: TrackingStartedListener) {} + override fun disableHeader(state1: Int, state2: Int, animated: Boolean) {} + override fun startExpandLatencyTracking() {} + override fun startBouncerPreHideAnimation() {} + override fun dozeTimeTick() {} + override fun resetViews(animate: Boolean) {} + override val barState: Int = 0 + override fun applyLaunchAnimationProgress(linearProgress: Float) {} + override fun closeUserSwitcherIfOpen(): Boolean { + return false + } + override fun onBackPressed() {} + override fun setIsLaunchAnimationRunning(running: Boolean) {} + override fun setAlpha(alpha: Int, animate: Boolean) {} + override fun setAlphaChangeAnimationEndAction(r: Runnable) {} + override fun setPulsing(pulsing: Boolean) {} + override fun setQsScrimEnabled(qsScrimEnabled: Boolean) {} + override fun setAmbientIndicationTop(ambientIndicationTop: Int, ambientTextVisible: Boolean) {} + override fun updateSystemUiStateFlags() {} + override fun updateTouchableRegion() {} + override fun addOnGlobalLayoutListener(listener: ViewTreeObserver.OnGlobalLayoutListener) {} + override fun removeOnGlobalLayoutListener(listener: ViewTreeObserver.OnGlobalLayoutListener) {} + override fun postToView(action: Runnable): Boolean { + return false + } + override fun transitionToExpandedShade(delay: Long) {} + override val isUnlockHintRunning: Boolean = false + + override fun resetViewGroupFade() {} + override fun setKeyguardTransitionProgress(keyguardAlpha: Float, keyguardTranslationY: Int) {} + override fun setOverStretchAmount(amount: Float) {} + override fun setKeyguardStatusBarAlpha(alpha: Float) {} + override fun showAodUi() {} + override fun isFullyExpanded(): Boolean { + return false + } + override fun handleExternalTouch(event: MotionEvent): Boolean { + return false + } + override fun startTrackingExpansionFromStatusBar() {} + override val shadeHeadsUpTracker = ShadeHeadsUpTrackerEmptyImpl() + override val shadeFoldAnimator = ShadeFoldAnimatorEmptyImpl() +} + +class ShadeHeadsUpTrackerEmptyImpl : ShadeHeadsUpTracker { + override fun addTrackingHeadsUpListener(listener: Consumer<ExpandableNotificationRow>) {} + override fun removeTrackingHeadsUpListener(listener: Consumer<ExpandableNotificationRow>) {} + override fun setHeadsUpAppearanceController( + headsUpAppearanceController: HeadsUpAppearanceController? + ) {} + override val trackedHeadsUpNotification: ExpandableNotificationRow? = null +} + +class ShadeFoldAnimatorEmptyImpl : ShadeFoldAnimator { + override fun prepareFoldToAodAnimation() {} + override fun startFoldToAodAnimation( + startAction: Runnable, + endAction: Runnable, + cancelAction: Runnable, + ) {} + override fun cancelFoldToAodAnimation() {} + override val view: ViewGroup? = null +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/DreamCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/DreamCoordinator.kt new file mode 100644 index 000000000000..d268e358690f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/DreamCoordinator.kt @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2023 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.collection.coordinator + +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyguard.data.repository.KeyguardRepository +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.statusbar.StatusBarState +import com.android.systemui.statusbar.SysuiStatusBarStateController +import com.android.systemui.statusbar.notification.collection.NotifPipeline +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope +import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +/** + * Filter out notifications on the lockscreen if the lockscreen hosted dream is active. If the user + * stops dreaming, pulls the shade down or unlocks the device, then the notifications are unhidden. + */ +@CoordinatorScope +class DreamCoordinator +@Inject +constructor( + private val statusBarStateController: SysuiStatusBarStateController, + @Application private val scope: CoroutineScope, + private val keyguardRepository: KeyguardRepository, +) : Coordinator { + private var isOnKeyguard = false + private var isLockscreenHostedDream = false + + override fun attach(pipeline: NotifPipeline) { + pipeline.addPreGroupFilter(filter) + statusBarStateController.addCallback(statusBarStateListener) + scope.launch { attachFilterOnDreamingStateChange() } + recordStatusBarState(statusBarStateController.state) + } + + private val filter = + object : NotifFilter("LockscreenHostedDreamFilter") { + var isFiltering = false + override fun shouldFilterOut(entry: NotificationEntry, now: Long): Boolean { + return isFiltering + } + inline fun update(msg: () -> String) { + val wasFiltering = isFiltering + isFiltering = isLockscreenHostedDream && isOnKeyguard + if (wasFiltering != isFiltering) { + invalidateList(msg()) + } + } + } + + private val statusBarStateListener = + object : StatusBarStateController.StateListener { + override fun onStateChanged(newState: Int) { + recordStatusBarState(newState) + } + } + + private suspend fun attachFilterOnDreamingStateChange() { + keyguardRepository.isActiveDreamLockscreenHosted.collect { isDreaming -> + recordDreamingState(isDreaming) + } + } + + private fun recordStatusBarState(newState: Int) { + isOnKeyguard = newState == StatusBarState.KEYGUARD + filter.update { "recordStatusBarState: " + StatusBarState.toString(newState) } + } + + private fun recordDreamingState(isDreaming: Boolean) { + isLockscreenHostedDream = isDreaming + filter.update { "recordLockscreenHostedDreamState: $isDreaming" } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt index e5953cfc07cd..0ccab9e46b72 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt @@ -15,6 +15,8 @@ */ package com.android.systemui.statusbar.notification.collection.coordinator +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED import com.android.systemui.statusbar.notification.collection.NotifPipeline import com.android.systemui.statusbar.notification.collection.PipelineDumpable import com.android.systemui.statusbar.notification.collection.PipelineDumper @@ -31,32 +33,34 @@ interface NotifCoordinators : Coordinator, PipelineDumpable @CoordinatorScope class NotifCoordinatorsImpl @Inject constructor( - sectionStyleProvider: SectionStyleProvider, - dataStoreCoordinator: DataStoreCoordinator, - hideLocallyDismissedNotifsCoordinator: HideLocallyDismissedNotifsCoordinator, - hideNotifsForOtherUsersCoordinator: HideNotifsForOtherUsersCoordinator, - keyguardCoordinator: KeyguardCoordinator, - rankingCoordinator: RankingCoordinator, - appOpsCoordinator: AppOpsCoordinator, - deviceProvisionedCoordinator: DeviceProvisionedCoordinator, - bubbleCoordinator: BubbleCoordinator, - headsUpCoordinator: HeadsUpCoordinator, - gutsCoordinator: GutsCoordinator, - conversationCoordinator: ConversationCoordinator, - debugModeCoordinator: DebugModeCoordinator, - groupCountCoordinator: GroupCountCoordinator, - groupWhenCoordinator: GroupWhenCoordinator, - mediaCoordinator: MediaCoordinator, - preparationCoordinator: PreparationCoordinator, - remoteInputCoordinator: RemoteInputCoordinator, - rowAppearanceCoordinator: RowAppearanceCoordinator, - stackCoordinator: StackCoordinator, - shadeEventCoordinator: ShadeEventCoordinator, - smartspaceDedupingCoordinator: SmartspaceDedupingCoordinator, - viewConfigCoordinator: ViewConfigCoordinator, - visualStabilityCoordinator: VisualStabilityCoordinator, - sensitiveContentCoordinator: SensitiveContentCoordinator, - dismissibilityCoordinator: DismissibilityCoordinator, + sectionStyleProvider: SectionStyleProvider, + featureFlags: FeatureFlags, + dataStoreCoordinator: DataStoreCoordinator, + hideLocallyDismissedNotifsCoordinator: HideLocallyDismissedNotifsCoordinator, + hideNotifsForOtherUsersCoordinator: HideNotifsForOtherUsersCoordinator, + keyguardCoordinator: KeyguardCoordinator, + rankingCoordinator: RankingCoordinator, + appOpsCoordinator: AppOpsCoordinator, + deviceProvisionedCoordinator: DeviceProvisionedCoordinator, + bubbleCoordinator: BubbleCoordinator, + headsUpCoordinator: HeadsUpCoordinator, + gutsCoordinator: GutsCoordinator, + conversationCoordinator: ConversationCoordinator, + debugModeCoordinator: DebugModeCoordinator, + groupCountCoordinator: GroupCountCoordinator, + groupWhenCoordinator: GroupWhenCoordinator, + mediaCoordinator: MediaCoordinator, + preparationCoordinator: PreparationCoordinator, + remoteInputCoordinator: RemoteInputCoordinator, + rowAppearanceCoordinator: RowAppearanceCoordinator, + stackCoordinator: StackCoordinator, + shadeEventCoordinator: ShadeEventCoordinator, + smartspaceDedupingCoordinator: SmartspaceDedupingCoordinator, + viewConfigCoordinator: ViewConfigCoordinator, + visualStabilityCoordinator: VisualStabilityCoordinator, + sensitiveContentCoordinator: SensitiveContentCoordinator, + dismissibilityCoordinator: DismissibilityCoordinator, + dreamCoordinator: DreamCoordinator, ) : NotifCoordinators { private val mCoreCoordinators: MutableList<CoreCoordinator> = ArrayList() @@ -96,6 +100,10 @@ class NotifCoordinatorsImpl @Inject constructor( mCoordinators.add(remoteInputCoordinator) mCoordinators.add(dismissibilityCoordinator) + if (featureFlags.isEnabled(LOCKSCREEN_WALLPAPER_DREAM_ENABLED)) { + mCoordinators.add(dreamCoordinator) + } + // Manually add Ordered Sections mOrderedSections.add(headsUpCoordinator.sectioner) // HeadsUp mOrderedSections.add(appOpsCoordinator.sectioner) // ForegroundService diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowModule.java index b2a3780c1024..867e08b2e743 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowModule.java @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.notification.row; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.Flags; import dagger.Binds; import dagger.Module; @@ -58,9 +59,13 @@ public abstract class NotificationRowModule { @ElementsIntoSet @Named(NOTIF_REMOTEVIEWS_FACTORIES) static Set<NotifRemoteViewsFactory> provideNotifRemoteViewsFactories( - FeatureFlags featureFlags + FeatureFlags featureFlags, + PrecomputedTextViewFactory precomputedTextViewFactory ) { final Set<NotifRemoteViewsFactory> replacementFactories = new HashSet<>(); + if (featureFlags.isEnabled(Flags.PRECOMPUTED_TEXT)) { + replacementFactories.add(precomputedTextViewFactory); + } return replacementFactories; } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PrecomputedImageFloatingTextView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PrecomputedImageFloatingTextView.kt new file mode 100644 index 000000000000..57c82dc8d47a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PrecomputedImageFloatingTextView.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2023 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.row + +import android.content.Context +import android.util.AttributeSet +import android.widget.RemoteViews +import com.android.internal.widget.ImageFloatingTextView + +/** Precomputed version of [ImageFloatingTextView] */ +@RemoteViews.RemoteView +class PrecomputedImageFloatingTextView +@JvmOverloads +constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : + ImageFloatingTextView(context, attrs, defStyleAttr), TextPrecomputer { + + override fun setTextAsync(text: CharSequence?): Runnable = precompute(this, text) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PrecomputedTextView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PrecomputedTextView.kt new file mode 100644 index 000000000000..8508b1fd8e6b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PrecomputedTextView.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2023 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.row + +import android.content.Context +import android.util.AttributeSet +import android.widget.RemoteViews +import android.widget.TextView + +/** + * A user interface element that uses the PrecomputedText API to display text in a notification, + * with the help of RemoteViews. + */ +@RemoteViews.RemoteView +class PrecomputedTextView +@JvmOverloads +constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : + TextView(context, attrs, defStyleAttr), TextPrecomputer { + + override fun setTextAsync(text: CharSequence?): Runnable = precompute(this, text) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PrecomputedTextViewFactory.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PrecomputedTextViewFactory.kt new file mode 100644 index 000000000000..b0023305ecdd --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PrecomputedTextViewFactory.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2023 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.row + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.widget.TextView +import com.android.internal.widget.ImageFloatingTextView +import javax.inject.Inject + +class PrecomputedTextViewFactory @Inject constructor() : NotifRemoteViewsFactory { + override fun instantiate( + parent: View?, + name: String, + context: Context, + attrs: AttributeSet + ): View? { + return when (name) { + TextView::class.java.name, + TextView::class.java.simpleName -> PrecomputedTextView(context, attrs) + ImageFloatingTextView::class.java.name -> + PrecomputedImageFloatingTextView(context, attrs) + else -> null + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/TextPrecomputer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/TextPrecomputer.kt new file mode 100644 index 000000000000..49f4a33ed3e9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/TextPrecomputer.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2023 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.row + +import android.text.PrecomputedText +import android.text.Spannable +import android.util.Log +import android.widget.TextView + +internal interface TextPrecomputer { + /** + * Creates PrecomputedText from given text and returns a runnable which sets precomputed text to + * the textview on main thread. + * + * @param text text to be converted to PrecomputedText + * @return Runnable that sets precomputed text on the main thread + */ + fun precompute( + textView: TextView, + text: CharSequence?, + logException: Boolean = true + ): Runnable { + val precomputedText: Spannable? = + text?.let { PrecomputedText.create(it, textView.textMetricsParams) } + + return Runnable { + try { + textView.text = precomputedText + } catch (exception: IllegalArgumentException) { + if (logException) { + Log.wtf( + /* tag = */ TAG, + /* msg = */ "PrecomputedText setText failed for TextView:$textView", + /* tr = */ exception + ) + } + textView.text = text + } + } + } + + private companion object { + private const val TAG = "TextPrecomputer" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt new file mode 100644 index 000000000000..874450b36bf0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2023 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.domain.interactor + +import android.content.Context +import com.android.systemui.R +import com.android.systemui.common.ui.data.repository.ConfigurationRepository +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart + +/** Encapsulates business-logic specifically related to the shared notification stack container. */ +class SharedNotificationContainerInteractor +@Inject +constructor( + configurationRepository: ConfigurationRepository, + private val context: Context, +) { + val configurationBasedDimensions: Flow<ConfigurationBasedDimensions> = + configurationRepository.onAnyConfigurationChange + .onStart { emit(Unit) } + .map { _ -> + with(context.resources) { + ConfigurationBasedDimensions( + useSplitShade = getBoolean(R.bool.config_use_split_notification_shade), + useLargeScreenHeader = + getBoolean(R.bool.config_use_large_screen_shade_header), + marginHorizontal = + getDimensionPixelSize(R.dimen.notification_panel_margin_horizontal), + marginBottom = + getDimensionPixelSize(R.dimen.notification_panel_margin_bottom), + marginTop = getDimensionPixelSize(R.dimen.notification_panel_margin_top), + marginTopLargeScreen = + getDimensionPixelSize(R.dimen.large_screen_shade_header_height), + ) + } + } + .distinctUntilChanged() + + data class ConfigurationBasedDimensions( + val useSplitShade: Boolean, + val useLargeScreenHeader: Boolean, + val marginHorizontal: Int, + val marginBottom: Int, + val marginTop: Int, + val marginTopLargeScreen: Int, + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/SharedNotificationContainer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/SharedNotificationContainer.kt new file mode 100644 index 000000000000..688843de06f2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/SharedNotificationContainer.kt @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2023 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.view + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.constraintlayout.widget.ConstraintSet +import androidx.constraintlayout.widget.ConstraintSet.BOTTOM +import androidx.constraintlayout.widget.ConstraintSet.END +import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID +import androidx.constraintlayout.widget.ConstraintSet.START +import androidx.constraintlayout.widget.ConstraintSet.TOP +import androidx.constraintlayout.widget.ConstraintSet.VERTICAL +import com.android.systemui.R + +/** + * Container for the stack scroller, so that the bounds can be externally specified, such as from + * the keyguard or shade scenes. + */ +class SharedNotificationContainer( + context: Context, + private val attrs: AttributeSet?, +) : + ConstraintLayout( + context, + attrs, + ) { + + private val baseConstraintSet = ConstraintSet() + + init { + baseConstraintSet.apply { + create(R.id.nssl_guideline, VERTICAL) + setGuidelinePercent(R.id.nssl_guideline, 0.5f) + } + baseConstraintSet.applyTo(this) + } + + fun addNotificationStackScrollLayout(nssl: View) { + addView(nssl) + } + + fun updateConstraints( + useSplitShade: Boolean, + marginStart: Int, + marginTop: Int, + marginEnd: Int, + marginBottom: Int + ) { + val constraintSet = ConstraintSet() + constraintSet.clone(baseConstraintSet) + + val startConstraintId = + if (useSplitShade) { + R.id.nssl_guideline + } else { + PARENT_ID + } + val nsslId = R.id.notification_stack_scroller + constraintSet.apply { + connect(nsslId, START, startConstraintId, START) + connect(nsslId, END, PARENT_ID, END) + connect(nsslId, BOTTOM, PARENT_ID, BOTTOM) + connect(nsslId, TOP, PARENT_ID, TOP) + setMargin(nsslId, START, marginStart) + setMargin(nsslId, END, marginEnd) + setMargin(nsslId, TOP, marginTop) + setMargin(nsslId, BOTTOM, marginBottom) + } + constraintSet.applyTo(this) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt new file mode 100644 index 000000000000..fb1d55d6bb7b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2023 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.viewbinder + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer +import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel +import kotlinx.coroutines.launch + +/** Binds the shared notification container to its view-model. */ +object SharedNotificationContainerBinder { + + @JvmStatic + fun bind( + view: SharedNotificationContainer, + viewModel: SharedNotificationContainerViewModel, + ) { + view.repeatWhenAttached { + repeatOnLifecycle(Lifecycle.State.STARTED) { + launch { + viewModel.configurationBasedDimensions.collect { + view.updateConstraints( + useSplitShade = it.useSplitShade, + marginStart = it.marginStart, + marginTop = it.marginTop, + marginEnd = it.marginEnd, + marginBottom = it.marginBottom, + ) + } + } + } + } + } +} 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 new file mode 100644 index 000000000000..b2e5ac116e46 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2023 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.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map + +/** View-model for the shared notification container */ +class SharedNotificationContainerViewModel +@Inject +constructor( + interactor: SharedNotificationContainerInteractor, +) { + val configurationBasedDimensions: Flow<ConfigurationBasedDimensions> = + interactor.configurationBasedDimensions + .map { + ConfigurationBasedDimensions( + marginStart = if (it.useSplitShade) 0 else it.marginHorizontal, + marginEnd = it.marginHorizontal, + marginBottom = it.marginBottom, + marginTop = + if (it.useLargeScreenHeader) it.marginTopLargeScreen else it.marginTop, + useSplitShade = it.useSplitShade, + ) + } + .distinctUntilChanged() + + data class ConfigurationBasedDimensions( + val marginStart: Int, + val marginTop: Int, + val marginEnd: Int, + val marginBottom: Int, + val useSplitShade: Boolean, + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java index 66d217b34ba4..dc021feffd63 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java @@ -160,6 +160,7 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp private KeyguardViewController mKeyguardViewController; private DozeScrimController mDozeScrimController; private KeyguardViewMediator mKeyguardViewMediator; + private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; private PendingAuthenticated mPendingAuthenticated = null; private boolean mHasScreenTurnedOnSinceAuthenticating; private boolean mFadedAwayAfterWakeAndUnlock; @@ -280,7 +281,8 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp LatencyTracker latencyTracker, ScreenOffAnimationController screenOffAnimationController, VibratorHelper vibrator, - SystemClock systemClock + SystemClock systemClock, + StatusBarKeyguardViewManager statusBarKeyguardViewManager ) { mPowerManager = powerManager; mUpdateMonitor = keyguardUpdateMonitor; @@ -308,6 +310,7 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp mVibratorHelper = vibrator; mLogger = biometricUnlockLogger; mSystemClock = systemClock; + mStatusBarKeyguardViewManager = statusBarKeyguardViewManager; dumpManager.registerDumpable(this); } @@ -449,8 +452,19 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp // During wake and unlock, we need to draw black before waking up to avoid abrupt // brightness changes due to display state transitions. Runnable wakeUp = ()-> { - if (!wasDeviceInteractive || mUpdateMonitor.isDreaming()) { + // Check to see if we are still locked when we are waking and unlocking from dream. + // This runnable should be executed after unlock. If that's true, we could be not + // dreaming, but still locked. In this case, we should attempt to authenticate instead + // of waking up. + if (mode == MODE_WAKE_AND_UNLOCK_FROM_DREAM + && !mKeyguardStateController.isUnlocked() + && !mUpdateMonitor.isDreaming()) { + // Post wakeUp runnable is called from a callback in keyguard. + mHandler.post(() -> mKeyguardViewController.notifyKeyguardAuthenticated( + false /* primaryAuth */)); + } else if (!wasDeviceInteractive || mUpdateMonitor.isDreaming()) { mLogger.i("bio wakelock: Authenticated, waking up..."); + mPowerManager.wakeUp( mSystemClock.uptimeMillis(), PowerManager.WAKE_REASON_BIOMETRIC, @@ -462,7 +476,7 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp Trace.endSection(); }; - if (mMode != MODE_NONE) { + if (mMode != MODE_NONE && mMode != MODE_WAKE_AND_UNLOCK_FROM_DREAM) { wakeUp.run(); } switch (mMode) { @@ -484,6 +498,10 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp Trace.endSection(); break; case MODE_WAKE_AND_UNLOCK_FROM_DREAM: + // In the case of waking and unlocking from dream, waking up is delayed until after + // unlock is complete to avoid conflicts during each sequence's transitions. + mStatusBarKeyguardViewManager.addAfterKeyguardGoneRunnable(wakeUp); + // Execution falls through here to proceed unlocking. case MODE_WAKE_AND_UNLOCK_PULSING: case MODE_WAKE_AND_UNLOCK: if (mMode == MODE_WAKE_AND_UNLOCK_PULSING) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index 0a7ee52fba46..5fb729cb26f1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -413,7 +413,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { private final Point mCurrentDisplaySize = new Point(); - protected NotificationShadeWindowView mNotificationShadeWindowView; protected PhoneStatusBarView mStatusBarView; private PhoneStatusBarViewController mPhoneStatusBarViewController; private PhoneStatusBarTransitions mStatusBarTransitions; @@ -456,7 +455,8 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { private final FalsingManager mFalsingManager; private final BroadcastDispatcher mBroadcastDispatcher; private final ConfigurationController mConfigurationController; - protected NotificationShadeWindowViewController mNotificationShadeWindowViewController; + private final Lazy<NotificationShadeWindowViewController> + mNotificationShadeWindowViewControllerLazy; private final DozeParameters mDozeParameters; private final Lazy<BiometricUnlockController> mBiometricUnlockControllerLazy; private final CentralSurfacesComponent.Factory mCentralSurfacesComponentFactory; @@ -722,6 +722,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { Lazy<AssistManager> assistManagerLazy, ConfigurationController configurationController, NotificationShadeWindowController notificationShadeWindowController, + Lazy<NotificationShadeWindowViewController> notificationShadeWindowViewControllerLazy, NotificationShelfController notificationShelfController, NotificationStackScrollLayoutController notificationStackScrollLayoutController, DozeParameters dozeParameters, @@ -825,6 +826,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mAssistManagerLazy = assistManagerLazy; mConfigurationController = configurationController; mNotificationShadeWindowController = notificationShadeWindowController; + mNotificationShadeWindowViewControllerLazy = notificationShadeWindowViewControllerLazy; mNotificationShelfController = notificationShelfController; mStackScrollerController = notificationStackScrollLayoutController; mStackScroller = mStackScrollerController.getView(); @@ -1073,7 +1075,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mDozeServiceHost.initialize( this, mStatusBarKeyguardViewManager, - mNotificationShadeWindowViewController, + getNotificationShadeWindowViewController(), mShadeSurface, mAmbientIndicationContainer); updateLightRevealScrimVisibility(); @@ -1235,8 +1237,8 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { updateTheme(); inflateStatusBarWindow(); - mNotificationShadeWindowView.setOnTouchListener(getStatusBarWindowTouchListener()); - mWallpaperController.setRootView(mNotificationShadeWindowView); + getNotificationShadeWindowView().setOnTouchListener(getStatusBarWindowTouchListener()); + mWallpaperController.setRootView(getNotificationShadeWindowView()); // TODO: Deal with the ugliness that comes from having some of the status bar broken out // into fragments, but the rest here, it leaves some awkward lifecycle and whatnot. @@ -1257,7 +1259,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mStatusBarView = statusBarView; mPhoneStatusBarViewController = statusBarViewController; mStatusBarTransitions = statusBarTransitions; - mNotificationShadeWindowViewController + getNotificationShadeWindowViewController() .setStatusBarViewController(mPhoneStatusBarViewController); // Ensure we re-propagate panel expansion values to the panel controller and // any listeners it may have, such as PanelBar. This will also ensure we @@ -1271,7 +1273,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mStatusBarInitializer.initializeStatusBar( mCentralSurfacesComponent::createCollapsedStatusBarFragment); - mStatusBarTouchableRegionManager.setup(this, mNotificationShadeWindowView); + mStatusBarTouchableRegionManager.setup(this, getNotificationShadeWindowView()); createNavigationBar(result); @@ -1279,7 +1281,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mLockscreenWallpaper = mLockscreenWallpaperLazy.get(); } - mAmbientIndicationContainer = mNotificationShadeWindowView.findViewById( + mAmbientIndicationContainer = getNotificationShadeWindowView().findViewById( R.id.ambient_indication_container); mAutoHideController.setStatusBar(new AutoHideUiElement() { @@ -1304,10 +1306,10 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { } }); - ScrimView scrimBehind = mNotificationShadeWindowView.findViewById(R.id.scrim_behind); - ScrimView notificationsScrim = mNotificationShadeWindowView + ScrimView scrimBehind = getNotificationShadeWindowView().findViewById(R.id.scrim_behind); + ScrimView notificationsScrim = getNotificationShadeWindowView() .findViewById(R.id.scrim_notifications); - ScrimView scrimInFront = mNotificationShadeWindowView.findViewById(R.id.scrim_in_front); + ScrimView scrimInFront = getNotificationShadeWindowView().findViewById(R.id.scrim_in_front); mScrimController.setScrimVisibleListener(scrimsVisible -> { mNotificationShadeWindowController.setScrimsVisibility(scrimsVisible); @@ -1345,7 +1347,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mNotificationShelfController, mHeadsUpManager); - BackDropView backdrop = mNotificationShadeWindowView.findViewById(R.id.backdrop); + BackDropView backdrop = getNotificationShadeWindowView().findViewById(R.id.backdrop); if (mWallpaperManager.isLockscreenLiveWallpaperEnabled()) { mMediaManager.setup(null, null, null, mScrimController, null); } else { @@ -1364,7 +1366,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { }); // Set up the quick settings tile panel - final View container = mNotificationShadeWindowView.findViewById(R.id.qs_frame); + final View container = getNotificationShadeWindowView().findViewById(R.id.qs_frame); if (container != null) { FragmentHostManager fragmentHostManager = mFragmentService.getFragmentHostManager(container); @@ -1379,7 +1381,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { .withDefault(this::createDefaultQSFragment) .build()); mBrightnessMirrorController = new BrightnessMirrorController( - mNotificationShadeWindowView, + getNotificationShadeWindowView(), mShadeSurface, mNotificationShadeDepthControllerLazy.get(), mBrightnessSliderFactory, @@ -1396,7 +1398,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { }); } - mReportRejectedTouch = mNotificationShadeWindowView + mReportRejectedTouch = getNotificationShadeWindowView() .findViewById(R.id.report_rejected_touch); if (mReportRejectedTouch != null) { updateReportRejectedTouchVisibility(); @@ -1544,7 +1546,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { protected QS createDefaultQSFragment() { return mFragmentService - .getFragmentHostManager(mNotificationShadeWindowView) + .getFragmentHostManager(getNotificationShadeWindowView()) .create(QSFragment.class); } @@ -1553,7 +1555,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mActivityLaunchAnimator.setCallback(mActivityLaunchAnimatorCallback); mActivityLaunchAnimator.addListener(mActivityLaunchAnimatorListener); mNotificationAnimationProvider = new NotificationLaunchAnimatorControllerProvider( - mNotificationShadeWindowViewController, + getNotificationShadeWindowViewController(), mNotifListContainer, mHeadsUpManager, mJankMonitor); @@ -1592,7 +1594,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mAutoHideController.checkUserAutoHide(event); mRemoteInputManager.checkRemoteInputOutside(event); mShadeController.onStatusBarTouch(event); - return mNotificationShadeWindowView.onTouchEvent(event); + return getNotificationShadeWindowView().onTouchEvent(event); }; } @@ -1606,15 +1608,12 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mCentralSurfacesComponent::createCollapsedStatusBarFragment); ViewGroup windowRootView = mCentralSurfacesComponent.getWindowRootView(); - mNotificationShadeWindowView = mCentralSurfacesComponent.getNotificationShadeWindowView(); - mNotificationShadeWindowViewController = mCentralSurfacesComponent - .getNotificationShadeWindowViewController(); // TODO(b/277762009): Inject [NotificationShadeWindowView] directly into the controller. // (Right now, there's a circular dependency.) mNotificationShadeWindowController.setWindowRootView(windowRootView); - mNotificationShadeWindowViewController.setupExpandedStatusBar(); + getNotificationShadeWindowViewController().setupExpandedStatusBar(); mShadeController.setNotificationShadeWindowViewController( - mNotificationShadeWindowViewController); + getNotificationShadeWindowViewController()); mBackActionInteractor.setup(mQsController, mShadeSurface); mPresenter = mCentralSurfacesComponent.getNotificationPresenter(); mNotificationActivityStarter = mCentralSurfacesComponent.getNotificationActivityStarter(); @@ -1633,6 +1632,14 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mCommandQueue.addCallback(mCommandQueueCallbacks); } + protected NotificationShadeWindowViewController getNotificationShadeWindowViewController() { + return mNotificationShadeWindowViewControllerLazy.get(); + } + + protected NotificationShadeWindowView getNotificationShadeWindowView() { + return getNotificationShadeWindowViewController().getView(); + } + protected void startKeyguard() { Trace.beginSection("CentralSurfaces#startKeyguard"); mStatusBarStateController.addCallback(mStateListener, @@ -1688,7 +1695,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { @Override public AuthKeyguardMessageArea getKeyguardMessageArea() { - return mNotificationShadeWindowViewController.getKeyguardMessageArea(); + return getNotificationShadeWindowViewController().getKeyguardMessageArea(); } @Override @@ -1982,11 +1989,9 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { pw.print(" mWallpaperSupported= "); pw.println(mWallpaperSupported); pw.println(" ShadeWindowView: "); - if (mNotificationShadeWindowViewController != null) { - mNotificationShadeWindowViewController.dump(pw, args); - CentralSurfaces.dumpBarTransitions( - pw, "PhoneStatusBarTransitions", mStatusBarTransitions); - } + getNotificationShadeWindowViewController().dump(pw, args); + CentralSurfaces.dumpBarTransitions( + pw, "PhoneStatusBarTransitions", mStatusBarTransitions); pw.println(" mMediaManager: "); if (mMediaManager != null) { @@ -2860,7 +2865,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { updateVisibleToUser(); updateNotificationPanelTouchState(); - mNotificationShadeWindowViewController.cancelCurrentTouch(); + getNotificationShadeWindowViewController().cancelCurrentTouch(); if (mLaunchCameraOnFinishedGoingToSleep) { mLaunchCameraOnFinishedGoingToSleep = false; @@ -3172,12 +3177,6 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { updateScrimController(); } - @VisibleForTesting - public void setNotificationShadeWindowViewController( - NotificationShadeWindowViewController nswvc) { - mNotificationShadeWindowViewController = nswvc; - } - /** * Set the amount of progress we are currently in if we're transitioning to the full shade. * 0.0f means we're not transitioning yet, while 1 means we're all the way in the full @@ -3412,7 +3411,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mVisible = visible; if (visible) { DejankUtils.notifyRendererOfExpensiveFrame( - mNotificationShadeWindowView, "onShadeVisibilityChanged"); + getNotificationShadeWindowView(), "onShadeVisibilityChanged"); } else { mGutsManager.closeAndSaveGuts(true /* removeLeavebehind */, true /* force */, true /* removeControls */, -1 /* x */, -1 /* y */, true /* resetMenu */); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt index ff1b31d8848f..924aac4e70be 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBypassController.kt @@ -50,12 +50,6 @@ open class KeyguardBypassController : Dumpable, StackScrollAlgorithm.BypassContr @DevicePostureInt private var postureState: Int = DEVICE_POSTURE_UNKNOWN private var pendingUnlock: PendingUnlock? = null private val listeners = mutableListOf<OnBypassStateChangedListener>() - private val postureCallback = DevicePostureController.Callback { posture -> - if (postureState != posture) { - postureState = posture - notifyListeners() - } - } private val faceAuthEnabledChangedCallback = object : KeyguardStateController.Callback { override fun onFaceAuthEnabledChanged() = notifyListeners() } @@ -162,10 +156,8 @@ open class KeyguardBypassController : Dumpable, StackScrollAlgorithm.BypassContr val dismissByDefault = if (context.resources.getBoolean( com.android.internal.R.bool.config_faceAuthDismissesKeyguard)) 1 else 0 - tunerService.addTunable(object : TunerService.Tunable { - override fun onTuningChanged(key: String?, newValue: String?) { - bypassEnabled = tunerService.getValue(key, dismissByDefault) != 0 - } + tunerService.addTunable({ key, _ -> + bypassEnabled = tunerService.getValue(key, dismissByDefault) != 0 }, Settings.Secure.FACE_UNLOCK_DISMISSES_KEYGUARD) lockscreenUserManager.addUserChangedListener( object : NotificationLockscreenUserManager.UserChangedListener { @@ -281,8 +273,6 @@ open class KeyguardBypassController : Dumpable, StackScrollAlgorithm.BypassContr } companion object { - const val BYPASS_FADE_DURATION = 67 - private const val FACE_UNLOCK_BYPASS_NO_OVERRIDE = 0 private const val FACE_UNLOCK_BYPASS_ALWAYS = 1 private const val FACE_UNLOCK_BYPASS_NEVER = 2 diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt index f0fc1432c5a3..862f169b2176 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt @@ -30,6 +30,7 @@ import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.shade.ShadeController import com.android.systemui.shade.ShadeLogger +import com.android.systemui.shade.ShadeViewController import com.android.systemui.shared.animation.UnfoldMoveFromCenterAnimator import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.unfold.SysUIUnfoldComponent @@ -51,6 +52,7 @@ class PhoneStatusBarViewController private constructor( @Named(UNFOLD_STATUS_BAR) private val progressProvider: ScopedUnfoldTransitionProgressProvider?, private val centralSurfaces: CentralSurfaces, private val shadeController: ShadeController, + private val shadeViewController: ShadeViewController, private val shadeLogger: ShadeLogger, private val moveFromCenterAnimationController: StatusBarMoveFromCenterAnimationController?, private val userChipViewModel: StatusBarUserChipViewModel, @@ -165,20 +167,20 @@ class PhoneStatusBarViewController private constructor( if (event.action == MotionEvent.ACTION_DOWN) { // If the view that would receive the touch is disabled, just have status // bar eat the gesture. - if (!centralSurfaces.shadeViewController.isViewEnabled) { + if (!shadeViewController.isViewEnabled) { shadeLogger.logMotionEvent(event, "onTouchForwardedFromStatusBar: panel view disabled") return true } - if (centralSurfaces.shadeViewController.isFullyCollapsed && + if (shadeViewController.isFullyCollapsed && event.y < 1f) { // b/235889526 Eat events on the top edge of the phone when collapsed shadeLogger.logMotionEvent(event, "top edge touch ignored") return true } - centralSurfaces.shadeViewController.startTrackingExpansionFromStatusBar() + shadeViewController.startTrackingExpansionFromStatusBar() } - return centralSurfaces.shadeViewController.handleExternalTouch(event) + return shadeViewController.handleExternalTouch(event) } } @@ -222,6 +224,7 @@ class PhoneStatusBarViewController private constructor( private val userChipViewModel: StatusBarUserChipViewModel, private val centralSurfaces: CentralSurfaces, private val shadeController: ShadeController, + private val shadeViewController: ShadeViewController, private val shadeLogger: ShadeLogger, private val viewUtil: ViewUtil, private val configurationController: ConfigurationController, @@ -241,6 +244,7 @@ class PhoneStatusBarViewController private constructor( progressProvider.getOrNull(), centralSurfaces, shadeController, + shadeViewController, shadeLogger, statusBarMoveFromCenterAnimationController, userChipViewModel, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHeadsUpChangeListener.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHeadsUpChangeListener.java index 481cf3ceb197..9a295e63fb9e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHeadsUpChangeListener.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHeadsUpChangeListener.java @@ -21,6 +21,7 @@ import com.android.systemui.shade.ShadeViewController; import com.android.systemui.statusbar.NotificationRemoteInputManager; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.StatusBarState; +import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController; import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent; import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener; import com.android.systemui.statusbar.window.StatusBarWindowController; @@ -35,6 +36,7 @@ public class StatusBarHeadsUpChangeListener implements OnHeadsUpChangedListener private final NotificationShadeWindowController mNotificationShadeWindowController; private final StatusBarWindowController mStatusBarWindowController; private final ShadeViewController mShadeViewController; + private final NotificationStackScrollLayoutController mNsslController; private final KeyguardBypassController mKeyguardBypassController; private final HeadsUpManagerPhone mHeadsUpManager; private final StatusBarStateController mStatusBarStateController; @@ -45,14 +47,15 @@ public class StatusBarHeadsUpChangeListener implements OnHeadsUpChangedListener NotificationShadeWindowController notificationShadeWindowController, StatusBarWindowController statusBarWindowController, ShadeViewController shadeViewController, + NotificationStackScrollLayoutController nsslController, KeyguardBypassController keyguardBypassController, HeadsUpManagerPhone headsUpManager, StatusBarStateController statusBarStateController, NotificationRemoteInputManager notificationRemoteInputManager) { - mNotificationShadeWindowController = notificationShadeWindowController; mStatusBarWindowController = statusBarWindowController; mShadeViewController = shadeViewController; + mNsslController = nsslController; mKeyguardBypassController = keyguardBypassController; mHeadsUpManager = headsUpManager; mStatusBarStateController = statusBarStateController; @@ -85,8 +88,7 @@ public class StatusBarHeadsUpChangeListener implements OnHeadsUpChangedListener //animation // is finished. mHeadsUpManager.setHeadsUpGoingAway(true); - mShadeViewController.getNotificationStackScrollLayoutController() - .runAfterAnimationFinished(() -> { + mNsslController.runAfterAnimationFinished(() -> { if (!mHeadsUpManager.hasPinnedHeadsUp()) { mNotificationShadeWindowController.setHeadsUpShowing(false); mHeadsUpManager.setHeadsUpGoingAway(false); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java index 35285b222f63..e63fecd33383 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java @@ -28,7 +28,6 @@ import android.service.vr.IVrStateCallbacks; import android.util.Log; import android.util.Slog; import android.view.View; -import android.view.accessibility.AccessibilityManager; import com.android.internal.statusbar.IStatusBarService; import com.android.systemui.InitController; @@ -50,7 +49,6 @@ import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.notification.AboveShelfObserver; import com.android.systemui.statusbar.notification.DynamicPrivacyController; -import com.android.systemui.statusbar.notification.NotifPipelineFlags; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.render.NotifShadeEventSource; import com.android.systemui.statusbar.notification.domain.interactor.NotificationsInteractor; @@ -59,7 +57,6 @@ import com.android.systemui.statusbar.notification.interruption.NotificationInte import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.NotificationGutsManager; import com.android.systemui.statusbar.notification.row.NotificationGutsManager.OnSettingsClickListener; -import com.android.systemui.statusbar.notification.row.NotificationInfo.CheckSaveListener; import com.android.systemui.statusbar.notification.stack.NotificationListContainer; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController; import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent; @@ -78,21 +75,18 @@ class StatusBarNotificationPresenter implements NotificationPresenter, CommandQu private final NotifShadeEventSource mNotifShadeEventSource; private final NotificationMediaManager mMediaManager; private final NotificationGutsManager mGutsManager; - private final ShadeViewController mNotificationPanel; private final HeadsUpManagerPhone mHeadsUpManager; private final AboveShelfObserver mAboveShelfObserver; private final DozeScrimController mDozeScrimController; private final CentralSurfaces mCentralSurfaces; private final NotificationsInteractor mNotificationsInteractor; + private final NotificationStackScrollLayoutController mNsslController; private final LockscreenShadeTransitionController mShadeTransitionController; private final PowerInteractor mPowerInteractor; private final CommandQueue mCommandQueue; - - private final AccessibilityManager mAccessibilityManager; private final KeyguardManager mKeyguardManager; private final NotificationShadeWindowController mNotificationShadeWindowController; - private final NotifPipelineFlags mNotifPipelineFlags; private final IStatusBarService mBarService; private final DynamicPrivacyController mDynamicPrivacyController; private final NotificationListContainer mNotifListContainer; @@ -123,11 +117,9 @@ class StatusBarNotificationPresenter implements NotificationPresenter, CommandQu NotifShadeEventSource notifShadeEventSource, NotificationMediaManager notificationMediaManager, NotificationGutsManager notificationGutsManager, - LockscreenGestureLogger lockscreenGestureLogger, InitController initController, NotificationInterruptStateProvider notificationInterruptStateProvider, NotificationRemoteInputManager remoteInputManager, - NotifPipelineFlags notifPipelineFlags, NotificationRemoteInputManager.Callback remoteInputManagerCallback, NotificationListContainer notificationListContainer) { mActivityStarter = activityStarter; @@ -139,6 +131,7 @@ class StatusBarNotificationPresenter implements NotificationPresenter, CommandQu // TODO: use KeyguardStateController#isOccluded to remove this dependency mCentralSurfaces = centralSurfaces; mNotificationsInteractor = notificationsInteractor; + mNsslController = stackScrollerController; mShadeTransitionController = shadeTransitionController; mPowerInteractor = powerInteractor; mCommandQueue = commandQueue; @@ -149,10 +142,8 @@ class StatusBarNotificationPresenter implements NotificationPresenter, CommandQu mGutsManager = notificationGutsManager; mAboveShelfObserver = new AboveShelfObserver(stackScrollerController.getView()); mNotificationShadeWindowController = notificationShadeWindowController; - mNotifPipelineFlags = notifPipelineFlags; mAboveShelfObserver.setListener(statusBarWindow.findViewById( R.id.notification_container_parent)); - mAccessibilityManager = context.getSystemService(AccessibilityManager.class); mDozeScrimController = dozeScrimController; mKeyguardManager = context.getSystemService(KeyguardManager.class); mBarService = IStatusBarService.Stub.asInterface( @@ -170,7 +161,7 @@ class StatusBarNotificationPresenter implements NotificationPresenter, CommandQu } remoteInputManager.setUpWithCallback( remoteInputManagerCallback, - mNotificationPanel.getShadeNotificationPresenter().createRemoteInputDelegate()); + mNsslController.createDelegate()); initController.addPostInitTask(() -> { mNotifShadeEventSource.setShadeEmptiedCallback(this::maybeClosePanelForShadeEmptied); @@ -202,7 +193,7 @@ class StatusBarNotificationPresenter implements NotificationPresenter, CommandQu } private void maybeEndAmbientPulse() { - if (mNotificationPanel.getShadeNotificationPresenter().hasPulsingNotifications() + if (mNsslController.getNotificationListContainer().hasPulsingNotifications() && !mHeadsUpManager.hasNotifications()) { // We were showing a pulse for a notification, but no notifications are pulsing anymore. // Finish the pulse. @@ -272,22 +263,6 @@ class StatusBarNotificationPresenter implements NotificationPresenter, CommandQu } }; - private final CheckSaveListener mCheckSaveListener = new CheckSaveListener() { - @Override - public void checkSave(Runnable saveImportance, StatusBarNotification sbn) { - // If the user has security enabled, show challenge if the setting is changed. - if (mLockscreenUserManager.isLockscreenPublicMode(sbn.getUser().getIdentifier()) - && mKeyguardManager.isKeyguardLocked()) { - onLockedNotificationImportanceChange(() -> { - saveImportance.run(); - return true; - }); - } else { - saveImportance.run(); - } - } - }; - private final OnSettingsClickListener mOnSettingsClickListener = new OnSettingsClickListener() { @Override public void onSettingsClick(String key) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesComponent.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesComponent.java index 4ae460a3f0e1..e77f419f74e1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesComponent.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesComponent.java @@ -21,8 +21,6 @@ import static com.android.systemui.statusbar.phone.dagger.StatusBarViewModule.ST import static java.lang.annotation.RetentionPolicy.RUNTIME; import com.android.systemui.scene.ui.view.WindowRootView; -import com.android.systemui.shade.NotificationShadeWindowView; -import com.android.systemui.shade.NotificationShadeWindowViewController; import com.android.systemui.shade.ShadeHeaderController; import com.android.systemui.statusbar.NotificationPresenter; import com.android.systemui.statusbar.notification.NotificationActivityStarter; @@ -77,16 +75,6 @@ public interface CentralSurfacesComponent { WindowRootView getWindowRootView(); /** - * Creates or returns a {@link NotificationShadeWindowView}. - */ - NotificationShadeWindowView getNotificationShadeWindowView(); - - /** - * Creates a NotificationShadeWindowViewController. - */ - NotificationShadeWindowViewController getNotificationShadeWindowViewController(); - - /** * Creates a StatusBarHeadsUpChangeListener. */ StatusBarHeadsUpChangeListener getStatusBarHeadsUpChangeListener(); diff --git a/packages/SystemUI/src/com/android/systemui/unfold/FoldAodAnimationController.kt b/packages/SystemUI/src/com/android/systemui/unfold/FoldAodAnimationController.kt index eed7950abacb..cbe402017c41 100644 --- a/packages/SystemUI/src/com/android/systemui/unfold/FoldAodAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/unfold/FoldAodAnimationController.kt @@ -94,7 +94,7 @@ constructor( wakefulnessLifecycle.addObserver(this) // TODO(b/254878364): remove this call to NPVC.getView() - getShadeFoldAnimator().view.repeatWhenAttached { + getShadeFoldAnimator().view?.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.STARTED) { listenForDozing(this) } } } @@ -161,10 +161,9 @@ constructor( // but we should wait for the initial animation preparations to be drawn // (setting initial alpha/translation) // TODO(b/254878364): remove this call to NPVC.getView() - OneShotPreDrawListener.add( - getShadeFoldAnimator().view, - onReady - ) + getShadeFoldAnimator().view?.let { + OneShotPreDrawListener.add(it, onReady) + } } else { // No animation, call ready callback immediately onReady.run() diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java index 349f3684659c..87b2697611d2 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java @@ -34,6 +34,7 @@ import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; import static com.android.internal.jank.InteractionJankMonitor.CUJ_VOLUME_CONTROL; import static com.android.internal.jank.InteractionJankMonitor.Configuration.Builder; +import static com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION; import static com.android.systemui.volume.Events.DISMISS_REASON_POSTURE_CHANGED; import static com.android.systemui.volume.Events.DISMISS_REASON_SETTINGS_CLICKED; @@ -82,6 +83,7 @@ import android.util.Slog; import android.util.SparseBooleanArray; import android.view.ContextThemeWrapper; import android.view.Gravity; +import android.view.HapticFeedbackConstants; import android.view.MotionEvent; import android.view.View; import android.view.View.AccessibilityDelegate; @@ -120,6 +122,7 @@ import com.android.systemui.Dumpable; import com.android.systemui.Prefs; import com.android.systemui.R; import com.android.systemui.dump.DumpManager; +import com.android.systemui.flags.FeatureFlags; import com.android.systemui.media.dialog.MediaOutputDialogFactory; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.VolumeDialog; @@ -300,6 +303,7 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, private final DevicePostureController mDevicePostureController; private @DevicePostureController.DevicePostureInt int mDevicePosture; private int mOrientation; + private final FeatureFlags mFeatureFlags; public VolumeDialogImpl( Context context, @@ -315,7 +319,9 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, CsdWarningDialog.Factory csdWarningDialogFactory, DevicePostureController devicePostureController, Looper looper, - DumpManager dumpManager) { + DumpManager dumpManager, + FeatureFlags featureFlags) { + mFeatureFlags = featureFlags; mContext = new ContextThemeWrapper(context, R.style.volume_dialog_theme); mHandler = new H(looper); @@ -1319,12 +1325,14 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, private void provideTouchFeedbackH(int newRingerMode) { VibrationEffect effect = null; + int hapticConstant = HapticFeedbackConstants.NO_HAPTICS; switch (newRingerMode) { case RINGER_MODE_NORMAL: mController.scheduleTouchFeedback(); break; case RINGER_MODE_SILENT: effect = VibrationEffect.get(VibrationEffect.EFFECT_CLICK); + hapticConstant = HapticFeedbackConstants.TOGGLE_OFF; break; case RINGER_MODE_VIBRATE: // Feedback handled by onStateChange, for feedback both when user toggles @@ -1332,8 +1340,11 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, break; default: effect = VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK); + hapticConstant = HapticFeedbackConstants.TOGGLE_ON; } - if (effect != null) { + if (mFeatureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) { + mDialogView.performHapticFeedback(hapticConstant); + } else if (effect != null) { mController.vibrate(effect); } } @@ -1770,7 +1781,22 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, && mState.ringerModeInternal != -1 && mState.ringerModeInternal != state.ringerModeInternal && state.ringerModeInternal == AudioManager.RINGER_MODE_VIBRATE) { - mController.vibrate(VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK)); + + if (mFeatureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) { + if (mShowing) { + // The dialog view is responsible for triggering haptics in the oneway API + mDialogView.performHapticFeedback(HapticFeedbackConstants.TOGGLE_ON); + } + /* + TODO(b/290642122): If the dialog is not showing, we have the case where haptics is + enabled by dragging the volume slider of Settings to a value of 0. This must be + handled by view Slices in Settings whilst using the performHapticFeedback API. + */ + + } else { + // Old behavior only active if the oneway API is not used. + mController.vibrate(VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK)); + } } mState = state; mDynamic.clear(); diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java index d0edc6e7ce4c..cc9f3e14216e 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java +++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java @@ -22,6 +22,7 @@ import android.os.Looper; import com.android.internal.jank.InteractionJankMonitor; import com.android.systemui.dump.DumpManager; +import com.android.systemui.flags.FeatureFlags; import com.android.systemui.media.dialog.MediaOutputDialogFactory; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.VolumeDialog; @@ -61,7 +62,8 @@ public interface VolumeModule { InteractionJankMonitor interactionJankMonitor, CsdWarningDialog.Factory csdFactory, DevicePostureController devicePostureController, - DumpManager dumpManager) { + DumpManager dumpManager, + FeatureFlags featureFlags) { VolumeDialogImpl impl = new VolumeDialogImpl( context, volumeDialogController, @@ -76,7 +78,8 @@ public interface VolumeModule { csdFactory, devicePostureController, Looper.getMainLooper(), - dumpManager); + dumpManager, + featureFlags); impl.setStreamImportant(AudioManager.STREAM_SYSTEM, false); impl.setAutomute(true); impl.setSilentMode(false); diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt index 5d75428b8fb4..cb182297eae1 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt @@ -76,7 +76,7 @@ class KeyguardPatternViewControllerTest : SysuiTestCase() { private lateinit var mKeyguardMessageAreaController: KeyguardMessageAreaController<BouncerKeyguardMessageArea> - @Mock private lateinit var mPostureController: DevicePostureController + @Mock private lateinit var mPostureController: DevicePostureController private lateinit var mKeyguardPatternViewController: KeyguardPatternViewController private lateinit var fakeFeatureFlags: FakeFeatureFlags @@ -119,7 +119,7 @@ class KeyguardPatternViewControllerTest : SysuiTestCase() { mKeyguardPatternViewController.onViewAttached() - assertThat(getPatternTopGuideline()).isEqualTo(getExpectedTopGuideline()) + assertThat(getPatternTopGuideline()).isEqualTo(getHalfOpenedBouncerHeightRatio()) } @Test @@ -131,15 +131,20 @@ class KeyguardPatternViewControllerTest : SysuiTestCase() { mKeyguardPatternViewController.onViewAttached() // Verify view begins in posture state DEVICE_POSTURE_HALF_OPENED - assertThat(getPatternTopGuideline()).isEqualTo(getExpectedTopGuideline()) + assertThat(getPatternTopGuideline()).isEqualTo(getHalfOpenedBouncerHeightRatio()) // Simulate posture change to state DEVICE_POSTURE_OPENED with callback verify(mPostureController).addCallback(postureCallbackCaptor.capture()) val postureCallback: DevicePostureController.Callback = postureCallbackCaptor.value postureCallback.onPostureChanged(DEVICE_POSTURE_OPENED) - // Verify view is now in posture state DEVICE_POSTURE_OPENED - assertThat(getPatternTopGuideline()).isNotEqualTo(getExpectedTopGuideline()) + // Simulate posture change to same state with callback + assertThat(getPatternTopGuideline()).isNotEqualTo(getHalfOpenedBouncerHeightRatio()) + + postureCallback.onPostureChanged(DEVICE_POSTURE_OPENED) + + // Verify view is still in posture state DEVICE_POSTURE_OPENED + assertThat(getPatternTopGuideline()).isNotEqualTo(getHalfOpenedBouncerHeightRatio()) } private fun getPatternTopGuideline(): Float { @@ -150,7 +155,7 @@ class KeyguardPatternViewControllerTest : SysuiTestCase() { return cs.getConstraint(R.id.pattern_top_guideline).layout.guidePercent } - private fun getExpectedTopGuideline(): Float { + private fun getHalfOpenedBouncerHeightRatio(): Float { return mContext.resources.getFloat(R.dimen.half_opened_bouncer_height_ratio) } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt index d256ee163877..4dc7652f83cf 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt @@ -19,6 +19,8 @@ package com.android.keyguard import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.view.View +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.constraintlayout.widget.ConstraintSet import androidx.test.filters.SmallTest import com.android.internal.util.LatencyTracker import com.android.internal.widget.LockPatternUtils @@ -32,6 +34,8 @@ import com.android.systemui.flags.Flags import com.android.systemui.statusbar.policy.DevicePostureController import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_HALF_OPENED import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_OPENED +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -51,7 +55,10 @@ import org.mockito.MockitoAnnotations @RunWith(AndroidTestingRunner::class) @TestableLooper.RunWithLooper class KeyguardPinViewControllerTest : SysuiTestCase() { - @Mock private lateinit var keyguardPinView: KeyguardPINView + + private lateinit var objectKeyguardPINView: KeyguardPINView + + @Mock private lateinit var mockKeyguardPinView: KeyguardPINView @Mock private lateinit var keyguardMessageArea: BouncerKeyguardMessageArea @@ -83,64 +90,73 @@ class KeyguardPinViewControllerTest : SysuiTestCase() { @Mock lateinit var deleteButton: NumPadButton @Mock lateinit var enterButton: View - private lateinit var pinViewController: KeyguardPinViewController - @Captor lateinit var postureCallbackCaptor: ArgumentCaptor<DevicePostureController.Callback> @Before fun setup() { MockitoAnnotations.initMocks(this) - Mockito.`when`(keyguardPinView.requireViewById<View>(R.id.bouncer_message_area)) + Mockito.`when`(mockKeyguardPinView.requireViewById<View>(R.id.bouncer_message_area)) .thenReturn(keyguardMessageArea) Mockito.`when`( keyguardMessageAreaControllerFactory.create(any(KeyguardMessageArea::class.java)) ) .thenReturn(keyguardMessageAreaController) - `when`(keyguardPinView.passwordTextViewId).thenReturn(R.id.pinEntry) - `when`(keyguardPinView.findViewById<PasswordTextView>(R.id.pinEntry)) + `when`(mockKeyguardPinView.passwordTextViewId).thenReturn(R.id.pinEntry) + `when`(mockKeyguardPinView.findViewById<PasswordTextView>(R.id.pinEntry)) .thenReturn(passwordTextView) - `when`(keyguardPinView.resources).thenReturn(context.resources) - `when`(keyguardPinView.findViewById<NumPadButton>(R.id.delete_button)) + `when`(mockKeyguardPinView.resources).thenReturn(context.resources) + `when`(mockKeyguardPinView.findViewById<NumPadButton>(R.id.delete_button)) .thenReturn(deleteButton) - `when`(keyguardPinView.findViewById<View>(R.id.key_enter)).thenReturn(enterButton) + `when`(mockKeyguardPinView.findViewById<View>(R.id.key_enter)).thenReturn(enterButton) // For posture tests: - `when`(keyguardPinView.buttons).thenReturn(arrayOf()) + `when`(mockKeyguardPinView.buttons).thenReturn(arrayOf()) `when`(lockPatternUtils.getPinLength(anyInt())).thenReturn(6) - pinViewController = - KeyguardPinViewController( - keyguardPinView, - keyguardUpdateMonitor, - securityMode, - lockPatternUtils, - mKeyguardSecurityCallback, - keyguardMessageAreaControllerFactory, - mLatencyTracker, - liftToActivateListener, - mEmergencyButtonController, - falsingCollector, - postureController, - featureFlags - ) + objectKeyguardPINView = + View.inflate(mContext, R.layout.keyguard_pin_view, null) + .findViewById(R.id.keyguard_pin_view) as KeyguardPINView + } + + private fun constructPinViewController( + mKeyguardPinView: KeyguardPINView + ): KeyguardPinViewController { + return KeyguardPinViewController( + mKeyguardPinView, + keyguardUpdateMonitor, + securityMode, + lockPatternUtils, + mKeyguardSecurityCallback, + keyguardMessageAreaControllerFactory, + mLatencyTracker, + liftToActivateListener, + mEmergencyButtonController, + falsingCollector, + postureController, + featureFlags + ) } @Test - fun onViewAttached_deviceHalfFolded_propagatedToPinView() { - `when`(postureController.devicePosture).thenReturn(DEVICE_POSTURE_HALF_OPENED) + fun onViewAttached_deviceHalfFolded_propagatedToPatternView() { + val pinViewController = constructPinViewController(objectKeyguardPINView) + overrideResource(R.dimen.half_opened_bouncer_height_ratio, 0.5f) + whenever(postureController.devicePosture).thenReturn(DEVICE_POSTURE_HALF_OPENED) pinViewController.onViewAttached() - verify(keyguardPinView).onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED) + assertThat(getPinTopGuideline()).isEqualTo(getHalfOpenedBouncerHeightRatio()) } @Test - fun onDevicePostureChanged_deviceHalfFolded_propagatedToPinView() { - `when`(postureController.devicePosture).thenReturn(DEVICE_POSTURE_HALF_OPENED) + fun onDevicePostureChanged_deviceOpened_propagatedToPatternView() { + val pinViewController = constructPinViewController(objectKeyguardPINView) + overrideResource(R.dimen.half_opened_bouncer_height_ratio, 0.5f) - // Verify view begins in posture state DEVICE_POSTURE_HALF_OPENED + whenever(postureController.devicePosture).thenReturn(DEVICE_POSTURE_HALF_OPENED) pinViewController.onViewAttached() - verify(keyguardPinView).onDevicePostureChanged(DEVICE_POSTURE_HALF_OPENED) + // Verify view begins in posture state DEVICE_POSTURE_HALF_OPENED + assertThat(getPinTopGuideline()).isEqualTo(getHalfOpenedBouncerHeightRatio()) // Simulate posture change to state DEVICE_POSTURE_OPENED with callback verify(postureController).addCallback(postureCallbackCaptor.capture()) @@ -148,31 +164,57 @@ class KeyguardPinViewControllerTest : SysuiTestCase() { postureCallback.onPostureChanged(DEVICE_POSTURE_OPENED) // Verify view is now in posture state DEVICE_POSTURE_OPENED - verify(keyguardPinView).onDevicePostureChanged(DEVICE_POSTURE_OPENED) + assertThat(getPinTopGuideline()).isNotEqualTo(getHalfOpenedBouncerHeightRatio()) + + // Simulate posture change to same state with callback + postureCallback.onPostureChanged(DEVICE_POSTURE_OPENED) + + // Verify view is still in posture state DEVICE_POSTURE_OPENED + assertThat(getPinTopGuideline()).isNotEqualTo(getHalfOpenedBouncerHeightRatio()) + } + + private fun getPinTopGuideline(): Float { + val cs = ConstraintSet() + val container = objectKeyguardPINView.findViewById(R.id.pin_container) as ConstraintLayout + cs.clone(container) + return cs.getConstraint(R.id.pin_pad_top_guideline).layout.guidePercent + } + + private fun getHalfOpenedBouncerHeightRatio(): Float { + return mContext.resources.getFloat(R.dimen.half_opened_bouncer_height_ratio) } @Test fun startAppearAnimation() { + val pinViewController = constructPinViewController(mockKeyguardPinView) + pinViewController.startAppearAnimation() + verify(keyguardMessageAreaController) .setMessage(context.resources.getString(R.string.keyguard_enter_your_pin), false) } @Test fun startAppearAnimation_withExistingMessage() { + val pinViewController = constructPinViewController(mockKeyguardPinView) Mockito.`when`(keyguardMessageAreaController.message).thenReturn("Unlock to continue.") + pinViewController.startAppearAnimation() + verify(keyguardMessageAreaController, Mockito.never()).setMessage(anyString(), anyBoolean()) } @Test fun startAppearAnimation_withAutoPinConfirmationFailedPasswordAttemptsLessThan5() { + val pinViewController = constructPinViewController(mockKeyguardPinView) `when`(featureFlags.isEnabled(Flags.AUTO_PIN_CONFIRMATION)).thenReturn(true) + `when`(lockPatternUtils.getPinLength(anyInt())).thenReturn(6) `when`(lockPatternUtils.isAutoPinConfirmEnabled(anyInt())).thenReturn(true) `when`(lockPatternUtils.getCurrentFailedPasswordAttempts(anyInt())).thenReturn(3) `when`(passwordTextView.text).thenReturn("") pinViewController.startAppearAnimation() + verify(deleteButton).visibility = View.INVISIBLE verify(enterButton).visibility = View.INVISIBLE verify(passwordTextView).setUsePinShapes(true) @@ -181,12 +223,15 @@ class KeyguardPinViewControllerTest : SysuiTestCase() { @Test fun startAppearAnimation_withAutoPinConfirmationFailedPasswordAttemptsMoreThan5() { + val pinViewController = constructPinViewController(mockKeyguardPinView) `when`(featureFlags.isEnabled(Flags.AUTO_PIN_CONFIRMATION)).thenReturn(true) + `when`(lockPatternUtils.getPinLength(anyInt())).thenReturn(6) `when`(lockPatternUtils.isAutoPinConfirmEnabled(anyInt())).thenReturn(true) `when`(lockPatternUtils.getCurrentFailedPasswordAttempts(anyInt())).thenReturn(6) `when`(passwordTextView.text).thenReturn("") pinViewController.startAppearAnimation() + verify(deleteButton).visibility = View.VISIBLE verify(enterButton).visibility = View.VISIBLE verify(passwordTextView).setUsePinShapes(true) @@ -195,7 +240,10 @@ class KeyguardPinViewControllerTest : SysuiTestCase() { @Test fun handleLockout_readsNumberOfErrorAttempts() { + val pinViewController = constructPinViewController(mockKeyguardPinView) + pinViewController.handleAttemptLockout(0) + verify(lockPatternUtils).getCurrentFailedPasswordAttempts(anyInt()) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt index da9ceb47446a..212dad78d5b2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/animation/ViewHierarchyAnimatorTest.kt @@ -8,6 +8,7 @@ import android.view.ViewGroup import android.widget.LinearLayout import android.widget.RelativeLayout import androidx.test.filters.SmallTest +import com.android.app.animation.Interpolators import com.android.systemui.SysuiTestCase import com.android.systemui.util.children import junit.framework.Assert.assertEquals @@ -19,7 +20,6 @@ import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import com.android.app.animation.Interpolators @SmallTest @RunWith(AndroidTestingRunner::class) @@ -178,7 +178,7 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { } @Test - fun animatesRootAndChildren() { + fun animatesRootAndChildren_withoutExcludedViews() { setUpRootWithChildren() val success = ViewHierarchyAnimator.animate(rootView) @@ -208,6 +208,40 @@ ViewHierarchyAnimatorTest : SysuiTestCase() { } @Test + fun animatesRootAndChildren_withExcludedViews() { + setUpRootWithChildren() + + val success = ViewHierarchyAnimator.animate( + rootView, + excludedViews = setOf(rootView.getChildAt(0)) + ) + // Change all bounds. + rootView.measure( + View.MeasureSpec.makeMeasureSpec(180, View.MeasureSpec.EXACTLY), + View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY) + ) + rootView.layout(10 /* l */, 20 /* t */, 200 /* r */, 120 /* b */) + + assertTrue(success) + assertNotNull(rootView.getTag(R.id.tag_animator)) + assertNull(rootView.getChildAt(0).getTag(R.id.tag_animator)) + assertNotNull(rootView.getChildAt(1).getTag(R.id.tag_animator)) + // The initial values for the affected views should be those of the previous layout, while + // the excluded view should be at the final values from the beginning. + checkBounds(rootView, l = 0, t = 0, r = 200, b = 100) + checkBounds(rootView.getChildAt(0), l = 0, t = 0, r = 90, b = 100) + checkBounds(rootView.getChildAt(1), l = 100, t = 0, r = 200, b = 100) + endAnimation(rootView) + assertNull(rootView.getTag(R.id.tag_animator)) + assertNull(rootView.getChildAt(0).getTag(R.id.tag_animator)) + assertNull(rootView.getChildAt(1).getTag(R.id.tag_animator)) + // The end values should be those of the latest layout. + checkBounds(rootView, l = 10, t = 20, r = 200, b = 120) + checkBounds(rootView.getChildAt(0), l = 0, t = 0, r = 90, b = 100) + checkBounds(rootView.getChildAt(1), l = 90, t = 0, r = 180, b = 100) + } + + @Test fun animatesInvisibleViews() { rootView.layout(10 /* l */, 10 /* t */, 50 /* r */, 50 /* b */) rootView.visibility = View.INVISIBLE diff --git a/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt index c223c5af6079..a6ad4b24b63d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt @@ -103,19 +103,6 @@ class AuthenticationInteractorTest : SysuiTestCase() { } @Test - fun toggleBypassEnabled() = - testScope.runTest { - val isBypassEnabled by collectLastValue(underTest.isBypassEnabled) - assertThat(isBypassEnabled).isFalse() - - underTest.toggleBypassEnabled() - assertThat(isBypassEnabled).isTrue() - - underTest.toggleBypassEnabled() - assertThat(isBypassEnabled).isFalse() - } - - @Test fun isAuthenticationRequired_lockedAndSecured_true() = testScope.runTest { utils.authenticationRepository.setUnlocked(false) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt index e9f0d561371c..f541815d2711 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt @@ -41,6 +41,7 @@ import com.android.systemui.keyguard.shared.model.WakefulnessState import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.phone.BiometricUnlockController import com.android.systemui.statusbar.phone.DozeParameters +import com.android.systemui.statusbar.phone.KeyguardBypassController import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.whenever @@ -73,6 +74,7 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { @Mock private lateinit var biometricUnlockController: BiometricUnlockController @Mock private lateinit var dozeTransitionListener: DozeTransitionListener @Mock private lateinit var authController: AuthController + @Mock private lateinit var keyguardBypassController: KeyguardBypassController @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor @Mock private lateinit var dreamOverlayCallbackController: DreamOverlayCallbackController @Mock private lateinit var dozeParameters: DozeParameters @@ -92,6 +94,7 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { wakefulnessLifecycle, biometricUnlockController, keyguardStateController, + keyguardBypassController, keyguardUpdateMonitor, dozeTransitionListener, dozeParameters, @@ -186,6 +189,20 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { } @Test + fun isBypassEnabled_disabledInController() { + whenever(keyguardBypassController.isBypassEnabled).thenReturn(false) + whenever(keyguardBypassController.bypassEnabled).thenReturn(false) + assertThat(underTest.isBypassEnabled()).isFalse() + } + + @Test + fun isBypassEnabled_enabledInController() { + whenever(keyguardBypassController.isBypassEnabled).thenReturn(true) + whenever(keyguardBypassController.bypassEnabled).thenReturn(true) + assertThat(underTest.isBypassEnabled()).isTrue() + } + + @Test fun isAodAvailable() = runTest { val flow = underTest.isAodAvailable var isAodAvailable = collectLastValue(flow) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt index b4b307301138..9a90a5ceb259 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt @@ -216,9 +216,6 @@ public class MediaControlPanelTest : SysuiTestCase() { @Mock private lateinit var recCardTitle: TextView @Mock private lateinit var coverItem: ImageView @Mock private lateinit var matrix: Matrix - private lateinit var coverItem1: ImageView - private lateinit var coverItem2: ImageView - private lateinit var coverItem3: ImageView private lateinit var recTitle1: TextView private lateinit var recTitle2: TextView private lateinit var recTitle3: TextView @@ -233,7 +230,6 @@ public class MediaControlPanelTest : SysuiTestCase() { FakeFeatureFlags().apply { this.set(Flags.UMO_SURFACE_RIPPLE, false) this.set(Flags.UMO_TURBULENCE_NOISE, false) - this.set(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE, false) } @Mock private lateinit var globalSettings: GlobalSettings @@ -467,21 +463,25 @@ public class MediaControlPanelTest : SysuiTestCase() { recSubtitle3 = TextView(context) whenever(recommendationViewHolder.recommendations).thenReturn(view) - whenever(recommendationViewHolder.cardIcon).thenReturn(appIcon) - - // Add a recommendation item - coverItem1 = ImageView(context).also { it.setId(R.id.media_cover1) } - coverItem2 = ImageView(context).also { it.setId(R.id.media_cover2) } - coverItem3 = ImageView(context).also { it.setId(R.id.media_cover3) } - + whenever(recommendationViewHolder.mediaAppIcons) + .thenReturn(listOf(recAppIconItem, recAppIconItem, recAppIconItem)) + whenever(recommendationViewHolder.cardTitle).thenReturn(recCardTitle) whenever(recommendationViewHolder.mediaCoverItems) - .thenReturn(listOf(coverItem1, coverItem2, coverItem3)) + .thenReturn(listOf(coverItem, coverItem, coverItem)) whenever(recommendationViewHolder.mediaCoverContainers) .thenReturn(listOf(coverContainer1, coverContainer2, coverContainer3)) whenever(recommendationViewHolder.mediaTitles) .thenReturn(listOf(recTitle1, recTitle2, recTitle3)) whenever(recommendationViewHolder.mediaSubtitles) .thenReturn(listOf(recSubtitle1, recSubtitle2, recSubtitle3)) + whenever(recommendationViewHolder.mediaProgressBars) + .thenReturn(listOf(recProgressBar1, recProgressBar2, recProgressBar3)) + whenever(coverItem.imageMatrix).thenReturn(matrix) + + // set ids for recommendation containers + whenever(coverContainer1.id).thenReturn(1) + whenever(coverContainer2.id).thenReturn(2) + whenever(coverContainer3.id).thenReturn(3) whenever(recommendationViewHolder.gutsViewHolder).thenReturn(gutsViewHolder) @@ -1561,7 +1561,8 @@ public class MediaControlPanelTest : SysuiTestCase() { verify(viewHolder.player).contentDescription = descriptionCaptor.capture() val description = descriptionCaptor.value.toString() - assertThat(description).contains(REC_APP_NAME) + assertThat(description) + .isEqualTo(context.getString(R.string.controls_media_smartspace_rec_header)) } @Test @@ -1585,7 +1586,8 @@ public class MediaControlPanelTest : SysuiTestCase() { verify(viewHolder.player).contentDescription = descriptionCaptor.capture() val description = descriptionCaptor.value.toString() - assertThat(description).contains(REC_APP_NAME) + assertThat(description) + .isEqualTo(context.getString(R.string.controls_media_smartspace_rec_header)) } @Test @@ -2151,7 +2153,6 @@ public class MediaControlPanelTest : SysuiTestCase() { @Test fun bindRecommendation_setAfterExecutors() { - setupUpdatedRecommendationViewHolder() val albumArt = getColorIcon(Color.RED) val data = smartspaceData.copy( @@ -2189,7 +2190,6 @@ public class MediaControlPanelTest : SysuiTestCase() { @Test fun bindRecommendationWithProgressBars() { useRealConstraintSets() - setupUpdatedRecommendationViewHolder() val albumArt = getColorIcon(Color.RED) val bundle = Bundle().apply { @@ -2236,7 +2236,6 @@ public class MediaControlPanelTest : SysuiTestCase() { @Test fun bindRecommendation_carouselNotFitThreeRecs_OrientationPortrait() { useRealConstraintSets() - setupUpdatedRecommendationViewHolder() val albumArt = getColorIcon(Color.RED) val data = smartspaceData.copy( @@ -2290,7 +2289,6 @@ public class MediaControlPanelTest : SysuiTestCase() { @Test fun bindRecommendation_carouselNotFitThreeRecs_OrientationLandscape() { useRealConstraintSets() - setupUpdatedRecommendationViewHolder() val albumArt = getColorIcon(Color.RED) val data = smartspaceData.copy( @@ -2505,27 +2503,6 @@ public class MediaControlPanelTest : SysuiTestCase() { verify(activityStarter).postStartActivityDismissingKeyguard(eq(pendingIntent)) } - private fun setupUpdatedRecommendationViewHolder() { - fakeFeatureFlag.set(Flags.MEDIA_RECOMMENDATION_CARD_UPDATE, true) - whenever(recommendationViewHolder.mediaAppIcons) - .thenReturn(listOf(recAppIconItem, recAppIconItem, recAppIconItem)) - whenever(recommendationViewHolder.cardTitle).thenReturn(recCardTitle) - whenever(recommendationViewHolder.mediaCoverContainers) - .thenReturn(listOf(coverContainer1, coverContainer2, coverContainer3)) - whenever(recommendationViewHolder.mediaCoverItems) - .thenReturn(listOf(coverItem, coverItem, coverItem)) - whenever(recommendationViewHolder.mediaProgressBars) - .thenReturn(listOf(recProgressBar1, recProgressBar2, recProgressBar3)) - whenever(recommendationViewHolder.mediaSubtitles) - .thenReturn(listOf(recSubtitle1, recSubtitle2, recSubtitle3)) - whenever(coverItem.imageMatrix).thenReturn(matrix) - - // set ids for recommendation containers - whenever(coverContainer1.id).thenReturn(1) - whenever(coverContainer2.id).thenReturn(2) - whenever(coverContainer3.id).thenReturn(3) - } - private fun getColorIcon(color: Int): Icon { val bmp = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888) val canvas = Canvas(bmp) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt index c9956f36dbeb..ba97df910e43 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaViewControllerTest.kt @@ -201,8 +201,8 @@ class MediaViewControllerTest : SysuiTestCase() { whenever(mockCopiedState.widgetStates) .thenReturn( mutableMapOf( - R.id.media_title1 to mediaTitleWidgetState, - R.id.media_subtitle1 to mediaSubTitleWidgetState, + R.id.media_title to mediaTitleWidgetState, + R.id.media_subtitle to mediaSubTitleWidgetState, R.id.media_cover1_container to mediaContainerWidgetState ) ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/power/domain/interactor/PowerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/power/domain/interactor/PowerInteractorTest.kt index 45bb9313264c..435a1f1327d9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/power/domain/interactor/PowerInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/power/domain/interactor/PowerInteractorTest.kt @@ -182,6 +182,32 @@ class PowerInteractorTest : SysuiTestCase() { assertThat(repository.lastWakeReason).isEqualTo(PowerManager.WAKE_REASON_APPLICATION) } + @Test + fun wakeUpIfDreaming_dreaming_woken() { + // GIVEN device is dreaming + whenever(statusBarStateController.isDreaming).thenReturn(true) + + // WHEN wakeUpIfDreaming is called + underTest.wakeUpIfDreaming("testReason", PowerManager.WAKE_REASON_GESTURE) + + // THEN device is woken up + assertThat(repository.lastWakeWhy).isEqualTo("testReason") + assertThat(repository.lastWakeReason).isEqualTo(PowerManager.WAKE_REASON_GESTURE) + } + + @Test + fun wakeUpIfDreaming_notDreaming_notWoken() { + // GIVEN device is not dreaming + whenever(statusBarStateController.isDreaming).thenReturn(false) + + // WHEN wakeUpIfDreaming is called + underTest.wakeUpIfDreaming("why", PowerManager.WAKE_REASON_TAP) + + // THEN device is not woken + assertThat(repository.lastWakeWhy).isNull() + assertThat(repository.lastWakeReason).isNull() + } + companion object { private val IMMEDIATE = Dispatchers.Main.immediate } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt index a0d8f98a4ad1..9d9d0c7de2ad 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt @@ -154,6 +154,21 @@ class QSPanelControllerTest : SysuiTestCase() { verify(qsPanel).setCanCollapse(true) } + @Test + fun multipleListeningOnlyCallsBrightnessControllerOnce() { + controller.setListening(true, true) + controller.setListening(true, false) + controller.setListening(true, true) + + verify(brightnessController).registerCallbacks() + + controller.setListening(false, true) + controller.setListening(false, false) + controller.setListening(false, true) + + verify(brightnessController).unregisterCallbacks() + } + private fun setShouldUseSplitShade(shouldUse: Boolean) { testableResources.addOverride(R.bool.config_use_split_notification_shade, shouldUse) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartableTest.kt index 3e9ddcb06389..5638d708cf30 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartableTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartableTest.kt @@ -28,7 +28,6 @@ import com.android.systemui.scene.shared.model.SceneContainerNames import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.map import kotlinx.coroutines.test.runTest import org.junit.Before @@ -36,7 +35,6 @@ import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 -@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(JUnit4::class) class SystemUiDefaultSceneContainerStartableTest : SysuiTestCase() { @@ -385,7 +383,7 @@ class SystemUiDefaultSceneContainerStartableTest : SysuiTestCase() { ) { featureFlags.set(Flags.SCENE_CONTAINER, isFeatureEnabled) authenticationRepository.setUnlocked(isDeviceUnlocked) - authenticationRepository.setBypassEnabled(isBypassEnabled) + keyguardRepository.setBypassEnabled(isBypassEnabled) initialSceneKey?.let { sceneInteractor.setCurrentScene(SceneContainerNames.SYSTEM_UI_DEFAULT, SceneModel(it)) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessControllerTest.kt new file mode 100644 index 000000000000..2b7840533df2 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessControllerTest.kt @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2023 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.settings.brightness + +import android.hardware.display.DisplayManager +import android.os.Handler +import android.service.vr.IVrManager +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.testing.TestableLooper.RunWithLooper +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.settings.DisplayTracker +import com.android.systemui.settings.UserTracker +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.settings.FakeSettings +import com.android.systemui.util.time.FakeSystemClock +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@RunWithLooper +class BrightnessControllerTest : SysuiTestCase() { + + private val executor = FakeExecutor(FakeSystemClock()) + private val secureSettings = FakeSettings() + @Mock private lateinit var toggleSlider: ToggleSlider + @Mock private lateinit var userTracker: UserTracker + @Mock private lateinit var displayTracker: DisplayTracker + @Mock private lateinit var displayManager: DisplayManager + @Mock private lateinit var iVrManager: IVrManager + + private lateinit var testableLooper: TestableLooper + + private lateinit var underTest: BrightnessController + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + testableLooper = TestableLooper.get(this) + + underTest = + BrightnessController( + context, + toggleSlider, + userTracker, + displayTracker, + displayManager, + secureSettings, + iVrManager, + executor, + mock(), + Handler(testableLooper.looper) + ) + } + + @Test + fun registerCallbacksMultipleTimes_onlyOneRegistration() { + val repeats = 100 + repeat(repeats) { underTest.registerCallbacks() } + val messagesProcessed = testableLooper.processMessagesNonBlocking(repeats) + + verify(displayTracker).addBrightnessChangeCallback(any(), any()) + verify(iVrManager).registerListener(any()) + + assertThat(messagesProcessed).isEqualTo(1) + } + + @Test + fun unregisterCallbacksMultipleTimes_onlyOneUnregistration() { + val repeats = 100 + underTest.registerCallbacks() + testableLooper.processAllMessages() + + repeat(repeats) { underTest.unregisterCallbacks() } + val messagesProcessed = testableLooper.processMessagesNonBlocking(repeats) + + verify(displayTracker).removeCallback(any()) + verify(iVrManager).unregisterListener(any()) + + assertThat(messagesProcessed).isEqualTo(1) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt index 5c35913f6e20..ed1397ff7013 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt @@ -18,7 +18,6 @@ package com.android.systemui.settings.brightness import android.content.Intent import android.graphics.Rect -import android.os.Handler import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.view.View @@ -29,8 +28,6 @@ import androidx.test.rule.ActivityTestRule import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.activity.SingleActivityFactory -import com.android.systemui.settings.FakeDisplayTracker -import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper import com.android.systemui.util.concurrency.DelayableExecutor import com.android.systemui.util.concurrency.FakeExecutor @@ -53,28 +50,24 @@ import org.mockito.MockitoAnnotations @TestableLooper.RunWithLooper class BrightnessDialogTest : SysuiTestCase() { - @Mock private lateinit var userTracker: UserTracker @Mock private lateinit var brightnessSliderControllerFactory: BrightnessSliderController.Factory - @Mock private lateinit var backgroundHandler: Handler @Mock private lateinit var brightnessSliderController: BrightnessSliderController + @Mock private lateinit var brightnessControllerFactory: BrightnessController.Factory + @Mock private lateinit var brightnessController: BrightnessController @Mock private lateinit var accessibilityMgr: AccessibilityManagerWrapper private val clock = FakeSystemClock() private val mainExecutor = FakeExecutor(clock) - private var displayTracker = FakeDisplayTracker(mContext) - @Rule @JvmField var activityRule = ActivityTestRule( /* activityFactory= */ SingleActivityFactory { TestDialog( - userTracker, - displayTracker, brightnessSliderControllerFactory, + brightnessControllerFactory, mainExecutor, - backgroundHandler, accessibilityMgr ) }, @@ -88,6 +81,7 @@ class BrightnessDialogTest : SysuiTestCase() { `when`(brightnessSliderControllerFactory.create(any(), any())) .thenReturn(brightnessSliderController) `when`(brightnessSliderController.rootView).thenReturn(View(context)) + `when`(brightnessControllerFactory.create(any())).thenReturn(brightnessController) } @After @@ -178,19 +172,15 @@ class BrightnessDialogTest : SysuiTestCase() { } class TestDialog( - userTracker: UserTracker, - displayTracker: FakeDisplayTracker, brightnessSliderControllerFactory: BrightnessSliderController.Factory, + brightnessControllerFactory: BrightnessController.Factory, mainExecutor: DelayableExecutor, - backgroundHandler: Handler, accessibilityMgr: AccessibilityManagerWrapper ) : BrightnessDialog( - userTracker, - displayTracker, brightnessSliderControllerFactory, + brightnessControllerFactory, mainExecutor, - backgroundHandler, accessibilityMgr ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/LockscreenHostedDreamGestureListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/LockscreenHostedDreamGestureListenerTest.kt new file mode 100644 index 000000000000..24d62fba8471 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/LockscreenHostedDreamGestureListenerTest.kt @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shade + +import android.os.PowerManager +import android.testing.AndroidTestingRunner +import android.view.MotionEvent +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor +import com.android.systemui.classifier.FalsingCollector +import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.plugins.FalsingManager +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.power.data.repository.FakePowerRepository +import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.statusbar.StatusBarState +import com.android.systemui.statusbar.phone.ScreenOffAnimationController +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers +import org.mockito.Mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@SmallTest +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidTestingRunner::class) +class LockscreenHostedDreamGestureListenerTest : SysuiTestCase() { + @Mock private lateinit var falsingManager: FalsingManager + @Mock private lateinit var falsingCollector: FalsingCollector + @Mock private lateinit var statusBarStateController: StatusBarStateController + @Mock private lateinit var shadeLogger: ShadeLogger + @Mock private lateinit var screenOffAnimationController: ScreenOffAnimationController + @Mock private lateinit var primaryBouncerInteractor: PrimaryBouncerInteractor + + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) + + private lateinit var powerRepository: FakePowerRepository + private lateinit var keyguardRepository: FakeKeyguardRepository + private lateinit var underTest: LockscreenHostedDreamGestureListener + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + powerRepository = FakePowerRepository() + keyguardRepository = FakeKeyguardRepository() + + underTest = + LockscreenHostedDreamGestureListener( + falsingManager, + PowerInteractor( + powerRepository, + keyguardRepository, + falsingCollector, + screenOffAnimationController, + statusBarStateController, + ), + statusBarStateController, + primaryBouncerInteractor, + keyguardRepository, + shadeLogger, + ) + whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD) + whenever(primaryBouncerInteractor.isBouncerShowing()).thenReturn(false) + } + + @Test + fun testGestureDetector_onSingleTap_whileDreaming() = + testScope.runTest { + // GIVEN device dreaming and the dream is hosted in lockscreen + whenever(statusBarStateController.isDreaming).thenReturn(true) + keyguardRepository.setIsActiveDreamLockscreenHosted(true) + testScope.runCurrent() + + // GIVEN the falsing manager does NOT think the tap is a false tap + whenever(falsingManager.isFalseTap(ArgumentMatchers.anyInt())).thenReturn(false) + + // WHEN there's a tap + underTest.onSingleTapUp(upEv) + + // THEN wake up device if dreaming + Truth.assertThat(powerRepository.lastWakeWhy).isNotNull() + Truth.assertThat(powerRepository.lastWakeReason).isEqualTo(PowerManager.WAKE_REASON_TAP) + } + + @Test + fun testGestureDetector_onSingleTap_notOnKeyguard() = + testScope.runTest { + // GIVEN device dreaming and the dream is hosted in lockscreen + whenever(statusBarStateController.isDreaming).thenReturn(true) + keyguardRepository.setIsActiveDreamLockscreenHosted(true) + testScope.runCurrent() + + // GIVEN shade is open + whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE) + + // GIVEN the falsing manager does NOT think the tap is a false tap + whenever(falsingManager.isFalseTap(ArgumentMatchers.anyInt())).thenReturn(false) + + // WHEN there's a tap + underTest.onSingleTapUp(upEv) + + // THEN the falsing manager never gets a call + verify(falsingManager, never()).isFalseTap(ArgumentMatchers.anyInt()) + } + + @Test + fun testGestureDetector_onSingleTap_bouncerShown() = + testScope.runTest { + // GIVEN device dreaming and the dream is hosted in lockscreen + whenever(statusBarStateController.isDreaming).thenReturn(true) + keyguardRepository.setIsActiveDreamLockscreenHosted(true) + testScope.runCurrent() + + // GIVEN bouncer is expanded + whenever(primaryBouncerInteractor.isBouncerShowing()).thenReturn(true) + + // GIVEN the falsing manager does NOT think the tap is a false tap + whenever(falsingManager.isFalseTap(ArgumentMatchers.anyInt())).thenReturn(false) + + // WHEN there's a tap + underTest.onSingleTapUp(upEv) + + // THEN the falsing manager never gets a call + verify(falsingManager, never()).isFalseTap(ArgumentMatchers.anyInt()) + } + + @Test + fun testGestureDetector_onSingleTap_falsing() = + testScope.runTest { + // GIVEN device dreaming and the dream is hosted in lockscreen + whenever(statusBarStateController.isDreaming).thenReturn(true) + keyguardRepository.setIsActiveDreamLockscreenHosted(true) + testScope.runCurrent() + + // GIVEN the falsing manager thinks the tap is a false tap + whenever(falsingManager.isFalseTap(ArgumentMatchers.anyInt())).thenReturn(true) + + // WHEN there's a tap + underTest.onSingleTapUp(upEv) + + // THEN the device doesn't wake up + Truth.assertThat(powerRepository.lastWakeWhy).isNull() + Truth.assertThat(powerRepository.lastWakeReason).isNull() + } + + @Test + fun testSingleTap_notDreaming_noFalsingCheck() = + testScope.runTest { + // GIVEN device not dreaming with lockscreen hosted dream + whenever(statusBarStateController.isDreaming).thenReturn(false) + keyguardRepository.setIsActiveDreamLockscreenHosted(false) + testScope.runCurrent() + + // WHEN there's a tap + underTest.onSingleTapUp(upEv) + + // THEN the falsing manager never gets a call + verify(falsingManager, never()).isFalseTap(ArgumentMatchers.anyInt()) + } +} + +private val upEv = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 0f, 0f, 0) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt index 5fb3a7955b5c..2a398c55560c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt @@ -111,6 +111,8 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { @Mock private lateinit var lockIconViewController: LockIconViewController @Mock private lateinit var phoneStatusBarViewController: PhoneStatusBarViewController @Mock private lateinit var pulsingGestureListener: PulsingGestureListener + @Mock + private lateinit var mLockscreenHostedDreamGestureListener: LockscreenHostedDreamGestureListener @Mock private lateinit var notificationInsetsController: NotificationInsetsController @Mock lateinit var keyguardBouncerComponentFactory: KeyguardBouncerComponent.Factory @Mock lateinit var keyguardBouncerComponent: KeyguardBouncerComponent @@ -147,6 +149,7 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { featureFlags.set(Flags.DUAL_SHADE, false) featureFlags.set(Flags.SPLIT_SHADE_SUBPIXEL_OPTIMIZATION, true) featureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true) + featureFlags.set(Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED, false) val inputProxy = MultiShadeInputProxy() testScope = TestScope() @@ -183,6 +186,7 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { notificationInsetsController, ambientState, pulsingGestureListener, + mLockscreenHostedDreamGestureListener, keyguardBouncerViewModel, keyguardBouncerComponentFactory, mock(KeyguardMessageAreaController.Factory::class.java), diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt index 544137e95779..d9eb9b9166b3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt @@ -113,6 +113,8 @@ class NotificationShadeWindowViewTest : SysuiTestCase() { @Mock private lateinit var keyguardUnlockAnimationController: KeyguardUnlockAnimationController @Mock private lateinit var ambientState: AmbientState @Mock private lateinit var pulsingGestureListener: PulsingGestureListener + @Mock + private lateinit var mLockscreenHostedDreamGestureListener: LockscreenHostedDreamGestureListener @Mock private lateinit var keyguardBouncerViewModel: KeyguardBouncerViewModel @Mock private lateinit var keyguardBouncerComponentFactory: KeyguardBouncerComponent.Factory @Mock private lateinit var keyguardBouncerComponent: KeyguardBouncerComponent @@ -161,6 +163,7 @@ class NotificationShadeWindowViewTest : SysuiTestCase() { featureFlags.set(Flags.DUAL_SHADE, false) featureFlags.set(Flags.SPLIT_SHADE_SUBPIXEL_OPTIMIZATION, true) featureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true) + featureFlags.set(Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED, false) val inputProxy = MultiShadeInputProxy() testScope = TestScope() val multiShadeInteractor = @@ -196,6 +199,7 @@ class NotificationShadeWindowViewTest : SysuiTestCase() { notificationInsetsController, ambientState, pulsingGestureListener, + mLockscreenHostedDreamGestureListener, keyguardBouncerViewModel, keyguardBouncerComponentFactory, Mockito.mock(KeyguardMessageAreaController.Factory::class.java), diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationQSContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt index 168cbb7b8da3..2bc112d68ae2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationQSContainerControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt @@ -1,7 +1,24 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.android.systemui.shade import android.testing.AndroidTestingRunner import android.testing.TestableLooper +import android.testing.TestableResources import android.view.View import android.view.ViewGroup import android.view.WindowInsets @@ -12,6 +29,8 @@ import androidx.constraintlayout.widget.ConstraintSet import androidx.test.filters.SmallTest import com.android.systemui.R import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.fragments.FragmentHostManager import com.android.systemui.fragments.FragmentService import com.android.systemui.navigationbar.NavigationModeController @@ -19,7 +38,10 @@ import com.android.systemui.navigationbar.NavigationModeController.ModeChangedLi import com.android.systemui.plugins.qs.QS import com.android.systemui.recents.OverviewProxyService import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener +import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.mockito.capture +import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import java.util.function.Consumer @@ -40,77 +62,63 @@ import org.mockito.Mockito.never import org.mockito.Mockito.reset import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations -import org.mockito.Mockito.`when` as whenever -@SmallTest +/** Uses Flags.MIGRATE_NSSL set to false. If all goes well, this set of tests will be deleted. */ @RunWith(AndroidTestingRunner::class) @TestableLooper.RunWithLooper -class NotificationQSContainerControllerTest : SysuiTestCase() { +@SmallTest +class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() { + + @Mock lateinit var view: NotificationsQuickSettingsContainer + @Mock lateinit var navigationModeController: NavigationModeController + @Mock lateinit var overviewProxyService: OverviewProxyService + @Mock lateinit var shadeHeaderController: ShadeHeaderController + @Mock lateinit var shadeExpansionStateManager: ShadeExpansionStateManager + @Mock lateinit var fragmentService: FragmentService + @Mock lateinit var fragmentHostManager: FragmentHostManager + @Mock + lateinit var notificationStackScrollLayoutController: NotificationStackScrollLayoutController - companion object { - const val STABLE_INSET_BOTTOM = 100 - const val CUTOUT_HEIGHT = 50 - const val GESTURES_NAVIGATION = WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL - const val BUTTONS_NAVIGATION = WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON - const val NOTIFICATIONS_MARGIN = 50 - const val SCRIM_MARGIN = 10 - const val FOOTER_ACTIONS_INSET = 2 - const val FOOTER_ACTIONS_PADDING = 2 - const val FOOTER_ACTIONS_OFFSET = FOOTER_ACTIONS_INSET + FOOTER_ACTIONS_PADDING - const val QS_PADDING_OFFSET = SCRIM_MARGIN + FOOTER_ACTIONS_OFFSET - } + @Captor lateinit var navigationModeCaptor: ArgumentCaptor<ModeChangedListener> + @Captor lateinit var taskbarVisibilityCaptor: ArgumentCaptor<OverviewProxyListener> + @Captor lateinit var windowInsetsCallbackCaptor: ArgumentCaptor<Consumer<WindowInsets>> + @Captor lateinit var constraintSetCaptor: ArgumentCaptor<ConstraintSet> + @Captor lateinit var attachStateListenerCaptor: ArgumentCaptor<View.OnAttachStateChangeListener> - @Mock - private lateinit var navigationModeController: NavigationModeController - @Mock - private lateinit var overviewProxyService: OverviewProxyService - @Mock - private lateinit var notificationsQSContainer: NotificationsQuickSettingsContainer - @Mock - private lateinit var mShadeHeaderController: ShadeHeaderController - @Mock - private lateinit var shadeExpansionStateManager: ShadeExpansionStateManager - @Mock - private lateinit var fragmentService: FragmentService - @Mock - private lateinit var fragmentHostManager: FragmentHostManager - @Captor - lateinit var navigationModeCaptor: ArgumentCaptor<ModeChangedListener> - @Captor - lateinit var taskbarVisibilityCaptor: ArgumentCaptor<OverviewProxyListener> - @Captor - lateinit var windowInsetsCallbackCaptor: ArgumentCaptor<Consumer<WindowInsets>> - @Captor - lateinit var constraintSetCaptor: ArgumentCaptor<ConstraintSet> - @Captor - lateinit var attachStateListenerCaptor: ArgumentCaptor<View.OnAttachStateChangeListener> - - private lateinit var controller: NotificationsQSContainerController + lateinit var underTest: NotificationsQSContainerController + + private lateinit var fakeResources: TestableResources + private lateinit var featureFlags: FakeFeatureFlags private lateinit var navigationModeCallback: ModeChangedListener private lateinit var taskbarVisibilityCallback: OverviewProxyListener private lateinit var windowInsetsCallback: Consumer<WindowInsets> - private lateinit var delayableExecutor: FakeExecutor private lateinit var fakeSystemClock: FakeSystemClock + private lateinit var delayableExecutor: FakeExecutor @Before fun setup() { MockitoAnnotations.initMocks(this) - mContext.ensureTestableResources() - whenever(notificationsQSContainer.context).thenReturn(mContext) - whenever(notificationsQSContainer.resources).thenReturn(mContext.resources) - whenever(fragmentService.getFragmentHostManager(any())).thenReturn(fragmentHostManager) fakeSystemClock = FakeSystemClock() delayableExecutor = FakeExecutor(fakeSystemClock) + featureFlags = FakeFeatureFlags().apply { set(Flags.MIGRATE_NSSL, false) } + mContext.ensureTestableResources() + whenever(view.context).thenReturn(mContext) + whenever(view.resources).thenReturn(mContext.resources) + + whenever(fragmentService.getFragmentHostManager(any())).thenReturn(fragmentHostManager) - controller = NotificationsQSContainerController( - notificationsQSContainer, + underTest = + NotificationsQSContainerController( + view, navigationModeController, overviewProxyService, - mShadeHeaderController, + shadeHeaderController, shadeExpansionStateManager, fragmentService, - delayableExecutor - ) + delayableExecutor, + featureFlags, + notificationStackScrollLayoutController, + ) overrideResource(R.dimen.split_shade_notifications_scrim_margin_bottom, SCRIM_MARGIN) overrideResource(R.dimen.notification_panel_margin_bottom, NOTIFICATIONS_MARGIN) @@ -118,38 +126,72 @@ class NotificationQSContainerControllerTest : SysuiTestCase() { overrideResource(R.dimen.qs_footer_actions_bottom_padding, FOOTER_ACTIONS_PADDING) overrideResource(R.dimen.qs_footer_action_inset, FOOTER_ACTIONS_INSET) whenever(navigationModeController.addListener(navigationModeCaptor.capture())) - .thenReturn(GESTURES_NAVIGATION) + .thenReturn(GESTURES_NAVIGATION) doNothing().`when`(overviewProxyService).addCallback(taskbarVisibilityCaptor.capture()) - doNothing().`when`(notificationsQSContainer) - .setInsetsChangedListener(windowInsetsCallbackCaptor.capture()) - doNothing().`when`(notificationsQSContainer).applyConstraints(constraintSetCaptor.capture()) - doNothing().`when`(notificationsQSContainer) - .addOnAttachStateChangeListener(attachStateListenerCaptor.capture()) - controller.init() - attachStateListenerCaptor.value.onViewAttachedToWindow(notificationsQSContainer) + doNothing().`when`(view).setInsetsChangedListener(windowInsetsCallbackCaptor.capture()) + doNothing().`when`(view).applyConstraints(constraintSetCaptor.capture()) + doNothing().`when`(view).addOnAttachStateChangeListener(attachStateListenerCaptor.capture()) + underTest.init() + attachStateListenerCaptor.value.onViewAttachedToWindow(view) navigationModeCallback = navigationModeCaptor.value taskbarVisibilityCallback = taskbarVisibilityCaptor.value windowInsetsCallback = windowInsetsCallbackCaptor.value + + Mockito.clearInvocations(view) + } + + @Test + fun testSmallScreen_updateResources_splitShadeHeightIsSet() { + overrideResource(R.bool.config_use_large_screen_shade_header, false) + overrideResource(R.dimen.qs_header_height, 1) + overrideResource(R.dimen.large_screen_shade_header_height, 2) + + underTest.updateResources() + + val captor = ArgumentCaptor.forClass(ConstraintSet::class.java) + verify(view).applyConstraints(capture(captor)) + assertThat(captor.value.getHeight(R.id.split_shade_status_bar)).isEqualTo(1) + } + + @Test + fun testLargeScreen_updateResources_splitShadeHeightIsSet() { + overrideResource(R.bool.config_use_large_screen_shade_header, true) + overrideResource(R.dimen.qs_header_height, 1) + overrideResource(R.dimen.large_screen_shade_header_height, 2) + + underTest.updateResources() + + val captor = ArgumentCaptor.forClass(ConstraintSet::class.java) + verify(view).applyConstraints(capture(captor)) + assertThat(captor.value.getHeight(R.id.split_shade_status_bar)).isEqualTo(2) } @Test fun testTaskbarVisibleInSplitShade() { enableSplitShade() - given(taskbarVisible = true, - navigationMode = GESTURES_NAVIGATION, - insets = windowInsets().withStableBottom()) - then(expectedContainerPadding = 0, // taskbar should disappear when shade is expanded - expectedNotificationsMargin = NOTIFICATIONS_MARGIN, - expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET) + given( + taskbarVisible = true, + navigationMode = GESTURES_NAVIGATION, + insets = windowInsets().withStableBottom() + ) + then( + expectedContainerPadding = 0, // taskbar should disappear when shade is expanded + expectedNotificationsMargin = NOTIFICATIONS_MARGIN, + expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET + ) - given(taskbarVisible = true, - navigationMode = BUTTONS_NAVIGATION, - insets = windowInsets().withStableBottom()) - then(expectedContainerPadding = STABLE_INSET_BOTTOM, - expectedNotificationsMargin = NOTIFICATIONS_MARGIN, - expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET) + given( + taskbarVisible = true, + navigationMode = BUTTONS_NAVIGATION, + insets = windowInsets().withStableBottom() + ) + then( + expectedContainerPadding = STABLE_INSET_BOTTOM, + expectedNotificationsMargin = NOTIFICATIONS_MARGIN, + expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET + ) } @Test @@ -157,161 +199,185 @@ class NotificationQSContainerControllerTest : SysuiTestCase() { // when taskbar is not visible, it means we're on the home screen enableSplitShade() - given(taskbarVisible = false, - navigationMode = GESTURES_NAVIGATION, - insets = windowInsets().withStableBottom()) - then(expectedContainerPadding = 0, - expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET) + given( + taskbarVisible = false, + navigationMode = GESTURES_NAVIGATION, + insets = windowInsets().withStableBottom() + ) + then( + expectedContainerPadding = 0, + expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET + ) - given(taskbarVisible = false, - navigationMode = BUTTONS_NAVIGATION, - insets = windowInsets().withStableBottom()) - then(expectedContainerPadding = 0, // qs goes full height as it's not obscuring nav buttons - expectedNotificationsMargin = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN, - expectedQsPadding = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET) + given( + taskbarVisible = false, + navigationMode = BUTTONS_NAVIGATION, + insets = windowInsets().withStableBottom() + ) + then( + expectedContainerPadding = 0, // qs goes full height as it's not obscuring nav buttons + expectedNotificationsMargin = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN, + expectedQsPadding = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET + ) } @Test fun testTaskbarNotVisibleInSplitShadeWithCutout() { enableSplitShade() - given(taskbarVisible = false, - navigationMode = GESTURES_NAVIGATION, - insets = windowInsets().withCutout()) - then(expectedContainerPadding = CUTOUT_HEIGHT, - expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET) + given( + taskbarVisible = false, + navigationMode = GESTURES_NAVIGATION, + insets = windowInsets().withCutout() + ) + then( + expectedContainerPadding = CUTOUT_HEIGHT, + expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET + ) - given(taskbarVisible = false, - navigationMode = BUTTONS_NAVIGATION, - insets = windowInsets().withCutout().withStableBottom()) - then(expectedContainerPadding = 0, - expectedNotificationsMargin = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN, - expectedQsPadding = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET) + given( + taskbarVisible = false, + navigationMode = BUTTONS_NAVIGATION, + insets = windowInsets().withCutout().withStableBottom() + ) + then( + expectedContainerPadding = 0, + expectedNotificationsMargin = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN, + expectedQsPadding = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET + ) } @Test fun testTaskbarVisibleInSinglePaneShade() { disableSplitShade() - given(taskbarVisible = true, - navigationMode = GESTURES_NAVIGATION, - insets = windowInsets().withStableBottom()) - then(expectedContainerPadding = 0, - expectedQsPadding = STABLE_INSET_BOTTOM) + given( + taskbarVisible = true, + navigationMode = GESTURES_NAVIGATION, + insets = windowInsets().withStableBottom() + ) + then(expectedContainerPadding = 0, expectedQsPadding = STABLE_INSET_BOTTOM) - given(taskbarVisible = true, - navigationMode = BUTTONS_NAVIGATION, - insets = windowInsets().withStableBottom()) - then(expectedContainerPadding = STABLE_INSET_BOTTOM, - expectedQsPadding = STABLE_INSET_BOTTOM) + given( + taskbarVisible = true, + navigationMode = BUTTONS_NAVIGATION, + insets = windowInsets().withStableBottom() + ) + then( + expectedContainerPadding = STABLE_INSET_BOTTOM, + expectedQsPadding = STABLE_INSET_BOTTOM + ) } @Test fun testTaskbarNotVisibleInSinglePaneShade() { disableSplitShade() - given(taskbarVisible = false, - navigationMode = GESTURES_NAVIGATION, - insets = emptyInsets()) + given(taskbarVisible = false, navigationMode = GESTURES_NAVIGATION, insets = emptyInsets()) then(expectedContainerPadding = 0) - given(taskbarVisible = false, - navigationMode = GESTURES_NAVIGATION, - insets = windowInsets().withCutout().withStableBottom()) + given( + taskbarVisible = false, + navigationMode = GESTURES_NAVIGATION, + insets = windowInsets().withCutout().withStableBottom() + ) then(expectedContainerPadding = CUTOUT_HEIGHT, expectedQsPadding = STABLE_INSET_BOTTOM) - given(taskbarVisible = false, - navigationMode = BUTTONS_NAVIGATION, - insets = windowInsets().withStableBottom()) - then(expectedContainerPadding = 0, - expectedNotificationsMargin = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN, - expectedQsPadding = STABLE_INSET_BOTTOM) + given( + taskbarVisible = false, + navigationMode = BUTTONS_NAVIGATION, + insets = windowInsets().withStableBottom() + ) + then( + expectedContainerPadding = 0, + expectedNotificationsMargin = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN, + expectedQsPadding = STABLE_INSET_BOTTOM + ) } @Test fun testDetailShowingInSinglePaneShade() { disableSplitShade() - controller.setDetailShowing(true) + underTest.setDetailShowing(true) // always sets spacings to 0 - given(taskbarVisible = false, - navigationMode = GESTURES_NAVIGATION, - insets = windowInsets().withStableBottom()) - then(expectedContainerPadding = 0, - expectedNotificationsMargin = 0) + given( + taskbarVisible = false, + navigationMode = GESTURES_NAVIGATION, + insets = windowInsets().withStableBottom() + ) + then(expectedContainerPadding = 0, expectedNotificationsMargin = 0) - given(taskbarVisible = false, - navigationMode = BUTTONS_NAVIGATION, - insets = emptyInsets()) - then(expectedContainerPadding = 0, - expectedNotificationsMargin = 0) + given(taskbarVisible = false, navigationMode = BUTTONS_NAVIGATION, insets = emptyInsets()) + then(expectedContainerPadding = 0, expectedNotificationsMargin = 0) } @Test fun testDetailShowingInSplitShade() { enableSplitShade() - controller.setDetailShowing(true) + underTest.setDetailShowing(true) - given(taskbarVisible = false, - navigationMode = GESTURES_NAVIGATION, - insets = windowInsets().withStableBottom()) + given( + taskbarVisible = false, + navigationMode = GESTURES_NAVIGATION, + insets = windowInsets().withStableBottom() + ) then(expectedContainerPadding = 0) // should not influence spacing - given(taskbarVisible = false, - navigationMode = BUTTONS_NAVIGATION, - insets = emptyInsets()) + given(taskbarVisible = false, navigationMode = BUTTONS_NAVIGATION, insets = emptyInsets()) then(expectedContainerPadding = 0) } @Test fun testNotificationsMarginBottomIsUpdated() { - Mockito.clearInvocations(notificationsQSContainer) + Mockito.clearInvocations(view) enableSplitShade() - verify(notificationsQSContainer).setNotificationsMarginBottom(NOTIFICATIONS_MARGIN) + verify(view).setNotificationsMarginBottom(NOTIFICATIONS_MARGIN) overrideResource(R.dimen.notification_panel_margin_bottom, 100) disableSplitShade() - verify(notificationsQSContainer).setNotificationsMarginBottom(100) + verify(view).setNotificationsMarginBottom(100) } @Test fun testSplitShadeLayout_isAlignedToGuideline() { enableSplitShade() - controller.updateResources() - assertThat(getConstraintSetLayout(R.id.qs_frame).endToEnd) - .isEqualTo(R.id.qs_edge_guideline) + underTest.updateResources() + assertThat(getConstraintSetLayout(R.id.qs_frame).endToEnd).isEqualTo(R.id.qs_edge_guideline) assertThat(getConstraintSetLayout(R.id.notification_stack_scroller).startToStart) - .isEqualTo(R.id.qs_edge_guideline) + .isEqualTo(R.id.qs_edge_guideline) } @Test fun testSinglePaneLayout_childrenHaveEqualMargins() { disableSplitShade() - controller.updateResources() + underTest.updateResources() val qsStartMargin = getConstraintSetLayout(R.id.qs_frame).startMargin val qsEndMargin = getConstraintSetLayout(R.id.qs_frame).endMargin val notifStartMargin = getConstraintSetLayout(R.id.notification_stack_scroller).startMargin val notifEndMargin = getConstraintSetLayout(R.id.notification_stack_scroller).endMargin - assertThat(qsStartMargin == qsEndMargin && - notifStartMargin == notifEndMargin && - qsStartMargin == notifStartMargin - ).isTrue() + assertThat( + qsStartMargin == qsEndMargin && + notifStartMargin == notifEndMargin && + qsStartMargin == notifStartMargin + ) + .isTrue() } @Test fun testSplitShadeLayout_childrenHaveInsideMarginsOfZero() { enableSplitShade() - controller.updateResources() + underTest.updateResources() assertThat(getConstraintSetLayout(R.id.qs_frame).endMargin).isEqualTo(0) assertThat(getConstraintSetLayout(R.id.notification_stack_scroller).startMargin) - .isEqualTo(0) + .isEqualTo(0) } @Test fun testSplitShadeLayout_qsFrameHasHorizontalMarginsOfZero() { enableSplitShade() - controller.updateResources() + underTest.updateResources() assertThat(getConstraintSetLayout(R.id.qs_frame).endMargin).isEqualTo(0) assertThat(getConstraintSetLayout(R.id.qs_frame).startMargin).isEqualTo(0) } @@ -322,62 +388,64 @@ class NotificationQSContainerControllerTest : SysuiTestCase() { val largeScreenHeaderHeight = 100 overrideResource(R.dimen.large_screen_shade_header_height, largeScreenHeaderHeight) - controller.updateResources() + underTest.updateResources() assertThat(getConstraintSetLayout(R.id.qs_frame).topMargin) - .isEqualTo(largeScreenHeaderHeight) + .isEqualTo(largeScreenHeaderHeight) assertThat(getConstraintSetLayout(R.id.notification_stack_scroller).topMargin) - .isEqualTo(largeScreenHeaderHeight) + .isEqualTo(largeScreenHeaderHeight) } @Test fun testSmallScreenLayout_qsAndNotifsTopMarginIsZero() { setSmallScreen() - controller.updateResources() + underTest.updateResources() assertThat(getConstraintSetLayout(R.id.qs_frame).topMargin).isEqualTo(0) - assertThat(getConstraintSetLayout(R.id.notification_stack_scroller).topMargin) - .isEqualTo(0) + assertThat(getConstraintSetLayout(R.id.notification_stack_scroller).topMargin).isEqualTo(0) } @Test fun testSinglePaneShadeLayout_qsFrameHasHorizontalMarginsSetToCorrectValue() { disableSplitShade() - controller.updateResources() - val notificationPanelMarginHorizontal = context.resources - .getDimensionPixelSize(R.dimen.notification_panel_margin_horizontal) + underTest.updateResources() + val notificationPanelMarginHorizontal = + mContext.resources.getDimensionPixelSize(R.dimen.notification_panel_margin_horizontal) assertThat(getConstraintSetLayout(R.id.qs_frame).endMargin) - .isEqualTo(notificationPanelMarginHorizontal) + .isEqualTo(notificationPanelMarginHorizontal) assertThat(getConstraintSetLayout(R.id.qs_frame).startMargin) - .isEqualTo(notificationPanelMarginHorizontal) + .isEqualTo(notificationPanelMarginHorizontal) } @Test fun testSinglePaneShadeLayout_isAlignedToParent() { disableSplitShade() - controller.updateResources() + underTest.updateResources() assertThat(getConstraintSetLayout(R.id.qs_frame).endToEnd) - .isEqualTo(ConstraintSet.PARENT_ID) + .isEqualTo(ConstraintSet.PARENT_ID) assertThat(getConstraintSetLayout(R.id.notification_stack_scroller).startToStart) - .isEqualTo(ConstraintSet.PARENT_ID) + .isEqualTo(ConstraintSet.PARENT_ID) } @Test fun testAllChildrenOfNotificationContainer_haveIds() { // set dimen to 0 to avoid triggering updating bottom spacing overrideResource(R.dimen.split_shade_notifications_scrim_margin_bottom, 0) - val container = NotificationsQuickSettingsContainer(context, null) + val container = NotificationsQuickSettingsContainer(mContext, null) container.removeAllViews() container.addView(newViewWithId(1)) container.addView(newViewWithId(View.NO_ID)) - val controller = NotificationsQSContainerController( + val controller = + NotificationsQSContainerController( container, navigationModeController, overviewProxyService, - mShadeHeaderController, + shadeHeaderController, shadeExpansionStateManager, fragmentService, - delayableExecutor - ) + delayableExecutor, + featureFlags, + notificationStackScrollLayoutController, + ) controller.updateConstraints() assertThat(container.getChildAt(0).id).isEqualTo(1) @@ -388,44 +456,46 @@ class NotificationQSContainerControllerTest : SysuiTestCase() { fun testWindowInsetDebounce() { disableSplitShade() - given(taskbarVisible = false, + given( + taskbarVisible = false, navigationMode = GESTURES_NAVIGATION, insets = emptyInsets(), - applyImmediately = false) + applyImmediately = false + ) fakeSystemClock.advanceTime(INSET_DEBOUNCE_MILLIS / 2) windowInsetsCallback.accept(windowInsets().withStableBottom()) delayableExecutor.advanceClockToLast() delayableExecutor.runAllReady() - verify(notificationsQSContainer, never()).setQSContainerPaddingBottom(0) - verify(notificationsQSContainer).setQSContainerPaddingBottom(STABLE_INSET_BOTTOM) + verify(view, never()).setQSContainerPaddingBottom(0) + verify(view).setQSContainerPaddingBottom(STABLE_INSET_BOTTOM) } @Test fun testStartCustomizingWithDuration() { - controller.setCustomizerShowing(true, 100L) - verify(mShadeHeaderController).startCustomizingAnimation(true, 100L) + underTest.setCustomizerShowing(true, 100L) + verify(shadeHeaderController).startCustomizingAnimation(true, 100L) } @Test fun testEndCustomizingWithDuration() { - controller.setCustomizerShowing(true, 0L) // Only tracks changes - reset(mShadeHeaderController) + underTest.setCustomizerShowing(true, 0L) // Only tracks changes + reset(shadeHeaderController) - controller.setCustomizerShowing(false, 100L) - verify(mShadeHeaderController).startCustomizingAnimation(false, 100L) + underTest.setCustomizerShowing(false, 100L) + verify(shadeHeaderController).startCustomizingAnimation(false, 100L) } @Test fun testTagListenerAdded() { - verify(fragmentHostManager).addTagListener(eq(QS.TAG), eq(notificationsQSContainer)) + verify(fragmentHostManager).addTagListener(eq(QS.TAG), eq(view)) } @Test fun testTagListenerRemoved() { - attachStateListenerCaptor.value.onViewDetachedFromWindow(notificationsQSContainer) - verify(fragmentHostManager).removeTagListener(eq(QS.TAG), eq(notificationsQSContainer)) + attachStateListenerCaptor.value.onViewDetachedFromWindow(view) + verify(fragmentHostManager).removeTagListener(eq(QS.TAG), eq(view)) } private fun disableSplitShade() { @@ -438,7 +508,7 @@ class NotificationQSContainerControllerTest : SysuiTestCase() { private fun setSplitShadeEnabled(enabled: Boolean) { overrideResource(R.bool.config_use_split_notification_shade, enabled) - controller.updateResources() + underTest.updateResources() } private fun setSmallScreen() { @@ -459,7 +529,7 @@ class NotificationQSContainerControllerTest : SysuiTestCase() { insets: WindowInsets, applyImmediately: Boolean = true ) { - Mockito.clearInvocations(notificationsQSContainer) + Mockito.clearInvocations(view) taskbarVisibilityCallback.onTaskbarStatusUpdated(taskbarVisible, false) navigationModeCallback.onNavigationModeChanged(navigationMode) windowInsetsCallback.accept(insets) @@ -474,12 +544,10 @@ class NotificationQSContainerControllerTest : SysuiTestCase() { expectedNotificationsMargin: Int = NOTIFICATIONS_MARGIN, expectedQsPadding: Int = 0 ) { - verify(notificationsQSContainer) - .setPadding(anyInt(), anyInt(), anyInt(), eq(expectedContainerPadding)) - verify(notificationsQSContainer).setNotificationsMarginBottom(expectedNotificationsMargin) - verify(notificationsQSContainer) - .setQSContainerPaddingBottom(expectedQsPadding) - Mockito.clearInvocations(notificationsQSContainer) + verify(view).setPadding(anyInt(), anyInt(), anyInt(), eq(expectedContainerPadding)) + verify(view).setNotificationsMarginBottom(expectedNotificationsMargin) + verify(view).setQSContainerPaddingBottom(expectedQsPadding) + Mockito.clearInvocations(view) } private fun windowInsets() = mock(WindowInsets::class.java, RETURNS_DEEP_STUBS) @@ -503,10 +571,26 @@ class NotificationQSContainerControllerTest : SysuiTestCase() { private fun newViewWithId(id: Int): View { val view = View(mContext) view.id = id - val layoutParams = ConstraintLayout.LayoutParams( - ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT) + val layoutParams = + ConstraintLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) // required as cloning ConstraintSet fails if view doesn't have layout params view.layoutParams = layoutParams return view } + + companion object { + const val STABLE_INSET_BOTTOM = 100 + const val CUTOUT_HEIGHT = 50 + const val GESTURES_NAVIGATION = WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL + const val BUTTONS_NAVIGATION = WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON + const val NOTIFICATIONS_MARGIN = 50 + const val SCRIM_MARGIN = 10 + const val FOOTER_ACTIONS_INSET = 2 + const val FOOTER_ACTIONS_PADDING = 2 + const val FOOTER_ACTIONS_OFFSET = FOOTER_ACTIONS_INSET + FOOTER_ACTIONS_PADDING + const val QS_PADDING_OFFSET = SCRIM_MARGIN + FOOTER_ACTIONS_OFFSET + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt index d4751c86a87f..a5048187b1b4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt @@ -19,24 +19,47 @@ package com.android.systemui.shade import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.testing.TestableResources +import android.view.View +import android.view.ViewGroup +import android.view.WindowInsets +import android.view.WindowManagerPolicyConstants +import androidx.annotation.IdRes +import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.test.filters.SmallTest import com.android.systemui.R import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.fragments.FragmentHostManager import com.android.systemui.fragments.FragmentService import com.android.systemui.navigationbar.NavigationModeController +import com.android.systemui.navigationbar.NavigationModeController.ModeChangedListener +import com.android.systemui.plugins.qs.QS import com.android.systemui.recents.OverviewProxyService -import com.android.systemui.util.concurrency.DelayableExecutor +import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener +import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.capture import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat +import java.util.function.Consumer import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor +import org.mockito.Captor import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.Mockito.RETURNS_DEEP_STUBS +import org.mockito.Mockito.any +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.doNothing +import org.mockito.Mockito.eq +import org.mockito.Mockito.mock +import org.mockito.Mockito.never +import org.mockito.Mockito.reset import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @@ -51,19 +74,37 @@ class NotificationsQSContainerControllerTest : SysuiTestCase() { @Mock lateinit var shadeHeaderController: ShadeHeaderController @Mock lateinit var shadeExpansionStateManager: ShadeExpansionStateManager @Mock lateinit var fragmentService: FragmentService + @Mock lateinit var fragmentHostManager: FragmentHostManager + @Mock + lateinit var notificationStackScrollLayoutController: NotificationStackScrollLayoutController + + @Captor lateinit var navigationModeCaptor: ArgumentCaptor<ModeChangedListener> + @Captor lateinit var taskbarVisibilityCaptor: ArgumentCaptor<OverviewProxyListener> + @Captor lateinit var windowInsetsCallbackCaptor: ArgumentCaptor<Consumer<WindowInsets>> + @Captor lateinit var constraintSetCaptor: ArgumentCaptor<ConstraintSet> + @Captor lateinit var attachStateListenerCaptor: ArgumentCaptor<View.OnAttachStateChangeListener> lateinit var underTest: NotificationsQSContainerController private lateinit var fakeResources: TestableResources - - private val delayableExecutor: DelayableExecutor = FakeExecutor(FakeSystemClock()) + private lateinit var featureFlags: FakeFeatureFlags + private lateinit var navigationModeCallback: ModeChangedListener + private lateinit var taskbarVisibilityCallback: OverviewProxyListener + private lateinit var windowInsetsCallback: Consumer<WindowInsets> + private lateinit var fakeSystemClock: FakeSystemClock + private lateinit var delayableExecutor: FakeExecutor @Before fun setup() { MockitoAnnotations.initMocks(this) - fakeResources = TestableResources(context.resources) + fakeSystemClock = FakeSystemClock() + delayableExecutor = FakeExecutor(fakeSystemClock) + featureFlags = FakeFeatureFlags().apply { set(Flags.MIGRATE_NSSL, true) } + mContext.ensureTestableResources() + whenever(view.context).thenReturn(mContext) + whenever(view.resources).thenReturn(mContext.resources) - whenever(view.resources).thenReturn(fakeResources.resources) + whenever(fragmentService.getFragmentHostManager(any())).thenReturn(fragmentHostManager) underTest = NotificationsQSContainerController( @@ -74,16 +115,36 @@ class NotificationsQSContainerControllerTest : SysuiTestCase() { shadeExpansionStateManager, fragmentService, delayableExecutor, + featureFlags, + notificationStackScrollLayoutController, ) + + overrideResource(R.dimen.split_shade_notifications_scrim_margin_bottom, SCRIM_MARGIN) + overrideResource(R.dimen.notification_panel_margin_bottom, NOTIFICATIONS_MARGIN) + overrideResource(R.bool.config_use_split_notification_shade, false) + overrideResource(R.dimen.qs_footer_actions_bottom_padding, FOOTER_ACTIONS_PADDING) + overrideResource(R.dimen.qs_footer_action_inset, FOOTER_ACTIONS_INSET) + whenever(navigationModeController.addListener(navigationModeCaptor.capture())) + .thenReturn(GESTURES_NAVIGATION) + doNothing().`when`(overviewProxyService).addCallback(taskbarVisibilityCaptor.capture()) + doNothing().`when`(view).setInsetsChangedListener(windowInsetsCallbackCaptor.capture()) + doNothing().`when`(view).applyConstraints(constraintSetCaptor.capture()) + doNothing().`when`(view).addOnAttachStateChangeListener(attachStateListenerCaptor.capture()) + underTest.init() + attachStateListenerCaptor.value.onViewAttachedToWindow(view) + + navigationModeCallback = navigationModeCaptor.value + taskbarVisibilityCallback = taskbarVisibilityCaptor.value + windowInsetsCallback = windowInsetsCallbackCaptor.value + + Mockito.clearInvocations(view) } @Test fun testSmallScreen_updateResources_splitShadeHeightIsSet() { - with(fakeResources) { - addOverride(R.bool.config_use_large_screen_shade_header, false) - addOverride(R.dimen.qs_header_height, 1) - addOverride(R.dimen.large_screen_shade_header_height, 2) - } + overrideResource(R.bool.config_use_large_screen_shade_header, false) + overrideResource(R.dimen.qs_header_height, 1) + overrideResource(R.dimen.large_screen_shade_header_height, 2) underTest.updateResources() @@ -94,11 +155,9 @@ class NotificationsQSContainerControllerTest : SysuiTestCase() { @Test fun testLargeScreen_updateResources_splitShadeHeightIsSet() { - with(fakeResources) { - addOverride(R.bool.config_use_large_screen_shade_header, true) - addOverride(R.dimen.qs_header_height, 1) - addOverride(R.dimen.large_screen_shade_header_height, 2) - } + overrideResource(R.bool.config_use_large_screen_shade_header, true) + overrideResource(R.dimen.qs_header_height, 1) + overrideResource(R.dimen.large_screen_shade_header_height, 2) underTest.updateResources() @@ -106,4 +165,415 @@ class NotificationsQSContainerControllerTest : SysuiTestCase() { verify(view).applyConstraints(capture(captor)) assertThat(captor.value.getHeight(R.id.split_shade_status_bar)).isEqualTo(2) } + + @Test + fun testTaskbarVisibleInSplitShade() { + enableSplitShade() + + given( + taskbarVisible = true, + navigationMode = GESTURES_NAVIGATION, + insets = windowInsets().withStableBottom() + ) + then( + expectedContainerPadding = 0, // taskbar should disappear when shade is expanded + expectedNotificationsMargin = NOTIFICATIONS_MARGIN, + expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET + ) + + given( + taskbarVisible = true, + navigationMode = BUTTONS_NAVIGATION, + insets = windowInsets().withStableBottom() + ) + then( + expectedContainerPadding = STABLE_INSET_BOTTOM, + expectedNotificationsMargin = NOTIFICATIONS_MARGIN, + expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET + ) + } + + @Test + fun testTaskbarNotVisibleInSplitShade() { + // when taskbar is not visible, it means we're on the home screen + enableSplitShade() + + given( + taskbarVisible = false, + navigationMode = GESTURES_NAVIGATION, + insets = windowInsets().withStableBottom() + ) + then( + expectedContainerPadding = 0, + expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET + ) + + given( + taskbarVisible = false, + navigationMode = BUTTONS_NAVIGATION, + insets = windowInsets().withStableBottom() + ) + then( + expectedContainerPadding = 0, // qs goes full height as it's not obscuring nav buttons + expectedNotificationsMargin = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN, + expectedQsPadding = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET + ) + } + + @Test + fun testTaskbarNotVisibleInSplitShadeWithCutout() { + enableSplitShade() + + given( + taskbarVisible = false, + navigationMode = GESTURES_NAVIGATION, + insets = windowInsets().withCutout() + ) + then( + expectedContainerPadding = CUTOUT_HEIGHT, + expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET + ) + + given( + taskbarVisible = false, + navigationMode = BUTTONS_NAVIGATION, + insets = windowInsets().withCutout().withStableBottom() + ) + then( + expectedContainerPadding = 0, + expectedNotificationsMargin = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN, + expectedQsPadding = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET + ) + } + + @Test + fun testTaskbarVisibleInSinglePaneShade() { + disableSplitShade() + + given( + taskbarVisible = true, + navigationMode = GESTURES_NAVIGATION, + insets = windowInsets().withStableBottom() + ) + then(expectedContainerPadding = 0, expectedQsPadding = STABLE_INSET_BOTTOM) + + given( + taskbarVisible = true, + navigationMode = BUTTONS_NAVIGATION, + insets = windowInsets().withStableBottom() + ) + then( + expectedContainerPadding = STABLE_INSET_BOTTOM, + expectedQsPadding = STABLE_INSET_BOTTOM + ) + } + + @Test + fun testTaskbarNotVisibleInSinglePaneShade() { + disableSplitShade() + + given(taskbarVisible = false, navigationMode = GESTURES_NAVIGATION, insets = emptyInsets()) + then(expectedContainerPadding = 0) + + given( + taskbarVisible = false, + navigationMode = GESTURES_NAVIGATION, + insets = windowInsets().withCutout().withStableBottom() + ) + then(expectedContainerPadding = CUTOUT_HEIGHT, expectedQsPadding = STABLE_INSET_BOTTOM) + + given( + taskbarVisible = false, + navigationMode = BUTTONS_NAVIGATION, + insets = windowInsets().withStableBottom() + ) + then( + expectedContainerPadding = 0, + expectedNotificationsMargin = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN, + expectedQsPadding = STABLE_INSET_BOTTOM + ) + } + + @Test + fun testDetailShowingInSinglePaneShade() { + disableSplitShade() + underTest.setDetailShowing(true) + + // always sets spacings to 0 + given( + taskbarVisible = false, + navigationMode = GESTURES_NAVIGATION, + insets = windowInsets().withStableBottom() + ) + then(expectedContainerPadding = 0, expectedNotificationsMargin = 0) + + given(taskbarVisible = false, navigationMode = BUTTONS_NAVIGATION, insets = emptyInsets()) + then(expectedContainerPadding = 0, expectedNotificationsMargin = 0) + } + + @Test + fun testDetailShowingInSplitShade() { + enableSplitShade() + underTest.setDetailShowing(true) + + given( + taskbarVisible = false, + navigationMode = GESTURES_NAVIGATION, + insets = windowInsets().withStableBottom() + ) + then(expectedContainerPadding = 0) + + // should not influence spacing + given(taskbarVisible = false, navigationMode = BUTTONS_NAVIGATION, insets = emptyInsets()) + then(expectedContainerPadding = 0) + } + + @Test + fun testNotificationsMarginBottomIsUpdated() { + Mockito.clearInvocations(view) + enableSplitShade() + verify(view).setNotificationsMarginBottom(NOTIFICATIONS_MARGIN) + + overrideResource(R.dimen.notification_panel_margin_bottom, 100) + disableSplitShade() + verify(view).setNotificationsMarginBottom(100) + } + + @Test + fun testSplitShadeLayout_isAlignedToGuideline() { + enableSplitShade() + underTest.updateResources() + assertThat(getConstraintSetLayout(R.id.qs_frame).endToEnd).isEqualTo(R.id.qs_edge_guideline) + } + + @Test + fun testSinglePaneLayout_childrenHaveEqualMargins() { + disableSplitShade() + underTest.updateResources() + val qsStartMargin = getConstraintSetLayout(R.id.qs_frame).startMargin + val qsEndMargin = getConstraintSetLayout(R.id.qs_frame).endMargin + assertThat(qsStartMargin == qsEndMargin).isTrue() + } + + @Test + fun testSplitShadeLayout_childrenHaveInsideMarginsOfZero() { + enableSplitShade() + underTest.updateResources() + assertThat(getConstraintSetLayout(R.id.qs_frame).endMargin).isEqualTo(0) + } + + @Test + fun testSplitShadeLayout_qsFrameHasHorizontalMarginsOfZero() { + enableSplitShade() + underTest.updateResources() + assertThat(getConstraintSetLayout(R.id.qs_frame).endMargin).isEqualTo(0) + assertThat(getConstraintSetLayout(R.id.qs_frame).startMargin).isEqualTo(0) + } + + @Test + fun testLargeScreenLayout_qsAndNotifsTopMarginIsOfHeaderHeight() { + setLargeScreen() + val largeScreenHeaderHeight = 100 + overrideResource(R.dimen.large_screen_shade_header_height, largeScreenHeaderHeight) + + underTest.updateResources() + + assertThat(getConstraintSetLayout(R.id.qs_frame).topMargin) + .isEqualTo(largeScreenHeaderHeight) + } + + @Test + fun testSmallScreenLayout_qsAndNotifsTopMarginIsZero() { + setSmallScreen() + underTest.updateResources() + assertThat(getConstraintSetLayout(R.id.qs_frame).topMargin).isEqualTo(0) + } + + @Test + fun testSinglePaneShadeLayout_qsFrameHasHorizontalMarginsSetToCorrectValue() { + disableSplitShade() + underTest.updateResources() + val notificationPanelMarginHorizontal = + mContext.resources.getDimensionPixelSize(R.dimen.notification_panel_margin_horizontal) + assertThat(getConstraintSetLayout(R.id.qs_frame).endMargin) + .isEqualTo(notificationPanelMarginHorizontal) + assertThat(getConstraintSetLayout(R.id.qs_frame).startMargin) + .isEqualTo(notificationPanelMarginHorizontal) + } + + @Test + fun testSinglePaneShadeLayout_isAlignedToParent() { + disableSplitShade() + underTest.updateResources() + assertThat(getConstraintSetLayout(R.id.qs_frame).endToEnd) + .isEqualTo(ConstraintSet.PARENT_ID) + } + + @Test + fun testAllChildrenOfNotificationContainer_haveIds() { + // set dimen to 0 to avoid triggering updating bottom spacing + overrideResource(R.dimen.split_shade_notifications_scrim_margin_bottom, 0) + val container = NotificationsQuickSettingsContainer(mContext, null) + container.removeAllViews() + container.addView(newViewWithId(1)) + container.addView(newViewWithId(View.NO_ID)) + val controller = + NotificationsQSContainerController( + container, + navigationModeController, + overviewProxyService, + shadeHeaderController, + shadeExpansionStateManager, + fragmentService, + delayableExecutor, + featureFlags, + notificationStackScrollLayoutController, + ) + controller.updateConstraints() + + assertThat(container.getChildAt(0).id).isEqualTo(1) + assertThat(container.getChildAt(1).id).isNotEqualTo(View.NO_ID) + } + + @Test + fun testWindowInsetDebounce() { + disableSplitShade() + + given( + taskbarVisible = false, + navigationMode = GESTURES_NAVIGATION, + insets = emptyInsets(), + applyImmediately = false + ) + fakeSystemClock.advanceTime(INSET_DEBOUNCE_MILLIS / 2) + windowInsetsCallback.accept(windowInsets().withStableBottom()) + + delayableExecutor.advanceClockToLast() + delayableExecutor.runAllReady() + + verify(view, never()).setQSContainerPaddingBottom(0) + verify(view).setQSContainerPaddingBottom(STABLE_INSET_BOTTOM) + } + + @Test + fun testStartCustomizingWithDuration() { + underTest.setCustomizerShowing(true, 100L) + verify(shadeHeaderController).startCustomizingAnimation(true, 100L) + } + + @Test + fun testEndCustomizingWithDuration() { + underTest.setCustomizerShowing(true, 0L) // Only tracks changes + reset(shadeHeaderController) + + underTest.setCustomizerShowing(false, 100L) + verify(shadeHeaderController).startCustomizingAnimation(false, 100L) + } + + @Test + fun testTagListenerAdded() { + verify(fragmentHostManager).addTagListener(eq(QS.TAG), eq(view)) + } + + @Test + fun testTagListenerRemoved() { + attachStateListenerCaptor.value.onViewDetachedFromWindow(view) + verify(fragmentHostManager).removeTagListener(eq(QS.TAG), eq(view)) + } + + private fun disableSplitShade() { + setSplitShadeEnabled(false) + } + + private fun enableSplitShade() { + setSplitShadeEnabled(true) + } + + private fun setSplitShadeEnabled(enabled: Boolean) { + overrideResource(R.bool.config_use_split_notification_shade, enabled) + underTest.updateResources() + } + + private fun setSmallScreen() { + setLargeScreenEnabled(false) + } + + private fun setLargeScreen() { + setLargeScreenEnabled(true) + } + + private fun setLargeScreenEnabled(enabled: Boolean) { + overrideResource(R.bool.config_use_large_screen_shade_header, enabled) + } + + private fun given( + taskbarVisible: Boolean, + navigationMode: Int, + insets: WindowInsets, + applyImmediately: Boolean = true + ) { + Mockito.clearInvocations(view) + taskbarVisibilityCallback.onTaskbarStatusUpdated(taskbarVisible, false) + navigationModeCallback.onNavigationModeChanged(navigationMode) + windowInsetsCallback.accept(insets) + if (applyImmediately) { + delayableExecutor.advanceClockToLast() + delayableExecutor.runAllReady() + } + } + + fun then( + expectedContainerPadding: Int, + expectedNotificationsMargin: Int = NOTIFICATIONS_MARGIN, + expectedQsPadding: Int = 0 + ) { + verify(view).setPadding(anyInt(), anyInt(), anyInt(), eq(expectedContainerPadding)) + verify(view).setNotificationsMarginBottom(expectedNotificationsMargin) + verify(view).setQSContainerPaddingBottom(expectedQsPadding) + Mockito.clearInvocations(view) + } + + private fun windowInsets() = mock(WindowInsets::class.java, RETURNS_DEEP_STUBS) + + private fun emptyInsets() = mock(WindowInsets::class.java) + + private fun WindowInsets.withCutout(): WindowInsets { + whenever(displayCutout.safeInsetBottom).thenReturn(CUTOUT_HEIGHT) + return this + } + + private fun WindowInsets.withStableBottom(): WindowInsets { + whenever(stableInsetBottom).thenReturn(STABLE_INSET_BOTTOM) + return this + } + + private fun getConstraintSetLayout(@IdRes id: Int): ConstraintSet.Layout { + return constraintSetCaptor.value.getConstraint(id).layout + } + + private fun newViewWithId(id: Int): View { + val view = View(mContext) + view.id = id + val layoutParams = + ConstraintLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + // required as cloning ConstraintSet fails if view doesn't have layout params + view.layoutParams = layoutParams + return view + } + + companion object { + const val STABLE_INSET_BOTTOM = 100 + const val CUTOUT_HEIGHT = 50 + const val GESTURES_NAVIGATION = WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL + const val BUTTONS_NAVIGATION = WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON + const val NOTIFICATIONS_MARGIN = 50 + const val SCRIM_MARGIN = 10 + const val FOOTER_ACTIONS_INSET = 2 + const val FOOTER_ACTIONS_PADDING = 2 + const val FOOTER_ACTIONS_OFFSET = FOOTER_ACTIONS_INSET + FOOTER_ACTIONS_PADDING + const val QS_PADDING_OFFSET = SCRIM_MARGIN + FOOTER_ACTIONS_OFFSET + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/DreamCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/DreamCoordinatorTest.kt new file mode 100644 index 000000000000..a544cad03b77 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/DreamCoordinatorTest.kt @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2023 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.statusbar.notification.collection.coordinator + +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.statusbar.StatusBarState +import com.android.systemui.statusbar.SysuiStatusBarStateController +import com.android.systemui.statusbar.notification.collection.NotifPipeline +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder +import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter +import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Pluggable +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.withArgCaptor +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.clearInvocations +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class DreamCoordinatorTest : SysuiTestCase() { + @Mock private lateinit var statusBarStateController: SysuiStatusBarStateController + @Mock private lateinit var notifPipeline: NotifPipeline + @Mock private lateinit var filterListener: Pluggable.PluggableListener<NotifFilter> + + private val keyguardRepository = FakeKeyguardRepository() + private var fakeEntry: NotificationEntry = NotificationEntryBuilder().build() + val testDispatcher = UnconfinedTestDispatcher() + val testScope = TestScope(testDispatcher) + + private lateinit var filter: NotifFilter + private lateinit var statusBarListener: StatusBarStateController.StateListener + private lateinit var dreamCoordinator: DreamCoordinator + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD) + + // Build the coordinator + dreamCoordinator = + DreamCoordinator( + statusBarStateController, + testScope.backgroundScope, + keyguardRepository + ) + + // Attach the pipeline and capture the listeners/filters that it registers + dreamCoordinator.attach(notifPipeline) + + filter = withArgCaptor { verify(notifPipeline).addPreGroupFilter(capture()) } + filter.setInvalidationListener(filterListener) + + statusBarListener = withArgCaptor { + verify(statusBarStateController).addCallback(capture()) + } + } + + @Test + fun hideNotifications_whenDreamingAndOnKeyguard() = + testScope.runTest { + // GIVEN we are on keyguard and not dreaming + keyguardRepository.setKeyguardShowing(true) + keyguardRepository.setIsActiveDreamLockscreenHosted(false) + runCurrent() + + // THEN notifications are not filtered out + verifyPipelinesNotInvalidated() + assertThat(filter.shouldFilterOut(fakeEntry, 0L)).isFalse() + + // WHEN dreaming starts and the active dream is hosted in lockscreen + keyguardRepository.setIsActiveDreamLockscreenHosted(true) + runCurrent() + + // THEN pipeline is notified and notifications should all be filtered out + verifyPipelinesInvalidated() + assertThat(filter.shouldFilterOut(fakeEntry, 0L)).isTrue() + } + + @Test + fun showNotifications_whenDreamingAndNotOnKeyguard() = + testScope.runTest { + // GIVEN we are on the keyguard and active dream is hosted in lockscreen + keyguardRepository.setKeyguardShowing(true) + keyguardRepository.setIsActiveDreamLockscreenHosted(true) + runCurrent() + + // THEN pipeline is notified and notifications are all filtered out + verifyPipelinesInvalidated() + clearPipelineInvocations() + assertThat(filter.shouldFilterOut(fakeEntry, 0L)).isTrue() + + // WHEN we are no longer on the keyguard + statusBarListener.onStateChanged(StatusBarState.SHADE) + + // THEN pipeline is notified and notifications are not filtered out + verifyPipelinesInvalidated() + assertThat(filter.shouldFilterOut(fakeEntry, 0L)).isFalse() + } + + @Test + fun showNotifications_whenOnKeyguardAndNotDreaming() = + testScope.runTest { + // GIVEN we are on the keyguard and active dream is hosted in lockscreen + keyguardRepository.setKeyguardShowing(true) + keyguardRepository.setIsActiveDreamLockscreenHosted(true) + runCurrent() + + // THEN pipeline is notified and notifications are all filtered out + verifyPipelinesInvalidated() + clearPipelineInvocations() + assertThat(filter.shouldFilterOut(fakeEntry, 0L)).isTrue() + + // WHEN the lockscreen hosted dream stops + keyguardRepository.setIsActiveDreamLockscreenHosted(false) + runCurrent() + + // THEN pipeline is notified and notifications are not filtered out + verifyPipelinesInvalidated() + assertThat(filter.shouldFilterOut(fakeEntry, 0L)).isFalse() + } + + private fun verifyPipelinesInvalidated() { + verify(filterListener).onPluggableInvalidated(eq(filter), any()) + } + + private fun verifyPipelinesNotInvalidated() { + verify(filterListener, never()).onPluggableInvalidated(eq(filter), any()) + } + + private fun clearPipelineInvocations() { + clearInvocations(filterListener) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/TextPrecomputerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/TextPrecomputerTest.kt new file mode 100644 index 000000000000..d46763df8a75 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/TextPrecomputerTest.kt @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2023 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.row + +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper.RunWithLooper +import android.text.PrecomputedText +import android.text.TextPaint +import android.widget.TextView +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@RunWithLooper +class TextPrecomputerTest : SysuiTestCase() { + + private lateinit var textPrecomputer: TextPrecomputer + + private lateinit var textView: TextView + + @Before + fun before() { + textPrecomputer = object : TextPrecomputer {} + textView = TextView(mContext) + } + + @Test + fun precompute_returnRunnable() { + // WHEN + val precomputeResult = textPrecomputer.precompute(textView, TEXT) + + // THEN + assertThat(precomputeResult).isInstanceOf(Runnable::class.java) + } + + @Test + fun precomputeRunnable_anyText_setPrecomputedText() { + // WHEN + textPrecomputer.precompute(textView, TEXT).run() + + // THEN + assertThat(textView.text).isInstanceOf(PrecomputedText::class.java) + } + + @Test + fun precomputeRunnable_differentPrecomputedTextConfig_notSetPrecomputedText() { + // GIVEN + val precomputedTextRunnable = + textPrecomputer.precompute(textView, TEXT, logException = false) + + // WHEN + textView.textMetricsParams = PrecomputedText.Params.Builder(PAINT).build() + precomputedTextRunnable.run() + + // THEN + assertThat(textView.text).isInstanceOf(String::class.java) + } + + @Test + fun precomputeRunnable_nullText_setNull() { + // GIVEN + textView.text = TEXT + val precomputedTextRunnable = textPrecomputer.precompute(textView, null) + + // WHEN + precomputedTextRunnable.run() + + // THEN + assertThat(textView.text).isEqualTo("") + } + + private companion object { + private val PAINT = TextPaint() + private const val TEXT = "Example Notification Test" + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractorTest.kt new file mode 100644 index 000000000000..7bbb09483b5f --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractorTest.kt @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2023 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.domain.interactor + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository +import com.android.systemui.coroutines.collectLastValue +import com.google.common.truth.Truth.assertThat +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 SharedNotificationContainerInteractorTest : SysuiTestCase() { + private lateinit var configurationRepository: FakeConfigurationRepository + private lateinit var underTest: SharedNotificationContainerInteractor + + @Before + fun setUp() { + configurationRepository = FakeConfigurationRepository() + underTest = + SharedNotificationContainerInteractor( + configurationRepository, + mContext, + ) + } + + @Test + fun validateConfigValues() = runTest { + overrideResource(R.bool.config_use_split_notification_shade, true) + overrideResource(R.bool.config_use_large_screen_shade_header, false) + overrideResource(R.dimen.notification_panel_margin_horizontal, 0) + overrideResource(R.dimen.notification_panel_margin_bottom, 10) + overrideResource(R.dimen.notification_panel_margin_top, 10) + overrideResource(R.dimen.large_screen_shade_header_height, 0) + + val dimens = collectLastValue(underTest.configurationBasedDimensions) + + configurationRepository.onAnyConfigurationChange() + runCurrent() + + val lastDimens = dimens()!! + + assertThat(lastDimens.useSplitShade).isTrue() + assertThat(lastDimens.useLargeScreenHeader).isFalse() + assertThat(lastDimens.marginHorizontal).isEqualTo(0) + assertThat(lastDimens.marginBottom).isGreaterThan(0) + assertThat(lastDimens.marginTop).isGreaterThan(0) + assertThat(lastDimens.marginTopLargeScreen).isEqualTo(0) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt new file mode 100644 index 000000000000..afd995460151 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2023 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 androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor +import com.google.common.truth.Truth.assertThat +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 SharedNotificationContainerViewModelTest : SysuiTestCase() { + private lateinit var configurationRepository: FakeConfigurationRepository + private lateinit var sharedNotificationContainerInteractor: + SharedNotificationContainerInteractor + private lateinit var underTest: SharedNotificationContainerViewModel + + @Before + fun setUp() { + configurationRepository = FakeConfigurationRepository() + sharedNotificationContainerInteractor = + SharedNotificationContainerInteractor( + configurationRepository, + mContext, + ) + underTest = SharedNotificationContainerViewModel(sharedNotificationContainerInteractor) + } + + @Test + fun validateMarginStartInSplitShade() = runTest { + overrideResource(R.bool.config_use_split_notification_shade, true) + overrideResource(R.dimen.notification_panel_margin_horizontal, 20) + + val dimens = collectLastValue(underTest.configurationBasedDimensions) + + configurationRepository.onAnyConfigurationChange() + runCurrent() + + val lastDimens = dimens()!! + + assertThat(lastDimens.marginStart).isEqualTo(0) + } + + @Test + fun validateMarginStart() = runTest { + overrideResource(R.bool.config_use_split_notification_shade, false) + overrideResource(R.dimen.notification_panel_margin_horizontal, 20) + + val dimens = collectLastValue(underTest.configurationBasedDimensions) + + configurationRepository.onAnyConfigurationChange() + runCurrent() + + val lastDimens = dimens()!! + + assertThat(lastDimens.marginStart).isEqualTo(20) + } + + @Test + fun validateMarginEnd() = runTest { + overrideResource(R.dimen.notification_panel_margin_horizontal, 50) + + val dimens = collectLastValue(underTest.configurationBasedDimensions) + + configurationRepository.onAnyConfigurationChange() + runCurrent() + + val lastDimens = dimens()!! + + assertThat(lastDimens.marginEnd).isEqualTo(50) + } + + @Test + fun validateMarginBottom() = runTest { + overrideResource(R.dimen.notification_panel_margin_bottom, 50) + + val dimens = collectLastValue(underTest.configurationBasedDimensions) + + configurationRepository.onAnyConfigurationChange() + runCurrent() + + val lastDimens = dimens()!! + + assertThat(lastDimens.marginBottom).isEqualTo(50) + } + + @Test + fun validateMarginTopWithLargeScreenHeader() = runTest { + overrideResource(R.bool.config_use_large_screen_shade_header, true) + overrideResource(R.dimen.large_screen_shade_header_height, 50) + overrideResource(R.dimen.notification_panel_margin_top, 0) + + val dimens = collectLastValue(underTest.configurationBasedDimensions) + + configurationRepository.onAnyConfigurationChange() + runCurrent() + + val lastDimens = dimens()!! + + assertThat(lastDimens.marginTop).isEqualTo(50) + } + + @Test + fun validateMarginTop() = runTest { + overrideResource(R.bool.config_use_large_screen_shade_header, false) + overrideResource(R.dimen.large_screen_shade_header_height, 50) + overrideResource(R.dimen.notification_panel_margin_top, 0) + + val dimens = collectLastValue(underTest.configurationBasedDimensions) + + configurationRepository.onAnyConfigurationChange() + runCurrent() + + val lastDimens = dimens()!! + + assertThat(lastDimens.marginTop).isEqualTo(0) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java index 89f8bdbfe05b..479803e1dfac 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java @@ -142,7 +142,8 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { mNotificationMediaManager, mWakefulnessLifecycle, mScreenLifecycle, mAuthController, mStatusBarStateController, mSessionTracker, mLatencyTracker, mScreenOffAnimationController, mVibratorHelper, - mSystemClock + mSystemClock, + mStatusBarKeyguardViewManager ); mBiometricUnlockController.setKeyguardViewController(mStatusBarKeyguardViewManager); mBiometricUnlockController.addListener(mBiometricUnlockEventsListener); @@ -464,6 +465,69 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { } @Test + public void onSideFingerprintSuccess_dreaming_unlockThenWake() { + when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); + when(mWakefulnessLifecycle.getLastWakeReason()) + .thenReturn(PowerManager.WAKE_REASON_POWER_BUTTON); + final ArgumentCaptor<Runnable> afterKeyguardGoneRunnableCaptor = + ArgumentCaptor.forClass(Runnable.class); + givenDreamingLocked(); + mBiometricUnlockController.startWakeAndUnlock(BiometricSourceType.FINGERPRINT, true); + + // Make sure the BiometricUnlockController has registered a callback for when the keyguard + // is gone + verify(mStatusBarKeyguardViewManager).addAfterKeyguardGoneRunnable( + afterKeyguardGoneRunnableCaptor.capture()); + // Ensure that the power hasn't been told to wake up yet. + verify(mPowerManager, never()).wakeUp(anyLong(), anyInt(), anyString()); + // Check that the keyguard has been told to unlock. + verify(mKeyguardViewMediator).onWakeAndUnlocking(); + + // Simulate the keyguard disappearing. + afterKeyguardGoneRunnableCaptor.getValue().run(); + // Verify that the power manager has been told to wake up now. + verify(mPowerManager).wakeUp(anyLong(), anyInt(), anyString()); + } + + @Test + public void onSideFingerprintSuccess_dreaming_unlockIfStillLockedNotDreaming() { + when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); + when(mWakefulnessLifecycle.getLastWakeReason()) + .thenReturn(PowerManager.WAKE_REASON_POWER_BUTTON); + final ArgumentCaptor<Runnable> afterKeyguardGoneRunnableCaptor = + ArgumentCaptor.forClass(Runnable.class); + givenDreamingLocked(); + mBiometricUnlockController.startWakeAndUnlock(BiometricSourceType.FINGERPRINT, true); + + // Make sure the BiometricUnlockController has registered a callback for when the keyguard + // is gone + verify(mStatusBarKeyguardViewManager).addAfterKeyguardGoneRunnable( + afterKeyguardGoneRunnableCaptor.capture()); + // Ensure that the power hasn't been told to wake up yet. + verify(mPowerManager, never()).wakeUp(anyLong(), anyInt(), anyString()); + // Check that the keyguard has been told to unlock. + verify(mKeyguardViewMediator).onWakeAndUnlocking(); + + when(mUpdateMonitor.isDreaming()).thenReturn(false); + when(mKeyguardStateController.isUnlocked()).thenReturn(false); + + // Simulate the keyguard disappearing. + afterKeyguardGoneRunnableCaptor.getValue().run(); + + final ArgumentCaptor<Runnable> dismissKeyguardRunnableCaptor = + ArgumentCaptor.forClass(Runnable.class); + verify(mHandler).post(dismissKeyguardRunnableCaptor.capture()); + + // Verify that the power manager was not told to wake up. + verify(mPowerManager, never()).wakeUp(anyLong(), anyInt(), anyString()); + + dismissKeyguardRunnableCaptor.getValue().run(); + // Verify that the keyguard controller is told to unlock. + verify(mStatusBarKeyguardViewManager).notifyKeyguardAuthenticated(eq(false)); + } + + + @Test public void onSideFingerprintSuccess_oldPowerButtonPress_playHaptic() { // GIVEN side fingerprint enrolled, last wake reason was power button when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); @@ -537,6 +601,11 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(anyBoolean()); } + private void givenDreamingLocked() { + when(mUpdateMonitor.isDreaming()).thenReturn(true); + when(mUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true); + } + private void givenFingerprintModeUnlockCollapsing() { when(mUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true); when(mUpdateMonitor.isDeviceInteractive()).thenReturn(true); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java index 3d35233ad646..530085191e96 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java @@ -129,7 +129,6 @@ import com.android.systemui.settings.brightness.BrightnessSliderController; import com.android.systemui.shade.CameraLauncher; import com.android.systemui.shade.NotificationPanelView; import com.android.systemui.shade.NotificationPanelViewController; -import com.android.systemui.shade.NotificationShadeWindowView; import com.android.systemui.shade.NotificationShadeWindowViewController; import com.android.systemui.shade.QuickSettingsController; import com.android.systemui.shade.ShadeController; @@ -252,7 +251,6 @@ public class CentralSurfacesImplTest extends SysuiTestCase { @Mock private NotificationLogger.ExpansionStateLogger mExpansionStateLogger; @Mock private KeyguardUpdateMonitor mKeyguardUpdateMonitor; @Mock private StatusBarSignalPolicy mStatusBarSignalPolicy; - @Mock private NotificationShadeWindowView mNotificationShadeWindowView; @Mock private BroadcastDispatcher mBroadcastDispatcher; @Mock private AssistManager mAssistManager; @Mock private NotificationGutsManager mNotificationGutsManager; @@ -276,6 +274,8 @@ public class CentralSurfacesImplTest extends SysuiTestCase { @Mock private NotificationShadeWindowController mNotificationShadeWindowController; @Mock private NotificationIconAreaController mNotificationIconAreaController; @Mock private NotificationShadeWindowViewController mNotificationShadeWindowViewController; + @Mock private Lazy<NotificationShadeWindowViewController> + mNotificationShadeWindowViewControllerLazy; @Mock private NotificationShelfController mNotificationShelfController; @Mock private DozeParameters mDozeParameters; @Mock private Lazy<LockscreenWallpaper> mLockscreenWallpaperLazy; @@ -428,10 +428,10 @@ public class CentralSurfacesImplTest extends SysuiTestCase { when(mLockscreenWallpaperLazy.get()).thenReturn(mLockscreenWallpaper); when(mBiometricUnlockControllerLazy.get()).thenReturn(mBiometricUnlockController); when(mCameraLauncherLazy.get()).thenReturn(mCameraLauncher); + when(mNotificationShadeWindowViewControllerLazy.get()) + .thenReturn(mNotificationShadeWindowViewController); when(mStatusBarComponentFactory.create()).thenReturn(mCentralSurfacesComponent); - when(mCentralSurfacesComponent.getNotificationShadeWindowViewController()).thenReturn( - mNotificationShadeWindowViewController); doAnswer(invocation -> { ((Runnable) invocation.getArgument(0)).run(); return null; @@ -510,6 +510,7 @@ public class CentralSurfacesImplTest extends SysuiTestCase { () -> mAssistManager, configurationController, mNotificationShadeWindowController, + mNotificationShadeWindowViewControllerLazy, mNotificationShelfController, mStackScrollerController, mDozeParameters, @@ -586,9 +587,8 @@ public class CentralSurfacesImplTest extends SysuiTestCase { when(mKeyguardViewMediator.getViewMediatorCallback()).thenReturn( mKeyguardVieMediatorCallback); - // TODO: we should be able to call mCentralSurfaces.start() and have all the below values - // initialized automatically and make NPVC private. - mCentralSurfaces.mNotificationShadeWindowView = mNotificationShadeWindowView; + // TODO(b/277764509): we should be able to call mCentralSurfaces.start() and have all the + // below values initialized automatically. mCentralSurfaces.mDozeScrimController = mDozeScrimController; mCentralSurfaces.mPresenter = mNotificationPresenter; mCentralSurfaces.mKeyguardIndicationController = mKeyguardIndicationController; @@ -823,8 +823,6 @@ public class CentralSurfacesImplTest extends SysuiTestCase { */ @Test public void testPredictiveBackCallback_invocationCollapsesPanel() { - mCentralSurfaces.setNotificationShadeWindowViewController( - mNotificationShadeWindowViewController); mCentralSurfaces.handleVisibleToUserChanged(true); verify(mOnBackInvokedDispatcher).registerOnBackInvokedCallback( eq(OnBackInvokedDispatcher.PRIORITY_DEFAULT), @@ -841,8 +839,6 @@ public class CentralSurfacesImplTest extends SysuiTestCase { */ @Test public void testPredictiveBackAnimation_progressMaxScalesPanel() { - mCentralSurfaces.setNotificationShadeWindowViewController( - mNotificationShadeWindowViewController); mCentralSurfaces.handleVisibleToUserChanged(true); verify(mOnBackInvokedDispatcher).registerOnBackInvokedCallback( eq(OnBackInvokedDispatcher.PRIORITY_DEFAULT), @@ -864,8 +860,6 @@ public class CentralSurfacesImplTest extends SysuiTestCase { */ @Test public void testPredictiveBackAnimation_progressMinScalesPanel() { - mCentralSurfaces.setNotificationShadeWindowViewController( - mNotificationShadeWindowViewController); mCentralSurfaces.handleVisibleToUserChanged(true); verify(mOnBackInvokedDispatcher).registerOnBackInvokedCallback( eq(OnBackInvokedDispatcher.PRIORITY_DEFAULT), diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt index 2d96e594592c..c8ec1bf4af9f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt @@ -204,6 +204,7 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() { userChipViewModel, centralSurfacesImpl, shadeControllerImpl, + shadeViewController, shadeLogger, viewUtil, configurationController diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java index 5bd6ff4e73f2..cd8aaa2685c2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java @@ -42,7 +42,6 @@ import com.android.systemui.settings.FakeDisplayTracker; import com.android.systemui.shade.NotificationShadeWindowView; import com.android.systemui.shade.QuickSettingsController; import com.android.systemui.shade.ShadeController; -import com.android.systemui.shade.ShadeNotificationPresenter; import com.android.systemui.shade.ShadeViewController; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.LockscreenShadeTransitionController; @@ -52,7 +51,6 @@ import com.android.systemui.statusbar.NotificationRemoteInputManager; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.notification.DynamicPrivacyController; -import com.android.systemui.statusbar.notification.NotifPipelineFlags; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder; import com.android.systemui.statusbar.notification.collection.render.NotifShadeEventSource; @@ -86,14 +84,11 @@ public class StatusBarNotificationPresenterTest extends SysuiTestCase { mock(NotificationsInteractor.class); private final KeyguardStateController mKeyguardStateController = mock(KeyguardStateController.class); - private final NotifPipelineFlags mNotifPipelineFlags = mock(NotifPipelineFlags.class); private final InitController mInitController = new InitController(); @Before public void setup() { mMetricsLogger = new FakeMetricsLogger(); - LockscreenGestureLogger lockscreenGestureLogger = new LockscreenGestureLogger( - mMetricsLogger); mCommandQueue = new CommandQueue(mContext, new FakeDisplayTracker(mContext)); mDependency.injectTestDependency(StatusBarStateController.class, mock(SysuiStatusBarStateController.class)); @@ -111,8 +106,6 @@ public class StatusBarNotificationPresenterTest extends SysuiTestCase { when(notificationShadeWindowView.getResources()).thenReturn(mContext.getResources()); ShadeViewController shadeViewController = mock(ShadeViewController.class); - when(shadeViewController.getShadeNotificationPresenter()) - .thenReturn(mock(ShadeNotificationPresenter.class)); mStatusBarNotificationPresenter = new StatusBarNotificationPresenter( mContext, shadeViewController, @@ -135,11 +128,9 @@ public class StatusBarNotificationPresenterTest extends SysuiTestCase { mock(NotifShadeEventSource.class), mock(NotificationMediaManager.class), mock(NotificationGutsManager.class), - lockscreenGestureLogger, mInitController, mNotificationInterruptStateProvider, mock(NotificationRemoteInputManager.class), - mNotifPipelineFlags, mock(NotificationRemoteInputManager.Callback.class), mock(NotificationListContainer.class)); mInitController.executePostInitTasks(); 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 0c77529377ab..c81910855f78 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java @@ -16,6 +16,7 @@ package com.android.systemui.volume; +import static com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION; import static com.android.systemui.volume.Events.DISMISS_REASON_UNKNOWN; import static com.android.systemui.volume.Events.SHOW_REASON_UNKNOWN; import static com.android.systemui.volume.VolumeDialogControllerImpl.STREAMS; @@ -51,6 +52,7 @@ import com.android.systemui.Prefs; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.dump.DumpManager; +import com.android.systemui.flags.FakeFeatureFlags; import com.android.systemui.media.dialog.MediaOutputDialogFactory; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.VolumeDialogController; @@ -117,6 +119,8 @@ public class VolumeDialogImplTest extends SysuiTestCase { } }; + private FakeFeatureFlags mFeatureFlags; + @Before public void setup() throws Exception { MockitoAnnotations.initMocks(this); @@ -132,6 +136,8 @@ public class VolumeDialogImplTest extends SysuiTestCase { mConfigurationController = new FakeConfigurationController(); + mFeatureFlags = new FakeFeatureFlags(); + mDialog = new VolumeDialogImpl( getContext(), mVolumeDialogController, @@ -146,7 +152,8 @@ public class VolumeDialogImplTest extends SysuiTestCase { mCsdWarningDialogFactory, mPostureController, mTestableLooper.getLooper(), - mDumpManager); + mDumpManager, + mFeatureFlags); mDialog.init(0, null); State state = createShellState(); mDialog.onStateChangedH(state); @@ -254,6 +261,7 @@ public class VolumeDialogImplTest extends SysuiTestCase { @Test public void testVibrateOnRingerChangedToVibrate() { + mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false); final State initialSilentState = new State(); initialSilentState.ringerModeInternal = AudioManager.RINGER_MODE_SILENT; @@ -274,7 +282,30 @@ public class VolumeDialogImplTest extends SysuiTestCase { } @Test + public void testControllerDoesNotVibrateOnRingerChangedToVibrate_OnewayAPI_On() { + mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true); + final State initialSilentState = new State(); + initialSilentState.ringerModeInternal = AudioManager.RINGER_MODE_SILENT; + + final State vibrateState = new State(); + vibrateState.ringerModeInternal = AudioManager.RINGER_MODE_VIBRATE; + + // change ringer to silent + mDialog.onStateChangedH(initialSilentState); + + // expected: shouldn't call vibrate yet + verify(mVolumeDialogController, never()).vibrate(any()); + + // changed ringer to vibrate + mDialog.onStateChangedH(vibrateState); + + // expected: vibrate method of controller is not used + verify(mVolumeDialogController, never()).vibrate(any()); + } + + @Test public void testNoVibrateOnRingerInitialization() { + mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false); final State initialUnsetState = new State(); initialUnsetState.ringerModeInternal = -1; @@ -292,7 +323,42 @@ public class VolumeDialogImplTest extends SysuiTestCase { } @Test + public void testControllerDoesNotVibrateOnRingerInitialization_OnewayAPI_On() { + mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true); + final State initialUnsetState = new State(); + initialUnsetState.ringerModeInternal = -1; + + // ringer not initialized yet: + mDialog.onStateChangedH(initialUnsetState); + + final State vibrateState = new State(); + vibrateState.ringerModeInternal = AudioManager.RINGER_MODE_VIBRATE; + + // changed ringer to vibrate + mDialog.onStateChangedH(vibrateState); + + // shouldn't call vibrate on the controller either + verify(mVolumeDialogController, never()).vibrate(any()); + } + + @Test public void testSelectVibrateFromDrawer() { + mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false); + final State initialUnsetState = new State(); + initialUnsetState.ringerModeInternal = AudioManager.RINGER_MODE_NORMAL; + mDialog.onStateChangedH(initialUnsetState); + + mActiveRinger.performClick(); + mDrawerVibrate.performClick(); + + // Make sure we've actually changed the ringer mode. + verify(mVolumeDialogController, times(1)).setRingerMode( + AudioManager.RINGER_MODE_VIBRATE, false); + } + + @Test + public void testSelectVibrateFromDrawer_OnewayAPI_On() { + mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true); final State initialUnsetState = new State(); initialUnsetState.ringerModeInternal = AudioManager.RINGER_MODE_NORMAL; mDialog.onStateChangedH(initialUnsetState); @@ -307,6 +373,22 @@ public class VolumeDialogImplTest extends SysuiTestCase { @Test public void testSelectMuteFromDrawer() { + mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false); + final State initialUnsetState = new State(); + initialUnsetState.ringerModeInternal = AudioManager.RINGER_MODE_NORMAL; + mDialog.onStateChangedH(initialUnsetState); + + mActiveRinger.performClick(); + mDrawerMute.performClick(); + + // Make sure we've actually changed the ringer mode. + verify(mVolumeDialogController, times(1)).setRingerMode( + AudioManager.RINGER_MODE_SILENT, false); + } + + @Test + public void testSelectMuteFromDrawer_OnewayAPI_On() { + mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true); final State initialUnsetState = new State(); initialUnsetState.ringerModeInternal = AudioManager.RINGER_MODE_NORMAL; mDialog.onStateChangedH(initialUnsetState); @@ -321,6 +403,22 @@ public class VolumeDialogImplTest extends SysuiTestCase { @Test public void testSelectNormalFromDrawer() { + mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false); + final State initialUnsetState = new State(); + initialUnsetState.ringerModeInternal = AudioManager.RINGER_MODE_VIBRATE; + mDialog.onStateChangedH(initialUnsetState); + + mActiveRinger.performClick(); + mDrawerNormal.performClick(); + + // Make sure we've actually changed the ringer mode. + verify(mVolumeDialogController, times(1)).setRingerMode( + AudioManager.RINGER_MODE_NORMAL, false); + } + + @Test + public void testSelectNormalFromDrawer_OnewayAPI_On() { + mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true); final State initialUnsetState = new State(); initialUnsetState.ringerModeInternal = AudioManager.RINGER_MODE_VIBRATE; mDialog.onStateChangedH(initialUnsetState); @@ -383,7 +481,8 @@ public class VolumeDialogImplTest extends SysuiTestCase { mCsdWarningDialogFactory, devicePostureController, mTestableLooper.getLooper(), - mDumpManager + mDumpManager, + mFeatureFlags ); dialog.init(0 , null); @@ -423,7 +522,8 @@ public class VolumeDialogImplTest extends SysuiTestCase { mCsdWarningDialogFactory, devicePostureController, mTestableLooper.getLooper(), - mDumpManager + mDumpManager, + mFeatureFlags ); dialog.init(0, null); @@ -462,7 +562,8 @@ public class VolumeDialogImplTest extends SysuiTestCase { mCsdWarningDialogFactory, devicePostureController, mTestableLooper.getLooper(), - mDumpManager + mDumpManager, + mFeatureFlags ); dialog.init(0, null); @@ -503,7 +604,9 @@ public class VolumeDialogImplTest extends SysuiTestCase { mCsdWarningDialogFactory, mPostureController, mTestableLooper.getLooper(), - mDumpManager); + mDumpManager, + mFeatureFlags + ); dialog.init(0, null); verify(mPostureController, never()).removeCallback(any()); diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt index 28892baec3c5..c2e1ac70af80 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt @@ -31,9 +31,6 @@ class FakeAuthenticationRepository( private val currentTime: () -> Long, ) : AuthenticationRepository { - private val _isBypassEnabled = MutableStateFlow(false) - override val isBypassEnabled: StateFlow<Boolean> = _isBypassEnabled - private val _isAutoConfirmEnabled = MutableStateFlow(false) override val isAutoConfirmEnabled: StateFlow<Boolean> = _isAutoConfirmEnabled.asStateFlow() @@ -85,10 +82,6 @@ class FakeAuthenticationRepository( return (credentialOverride ?: DEFAULT_PIN).size } - override fun setBypassEnabled(isBypassEnabled: Boolean) { - _isBypassEnabled.value = isBypassEnabled - } - override suspend fun getFailedAuthenticationAttemptCount(): Int { return failedAttemptCount } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt index 63097401bc5a..b9d098fe2851 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt @@ -114,6 +114,11 @@ class FakeKeyguardRepository : KeyguardRepository { return _isKeyguardShowing.value } + private var _isBypassEnabled = false + override fun isBypassEnabled(): Boolean { + return _isBypassEnabled + } + override fun setAnimateDozingTransitions(animate: Boolean) { _animateBottomAreaDozingTransitions.tryEmit(animate) } @@ -198,6 +203,10 @@ class FakeKeyguardRepository : KeyguardRepository { _isKeyguardUnlocked.value = isUnlocked } + fun setBypassEnabled(isEnabled: Boolean) { + _isBypassEnabled = isEnabled + } + override fun isUdfpsSupported(): Boolean { return _isUdfpsSupported.value } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt index 47e1daf4008c..931798130499 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt @@ -142,6 +142,7 @@ class SceneTestUtils( repository = repository, backgroundDispatcher = testDispatcher, userRepository = userRepository, + keyguardRepository = keyguardRepository, clock = mock { whenever(elapsedRealtime()).thenAnswer { testScope.currentTime } } ) } diff --git a/services/Android.bp b/services/Android.bp index 7b64b4714017..53dc06805f14 100644 --- a/services/Android.bp +++ b/services/Android.bp @@ -164,6 +164,7 @@ java_library { "services.coverage", "services.credentials", "services.devicepolicy", + "services.flags", "services.midi", "services.musicsearch", "services.net", diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java index ecc0aa5c70b2..830d55ad866b 100644 --- a/services/core/java/com/android/server/StorageManagerService.java +++ b/services/core/java/com/android/server/StorageManagerService.java @@ -1269,6 +1269,21 @@ class StorageManagerService extends IStorageManager.Stub } } + /** + * This method checks if the volume is public and the volume is visible and the volume it is + * trying to mount doesn't have the same mount user id as the current user being maintained by + * StorageManagerService and change the mount Id. The checks are same as + * {@link StorageManagerService#maybeRemountVolumes(int)} + * @param VolumeInfo object to consider for changing the mountId + */ + private void updateVolumeMountIdIfRequired(VolumeInfo vol) { + synchronized (mLock) { + if (!vol.isPrimary() && vol.isVisible() && vol.getMountUserId() != mCurrentUserId) { + vol.mountUserId = mCurrentUserId; + } + } + } + private boolean supportsBlockCheckpoint() throws RemoteException { enforcePermission(android.Manifest.permission.MOUNT_FORMAT_FILESYSTEMS); return mVold.supportsBlockCheckpoint(); @@ -1382,13 +1397,14 @@ class StorageManagerService extends IStorageManager.Stub } @Override - public void onVolumeStateChanged(String volId, final int newState) { + public void onVolumeStateChanged(String volId, final int newState, final int userId) { synchronized (mLock) { final VolumeInfo vol = mVolumes.get(volId); if (vol != null) { final int oldState = vol.state; vol.state = newState; final VolumeInfo vInfo = new VolumeInfo(vol); + vInfo.mountUserId = userId; final SomeArgs args = SomeArgs.obtain(); args.arg1 = vInfo; args.argi1 = oldState; @@ -2232,7 +2248,7 @@ class StorageManagerService extends IStorageManager.Stub if (isMountDisallowed(vol)) { throw new SecurityException("Mounting " + volId + " restricted by policy"); } - + updateVolumeMountIdIfRequired(vol); mount(vol); } diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index 2f7c8905993d..d199006bd4e8 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -256,7 +256,7 @@ import java.util.function.Predicate; public final class ActiveServices { private static final String TAG = TAG_WITH_CLASS_NAME ? "ActiveServices" : TAG_AM; private static final String TAG_MU = TAG + POSTFIX_MU; - private static final String TAG_SERVICE = TAG + POSTFIX_SERVICE; + static final String TAG_SERVICE = TAG + POSTFIX_SERVICE; private static final String TAG_SERVICE_EXECUTING = TAG + POSTFIX_SERVICE_EXECUTING; private static final boolean DEBUG_DELAYED_SERVICE = DEBUG_SERVICE; @@ -850,8 +850,7 @@ public final class ActiveServices { // Service.startForeground()), at that point we will consult the BFSL check and the timeout // and make the necessary decisions. setFgsRestrictionLocked(callingPackage, callingPid, callingUid, service, r, userId, - backgroundStartPrivileges, false /* isBindService */, - !fgRequired /* isStartService */); + backgroundStartPrivileges, false /* isBindService */); if (!mAm.mUserController.exists(r.userId)) { Slog.w(TAG, "Trying to start service with non-existent user! " + r.userId); @@ -894,7 +893,7 @@ public final class ActiveServices { if (fgRequired) { logFgsBackgroundStart(r); - if (r.mAllowStartForeground == REASON_DENIED && isBgFgsRestrictionEnabled(r)) { + if (!r.isFgsAllowedStart() && isBgFgsRestrictionEnabled(r)) { String msg = "startForegroundService() not allowed due to " + "mAllowStartForeground false: service " + r.shortInstanceName; @@ -1060,7 +1059,7 @@ public final class ActiveServices { // Use that as a shortcut if possible to avoid having to recheck all the conditions. final boolean whileInUseAllowsUiJobScheduling = ActivityManagerService.doesReasonCodeAllowSchedulingUserInitiatedJobs( - r.mAllowWhileInUsePermissionInFgsReason); + r.getFgsAllowWIU()); r.updateAllowUiJobScheduling(whileInUseAllowsUiJobScheduling || mAm.canScheduleUserInitiatedJobs(callingUid, callingPid, callingPackage)); } else { @@ -2178,12 +2177,12 @@ public final class ActiveServices { // on a SHORT_SERVICE FGS. // See if the app could start an FGS or not. - r.mAllowStartForeground = REASON_DENIED; + r.clearFgsAllowStart(); setFgsRestrictionLocked(r.serviceInfo.packageName, r.app.getPid(), r.appInfo.uid, r.intent.getIntent(), r, r.userId, BackgroundStartPrivileges.NONE, - false /* isBindService */, false /* isStartService */); - if (r.mAllowStartForeground == REASON_DENIED) { + false /* isBindService */); + if (!r.isFgsAllowedStart()) { Slog.w(TAG_SERVICE, "FGS type change to/from SHORT_SERVICE: " + " BFSL DENIED."); } else { @@ -2191,13 +2190,13 @@ public final class ActiveServices { Slog.w(TAG_SERVICE, "FGS type change to/from SHORT_SERVICE: " + " BFSL Allowed: " + PowerExemptionManager.reasonCodeToString( - r.mAllowStartForeground)); + r.getFgsAllowStart())); } } final boolean fgsStartAllowed = !isBgFgsRestrictionEnabledForService - || (r.mAllowStartForeground != REASON_DENIED); + || r.isFgsAllowedStart(); if (fgsStartAllowed) { if (isNewTypeShortFgs) { @@ -2246,7 +2245,7 @@ public final class ActiveServices { setFgsRestrictionLocked(r.serviceInfo.packageName, r.app.getPid(), r.appInfo.uid, r.intent.getIntent(), r, r.userId, BackgroundStartPrivileges.NONE, - false /* isBindService */, false /* isStartService */); + false /* isBindService */); final String temp = "startForegroundDelayMs:" + delayMs; if (r.mInfoAllowStartForeground != null) { r.mInfoAllowStartForeground += "; " + temp; @@ -2266,20 +2265,21 @@ public final class ActiveServices { setFgsRestrictionLocked(r.serviceInfo.packageName, r.app.getPid(), r.appInfo.uid, r.intent.getIntent(), r, r.userId, BackgroundStartPrivileges.NONE, - false /* isBindService */, false /* isStartService */); + false /* isBindService */); } // If the foreground service is not started from TOP process, do not allow it to // have while-in-use location/camera/microphone access. - if (!r.mAllowWhileInUsePermissionInFgs) { + if (!r.isFgsAllowedWIU()) { Slog.w(TAG, "Foreground service started from background can not have " + "location/camera/microphone access: service " + r.shortInstanceName); } + r.maybeLogFgsLogicChange(); if (!bypassBfslCheck) { logFgsBackgroundStart(r); - if (r.mAllowStartForeground == REASON_DENIED + if (!r.isFgsAllowedStart() && isBgFgsRestrictionEnabledForService) { final String msg = "Service.startForeground() not allowed due to " + "mAllowStartForeground false: service " @@ -2378,9 +2378,9 @@ public final class ActiveServices { // The logging of FOREGROUND_SERVICE_STATE_CHANGED__STATE__ENTER event could // be deferred, make a copy of mAllowStartForeground and // mAllowWhileInUsePermissionInFgs. - r.mAllowStartForegroundAtEntering = r.mAllowStartForeground; + r.mAllowStartForegroundAtEntering = r.getFgsAllowStart(); r.mAllowWhileInUsePermissionInFgsAtEntering = - r.mAllowWhileInUsePermissionInFgs; + r.isFgsAllowedWIU(); r.mStartForegroundCount++; r.mFgsEnterTime = SystemClock.uptimeMillis(); if (!stopProcStatsOp) { @@ -2558,7 +2558,7 @@ public final class ActiveServices { policy.getForegroundServiceTypePolicyInfo(type, defaultToType); final @ForegroundServicePolicyCheckCode int code = policy.checkForegroundServiceTypePolicy( mAm.mContext, r.packageName, r.app.uid, r.app.getPid(), - r.mAllowWhileInUsePermissionInFgs, policyInfo); + r.isFgsAllowedWIU(), policyInfo); RuntimeException exception = null; switch (code) { case FGS_TYPE_POLICY_CHECK_DEPRECATED: { @@ -3744,8 +3744,7 @@ public final class ActiveServices { } } setFgsRestrictionLocked(callingPackage, callingPid, callingUid, service, s, userId, - BackgroundStartPrivileges.NONE, true /* isBindService */, - false /* isStartService */); + BackgroundStartPrivileges.NONE, true /* isBindService */); if (s.app != null) { ProcessServiceRecord servicePsr = s.app.mServices; @@ -7444,54 +7443,80 @@ public final class ActiveServices { * @param callingUid caller app's uid. * @param intent intent to start/bind service. * @param r the service to start. - * @param isStartService True if it's called from Context.startService(). - * False if it's called from Context.startForegroundService() or - * Service.startForeground(). + * @param isBindService True if it's called from bindService(). * @return true if allow, false otherwise. */ private void setFgsRestrictionLocked(String callingPackage, int callingPid, int callingUid, Intent intent, ServiceRecord r, int userId, - BackgroundStartPrivileges backgroundStartPrivileges, boolean isBindService, - boolean isStartService) { + BackgroundStartPrivileges backgroundStartPrivileges, boolean isBindService) { + + @ReasonCode int allowWIU; + @ReasonCode int allowStart; + + // If called from bindService(), do not update the actual fields, but instead + // keep it in a separate set of fields. + if (isBindService) { + allowWIU = r.mAllowWIUInBindService; + allowStart = r.mAllowStartInBindService; + } else { + allowWIU = r.mAllowWhileInUsePermissionInFgsReasonNoBinding; + allowStart = r.mAllowStartForegroundNoBinding; + } + // Check DeviceConfig flag. if (!mAm.mConstants.mFlagBackgroundFgsStartRestrictionEnabled) { - if (!r.mAllowWhileInUsePermissionInFgs) { + if (allowWIU == REASON_DENIED) { // BGFGS start restrictions are disabled. We're allowing while-in-use permissions. // Note REASON_OTHER since there's no other suitable reason. - r.mAllowWhileInUsePermissionInFgsReason = REASON_OTHER; + allowWIU = REASON_OTHER; } - r.mAllowWhileInUsePermissionInFgs = true; } - if (!r.mAllowWhileInUsePermissionInFgs - || (r.mAllowStartForeground == REASON_DENIED)) { + if ((allowWIU == REASON_DENIED) + || (allowStart == REASON_DENIED)) { @ReasonCode final int allowWhileInUse = shouldAllowFgsWhileInUsePermissionLocked( callingPackage, callingPid, callingUid, r.app, backgroundStartPrivileges); // We store them to compare the old and new while-in-use logics to each other. // (They're not used for any other purposes.) - if (!r.mAllowWhileInUsePermissionInFgs) { - r.mAllowWhileInUsePermissionInFgs = (allowWhileInUse != REASON_DENIED); - r.mAllowWhileInUsePermissionInFgsReason = allowWhileInUse; + if (allowWIU == REASON_DENIED) { + allowWIU = allowWhileInUse; } - if (r.mAllowStartForeground == REASON_DENIED) { - r.mAllowStartForeground = shouldAllowFgsStartForegroundWithBindingCheckLocked( + if (allowStart == REASON_DENIED) { + allowStart = shouldAllowFgsStartForegroundWithBindingCheckLocked( allowWhileInUse, callingPackage, callingPid, callingUid, intent, r, backgroundStartPrivileges, isBindService); } } + + if (isBindService) { + r.mAllowWIUInBindService = allowWIU; + r.mAllowStartInBindService = allowStart; + } else { + r.mAllowWhileInUsePermissionInFgsReasonNoBinding = allowWIU; + r.mAllowStartForegroundNoBinding = allowStart; + + // Also do a binding client check, unless called from bindService(). + if (r.mAllowWIUByBindings == REASON_DENIED) { + r.mAllowWIUByBindings = + shouldAllowFgsWhileInUsePermissionByBindingsLocked(callingUid); + } + if (r.mAllowStartByBindings == REASON_DENIED) { + r.mAllowStartByBindings = r.mAllowWIUByBindings; + } + } } /** * Reset various while-in-use and BFSL related information. */ void resetFgsRestrictionLocked(ServiceRecord r) { - r.mAllowWhileInUsePermissionInFgs = false; - r.mAllowWhileInUsePermissionInFgsReason = REASON_DENIED; - r.mAllowStartForeground = REASON_DENIED; + r.clearFgsAllowWIU(); + r.clearFgsAllowStart(); + r.mInfoAllowStartForeground = null; r.mInfoTempFgsAllowListReason = null; r.mLoggedInfoAllowStartForeground = false; - r.updateAllowUiJobScheduling(r.mAllowWhileInUsePermissionInFgs); + r.updateAllowUiJobScheduling(r.isFgsAllowedWIU()); } boolean canStartForegroundServiceLocked(int callingPid, int callingUid, String callingPackage) { @@ -8063,10 +8088,10 @@ public final class ActiveServices { */ if (!r.mLoggedInfoAllowStartForeground) { final String msg = "Background started FGS: " - + ((r.mAllowStartForeground != REASON_DENIED) ? "Allowed " : "Disallowed ") + + (r.isFgsAllowedStart() ? "Allowed " : "Disallowed ") + r.mInfoAllowStartForeground + (r.isShortFgs() ? " (Called on SHORT_SERVICE)" : ""); - if (r.mAllowStartForeground != REASON_DENIED) { + if (r.isFgsAllowedStart()) { if (ActivityManagerUtils.shouldSamplePackageForAtom(r.packageName, mAm.mConstants.mFgsStartAllowedLogSampleRate)) { Slog.wtfQuiet(TAG, msg); @@ -8106,8 +8131,8 @@ public final class ActiveServices { allowWhileInUsePermissionInFgs = r.mAllowWhileInUsePermissionInFgsAtEntering; fgsStartReasonCode = r.mAllowStartForegroundAtEntering; } else { - allowWhileInUsePermissionInFgs = r.mAllowWhileInUsePermissionInFgs; - fgsStartReasonCode = r.mAllowStartForeground; + allowWhileInUsePermissionInFgs = r.isFgsAllowedWIU(); + fgsStartReasonCode = r.getFgsAllowStart(); } final int callerTargetSdkVersion = r.mRecentCallerApplicationInfo != null ? r.mRecentCallerApplicationInfo.targetSdkVersion : 0; @@ -8296,8 +8321,7 @@ public final class ActiveServices { r.mFgsEnterTime = SystemClock.uptimeMillis(); r.foregroundServiceType = options.mForegroundServiceTypes; setFgsRestrictionLocked(callingPackage, callingPid, callingUid, intent, r, userId, - BackgroundStartPrivileges.NONE, false /* isBindService */, - false /* isStartService */); + BackgroundStartPrivileges.NONE, false /* isBindService */); final ProcessServiceRecord psr = callerApp.mServices; final boolean newService = psr.startService(r); // updateOomAdj. diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index eacc7c25b094..8012c2653277 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -559,6 +559,10 @@ public class ActivityManagerService extends IActivityManager.Stub // How long we wait for a launched process to attach to the activity manager // before we decide it's never going to come up for real. static final int PROC_START_TIMEOUT = 10 * 1000 * Build.HW_TIMEOUT_MULTIPLIER; + + // How long we wait for a launched process to complete its app startup before we ANR. + static final int BIND_APPLICATION_TIMEOUT = 10 * 1000 * Build.HW_TIMEOUT_MULTIPLIER; + // How long we wait to kill an application zygote, after the last process using // it has gone away. static final int KILL_APP_ZYGOTE_DELAY_MS = 5 * 1000; @@ -1625,6 +1629,7 @@ public class ActivityManagerService extends IActivityManager.Stub static final int UPDATE_CACHED_APP_HIGH_WATERMARK = 79; static final int ADD_UID_TO_OBSERVER_MSG = 80; static final int REMOVE_UID_FROM_OBSERVER_MSG = 81; + static final int BIND_APPLICATION_TIMEOUT_MSG = 82; static final int FIRST_BROADCAST_QUEUE_MSG = 200; @@ -1977,6 +1982,16 @@ public class ActivityManagerService extends IActivityManager.Stub case UPDATE_CACHED_APP_HIGH_WATERMARK: { mAppProfiler.mCachedAppsWatermarkData.updateCachedAppsSnapshot((long) msg.obj); } break; + case BIND_APPLICATION_TIMEOUT_MSG: { + ProcessRecord app = (ProcessRecord) msg.obj; + + final String anrMessage; + synchronized (app) { + anrMessage = "Process " + app + " failed to complete startup"; + } + + mAnrHelper.appNotResponding(app, TimeoutRecord.forAppStart(anrMessage)); + } break; } } } @@ -4737,6 +4752,12 @@ public class ActivityManagerService extends IActivityManager.Stub app.getDisabledCompatChanges(), serializedSystemFontMap, app.getStartElapsedTime(), app.getStartUptime()); } + + Message msg = mHandler.obtainMessage(BIND_APPLICATION_TIMEOUT_MSG); + msg.obj = app; + mHandler.sendMessageDelayed(msg, BIND_APPLICATION_TIMEOUT); + mHandler.removeMessages(PROC_START_TIMEOUT_MSG, app); + if (profilerInfo != null) { profilerInfo.closeFd(); profilerInfo = null; @@ -4811,7 +4832,7 @@ public class ActivityManagerService extends IActivityManager.Stub } if (app != null && app.getStartUid() == uid && app.getStartSeq() == startSeq) { - mHandler.removeMessages(PROC_START_TIMEOUT_MSG, app); + mHandler.removeMessages(BIND_APPLICATION_TIMEOUT_MSG, app); } else { Slog.wtf(TAG, "Mismatched or missing ProcessRecord: " + app + ". Pid: " + pid + ". Uid: " + uid); @@ -9721,6 +9742,7 @@ public class ActivityManagerService extends IActivityManager.Stub ResultReceiver resultReceiver) { final int callingUid = Binder.getCallingUid(); if (callingUid != ROOT_UID && callingUid != Process.SHELL_UID) { + resultReceiver.send(-1, null); throw new SecurityException("Shell commands are only callable by root or shell"); } (new ActivityManagerShellCommand(this, false)).exec( diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java index f5e2eb5ac783..8d2edaaadb63 100644 --- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java +++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java @@ -249,6 +249,7 @@ class BroadcastQueueModernImpl extends BroadcastQueue { private static final int MSG_CHECK_HEALTH = 5; private static final int MSG_CHECK_PENDING_COLD_START_VALIDITY = 6; private static final int MSG_PROCESS_FREEZABLE_CHANGED = 7; + private static final int MSG_UID_STATE_CHANGED = 8; private void enqueueUpdateRunningList() { mLocalHandler.removeMessages(MSG_UPDATE_RUNNING_LIST); @@ -295,6 +296,19 @@ class BroadcastQueueModernImpl extends BroadcastQueue { } return true; } + case MSG_UID_STATE_CHANGED: { + final int uid = (int) msg.obj; + final int procState = msg.arg1; + synchronized (mService) { + if (procState == ActivityManager.PROCESS_STATE_TOP) { + mUidForeground.put(uid, true); + } else { + mUidForeground.delete(uid); + } + refreshProcessQueuesLocked(uid); + } + return true; + } } return false; }; @@ -672,7 +686,7 @@ class BroadcastQueueModernImpl extends BroadcastQueue { @Override public void onProcessFreezableChangedLocked(@NonNull ProcessRecord app) { mLocalHandler.removeMessages(MSG_PROCESS_FREEZABLE_CHANGED, app); - mLocalHandler.sendMessage(mHandler.obtainMessage(MSG_PROCESS_FREEZABLE_CHANGED, app)); + mLocalHandler.obtainMessage(MSG_PROCESS_FREEZABLE_CHANGED, app).sendToTarget(); } @Override @@ -1601,14 +1615,9 @@ class BroadcastQueueModernImpl extends BroadcastQueue { @Override public void onUidStateChanged(int uid, int procState, long procStateSeq, int capability) { - synchronized (mService) { - if (procState == ActivityManager.PROCESS_STATE_TOP) { - mUidForeground.put(uid, true); - } else { - mUidForeground.delete(uid); - } - refreshProcessQueuesLocked(uid); - } + mLocalHandler.removeMessages(MSG_UID_STATE_CHANGED, uid); + mLocalHandler.obtainMessage(MSG_UID_STATE_CHANGED, procState, 0, uid) + .sendToTarget(); } }, ActivityManager.UID_OBSERVER_PROCSTATE, ActivityManager.PROCESS_STATE_TOP, "android"); diff --git a/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java b/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java index 38e7371e7075..4f5b5e1fbd68 100644 --- a/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java +++ b/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java @@ -479,8 +479,8 @@ public class ForegroundServiceTypeLoggerModule { r.appInfo.uid, r.shortInstanceName, fgsState, // FGS State - r.mAllowWhileInUsePermissionInFgs, // allowWhileInUsePermissionInFgs - r.mAllowStartForeground, // fgsStartReasonCode + r.isFgsAllowedWIU(), // allowWhileInUsePermissionInFgs + r.getFgsAllowStart(), // fgsStartReasonCode r.appInfo.targetSdkVersion, r.mRecentCallingUid, 0, // callerTargetSdkVersion diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java index a682c85f03b2..459c6ff3504a 100644 --- a/services/core/java/com/android/server/am/OomAdjuster.java +++ b/services/core/java/com/android/server/am/OomAdjuster.java @@ -2222,7 +2222,7 @@ public class OomAdjuster { if (s.isForeground) { final int fgsType = s.foregroundServiceType; - if (s.mAllowWhileInUsePermissionInFgs) { + if (s.isFgsAllowedWIU()) { capabilityFromFGS |= (fgsType & FOREGROUND_SERVICE_TYPE_LOCATION) != 0 ? PROCESS_CAPABILITY_FOREGROUND_LOCATION : 0; diff --git a/services/core/java/com/android/server/am/ServiceRecord.java b/services/core/java/com/android/server/am/ServiceRecord.java index 50fe6d71d26e..aabab61c36f4 100644 --- a/services/core/java/com/android/server/am/ServiceRecord.java +++ b/services/core/java/com/android/server/am/ServiceRecord.java @@ -21,9 +21,11 @@ import static android.app.PendingIntent.FLAG_IMMUTABLE; import static android.app.PendingIntent.FLAG_UPDATE_CURRENT; import static android.app.ProcessMemoryState.HOSTING_COMPONENT_TYPE_BOUND_SERVICE; import static android.os.PowerExemptionManager.REASON_DENIED; +import static android.os.PowerExemptionManager.reasonCodeToString; import static android.os.Process.INVALID_UID; import static com.android.internal.util.Preconditions.checkArgument; +import static com.android.server.am.ActiveServices.TAG_SERVICE; import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_FOREGROUND_SERVICE; import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM; import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME; @@ -172,11 +174,11 @@ final class ServiceRecord extends Binder implements ComponentName.WithComponentN private BackgroundStartPrivileges mBackgroundStartPrivilegesByStartMerged = BackgroundStartPrivileges.NONE; - // allow while-in-use permissions in foreground service or not. + // Reason code for allow while-in-use permissions in foreground service. + // If it's not DENIED, while-in-use permissions are allowed. // while-in-use permissions in FGS started from background might be restricted. - boolean mAllowWhileInUsePermissionInFgs; @PowerExemptionManager.ReasonCode - int mAllowWhileInUsePermissionInFgsReason = REASON_DENIED; + int mAllowWhileInUsePermissionInFgsReasonNoBinding = REASON_DENIED; // A copy of mAllowWhileInUsePermissionInFgs's value when the service is entering FGS state. boolean mAllowWhileInUsePermissionInFgsAtEntering; @@ -205,15 +207,114 @@ final class ServiceRecord extends Binder implements ComponentName.WithComponentN // allow the service becomes foreground service? Service started from background may not be // allowed to become a foreground service. - @PowerExemptionManager.ReasonCode int mAllowStartForeground = REASON_DENIED; + @PowerExemptionManager.ReasonCode + int mAllowStartForegroundNoBinding = REASON_DENIED; // A copy of mAllowStartForeground's value when the service is entering FGS state. - @PowerExemptionManager.ReasonCode int mAllowStartForegroundAtEntering = REASON_DENIED; + @PowerExemptionManager.ReasonCode + int mAllowStartForegroundAtEntering = REASON_DENIED; // Debug info why mAllowStartForeground is allowed or denied. String mInfoAllowStartForeground; // Debug info if mAllowStartForeground is allowed because of a temp-allowlist. ActivityManagerService.FgsTempAllowListItem mInfoTempFgsAllowListReason; // Is the same mInfoAllowStartForeground string has been logged before? Used for dedup. boolean mLoggedInfoAllowStartForeground; + + @PowerExemptionManager.ReasonCode + int mAllowWIUInBindService = REASON_DENIED; + + @PowerExemptionManager.ReasonCode + int mAllowWIUByBindings = REASON_DENIED; + + @PowerExemptionManager.ReasonCode + int mAllowStartInBindService = REASON_DENIED; + + @PowerExemptionManager.ReasonCode + int mAllowStartByBindings = REASON_DENIED; + + @PowerExemptionManager.ReasonCode + int getFgsAllowWIU() { + return mAllowWhileInUsePermissionInFgsReasonNoBinding != REASON_DENIED + ? mAllowWhileInUsePermissionInFgsReasonNoBinding + : mAllowWIUInBindService; + } + + boolean isFgsAllowedWIU() { + return getFgsAllowWIU() != REASON_DENIED; + } + + @PowerExemptionManager.ReasonCode + int getFgsAllowStart() { + return mAllowStartForegroundNoBinding != REASON_DENIED + ? mAllowStartForegroundNoBinding + : mAllowStartInBindService; + } + + boolean isFgsAllowedStart() { + return getFgsAllowStart() != REASON_DENIED; + } + + void clearFgsAllowWIU() { + mAllowWhileInUsePermissionInFgsReasonNoBinding = REASON_DENIED; + mAllowWIUInBindService = REASON_DENIED; + mAllowWIUByBindings = REASON_DENIED; + } + + void clearFgsAllowStart() { + mAllowStartForegroundNoBinding = REASON_DENIED; + mAllowStartInBindService = REASON_DENIED; + mAllowStartByBindings = REASON_DENIED; + } + + @PowerExemptionManager.ReasonCode + int reasonOr(@PowerExemptionManager.ReasonCode int first, + @PowerExemptionManager.ReasonCode int second) { + return first != REASON_DENIED ? first : second; + } + + boolean allowedChanged(@PowerExemptionManager.ReasonCode int first, + @PowerExemptionManager.ReasonCode int second) { + return (first == REASON_DENIED) != (second == REASON_DENIED); + } + + String changeMessage(@PowerExemptionManager.ReasonCode int first, + @PowerExemptionManager.ReasonCode int second) { + return reasonOr(first, second) == REASON_DENIED ? "DENIED" + : ("ALLOWED (" + + reasonCodeToString(first) + + "+" + + reasonCodeToString(second) + + ")"); + } + + void maybeLogFgsLogicChange() { + final int origWiu = reasonOr(mAllowWhileInUsePermissionInFgsReasonNoBinding, + mAllowWIUInBindService); + final int newWiu = reasonOr(mAllowWhileInUsePermissionInFgsReasonNoBinding, + mAllowWIUByBindings); + final int origStart = reasonOr(mAllowStartForegroundNoBinding, mAllowStartInBindService); + final int newStart = reasonOr(mAllowStartForegroundNoBinding, mAllowStartByBindings); + + final boolean wiuChanged = allowedChanged(origWiu, newWiu); + final boolean startChanged = allowedChanged(origStart, newStart); + + if (!wiuChanged && !startChanged) { + return; + } + final String message = "FGS logic changed:" + + (wiuChanged ? " [WIU changed]" : "") + + (startChanged ? " [BFSL changed]" : "") + + " OW:" // Orig-WIU + + changeMessage(mAllowWhileInUsePermissionInFgsReasonNoBinding, + mAllowWIUInBindService) + + " NW:" // New-WIU + + changeMessage(mAllowWhileInUsePermissionInFgsReasonNoBinding, mAllowWIUByBindings) + + " OS:" // Orig-start + + changeMessage(mAllowStartForegroundNoBinding, mAllowStartInBindService) + + " NS:" // New-start + + changeMessage(mAllowStartForegroundNoBinding, mAllowStartByBindings); + Slog.wtf(TAG_SERVICE, message); + } + // The number of times Service.startForeground() is called, after this service record is // created. (i.e. due to "bound" or "start".) It never decreases, even when stopForeground() // is called. @@ -502,7 +603,7 @@ final class ServiceRecord extends Binder implements ComponentName.WithComponentN ProtoUtils.toDuration(proto, ServiceRecordProto.RESTART_TIME, restartTime, now); proto.write(ServiceRecordProto.CREATED_FROM_FG, createdFromFg); proto.write(ServiceRecordProto.ALLOW_WHILE_IN_USE_PERMISSION_IN_FGS, - mAllowWhileInUsePermissionInFgs); + isFgsAllowedWIU()); if (startRequested || delayedStop || lastStartId != 0) { long startToken = proto.start(ServiceRecordProto.START); @@ -618,7 +719,13 @@ final class ServiceRecord extends Binder implements ComponentName.WithComponentN pw.println(mBackgroundStartPrivilegesByStartMerged); } pw.print(prefix); pw.print("mAllowWhileInUsePermissionInFgsReason="); - pw.println(PowerExemptionManager.reasonCodeToString(mAllowWhileInUsePermissionInFgsReason)); + pw.println(PowerExemptionManager.reasonCodeToString( + mAllowWhileInUsePermissionInFgsReasonNoBinding)); + + pw.print(prefix); pw.print("mAllowWIUInBindService="); + pw.println(PowerExemptionManager.reasonCodeToString(mAllowWIUInBindService)); + pw.print(prefix); pw.print("mAllowWIUByBindings="); + pw.println(PowerExemptionManager.reasonCodeToString(mAllowWIUByBindings)); pw.print(prefix); pw.print("allowUiJobScheduling="); pw.println(mAllowUiJobScheduling); pw.print(prefix); pw.print("recentCallingPackage="); @@ -626,7 +733,12 @@ final class ServiceRecord extends Binder implements ComponentName.WithComponentN pw.print(prefix); pw.print("recentCallingUid="); pw.println(mRecentCallingUid); pw.print(prefix); pw.print("allowStartForeground="); - pw.println(PowerExemptionManager.reasonCodeToString(mAllowStartForeground)); + pw.println(PowerExemptionManager.reasonCodeToString(mAllowStartForegroundNoBinding)); + pw.print(prefix); pw.print("mAllowStartInBindService="); + pw.println(PowerExemptionManager.reasonCodeToString(mAllowStartInBindService)); + pw.print(prefix); pw.print("mAllowStartByBindings="); + pw.println(PowerExemptionManager.reasonCodeToString(mAllowStartByBindings)); + pw.print(prefix); pw.print("startForegroundCount="); pw.println(mStartForegroundCount); pw.print(prefix); pw.print("infoAllowStartForeground="); diff --git a/services/core/java/com/android/server/biometrics/BiometricService.java b/services/core/java/com/android/server/biometrics/BiometricService.java index e8ffe4feb458..279aaf9d3253 100644 --- a/services/core/java/com/android/server/biometrics/BiometricService.java +++ b/services/core/java/com/android/server/biometrics/BiometricService.java @@ -17,6 +17,8 @@ package com.android.server.biometrics; import static android.Manifest.permission.USE_BIOMETRIC_INTERNAL; +import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE; +import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT; import static android.hardware.biometrics.BiometricManager.Authenticators; import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_IDLE; @@ -369,7 +371,7 @@ public class BiometricService extends SystemService { public boolean getConfirmationAlwaysRequired(@BiometricAuthenticator.Modality int modality, int userId) { switch (modality) { - case BiometricAuthenticator.TYPE_FACE: + case TYPE_FACE: if (!mFaceAlwaysRequireConfirmation.containsKey(userId)) { onChange(true /* selfChange */, FACE_UNLOCK_ALWAYS_REQUIRE_CONFIRMATION, @@ -567,22 +569,6 @@ public class BiometricService extends SystemService { Utils.combineAuthenticatorBundles(promptInfo); - // Set the default title if necessary. - if (promptInfo.isUseDefaultTitle()) { - if (TextUtils.isEmpty(promptInfo.getTitle())) { - promptInfo.setTitle(getContext() - .getString(R.string.biometric_dialog_default_title)); - } - } - - // Set the default subtitle if necessary. - if (promptInfo.isUseDefaultSubtitle()) { - if (TextUtils.isEmpty(promptInfo.getSubtitle())) { - promptInfo.setSubtitle(getContext() - .getString(R.string.biometric_dialog_default_subtitle)); - } - } - final long requestId = mRequestCounter.get(); mHandler.post(() -> handleAuthenticate( token, requestId, operationId, userId, receiver, opPackageName, promptInfo)); @@ -1302,6 +1288,33 @@ public class BiometricService extends SystemService { opPackageName, promptInfo.isDisallowBiometricsIfPolicyExists(), getContext(), mBiometricCameraManager); + // Set the default title if necessary. + if (promptInfo.isUseDefaultTitle()) { + if (TextUtils.isEmpty(promptInfo.getTitle())) { + promptInfo.setTitle(getContext() + .getString(R.string.biometric_dialog_default_title)); + } + } + + final int eligible = preAuthInfo.getEligibleModalities(); + final boolean hasEligibleFingerprintSensor = + (eligible & TYPE_FINGERPRINT) == TYPE_FINGERPRINT; + final boolean hasEligibleFaceSensor = (eligible & TYPE_FACE) == TYPE_FACE; + + // Set the subtitle according to the modality. + if (promptInfo.isUseDefaultSubtitle()) { + if (hasEligibleFingerprintSensor && hasEligibleFaceSensor) { + promptInfo.setSubtitle(getContext() + .getString(R.string.biometric_dialog_default_subtitle)); + } else if (hasEligibleFingerprintSensor) { + promptInfo.setSubtitle(getContext() + .getString(R.string.biometric_dialog_fingerprint_subtitle)); + } else if (hasEligibleFaceSensor) { + promptInfo.setSubtitle(getContext() + .getString(R.string.biometric_dialog_face_subtitle)); + } + } + final Pair<Integer, Integer> preAuthStatus = preAuthInfo.getPreAuthenticateStatus(); Slog.d(TAG, "handleAuthenticate: modality(" + preAuthStatus.first diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java index 63bb0261bc6d..cba5039f714d 100644 --- a/services/core/java/com/android/server/connectivity/Vpn.java +++ b/services/core/java/com/android/server/connectivity/Vpn.java @@ -3346,7 +3346,7 @@ public class Vpn { // Transforms do not need to be persisted; the IkeSession will keep // them alive for us mIpSecManager.applyTunnelModeTransform(mTunnelIface, direction, transform); - } catch (IOException e) { + } catch (IOException | IllegalArgumentException e) { Log.d(TAG, "Transform application failed for token " + token, e); onSessionLost(token, e); } @@ -3440,7 +3440,7 @@ public class Vpn { mTunnelIface, IpSecManager.DIRECTION_IN, inTransform); mIpSecManager.applyTunnelModeTransform( mTunnelIface, IpSecManager.DIRECTION_OUT, outTransform); - } catch (IOException e) { + } catch (IOException | IllegalArgumentException e) { Log.d(TAG, "Transform application failed for token " + token, e); onSessionLost(token, e); } diff --git a/services/core/java/com/android/server/graphics/fonts/FontManagerService.java b/services/core/java/com/android/server/graphics/fonts/FontManagerService.java index 080b947f44d6..c6a50ed2580d 100644 --- a/services/core/java/com/android/server/graphics/fonts/FontManagerService.java +++ b/services/core/java/com/android/server/graphics/fonts/FontManagerService.java @@ -46,6 +46,7 @@ import com.android.internal.security.VerityUtils; import com.android.internal.util.DumpUtils; import com.android.internal.util.Preconditions; import com.android.server.LocalServices; +import com.android.server.SystemServerInitThreadPool; import com.android.server.SystemService; import java.io.File; @@ -61,6 +62,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.concurrent.CompletableFuture; /** A service for managing system fonts. */ public final class FontManagerService extends IFontManager.Stub { @@ -136,10 +138,11 @@ public final class FontManagerService extends IFontManager.Stub { /** Class to manage FontManagerService's lifecycle. */ public static final class Lifecycle extends SystemService { private final FontManagerService mService; + private final CompletableFuture<Void> mServiceStarted = new CompletableFuture<>(); public Lifecycle(@NonNull Context context, boolean safeMode) { super(context); - mService = new FontManagerService(context, safeMode); + mService = new FontManagerService(context, safeMode, mServiceStarted); } @Override @@ -152,11 +155,20 @@ public final class FontManagerService extends IFontManager.Stub { if (!Typeface.ENABLE_LAZY_TYPEFACE_INITIALIZATION) { return null; } + mServiceStarted.join(); return mService.getCurrentFontMap(); } }); publishBinderService(Context.FONT_SERVICE, mService); } + + @Override + public void onBootPhase(int phase) { + if (phase == SystemService.PHASE_ACTIVITY_MANAGER_READY) { + // Wait for FontManagerService to start since it will be needed after this point. + mServiceStarted.join(); + } + } } private static class FsverityUtilImpl implements UpdatableFontDir.FsverityUtil { @@ -219,25 +231,30 @@ public final class FontManagerService extends IFontManager.Stub { @Nullable private SharedMemory mSerializedFontMap = null; - private FontManagerService(Context context, boolean safeMode) { + private FontManagerService( + Context context, boolean safeMode, CompletableFuture<Void> serviceStarted) { if (safeMode) { Slog.i(TAG, "Entering safe mode. Deleting all font updates."); UpdatableFontDir.deleteAllFiles(new File(FONT_FILES_DIR), new File(CONFIG_XML_FILE)); } mContext = context; mIsSafeMode = safeMode; - initialize(); - // Set system font map only if there is updatable font directory. - // If there is no updatable font directory, `initialize` will have already loaded the - // system font map, so there's no need to set the system font map again here. - if (mUpdatableFontDir != null) { - try { - Typeface.setSystemFontMap(getCurrentFontMap()); - } catch (IOException | ErrnoException e) { - Slog.w(TAG, "Failed to set system font map of system_server"); + SystemServerInitThreadPool.submit(() -> { + initialize(); + + // Set system font map only if there is updatable font directory. + // If there is no updatable font directory, `initialize` will have already loaded the + // system font map, so there's no need to set the system font map again here. + if (mUpdatableFontDir != null) { + try { + Typeface.setSystemFontMap(getCurrentFontMap()); + } catch (IOException | ErrnoException e) { + Slog.w(TAG, "Failed to set system font map of system_server"); + } } - } + serviceStarted.complete(null); + }, "FontManagerService_create"); } @Nullable diff --git a/services/core/java/com/android/server/os/NativeTombstoneManager.java b/services/core/java/com/android/server/os/NativeTombstoneManager.java index f8ee6b04e9a2..c7c8136ff0da 100644 --- a/services/core/java/com/android/server/os/NativeTombstoneManager.java +++ b/services/core/java/com/android/server/os/NativeTombstoneManager.java @@ -372,7 +372,7 @@ public final class NativeTombstoneManager { return false; } - if (Math.abs(exitInfo.getTimestamp() - mTimestampMs) > 5000) { + if (Math.abs(exitInfo.getTimestamp() - mTimestampMs) > 10000) { return false; } diff --git a/services/core/java/com/android/server/pm/AppsFilterImpl.java b/services/core/java/com/android/server/pm/AppsFilterImpl.java index 6505e8b91c06..21610c9510ac 100644 --- a/services/core/java/com/android/server/pm/AppsFilterImpl.java +++ b/services/core/java/com/android/server/pm/AppsFilterImpl.java @@ -1225,6 +1225,7 @@ public final class AppsFilterImpl extends AppsFilterLocked implements Watchable, setting.getPackageName(), siblingSetting, settings, users, USER_ALL, settings.size()); } + break; } } diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index 13fd2f261652..d737cc45b9f2 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -1077,8 +1077,6 @@ final class InstallPackageHelper { final boolean isApex = ((installFlags & PackageManager.INSTALL_APEX) != 0); final boolean isRollback = request.getInstallReason() == PackageManager.INSTALL_REASON_ROLLBACK; - final boolean extractProfile = - ((installFlags & PackageManager.INSTALL_DONT_EXTRACT_BASELINE_PROFILES) == 0); @PackageManagerService.ScanFlags int scanFlags = SCAN_NEW_INSTALL | SCAN_UPDATE_SIGNATURE; if (request.isInstallMove()) { // moving a complete application; perform an initial scan on the new install location @@ -1114,9 +1112,7 @@ final class InstallPackageHelper { @ParsingPackageUtils.ParseFlags final int parseFlags = mPm.getDefParseFlags() | ParsingPackageUtils.PARSE_CHATTY | ParsingPackageUtils.PARSE_ENFORCE_CODE - | (onExternal ? ParsingPackageUtils.PARSE_EXTERNAL_STORAGE : 0) - | (extractProfile - ? ParsingPackageUtils.PARSE_EXTRACT_BASELINE_PROFILES_FROM_APK : 0); + | (onExternal ? ParsingPackageUtils.PARSE_EXTERNAL_STORAGE : 0); Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "parsePackage"); final ParsedPackage parsedPackage; diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java index 23608c5b09e9..61361972daea 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerSession.java +++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java @@ -461,6 +461,9 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { private IntentSender mRemoteStatusReceiver; @GuardedBy("mLock") + private IntentSender mPreapprovalRemoteStatusReceiver; + + @GuardedBy("mLock") private PreapprovalDetails mPreapprovalDetails; /** Fields derived from commit parsing */ @@ -2040,11 +2043,11 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { * {@link #setPermissionsResult(boolean)} is called and then * {@link #MSG_PRE_APPROVAL_REQUEST} is handled to come back here to check again. */ - if (sendPendingUserActionIntentIfNeeded()) { + if (sendPendingUserActionIntentIfNeeded(/* forPreapproval= */true)) { return; } - dispatchSessionPreappoved(); + dispatchSessionPreapproved(); } private final class FileSystemConnector extends @@ -2499,14 +2502,16 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { * @return true if the session set requires user action for the installation, otherwise false. */ @WorkerThread - private boolean sendPendingUserActionIntentIfNeeded() { + private boolean sendPendingUserActionIntentIfNeeded(boolean forPreapproval) { // To support pre-approval request of atomic install, we allow child session to handle // the result by itself since it has the status receiver. if (isCommitted()) { assertNotChild("PackageInstallerSession#sendPendingUserActionIntentIfNeeded"); } - - final IntentSender statusReceiver = getRemoteStatusReceiver(); + // Since there are separate status receivers for session preapproval and commit, + // check whether user action is requested for session preapproval or commit + final IntentSender statusReceiver = forPreapproval ? getPreapprovalRemoteStatusReceiver() + : getRemoteStatusReceiver(); return sessionContains(s -> checkUserActionRequirement(s, statusReceiver)); } @@ -2529,7 +2534,8 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { * install. Since control may come back here more than 1 time, we must ensure that it's * value is not overwritten. */ - boolean wasUserActionIntentSent = sendPendingUserActionIntentIfNeeded(); + boolean wasUserActionIntentSent = + sendPendingUserActionIntentIfNeeded(/* forPreapproval= */false); if (mUserActionRequired == null) { mUserActionRequired = wasUserActionIntentSent; } @@ -2591,6 +2597,18 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } } + private IntentSender getPreapprovalRemoteStatusReceiver() { + synchronized (mLock) { + return mPreapprovalRemoteStatusReceiver; + } + } + + private void setPreapprovalRemoteStatusReceiver(IntentSender remoteStatusReceiver) { + synchronized (mLock) { + mPreapprovalRemoteStatusReceiver = remoteStatusReceiver; + } + } + /** * Prepares staged directory with any inherited APKs. */ @@ -2828,7 +2846,8 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { private void onVerificationComplete() { if (isStaged()) { mStagingManager.commitSession(mStagedSession); - sendUpdateToRemoteStatusReceiver(INSTALL_SUCCEEDED, "Session staged", null); + sendUpdateToRemoteStatusReceiver(INSTALL_SUCCEEDED, "Session staged", + /* extras= */ null, /* forPreapproval= */ false); return; } install(); @@ -4585,7 +4604,11 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } private void dispatchSessionFinished(int returnCode, String msg, Bundle extras) { - sendUpdateToRemoteStatusReceiver(returnCode, msg, extras); + // Session can be marked as finished due to user rejecting pre approval or commit request, + // any internal error or after successful completion. As such, check whether + // the session is in the preapproval stage or the commit stage. + sendUpdateToRemoteStatusReceiver(returnCode, msg, extras, + /* forPreapproval= */ isPreapprovalRequested() && !isCommitted()); synchronized (mLock) { mFinalStatus = returnCode; @@ -4607,8 +4630,10 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } } - private void sendUpdateToRemoteStatusReceiver(int returnCode, String msg, Bundle extras) { - final IntentSender statusReceiver = getRemoteStatusReceiver(); + private void sendUpdateToRemoteStatusReceiver(int returnCode, String msg, Bundle extras, + boolean forPreapproval) { + final IntentSender statusReceiver = forPreapproval ? getPreapprovalRemoteStatusReceiver() + : getRemoteStatusReceiver(); if (statusReceiver != null) { // Execute observer.onPackageInstalled on different thread as we don't want callers // inside the system server have to worry about catching the callbacks while they are @@ -4624,8 +4649,8 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } } - private void dispatchSessionPreappoved() { - final IntentSender target = getRemoteStatusReceiver(); + private void dispatchSessionPreapproved() { + final IntentSender target = getPreapprovalRemoteStatusReceiver(); final Intent intent = new Intent(); intent.putExtra(PackageInstaller.EXTRA_SESSION_ID, sessionId); intent.putExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_SUCCESS); @@ -4646,7 +4671,8 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { if (!mPm.isPreapprovalRequestAvailable()) { sendUpdateToRemoteStatusReceiver(INSTALL_FAILED_PRE_APPROVAL_NOT_AVAILABLE, - "Request user pre-approval is currently not available.", null /* extras */); + "Request user pre-approval is currently not available.", /* extras= */null, + /* preapproval= */true); return; } @@ -4668,7 +4694,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { synchronized (mLock) { assertPreparedAndNotSealedLocked("request of session " + sessionId); mPreapprovalDetails = details; - setRemoteStatusReceiver(statusReceiver); + setPreapprovalRemoteStatusReceiver(statusReceiver); } } diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java index 2fb1ed1bbc57..8d64bd9fb66a 100644 --- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java +++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java @@ -3456,10 +3456,6 @@ class PackageManagerShellCommand extends ShellCommand { sessionParams.installFlags |= PackageManager.INSTALL_BYPASS_LOW_TARGET_SDK_BLOCK; break; - case "--no-profile": - sessionParams.installFlags |= - PackageManager.INSTALL_DONT_EXTRACT_BASELINE_PROFILES; - break; default: throw new IllegalArgumentException("Unknown option " + opt); } @@ -4348,7 +4344,7 @@ class PackageManagerShellCommand extends ShellCommand { pw.println(" [--install-reason 0/1/2/3/4] [--originating-uri URI]"); pw.println(" [--referrer URI] [--abi ABI_NAME] [--force-sdk]"); pw.println(" [--preload] [--instant] [--full] [--dont-kill]"); - pw.println(" [--enable-rollback] [--no-profile]"); + pw.println(" [--enable-rollback]"); pw.println(" [--force-uuid internal|UUID] [--pkg PACKAGE] [-S BYTES]"); pw.println(" [--apex] [--force-non-staged] [--staged-ready-timeout TIMEOUT]"); pw.println(" [PATH [SPLIT...]|-]"); @@ -4381,7 +4377,6 @@ class PackageManagerShellCommand extends ShellCommand { pw.println(" --apex: install an .apex file, not an .apk"); pw.println(" --force-non-staged: force the installation to run under a non-staged"); pw.println(" session, which may complete without requiring a reboot"); - pw.println(" --no-profile: don't extract the profiles from the apk"); pw.println(" --staged-ready-timeout: By default, staged sessions wait " + DEFAULT_STAGED_READY_TIMEOUT_MS); pw.println(" milliseconds for pre-reboot verification to complete when"); @@ -4403,7 +4398,7 @@ class PackageManagerShellCommand extends ShellCommand { pw.println(" [--referrer URI] [--abi ABI_NAME] [--force-sdk]"); pw.println(" [--preload] [--instant] [--full] [--dont-kill]"); pw.println(" [--force-uuid internal|UUID] [--pkg PACKAGE] [--apex] [-S BYTES]"); - pw.println(" [--multi-package] [--staged] [--no-profile] [--update-ownership]"); + pw.println(" [--multi-package] [--staged] [--update-ownership]"); pw.println(" Like \"install\", but starts an install session. Use \"install-write\""); pw.println(" to push data into the session, and \"install-commit\" to finish."); pw.println(""); diff --git a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java index d737b1c6bfa6..e2cb87e72c7a 100644 --- a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java +++ b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java @@ -50,7 +50,6 @@ import android.content.pm.PackageManager; import android.content.pm.PackageManager.Property; import android.content.pm.Signature; import android.content.pm.SigningDetails; -import android.content.pm.dex.DexMetadataHelper; import android.content.pm.parsing.ApkLiteParseUtils; import android.content.pm.parsing.FrameworkParsingPackageUtils; import android.content.pm.parsing.PackageLite; @@ -74,7 +73,6 @@ import android.os.SystemProperties; import android.os.Trace; import android.os.UserHandle; import android.os.ext.SdkExtensions; -import android.os.incremental.IncrementalManager; import android.permission.PermissionManager; import android.text.TextUtils; import android.util.ArrayMap; @@ -242,11 +240,6 @@ public class ParsingPackageUtils { public static final int PARSE_IGNORE_OVERLAY_REQUIRED_SYSTEM_PROPERTY = 1 << 7; public static final int PARSE_APK_IN_APEX = 1 << 9; - /** - * This flag is to determine whether to extract the baseline profiles from the apk or not. - */ - public static final int PARSE_EXTRACT_BASELINE_PROFILES_FROM_APK = 1 << 10; - public static final int PARSE_CHATTY = 1 << 31; /** The total maximum number of activities, services, providers and activity-aliases */ @@ -258,16 +251,14 @@ public class ParsingPackageUtils { private static final int MAX_PERMISSION_NAME_LENGTH = 512; @IntDef(flag = true, prefix = { "PARSE_" }, value = { - PARSE_APK_IN_APEX, PARSE_CHATTY, PARSE_COLLECT_CERTIFICATES, PARSE_ENFORCE_CODE, PARSE_EXTERNAL_STORAGE, - PARSE_EXTRACT_BASELINE_PROFILES_FROM_APK, - PARSE_IGNORE_OVERLAY_REQUIRED_SYSTEM_PROPERTY, PARSE_IGNORE_PROCESSES, PARSE_IS_SYSTEM_DIR, PARSE_MUST_BE_APK, + PARSE_IGNORE_OVERLAY_REQUIRED_SYSTEM_PROPERTY, }) @Retention(RetentionPolicy.SOURCE) public @interface ParseFlags {} @@ -568,26 +559,6 @@ public class ParsingPackageUtils { pkg.setSigningDetails(SigningDetails.UNKNOWN); } - // 1. The apkFile is an apk file - // 2. The flags include PARSE_EXTRACT_PROFILE_FROM_APK - // 3. The apk patch is NOT an incremental path - // 4. If the .dm file exists in the current apk directory, it means the caller - // prepares the .dm file. Don't extract the profiles from the apk again. - if (ApkLiteParseUtils.isApkFile(apkFile) - && (flags & PARSE_EXTRACT_BASELINE_PROFILES_FROM_APK) != 0 - && !IncrementalManager.isIncrementalPath(apkPath) - && DexMetadataHelper.findDexMetadataForFile(apkFile) == null) { - // Extract the baseline profiles from the apk if the profiles exist in the assets - // directory in the apk. - boolean extractedResult = - DexMetadataHelper.extractBaselineProfilesToDexMetadataFileFromApk(assets, - apkPath); - - if (DEBUG_JAR) { - Slog.d(TAG, "Extract profiles " + (extractedResult ? "success" : "fail")); - } - } - return input.success(pkg); } catch (Exception e) { return input.error(INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION, diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 402bb5976632..7bbe8781e434 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -6271,10 +6271,10 @@ public class PhoneWindowManager implements WindowManagerPolicy { pw.println(endcallBehaviorToString(mEndcallBehavior)); pw.print(prefix); // TODO(b/117479243): handle it in InputPolicy - pw.print("mDisplayHomeButtonHandlers="); + pw.println("mDisplayHomeButtonHandlers="); for (int i = 0; i < mDisplayHomeButtonHandlers.size(); i++) { final int key = mDisplayHomeButtonHandlers.keyAt(i); - pw.println(mDisplayHomeButtonHandlers.get(key)); + pw.print(prefix); pw.print(" "); pw.println(mDisplayHomeButtonHandlers.get(key)); } pw.print(prefix); pw.print("mKeyguardOccluded="); pw.print(isKeyguardOccluded()); pw.print(" mKeyguardOccludedChanged="); pw.print(mKeyguardOccludedChanged); @@ -6545,6 +6545,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { private class HdmiVideoExtconUEventObserver extends ExtconStateObserver<Boolean> { private static final String HDMI_EXIST = "HDMI=1"; + private static final String DP_EXIST = "DP=1"; private static final String NAME = "hdmi"; private boolean init(ExtconInfo hdmi) { @@ -6575,7 +6576,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { public Boolean parseState(ExtconInfo extconIfno, String state) { // extcon event state changes from kernel4.9 // new state will be like STATE=HDMI=1 - return state.contains(HDMI_EXIST); + // or like STATE=DP=1 for newer kernel + return state.contains(HDMI_EXIST) || state.contains(DP_EXIST); } } diff --git a/services/core/java/com/android/server/security/FileIntegrityService.java b/services/core/java/com/android/server/security/FileIntegrityService.java index 5ae697315ed1..95296210be80 100644 --- a/services/core/java/com/android/server/security/FileIntegrityService.java +++ b/services/core/java/com/android/server/security/FileIntegrityService.java @@ -184,13 +184,7 @@ public class FileIntegrityService extends SystemService { } private void loadAllCertificates() { - // A better alternative to load certificates would be to read from .fs-verity kernel - // keyring, which fsverity_init loads to during earlier boot time from the same sources - // below. But since the read operation from keyring is not provided in kernel, we need to - // duplicate the same loading logic here. - // Load certificates trusted by the device manufacturer. - // NB: Directories need to be synced with system/security/fsverity_init/fsverity_init.cpp. final String relativeDir = "etc/security/fsverity"; loadCertificatesFromDirectory(Environment.getRootDirectory().toPath() .resolve(relativeDir)); diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index e261916dffed..349d11568e71 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -6404,9 +6404,9 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp // Don't do recursive work. return; } - mInEnsureActivitiesVisible = true; mAtmService.mTaskSupervisor.beginActivityVisibilityUpdate(); try { + mInEnsureActivitiesVisible = true; forAllRootTasks(rootTask -> { rootTask.ensureActivitiesVisible(starting, configChanges, preserveWindows, notifyClients); diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/LetterboxConfiguration.java index 7a201a77c966..e945bc1babd9 100644 --- a/services/core/java/com/android/server/wm/LetterboxConfiguration.java +++ b/services/core/java/com/android/server/wm/LetterboxConfiguration.java @@ -294,16 +294,15 @@ final class LetterboxConfiguration { @NonNull private final SynchedDeviceConfig mDeviceConfig; LetterboxConfiguration(@NonNull final Context systemUiContext) { - this(systemUiContext, - new LetterboxConfigurationPersister(systemUiContext, - () -> readLetterboxHorizontalReachabilityPositionFromConfig( - systemUiContext, /* forBookMode */ false), - () -> readLetterboxVerticalReachabilityPositionFromConfig( - systemUiContext, /* forTabletopMode */ false), - () -> readLetterboxHorizontalReachabilityPositionFromConfig( - systemUiContext, /* forBookMode */ true), - () -> readLetterboxVerticalReachabilityPositionFromConfig( - systemUiContext, /* forTabletopMode */ true))); + this(systemUiContext, new LetterboxConfigurationPersister( + () -> readLetterboxHorizontalReachabilityPositionFromConfig( + systemUiContext, /* forBookMode */ false), + () -> readLetterboxVerticalReachabilityPositionFromConfig( + systemUiContext, /* forTabletopMode */ false), + () -> readLetterboxHorizontalReachabilityPositionFromConfig( + systemUiContext, /* forBookMode */ true), + () -> readLetterboxVerticalReachabilityPositionFromConfig( + systemUiContext, /* forTabletopMode */ true))); } @VisibleForTesting diff --git a/services/core/java/com/android/server/wm/LetterboxConfigurationPersister.java b/services/core/java/com/android/server/wm/LetterboxConfigurationPersister.java index 756339701590..38aa903e3954 100644 --- a/services/core/java/com/android/server/wm/LetterboxConfigurationPersister.java +++ b/services/core/java/com/android/server/wm/LetterboxConfigurationPersister.java @@ -23,7 +23,6 @@ import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; import android.annotation.NonNull; import android.annotation.Nullable; -import android.content.Context; import android.os.Environment; import android.os.StrictMode; import android.os.StrictMode.ThreadPolicy; @@ -53,10 +52,8 @@ class LetterboxConfigurationPersister { private static final String TAG = TAG_WITH_CLASS_NAME ? "LetterboxConfigurationPersister" : TAG_WM; - @VisibleForTesting - static final String LETTERBOX_CONFIGURATION_FILENAME = "letterbox_config"; + private static final String LETTERBOX_CONFIGURATION_FILENAME = "letterbox_config"; - private final Context mContext; private final Supplier<Integer> mDefaultHorizontalReachabilitySupplier; private final Supplier<Integer> mDefaultVerticalReachabilitySupplier; private final Supplier<Integer> mDefaultBookModeReachabilitySupplier; @@ -97,36 +94,32 @@ class LetterboxConfigurationPersister { @NonNull private final PersisterQueue mPersisterQueue; - LetterboxConfigurationPersister(Context systemUiContext, - Supplier<Integer> defaultHorizontalReachabilitySupplier, - Supplier<Integer> defaultVerticalReachabilitySupplier, - Supplier<Integer> defaultBookModeReachabilitySupplier, - Supplier<Integer> defaultTabletopModeReachabilitySupplier) { - this(systemUiContext, defaultHorizontalReachabilitySupplier, - defaultVerticalReachabilitySupplier, - defaultBookModeReachabilitySupplier, - defaultTabletopModeReachabilitySupplier, + LetterboxConfigurationPersister( + @NonNull Supplier<Integer> defaultHorizontalReachabilitySupplier, + @NonNull Supplier<Integer> defaultVerticalReachabilitySupplier, + @NonNull Supplier<Integer> defaultBookModeReachabilitySupplier, + @NonNull Supplier<Integer> defaultTabletopModeReachabilitySupplier) { + this(defaultHorizontalReachabilitySupplier, defaultVerticalReachabilitySupplier, + defaultBookModeReachabilitySupplier, defaultTabletopModeReachabilitySupplier, Environment.getDataSystemDirectory(), new PersisterQueue(), - /* completionCallback */ null); + /* completionCallback */ null, LETTERBOX_CONFIGURATION_FILENAME); } @VisibleForTesting - LetterboxConfigurationPersister(Context systemUiContext, - Supplier<Integer> defaultHorizontalReachabilitySupplier, - Supplier<Integer> defaultVerticalReachabilitySupplier, - Supplier<Integer> defaultBookModeReachabilitySupplier, - Supplier<Integer> defaultTabletopModeReachabilitySupplier, - File configFolder, - PersisterQueue persisterQueue, @Nullable Consumer<String> completionCallback) { - mContext = systemUiContext.createDeviceProtectedStorageContext(); + LetterboxConfigurationPersister( + @NonNull Supplier<Integer> defaultHorizontalReachabilitySupplier, + @NonNull Supplier<Integer> defaultVerticalReachabilitySupplier, + @NonNull Supplier<Integer> defaultBookModeReachabilitySupplier, + @NonNull Supplier<Integer> defaultTabletopModeReachabilitySupplier, + @NonNull File configFolder, @NonNull PersisterQueue persisterQueue, + @Nullable Consumer<String> completionCallback, + @NonNull String letterboxConfigurationFileName) { mDefaultHorizontalReachabilitySupplier = defaultHorizontalReachabilitySupplier; mDefaultVerticalReachabilitySupplier = defaultVerticalReachabilitySupplier; - mDefaultBookModeReachabilitySupplier = - defaultBookModeReachabilitySupplier; - mDefaultTabletopModeReachabilitySupplier = - defaultTabletopModeReachabilitySupplier; + mDefaultBookModeReachabilitySupplier = defaultBookModeReachabilitySupplier; + mDefaultTabletopModeReachabilitySupplier = defaultTabletopModeReachabilitySupplier; mCompletionCallback = completionCallback; - final File prefFiles = new File(configFolder, LETTERBOX_CONFIGURATION_FILENAME); + final File prefFiles = new File(configFolder, letterboxConfigurationFileName); mConfigurationFile = new AtomicFile(prefFiles); mPersisterQueue = persisterQueue; runWithDiskReadsThreadPolicy(this::readCurrentConfiguration); diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java index 48420d22512e..2f0c303ec839 100644 --- a/services/core/java/com/android/server/wm/RootWindowContainer.java +++ b/services/core/java/com/android/server/wm/RootWindowContainer.java @@ -1849,9 +1849,8 @@ class RootWindowContainer extends WindowContainer<DisplayContent> // Don't do recursive work. return; } - + mTaskSupervisor.beginActivityVisibilityUpdate(); try { - mTaskSupervisor.beginActivityVisibilityUpdate(); // First the front root tasks. In case any are not fullscreen and are in front of home. for (int displayNdx = getChildCount() - 1; displayNdx >= 0; --displayNdx) { final DisplayContent display = getChildAt(displayNdx); @@ -3226,6 +3225,10 @@ class RootWindowContainer extends WindowContainer<DisplayContent> + "not idle", rootTask.getRootTaskId(), resumedActivity); return false; } + if (mTransitionController.isTransientLaunch(resumedActivity)) { + // Not idle if the transient transition animation is running. + return false; + } } // End power mode launch when idle. mService.endLaunchPowerMode(ActivityTaskManagerService.POWER_MODE_REASON_START_ACTIVITY); diff --git a/services/core/java/com/android/server/wm/SafeActivityOptions.java b/services/core/java/com/android/server/wm/SafeActivityOptions.java index c914fa10687f..fe3094e2edf0 100644 --- a/services/core/java/com/android/server/wm/SafeActivityOptions.java +++ b/services/core/java/com/android/server/wm/SafeActivityOptions.java @@ -293,26 +293,7 @@ public class SafeActivityOptions { throw new SecurityException(msg); } // Check if the caller is allowed to launch on the specified display area. - final WindowContainerToken daToken = options.getLaunchTaskDisplayArea(); - TaskDisplayArea taskDisplayArea = daToken != null - ? (TaskDisplayArea) WindowContainer.fromBinder(daToken.asBinder()) : null; - - // If we do not have a task display area token, check if the launch task display area - // feature id is specified. - if (taskDisplayArea == null) { - final int launchTaskDisplayAreaFeatureId = options.getLaunchTaskDisplayAreaFeatureId(); - if (launchTaskDisplayAreaFeatureId != FEATURE_UNDEFINED) { - final int launchDisplayId = options.getLaunchDisplayId() == INVALID_DISPLAY - ? DEFAULT_DISPLAY : options.getLaunchDisplayId(); - final DisplayContent dc = supervisor.mRootWindowContainer - .getDisplayContent(launchDisplayId); - if (dc != null) { - taskDisplayArea = dc.getItemFromTaskDisplayAreas(tda -> - tda.mFeatureId == launchTaskDisplayAreaFeatureId ? tda : null); - } - } - } - + final TaskDisplayArea taskDisplayArea = getLaunchTaskDisplayArea(options, supervisor); if (aInfo != null && taskDisplayArea != null && !supervisor.isCallerAllowedToLaunchOnTaskDisplayArea(callingPid, callingUid, taskDisplayArea, aInfo)) { @@ -428,6 +409,32 @@ public class SafeActivityOptions { } } + @VisibleForTesting + TaskDisplayArea getLaunchTaskDisplayArea(ActivityOptions options, + ActivityTaskSupervisor supervisor) { + final WindowContainerToken daToken = options.getLaunchTaskDisplayArea(); + TaskDisplayArea taskDisplayArea = daToken != null + ? (TaskDisplayArea) WindowContainer.fromBinder(daToken.asBinder()) : null; + if (taskDisplayArea != null) { + return taskDisplayArea; + } + + // If we do not have a task display area token, check if the launch task display area + // feature id is specified. + final int launchTaskDisplayAreaFeatureId = options.getLaunchTaskDisplayAreaFeatureId(); + if (launchTaskDisplayAreaFeatureId != FEATURE_UNDEFINED) { + final int launchDisplayId = options.getLaunchDisplayId() == INVALID_DISPLAY + ? DEFAULT_DISPLAY : options.getLaunchDisplayId(); + final DisplayContent dc = supervisor.mRootWindowContainer + .getDisplayContent(launchDisplayId); + if (dc != null) { + taskDisplayArea = dc.getItemFromTaskDisplayAreas(tda -> + tda.mFeatureId == launchTaskDisplayAreaFeatureId ? tda : null); + } + } + return taskDisplayArea; + } + private boolean isAssistant(ActivityTaskManagerService atmService, int callingUid) { if (atmService.mActiveVoiceInteractionServiceComponent == null) { return false; diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 92e90ae294d1..96190f2b8b3a 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -5251,17 +5251,18 @@ class Task extends TaskFragment { // Ensure that we do not trigger entering PiP an activity on the root pinned task. return; } - final boolean isTransient = opts != null && opts.getTransientLaunch(); final Task targetRootTask = toFrontTask != null ? toFrontTask.getRootTask() : toFrontActivity.getRootTask(); - if (targetRootTask != null && (targetRootTask.isActivityTypeAssistant() || isTransient)) { - // Ensure the task/activity being brought forward is not the assistant and is not - // transient. In the case of transient-launch, we want to wait until the end of the - // transition and only allow switch if the transient launch was committed. - return; - } - pipCandidate.supportsEnterPipOnTaskSwitch = true; - + final boolean isTransient = opts != null && opts.getTransientLaunch() + || (targetRootTask != null + && targetRootTask.mTransitionController.isTransientHide(targetRootTask)); + + // Ensure the task/activity being brought forward is not the assistant and is not transient + // nor transient hide target. In the case of transient-launch, we want to wait until the end + // of the transition and only allow to enter pip on task switch after the transient launch + // was committed. + pipCandidate.supportsEnterPipOnTaskSwitch = targetRootTask == null + || !(targetRootTask.isActivityTypeAssistant() || isTransient); } /** diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index a14354041b91..879457767595 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -1041,6 +1041,10 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { ar.getTaskFragment().startPausing(false /* uiSleeping */, null /* resuming */, "finishTransition"); } finally { + // Didn't schedule for pip, clear the supportsEnterPipOnTaskSwitch flag. + if (!ar.mPauseSchedulePendingForPip) { + ar.supportsEnterPipOnTaskSwitch = false; + } mController.mAtm.mTaskSupervisor.mUserLeaving = false; } // Return false anyway because there's no guarantee that the app will enter pip. diff --git a/services/flags/Android.bp b/services/flags/Android.bp new file mode 100644 index 000000000000..2d0337dce74f --- /dev/null +++ b/services/flags/Android.bp @@ -0,0 +1,17 @@ +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +java_library_static { + name: "services.flags", + defaults: ["platform_service_defaults"], + srcs: [ + "java/**/*.java", + ], + libs: ["services.core"], +} diff --git a/services/flags/OWNERS b/services/flags/OWNERS new file mode 100644 index 000000000000..3925b5c13c2d --- /dev/null +++ b/services/flags/OWNERS @@ -0,0 +1,6 @@ +# Bug component: 1306523 + +mankoff@google.com + +pixel@google.com +dsandler@android.com diff --git a/services/flags/java/com/android/server/flags/DynamicFlagBinderDelegate.java b/services/flags/java/com/android/server/flags/DynamicFlagBinderDelegate.java new file mode 100644 index 000000000000..0db328792cf3 --- /dev/null +++ b/services/flags/java/com/android/server/flags/DynamicFlagBinderDelegate.java @@ -0,0 +1,280 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.flags; + +import android.annotation.NonNull; +import android.flags.IFeatureFlagsCallback; +import android.flags.SyncableFlag; +import android.os.Build; +import android.os.IBinder; +import android.os.RemoteException; +import android.provider.DeviceConfig; +import android.util.Slog; + +import com.android.internal.os.BackgroundThread; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; + +/** + * Handles DynamicFlags for {@link FeatureFlagsBinder}. + * + * Dynamic flags are simultaneously simpler and more complicated than process stable flags. We can + * return whatever value is last known for a flag is, without too much worry about the flags + * changing (they are dynamic after all). However, we have to alert all the relevant clients + * about those flag changes, and need to be able to restore to a default value if the flag gets + * reset/erased during runtime. + */ +class DynamicFlagBinderDelegate { + + private final FlagOverrideStore mFlagStore; + private final FlagCache<DynamicFlagData> mDynamicFlags = new FlagCache<>(); + private final Map<Integer, Set<IFeatureFlagsCallback>> mCallbacks = new HashMap<>(); + private static final Function<Integer, Set<IFeatureFlagsCallback>> NEW_CALLBACK_SET = + k -> new HashSet<>(); + + private final DeviceConfig.OnPropertiesChangedListener mDeviceConfigListener = + new DeviceConfig.OnPropertiesChangedListener() { + @Override + public void onPropertiesChanged(@NonNull DeviceConfig.Properties properties) { + String ns = properties.getNamespace(); + for (String name : properties.getKeyset()) { + // Don't alert for flags we don't care about. + // Don't alert for flags that have been overridden locally. + if (!mDynamicFlags.contains(ns, name) || mFlagStore.contains(ns, name)) { + continue; + } + mFlagChangeCallback.onFlagChanged( + ns, name, properties.getString(name, null)); + } + } + }; + + private final FlagOverrideStore.FlagChangeCallback mFlagChangeCallback = + (namespace, name, value) -> { + // Don't bother with callbacks for non-dynamic flags. + if (!mDynamicFlags.contains(namespace, name)) { + return; + } + + // Don't bother with callbacks if nothing changed. + // Handling erasure (null) is special, as we may be restoring back to a value + // we were already at. + DynamicFlagData data = mDynamicFlags.getOrNull(namespace, name); + if (data == null) { + return; // shouldn't happen, but better safe than sorry. + } + if (value == null) { + if (data.getValue().equals(data.getDefaultValue())) { + return; + } + value = data.getDefaultValue(); + } else if (data.getValue().equals(value)) { + return; + } + data.setValue(value); + + final Set<IFeatureFlagsCallback> cbCopy; + synchronized (mCallbacks) { + cbCopy = new HashSet<>(); + + for (Integer pid : mCallbacks.keySet()) { + if (data.containsPid(pid)) { + cbCopy.addAll(mCallbacks.get(pid)); + } + } + } + SyncableFlag sFlag = new SyncableFlag(namespace, name, value, true); + cbCopy.forEach(cb -> { + try { + cb.onFlagChange(sFlag); + } catch (RemoteException e) { + Slog.w( + FeatureFlagsService.TAG, + "Failed to communicate flag change to client."); + } + }); + }; + + DynamicFlagBinderDelegate(FlagOverrideStore flagStore) { + mFlagStore = flagStore; + mFlagStore.setChangeCallback(mFlagChangeCallback); + } + + SyncableFlag syncDynamicFlag(int pid, SyncableFlag sf) { + if (!sf.isDynamic()) { + return sf; + } + + String ns = sf.getNamespace(); + String name = sf.getName(); + + // Dynamic flags don't need any special threading or synchronization considerations. + // We simply give them whatever the current value is. + // However, we do need to keep track of dynamic flags, so that we can alert + // about changes coming in from adb, DeviceConfig, or other sources. + // And also so that we can keep flags relatively consistent across processes. + + DynamicFlagData data = mDynamicFlags.getOrNull(ns, name); + String value = getFlagValue(ns, name, sf.getValue()); + // DeviceConfig listeners are per-namespace. + if (!mDynamicFlags.containsNamespace(ns)) { + DeviceConfig.addOnPropertiesChangedListener( + ns, BackgroundThread.getExecutor(), mDeviceConfigListener); + } + data.addClientPid(pid); + data.setValue(value); + // Store the default value so that if an override gets erased, we can restore + // to something. + data.setDefaultValue(sf.getValue()); + + return new SyncableFlag(sf.getNamespace(), sf.getName(), value, true); + } + + + void registerCallback(int pid, IFeatureFlagsCallback callback) { + // Always add callback so that we don't end up with a possible race/leak. + // We remove the callback directly if we fail to call #linkToDeath. + // If we tried to add the callback after we linked, then we could end up in a + // scenario where we link, then the binder dies, firing our BinderGriever which tries + // to remove the callback (which has not yet been added), then finally we add the + // callback, creating a leak. + Set<IFeatureFlagsCallback> callbacks; + synchronized (mCallbacks) { + callbacks = mCallbacks.computeIfAbsent(pid, NEW_CALLBACK_SET); + callbacks.add(callback); + } + try { + callback.asBinder().linkToDeath(new BinderGriever(pid), 0); + } catch (RemoteException e) { + Slog.e( + FeatureFlagsService.TAG, + "Failed to link to binder death. Callback not registered."); + synchronized (mCallbacks) { + callbacks.remove(callback); + } + } + } + + void unregisterCallback(int pid, IFeatureFlagsCallback callback) { + // No need to unlink, since the BinderGriever will essentially be a no-op. + // We would have to track our BinderGriever's in a map otherwise. + synchronized (mCallbacks) { + Set<IFeatureFlagsCallback> callbacks = + mCallbacks.computeIfAbsent(pid, NEW_CALLBACK_SET); + callbacks.remove(callback); + } + } + + String getFlagValue(String namespace, String name, String defaultValue) { + // If we already have a value cached, just use that. + String value = null; + DynamicFlagData data = mDynamicFlags.getOrNull(namespace, name); + if (data != null) { + value = data.getValue(); + } else { + // Put the value in the cache for future reference. + data = new DynamicFlagData(namespace, name); + mDynamicFlags.setIfChanged(namespace, name, data); + } + // If we're not in a release build, flags can be overridden locally on device. + if (!Build.IS_USER && value == null) { + value = mFlagStore.get(namespace, name); + } + // If we still don't have a value, maybe DeviceConfig does? + // Fallback to sf.getValue() here as well. + if (value == null) { + value = DeviceConfig.getString(namespace, name, defaultValue); + } + + return value; + } + + private static class DynamicFlagData { + private final String mNamespace; + private final String mName; + private final Set<Integer> mPids = new HashSet<>(); + private String mValue; + private String mDefaultValue; + + private DynamicFlagData(String namespace, String name) { + mNamespace = namespace; + mName = name; + } + + String getValue() { + return mValue; + } + + void setValue(String value) { + mValue = value; + } + + String getDefaultValue() { + return mDefaultValue; + } + + void setDefaultValue(String value) { + mDefaultValue = value; + } + + void addClientPid(int pid) { + mPids.add(pid); + } + + boolean containsPid(int pid) { + return mPids.contains(pid); + } + + @Override + public boolean equals(Object other) { + if (other == null || !(other instanceof DynamicFlagData)) { + return false; + } + + DynamicFlagData o = (DynamicFlagData) other; + + return mName.equals(o.mName) && mNamespace.equals(o.mNamespace) + && mValue.equals(o.mValue) && mDefaultValue.equals(o.mDefaultValue); + } + + @Override + public int hashCode() { + return mName.hashCode() + mNamespace.hashCode() + + mValue.hashCode() + mDefaultValue.hashCode(); + } + } + + + private class BinderGriever implements IBinder.DeathRecipient { + private final int mPid; + + private BinderGriever(int pid) { + mPid = pid; + } + + @Override + public void binderDied() { + synchronized (mCallbacks) { + mCallbacks.remove(mPid); + } + } + } +} diff --git a/services/flags/java/com/android/server/flags/FeatureFlagsBinder.java b/services/flags/java/com/android/server/flags/FeatureFlagsBinder.java new file mode 100644 index 000000000000..1fa85325aea6 --- /dev/null +++ b/services/flags/java/com/android/server/flags/FeatureFlagsBinder.java @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.flags; + +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.flags.IFeatureFlags; +import android.flags.IFeatureFlagsCallback; +import android.flags.SyncableFlag; +import android.os.Build; +import android.os.ParcelFileDescriptor; + +import com.android.internal.flags.CoreFlags; +import com.android.server.flags.FeatureFlagsService.PermissionsChecker; + +import java.io.FileOutputStream; +import java.util.ArrayList; +import java.util.List; + +class FeatureFlagsBinder extends IFeatureFlags.Stub { + private final FlagOverrideStore mFlagStore; + private final FlagsShellCommand mShellCommand; + private final FlagCache<String> mFlagCache = new FlagCache<>(); + private final DynamicFlagBinderDelegate mDynamicFlagDelegate; + private final PermissionsChecker mPermissionsChecker; + + FeatureFlagsBinder( + FlagOverrideStore flagStore, + FlagsShellCommand shellCommand, + PermissionsChecker permissionsChecker) { + mFlagStore = flagStore; + mShellCommand = shellCommand; + mDynamicFlagDelegate = new DynamicFlagBinderDelegate(flagStore); + mPermissionsChecker = permissionsChecker; + } + + @Override + public void registerCallback(IFeatureFlagsCallback callback) { + mDynamicFlagDelegate.registerCallback(getCallingPid(), callback); + } + + @Override + public void unregisterCallback(IFeatureFlagsCallback callback) { + mDynamicFlagDelegate.unregisterCallback(getCallingPid(), callback); + } + + // Note: The internals of this method should be kept in sync with queryFlags + // as they both should return identical results. The difference is that this method + // caches any values it receives and/or reads, whereas queryFlags does not. + + @Override + public List<SyncableFlag> syncFlags(List<SyncableFlag> incomingFlags) { + int pid = getCallingPid(); + List<SyncableFlag> outputFlags = new ArrayList<>(); + + boolean hasFullSyncPrivileges = false; + SecurityException permissionFailureException = null; + try { + assertSyncPermission(); + hasFullSyncPrivileges = true; + } catch (SecurityException e) { + permissionFailureException = e; + } + + for (SyncableFlag sf : incomingFlags) { + if (!hasFullSyncPrivileges && !CoreFlags.isCoreFlag(sf)) { + throw permissionFailureException; + } + + String ns = sf.getNamespace(); + String name = sf.getName(); + SyncableFlag outFlag; + if (sf.isDynamic()) { + outFlag = mDynamicFlagDelegate.syncDynamicFlag(pid, sf); + } else { + synchronized (mFlagCache) { + String value = mFlagCache.getOrNull(ns, name); + if (value == null) { + String overrideValue = Build.IS_USER ? null : mFlagStore.get(ns, name); + value = overrideValue != null ? overrideValue : sf.getValue(); + mFlagCache.setIfChanged(ns, name, value); + } + outFlag = new SyncableFlag(sf.getNamespace(), sf.getName(), value, false); + } + } + outputFlags.add(outFlag); + } + return outputFlags; + } + + @Override + public void overrideFlag(SyncableFlag flag) { + assertWritePermission(); + mFlagStore.set(flag.getNamespace(), flag.getName(), flag.getValue()); + } + + @Override + public void resetFlag(SyncableFlag flag) { + assertWritePermission(); + mFlagStore.erase(flag.getNamespace(), flag.getName()); + } + + @Override + public List<SyncableFlag> queryFlags(List<SyncableFlag> incomingFlags) { + assertSyncPermission(); + List<SyncableFlag> outputFlags = new ArrayList<>(); + for (SyncableFlag sf : incomingFlags) { + String ns = sf.getNamespace(); + String name = sf.getName(); + String value; + String storeValue = mFlagStore.get(ns, name); + boolean overridden = storeValue != null; + + if (sf.isDynamic()) { + value = mDynamicFlagDelegate.getFlagValue(ns, name, sf.getValue()); + } else { + value = mFlagCache.getOrNull(ns, name); + if (value == null) { + value = Build.IS_USER ? null : storeValue; + if (value == null) { + value = sf.getValue(); + } + } + } + outputFlags.add(new SyncableFlag( + sf.getNamespace(), sf.getName(), value, sf.isDynamic(), overridden)); + } + + return outputFlags; + } + + private void assertSyncPermission() { + mPermissionsChecker.assertSyncPermission(); + clearCallingIdentity(); + } + + private void assertWritePermission() { + mPermissionsChecker.assertWritePermission(); + clearCallingIdentity(); + } + + + @SystemApi + public int handleShellCommand( + @NonNull ParcelFileDescriptor in, + @NonNull ParcelFileDescriptor out, + @NonNull ParcelFileDescriptor err, + @NonNull String[] args) { + FileOutputStream fout = new FileOutputStream(out.getFileDescriptor()); + FileOutputStream ferr = new FileOutputStream(err.getFileDescriptor()); + + return mShellCommand.process(args, fout, ferr); + } +} diff --git a/services/flags/java/com/android/server/flags/FeatureFlagsService.java b/services/flags/java/com/android/server/flags/FeatureFlagsService.java new file mode 100644 index 000000000000..93b9e9e0dc8c --- /dev/null +++ b/services/flags/java/com/android/server/flags/FeatureFlagsService.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.flags; + +import static android.Manifest.permission.SYNC_FLAGS; +import static android.Manifest.permission.WRITE_FLAGS; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.flags.FeatureFlags; +import android.util.Slog; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.SystemService; + +/** + * A service that manages syncing {@link android.flags.FeatureFlags} across processes. + * + * This service holds flags stable for at least the lifetime of a process, meaning that if + * a process comes online with a flag set to true, any other process that connects here and + * tries to read the same flag will also receive the flag as true. The flag will remain stable + * until either all of the interested processes have died, or the device restarts. + * + * TODO(279054964): Add to dumpsys + * @hide + */ +public class FeatureFlagsService extends SystemService { + + static final String TAG = "FeatureFlagsService"; + private final FlagOverrideStore mFlagStore; + private final FlagsShellCommand mShellCommand; + + /** + * Initializes the system service. + * + * @param context The system server context. + */ + public FeatureFlagsService(Context context) { + super(context); + mFlagStore = new FlagOverrideStore( + new GlobalSettingsProxy(context.getContentResolver())); + mShellCommand = new FlagsShellCommand(mFlagStore); + } + + @Override + public void onStart() { + Slog.d(TAG, "Started Feature Flag Service"); + FeatureFlagsBinder service = new FeatureFlagsBinder( + mFlagStore, mShellCommand, new PermissionsChecker(getContext())); + publishBinderService( + Context.FEATURE_FLAGS_SERVICE, service); + publishLocalService(FeatureFlags.class, new FeatureFlags(service)); + } + + @Override + public void onBootPhase(int phase) { + super.onBootPhase(phase); + + if (phase == PHASE_SYSTEM_SERVICES_READY) { + // Immediately sync our core flags so that they get locked in. We don't want third-party + // apps to override them, and syncing immediately is the easiest way to prevent that. + FeatureFlags.getInstance().sync(); + } + } + + /** + * Delegate for checking flag permissions. + */ + @VisibleForTesting + public static class PermissionsChecker { + private final Context mContext; + + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) + public PermissionsChecker(Context context) { + mContext = context; + } + + /** + * Ensures that the caller has {@link SYNC_FLAGS} permission. + */ + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) + public void assertSyncPermission() { + if (mContext.checkCallingOrSelfPermission(SYNC_FLAGS) + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException( + "Non-core flag queried. Requires SYNC_FLAGS permission!"); + } + } + + /** + * Ensures that the caller has {@link WRITE_FLAGS} permission. + */ + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) + public void assertWritePermission() { + if (mContext.checkCallingPermission(WRITE_FLAGS) != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("Requires WRITE_FLAGS permission!"); + } + } + } +} diff --git a/services/flags/java/com/android/server/flags/FlagCache.java b/services/flags/java/com/android/server/flags/FlagCache.java new file mode 100644 index 000000000000..cee1578a5dde --- /dev/null +++ b/services/flags/java/com/android/server/flags/FlagCache.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.flags; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +/** + * Threadsafe cache of values that stores the supplied default on cache miss. + * + * @param <V> The type of value to store. + */ +public class FlagCache<V> { + private final Function<String, HashMap<String, V>> mNewHashMap = k -> new HashMap<>(); + + // Cache is organized first by namespace, then by name. All values are stored as strings. + final Map<String, Map<String, V>> mCache = new HashMap<>(); + + FlagCache() { + } + + /** + * Returns true if the namespace exists in the cache already. + */ + boolean containsNamespace(String namespace) { + synchronized (mCache) { + return mCache.containsKey(namespace); + } + } + + /** + * Returns true if the value is stored in the cache. + */ + boolean contains(String namespace, String name) { + synchronized (mCache) { + Map<String, V> nsCache = mCache.get(namespace); + return nsCache != null && nsCache.containsKey(name); + } + } + + /** + * Sets the value if it is different from what is currently stored. + * + * If the value is not set, or the current value is null, it will store the value and + * return true. + * + * @return True if the value was set. False if the value is the same. + */ + boolean setIfChanged(String namespace, String name, V value) { + synchronized (mCache) { + Map<String, V> nsCache = mCache.computeIfAbsent(namespace, mNewHashMap); + V curValue = nsCache.get(name); + if (curValue == null || !curValue.equals(value)) { + nsCache.put(name, value); + return true; + } + return false; + } + } + + /** + * Gets the current value from the cache, setting it if it is currently absent. + * + * @return The value that is now in the cache after the call to the method. + */ + V getOrSet(String namespace, String name, V defaultValue) { + synchronized (mCache) { + Map<String, V> nsCache = mCache.computeIfAbsent(namespace, mNewHashMap); + V value = nsCache.putIfAbsent(name, defaultValue); + return value == null ? defaultValue : value; + } + } + + /** + * Gets the current value from the cache, returning null if not present. + * + * @return The value that is now in the cache if there is one. + */ + V getOrNull(String namespace, String name) { + synchronized (mCache) { + Map<String, V> nsCache = mCache.get(namespace); + if (nsCache == null) { + return null; + } + return nsCache.get(name); + } + } +} diff --git a/services/flags/java/com/android/server/flags/FlagOverrideStore.java b/services/flags/java/com/android/server/flags/FlagOverrideStore.java new file mode 100644 index 000000000000..b1ddc7e67f68 --- /dev/null +++ b/services/flags/java/com/android/server/flags/FlagOverrideStore.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.flags; + +import android.database.Cursor; +import android.provider.Settings; + +import com.android.internal.annotations.VisibleForTesting; + +import java.util.HashMap; +import java.util.Map; + +/** + * Persistent storage for the {@link FeatureFlagsService}. + * + * The implementation stores data in Settings.<store> (generally {@link Settings.Global} + * is expected). + */ +@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) +public class FlagOverrideStore { + private static final String KEYNAME_PREFIX = "flag|"; + private static final String NAMESPACE_NAME_SEPARATOR = "."; + + private final SettingsProxy mSettingsProxy; + + private FlagChangeCallback mCallback; + + FlagOverrideStore(SettingsProxy settingsProxy) { + mSettingsProxy = settingsProxy; + } + + void setChangeCallback(FlagChangeCallback callback) { + mCallback = callback; + } + + /** Returns true if a non-null value is in the store. */ + boolean contains(String namespace, String name) { + return get(namespace, name) != null; + } + + /** Put a value in the store. */ + @VisibleForTesting + public void set(String namespace, String name, String value) { + mSettingsProxy.putString(getPropName(namespace, name), value); + mCallback.onFlagChanged(namespace, name, value); + } + + /** Read a value out of the store. */ + @VisibleForTesting + public String get(String namespace, String name) { + return mSettingsProxy.getString(getPropName(namespace, name)); + } + + /** Erase a value from the store. */ + @VisibleForTesting + public void erase(String namespace, String name) { + set(namespace, name, null); + } + + Map<String, Map<String, String>> getFlags() { + return getFlagsForNamespace(null); + } + + Map<String, Map<String, String>> getFlagsForNamespace(String namespace) { + Cursor c = mSettingsProxy.getContentResolver().query( + Settings.Global.CONTENT_URI, + new String[]{Settings.NameValueTable.NAME, Settings.NameValueTable.VALUE}, + null, // Doesn't support a "LIKE" query + null, + null + ); + + if (c == null) { + return Map.of(); + } + int keynamePrefixLength = KEYNAME_PREFIX.length(); + Map<String, Map<String, String>> results = new HashMap<>(); + while (c.moveToNext()) { + String key = c.getString(0); + if (!key.startsWith(KEYNAME_PREFIX) + || key.indexOf(NAMESPACE_NAME_SEPARATOR, keynamePrefixLength) < 0) { + continue; + } + String value = c.getString(1); + if (value == null || value.isEmpty()) { + continue; + } + String ns = key.substring(keynamePrefixLength, key.indexOf(NAMESPACE_NAME_SEPARATOR)); + if (namespace != null && !namespace.equals(ns)) { + continue; + } + String name = key.substring(key.indexOf(NAMESPACE_NAME_SEPARATOR) + 1); + results.putIfAbsent(ns, new HashMap<>()); + results.get(ns).put(name, value); + } + c.close(); + return results; + } + + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) + static String getPropName(String namespace, String name) { + return KEYNAME_PREFIX + namespace + NAMESPACE_NAME_SEPARATOR + name; + } + + interface FlagChangeCallback { + void onFlagChanged(String namespace, String name, String value); + } +} diff --git a/services/flags/java/com/android/server/flags/FlagsShellCommand.java b/services/flags/java/com/android/server/flags/FlagsShellCommand.java new file mode 100644 index 000000000000..b7896ee18714 --- /dev/null +++ b/services/flags/java/com/android/server/flags/FlagsShellCommand.java @@ -0,0 +1,214 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.flags; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.FastPrintWriter; + +import java.io.OutputStream; +import java.io.PrintWriter; +import java.util.Locale; +import java.util.Map; + +/** + * Process command line input for the flags service. + */ +@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) +public class FlagsShellCommand { + private final FlagOverrideStore mFlagStore; + + FlagsShellCommand(FlagOverrideStore flagStore) { + mFlagStore = flagStore; + } + + /** + * Interpret the command supplied in the constructor. + * + * @return Zero on success or non-zero on error. + */ + public int process( + String[] args, + OutputStream out, + OutputStream err) { + PrintWriter outPw = new FastPrintWriter(out); + PrintWriter errPw = new FastPrintWriter(err); + + if (args.length == 0) { + return printHelp(outPw); + } + switch (args[0].toLowerCase(Locale.ROOT)) { + case "help": + return printHelp(outPw); + case "list": + return listCmd(args, outPw, errPw); + case "set": + return setCmd(args, outPw, errPw); + case "get": + return getCmd(args, outPw, errPw); + case "erase": + return eraseCmd(args, outPw, errPw); + default: + return unknownCmd(outPw); + } + } + + private int printHelp(PrintWriter outPw) { + outPw.println("Feature Flags command, allowing listing, setting, getting, and erasing of"); + outPw.println("local flag overrides on a device."); + outPw.println(); + outPw.println("Commands:"); + outPw.println(" list [namespace]"); + outPw.println(" List all flag overrides. Namespace is optional."); + outPw.println(); + outPw.println(" get <namespace> <name>"); + outPw.println(" Return the string value of a specific flag, or <unset>"); + outPw.println(); + outPw.println(" set <namespace> <name> <value>"); + outPw.println(" Set a specific flag"); + outPw.println(); + outPw.println(" erase <namespace> <name>"); + outPw.println(" Unset a specific flag"); + outPw.flush(); + return 0; + } + + private int listCmd(String[] args, PrintWriter outPw, PrintWriter errPw) { + if (!validateNumArguments(args, 0, 1, args[0], errPw)) { + errPw.println("Expected `" + args[0] + " [namespace]`"); + errPw.flush(); + return -1; + } + Map<String, Map<String, String>> overrides; + if (args.length == 2) { + overrides = mFlagStore.getFlagsForNamespace(args[1]); + } else { + overrides = mFlagStore.getFlags(); + } + if (overrides.isEmpty()) { + outPw.println("No overrides set"); + } else { + int longestNamespaceLen = "namespace".length(); + int longestFlagLen = "flag".length(); + int longestValLen = "value".length(); + for (Map.Entry<String, Map<String, String>> namespace : overrides.entrySet()) { + longestNamespaceLen = Math.max(longestNamespaceLen, namespace.getKey().length()); + for (Map.Entry<String, String> flag : namespace.getValue().entrySet()) { + longestFlagLen = Math.max(longestFlagLen, flag.getKey().length()); + longestValLen = Math.max(longestValLen, flag.getValue().length()); + } + } + outPw.print(String.format("%-" + longestNamespaceLen + "s", "namespace")); + outPw.print(' '); + outPw.print(String.format("%-" + longestFlagLen + "s", "flag")); + outPw.print(' '); + outPw.println("value"); + for (int i = 0; i < longestNamespaceLen; i++) { + outPw.print('='); + } + outPw.print(' '); + for (int i = 0; i < longestFlagLen; i++) { + outPw.print('='); + } + outPw.print(' '); + for (int i = 0; i < longestValLen; i++) { + outPw.print('='); + } + outPw.println(); + for (Map.Entry<String, Map<String, String>> namespace : overrides.entrySet()) { + for (Map.Entry<String, String> flag : namespace.getValue().entrySet()) { + outPw.print( + String.format("%-" + longestNamespaceLen + "s", namespace.getKey())); + outPw.print(' '); + outPw.print(String.format("%-" + longestFlagLen + "s", flag.getKey())); + outPw.print(' '); + outPw.println(flag.getValue()); + } + } + } + outPw.flush(); + return 0; + } + + private int setCmd(String[] args, PrintWriter outPw, PrintWriter errPw) { + if (!validateNumArguments(args, 3, args[0], errPw)) { + errPw.println("Expected `" + args[0] + " <namespace> <name> <value>`"); + errPw.flush(); + return -1; + } + mFlagStore.set(args[1], args[2], args[3]); + outPw.println("Flag " + args[1] + "." + args[2] + " is now " + args[3]); + outPw.flush(); + return 0; + } + + private int getCmd(String[] args, PrintWriter outPw, PrintWriter errPw) { + if (!validateNumArguments(args, 2, args[0], errPw)) { + errPw.println("Expected `" + args[0] + " <namespace> <name>`"); + errPw.flush(); + return -1; + } + + String value = mFlagStore.get(args[1], args[2]); + outPw.print(args[1] + "." + args[2] + " is "); + if (value == null || value.isEmpty()) { + outPw.println("<unset>"); + } else { + outPw.println("\"" + value.translateEscapes() + "\""); + } + outPw.flush(); + return 0; + } + + private int eraseCmd(String[] args, PrintWriter outPw, PrintWriter errPw) { + if (!validateNumArguments(args, 2, args[0], errPw)) { + errPw.println("Expected `" + args[0] + " <namespace> <name>`"); + errPw.flush(); + return -1; + } + mFlagStore.erase(args[1], args[2]); + outPw.println("Erased " + args[1] + "." + args[2]); + return 0; + } + + private int unknownCmd(PrintWriter outPw) { + outPw.println("This command is unknown."); + printHelp(outPw); + outPw.flush(); + return -1; + } + + private boolean validateNumArguments( + String[] args, int exactly, String cmdName, PrintWriter errPw) { + return validateNumArguments(args, exactly, exactly, cmdName, errPw); + } + + private boolean validateNumArguments( + String[] args, int min, int max, String cmdName, PrintWriter errPw) { + int len = args.length - 1; // Discount the command itself. + if (len < min) { + errPw.println( + "Less than " + min + " arguments provided for \"" + cmdName + "\" command."); + return false; + } else if (len > max) { + errPw.println( + "More than " + max + " arguments provided for \"" + cmdName + "\" command."); + return false; + } + + return true; + } +} diff --git a/services/flags/java/com/android/server/flags/GlobalSettingsProxy.java b/services/flags/java/com/android/server/flags/GlobalSettingsProxy.java new file mode 100644 index 000000000000..acb7bb5a49db --- /dev/null +++ b/services/flags/java/com/android/server/flags/GlobalSettingsProxy.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.flags; + +import android.content.ContentResolver; +import android.net.Uri; +import android.provider.Settings; + +class GlobalSettingsProxy implements SettingsProxy { + private final ContentResolver mContentResolver; + + GlobalSettingsProxy(ContentResolver contentResolver) { + mContentResolver = contentResolver; + } + + @Override + public ContentResolver getContentResolver() { + return mContentResolver; + } + + @Override + public Uri getUriFor(String name) { + return Settings.Global.getUriFor(name); + } + + @Override + public String getStringForUser(String name, int userHandle) { + return Settings.Global.getStringForUser(mContentResolver, name, userHandle); + } + + @Override + public boolean putString(String name, String value, boolean overrideableByRestore) { + throw new UnsupportedOperationException( + "This method only exists publicly for Settings.System and Settings.Secure"); + } + + @Override + public boolean putStringForUser(String name, String value, int userHandle) { + return Settings.Global.putStringForUser(mContentResolver, name, value, userHandle); + } + + @Override + public boolean putStringForUser(String name, String value, String tag, boolean makeDefault, + int userHandle, boolean overrideableByRestore) { + return Settings.Global.putStringForUser( + mContentResolver, name, value, tag, makeDefault, userHandle, + overrideableByRestore); + } + + @Override + public boolean putString(String name, String value, String tag, boolean makeDefault) { + return Settings.Global.putString(mContentResolver, name, value, tag, makeDefault); + } +} diff --git a/services/flags/java/com/android/server/flags/SettingsProxy.java b/services/flags/java/com/android/server/flags/SettingsProxy.java new file mode 100644 index 000000000000..c6e85d5d1dc8 --- /dev/null +++ b/services/flags/java/com/android/server/flags/SettingsProxy.java @@ -0,0 +1,381 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.flags; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.UserIdInt; +import android.content.ContentResolver; +import android.database.ContentObserver; +import android.net.Uri; +import android.provider.Settings; + +/** + * Wrapper class meant to enable hermetic testing of {@link Settings}. + * + * Implementations of this class are expected to be constructed with a {@link ContentResolver} or, + * otherwise have access to an implicit one. All the proxy methods in this class exclude + * {@link ContentResolver} from their signature and rely on an internally defined one instead. + * + * Most methods in the {@link Settings} classes have default implementations defined. + * Implementations of this interfac need only concern themselves with getting and putting Strings. + * They should also override any methods for a class they are proxying that _are not_ defined, and + * throw an appropriate {@link UnsupportedOperationException}. For instance, {@link Settings.Global} + * does not define {@link #putString(String, String, boolean)}, so an implementation of this + * interface that proxies through to it should throw an exception when that method is called. + * + * This class adds in the following helpers as well: + * - {@link #getBool(String)} + * - {@link #putBool(String, boolean)} + * - {@link #registerContentObserver(Uri, ContentObserver)} + * + * ... and similar variations for all of those. + */ +public interface SettingsProxy { + + /** + * Returns the {@link ContentResolver} this instance uses. + */ + ContentResolver getContentResolver(); + + /** + * Construct the content URI for a particular name/value pair, + * useful for monitoring changes with a ContentObserver. + * @param name to look up in the table + * @return the corresponding content URI, or null if not present + */ + Uri getUriFor(String name); + + /**See {@link Settings.Secure#getString(ContentResolver, String)} */ + String getStringForUser(String name, int userHandle); + + /**See {@link Settings.Secure#putString(ContentResolver, String, String, boolean)} */ + boolean putString(String name, String value, boolean overrideableByRestore); + + /** See {@link Settings.Secure#putStringForUser(ContentResolver, String, String, int)} */ + boolean putStringForUser(String name, String value, int userHandle); + + /** + * See {@link Settings.Secure#putStringForUser(ContentResolver, String, String, String, boolean, + * int, boolean)} + */ + boolean putStringForUser(@NonNull String name, @Nullable String value, @Nullable String tag, + boolean makeDefault, @UserIdInt int userHandle, boolean overrideableByRestore); + + /** See {@link Settings.Secure#putString(ContentResolver, String, String, String, boolean)} */ + boolean putString(@NonNull String name, @Nullable String value, @Nullable String tag, + boolean makeDefault); + + /** + * Returns the user id for the associated {@link ContentResolver}. + */ + default int getUserId() { + return getContentResolver().getUserId(); + } + + /** See {@link Settings.Secure#getString(ContentResolver, String)} */ + default String getString(String name) { + return getStringForUser(name, getUserId()); + } + + /** See {@link Settings.Secure#putString(ContentResolver, String, String)} */ + default boolean putString(String name, String value) { + return putStringForUser(name, value, getUserId()); + } + /** See {@link Settings.Secure#getIntForUser(ContentResolver, String, int, int)} */ + default int getIntForUser(String name, int def, int userHandle) { + String v = getStringForUser(name, userHandle); + try { + return v != null ? Integer.parseInt(v) : def; + } catch (NumberFormatException e) { + return def; + } + } + + /** See {@link Settings.Secure#getInt(ContentResolver, String)} */ + default int getInt(String name) throws Settings.SettingNotFoundException { + return getIntForUser(name, getUserId()); + } + + /** See {@link Settings.Secure#getIntForUser(ContentResolver, String, int)} */ + default int getIntForUser(String name, int userHandle) + throws Settings.SettingNotFoundException { + String v = getStringForUser(name, userHandle); + try { + return Integer.parseInt(v); + } catch (NumberFormatException e) { + throw new Settings.SettingNotFoundException(name); + } + } + + /** See {@link Settings.Secure#putInt(ContentResolver, String, int)} */ + default boolean putInt(String name, int value) { + return putIntForUser(name, value, getUserId()); + } + + /** See {@link Settings.Secure#putIntForUser(ContentResolver, String, int, int)} */ + default boolean putIntForUser(String name, int value, int userHandle) { + return putStringForUser(name, Integer.toString(value), userHandle); + } + + /** + * Convenience function for retrieving a single settings value + * as a boolean. Note that internally setting values are always + * stored as strings; this function converts the string to a boolean + * for you. The default value will be returned if the setting is + * not defined or not a boolean. + * + * @param name The name of the setting to retrieve. + * @param def Value to return if the setting is not defined. + * + * @return The setting's current value, or 'def' if it is not defined + * or not a valid boolean. + */ + default boolean getBool(String name, boolean def) { + return getBoolForUser(name, def, getUserId()); + } + + /** See {@link #getBool(String, boolean)}. */ + default boolean getBoolForUser(String name, boolean def, int userHandle) { + return getIntForUser(name, def ? 1 : 0, userHandle) != 0; + } + + /** + * Convenience function for retrieving a single settings value + * as a boolean. Note that internally setting values are always + * stored as strings; this function converts the string to a boolean + * for you. + * <p> + * This version does not take a default value. If the setting has not + * been set, or the string value is not a number, + * it throws {@link Settings.SettingNotFoundException}. + * + * @param name The name of the setting to retrieve. + * + * @throws Settings.SettingNotFoundException Thrown if a setting by the given + * name can't be found or the setting value is not a boolean. + * + * @return The setting's current value. + */ + default boolean getBool(String name) throws Settings.SettingNotFoundException { + return getBoolForUser(name, getUserId()); + } + + /** See {@link #getBool(String)}. */ + default boolean getBoolForUser(String name, int userHandle) + throws Settings.SettingNotFoundException { + return getIntForUser(name, userHandle) != 0; + } + + /** + * Convenience function for updating a single settings value as a + * boolean. This will either create a new entry in the table if the + * given name does not exist, or modify the value of the existing row + * with that name. Note that internally setting values are always + * stored as strings, so this function converts the given value to a + * string before storing it. + * + * @param name The name of the setting to modify. + * @param value The new value for the setting. + * @return true if the value was set, false on database errors + */ + default boolean putBool(String name, boolean value) { + return putBoolForUser(name, value, getUserId()); + } + + /** See {@link #putBool(String, boolean)}. */ + default boolean putBoolForUser(String name, boolean value, int userHandle) { + return putIntForUser(name, value ? 1 : 0, userHandle); + } + + /** See {@link Settings.Secure#getLong(ContentResolver, String, long)} */ + default long getLong(String name, long def) { + return getLongForUser(name, def, getUserId()); + } + + /** See {@link Settings.Secure#getLongForUser(ContentResolver, String, long, int)} */ + default long getLongForUser(String name, long def, int userHandle) { + String valString = getStringForUser(name, userHandle); + long value; + try { + value = valString != null ? Long.parseLong(valString) : def; + } catch (NumberFormatException e) { + value = def; + } + return value; + } + + /** See {@link Settings.Secure#getLong(ContentResolver, String)} */ + default long getLong(String name) throws Settings.SettingNotFoundException { + return getLongForUser(name, getUserId()); + } + + /** See {@link Settings.Secure#getLongForUser(ContentResolver, String, int)} */ + default long getLongForUser(String name, int userHandle) + throws Settings.SettingNotFoundException { + String valString = getStringForUser(name, userHandle); + try { + return Long.parseLong(valString); + } catch (NumberFormatException e) { + throw new Settings.SettingNotFoundException(name); + } + } + + /** See {@link Settings.Secure#putLong(ContentResolver, String, long)} */ + default boolean putLong(String name, long value) { + return putLongForUser(name, value, getUserId()); + } + + /** See {@link Settings.Secure#putLongForUser(ContentResolver, String, long, int)} */ + default boolean putLongForUser(String name, long value, int userHandle) { + return putStringForUser(name, Long.toString(value), userHandle); + } + + /** See {@link Settings.Secure#getFloat(ContentResolver, String, float)} */ + default float getFloat(String name, float def) { + return getFloatForUser(name, def, getUserId()); + } + + /** See {@link Settings.Secure#getFloatForUser(ContentResolver, String, int)} */ + default float getFloatForUser(String name, float def, int userHandle) { + String v = getStringForUser(name, userHandle); + try { + return v != null ? Float.parseFloat(v) : def; + } catch (NumberFormatException e) { + return def; + } + } + + + /** See {@link Settings.Secure#getFloat(ContentResolver, String)} */ + default float getFloat(String name) throws Settings.SettingNotFoundException { + return getFloatForUser(name, getUserId()); + } + + /** See {@link Settings.Secure#getFloatForUser(ContentResolver, String, int)} */ + default float getFloatForUser(String name, int userHandle) + throws Settings.SettingNotFoundException { + String v = getStringForUser(name, userHandle); + if (v == null) { + throw new Settings.SettingNotFoundException(name); + } + try { + return Float.parseFloat(v); + } catch (NumberFormatException e) { + throw new Settings.SettingNotFoundException(name); + } + } + + /** See {@link Settings.Secure#putFloat(ContentResolver, String, float)} */ + default boolean putFloat(String name, float value) { + return putFloatForUser(name, value, getUserId()); + } + + /** See {@link Settings.Secure#putFloatForUser(ContentResolver, String, float, int)} */ + default boolean putFloatForUser(String name, float value, int userHandle) { + return putStringForUser(name, Float.toString(value), userHandle); + } + + /** + * Convenience wrapper around + * {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver)}.' + * + * Implicitly calls {@link #getUriFor(String)} on the passed in name. + */ + default void registerContentObserver(String name, ContentObserver settingsObserver) { + registerContentObserver(getUriFor(name), settingsObserver); + } + + /** + * Convenience wrapper around + * {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver)}.' + */ + default void registerContentObserver(Uri uri, ContentObserver settingsObserver) { + registerContentObserverForUser(uri, settingsObserver, getUserId()); + } + + /** + * Convenience wrapper around + * {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver)}. + * + * Implicitly calls {@link #getUriFor(String)} on the passed in name. + */ + default void registerContentObserver(String name, boolean notifyForDescendants, + ContentObserver settingsObserver) { + registerContentObserver(getUriFor(name), notifyForDescendants, settingsObserver); + } + + /** + * Convenience wrapper around + * {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver)}.' + */ + default void registerContentObserver(Uri uri, boolean notifyForDescendants, + ContentObserver settingsObserver) { + registerContentObserverForUser(uri, notifyForDescendants, settingsObserver, getUserId()); + } + + /** + * Convenience wrapper around + * {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver, int)} + * + * Implicitly calls {@link #getUriFor(String)} on the passed in name. + */ + default void registerContentObserverForUser( + String name, ContentObserver settingsObserver, int userHandle) { + registerContentObserverForUser( + getUriFor(name), settingsObserver, userHandle); + } + + /** + * Convenience wrapper around + * {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver, int)} + */ + default void registerContentObserverForUser( + Uri uri, ContentObserver settingsObserver, int userHandle) { + registerContentObserverForUser( + uri, false, settingsObserver, userHandle); + } + + /** + * Convenience wrapper around + * {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver, int)} + * + * Implicitly calls {@link #getUriFor(String)} on the passed in name. + */ + default void registerContentObserverForUser( + String name, boolean notifyForDescendants, ContentObserver settingsObserver, + int userHandle) { + registerContentObserverForUser( + getUriFor(name), notifyForDescendants, settingsObserver, userHandle); + } + + /** + * Convenience wrapper around + * {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver, int)} + */ + default void registerContentObserverForUser( + Uri uri, boolean notifyForDescendants, ContentObserver settingsObserver, + int userHandle) { + getContentResolver().registerContentObserver( + uri, notifyForDescendants, settingsObserver, userHandle); + } + + /** See {@link ContentResolver#unregisterContentObserver(ContentObserver)}. */ + default void unregisterContentObserver(ContentObserver settingsObserver) { + getContentResolver().unregisterContentObserver(settingsObserver); + } +} diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 39d33f0b6dd7..ee4bc1237bcb 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -133,6 +133,7 @@ import com.android.server.display.DisplayManagerService; import com.android.server.display.color.ColorDisplayService; import com.android.server.dreams.DreamManagerService; import com.android.server.emergency.EmergencyAffordanceService; +import com.android.server.flags.FeatureFlagsService; import com.android.server.gpu.GpuService; import com.android.server.grammaticalinflection.GrammaticalInflectionService; import com.android.server.graphics.fonts.FontManagerService; @@ -1117,6 +1118,12 @@ public final class SystemServer implements Dumpable { mSystemServiceManager.startService(DeviceIdentifiersPolicyService.class); t.traceEnd(); + // Starts a service for reading runtime flag overrides, and keeping processes + // in sync with one another. + t.traceBegin("StartFeatureFlagsService"); + mSystemServiceManager.startService(FeatureFlagsService.class); + t.traceEnd(); + // Uri Grants Manager. t.traceBegin("UriGrantsManagerService"); mSystemServiceManager.startService(UriGrantsManagerService.Lifecycle.class); diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java index 73eb237fa9e7..72c5333e0a02 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java @@ -125,6 +125,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.function.UnaryOperator; @@ -707,6 +708,9 @@ public class BroadcastQueueTest { private void waitForIdle() throws Exception { mLooper.release(); mQueue.waitForIdle(LOG_WRITER_INFO); + final CountDownLatch latch = new CountDownLatch(1); + mHandlerThread.getThreadHandler().post(latch::countDown); + latch.await(); mLooper = Objects.requireNonNull(InstrumentationRegistry.getInstrumentation() .acquireLooperManager(mHandlerThread.getLooper())); } @@ -2342,6 +2346,7 @@ public class BroadcastQueueTest { mUidObserver.onUidStateChanged(receiverGreenApp.info.uid, ActivityManager.PROCESS_STATE_TOP, 0, ActivityManager.PROCESS_CAPABILITY_NONE); + waitForIdle(); final Intent airplane = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED); final Intent timeTick = new Intent(Intent.ACTION_TIME_TICK); @@ -2375,6 +2380,7 @@ public class BroadcastQueueTest { mUidObserver.onUidStateChanged(receiverGreenApp.info.uid, ActivityManager.PROCESS_STATE_TOP, 0, ActivityManager.PROCESS_CAPABILITY_NONE); + waitForIdle(); final Intent timeTick = new Intent(Intent.ACTION_TIME_TICK); diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/PackageMonitorCallbackHelperTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/PackageMonitorCallbackHelperTest.java index 3ac59e9f0efe..f20633342759 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/PackageMonitorCallbackHelperTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/PackageMonitorCallbackHelperTest.java @@ -33,6 +33,8 @@ import android.os.Bundle; import android.os.Handler; import android.os.IRemoteCallback; import android.os.Looper; +import android.os.Process; +import android.util.SparseArray; import org.junit.After; import org.junit.Before; @@ -244,6 +246,54 @@ public class PackageMonitorCallbackHelperTest { verify(callback, after(WAIT_CALLBACK_CALLED_IN_MS).never()).sendResult(any()); } + @Test + public void testRegisterPackageMonitorCallbackInAllowList_callbackShouldCalled() + throws Exception { + IRemoteCallback callback = createMockPackageMonitorCallback(); + SparseArray<int[]> broadcastAllowList = new SparseArray<>(); + broadcastAllowList.put(0, new int[] {Binder.getCallingUid()}); + + mPackageMonitorCallbackHelper.registerPackageMonitorCallback(callback, 0 /* userId */, + Binder.getCallingUid()); + mPackageMonitorCallbackHelper.notifyPackageMonitor(Intent.ACTION_PACKAGE_ADDED, + FAKE_PACKAGE_NAME, createFakeBundle(), new int[]{0} /* userIds */, + null /* instantUserIds */, broadcastAllowList); + + verify(callback, after(WAIT_CALLBACK_CALLED_IN_MS).times(1)).sendResult(any()); + } + + @Test + public void testRegisterPackageMonitorCallbackNotInAllowList_callbackShouldNotCalled() + throws Exception { + IRemoteCallback callback = createMockPackageMonitorCallback(); + SparseArray<int[]> broadcastAllowList = new SparseArray<>(); + broadcastAllowList.put(0, new int[] {12345}); + + mPackageMonitorCallbackHelper.registerPackageMonitorCallback(callback, 0 /* userId */, + Binder.getCallingUid()); + mPackageMonitorCallbackHelper.notifyPackageMonitor(Intent.ACTION_PACKAGE_ADDED, + FAKE_PACKAGE_NAME, createFakeBundle(), new int[]{0} /* userIds */, + null /* instantUserIds */, broadcastAllowList); + + verify(callback, after(WAIT_CALLBACK_CALLED_IN_MS).never()).sendResult(any()); + } + + @Test + public void testRegisterPackageMonitorCallbackNotInAllowListSystemUid_callbackShouldCalled() + throws Exception { + IRemoteCallback callback = createMockPackageMonitorCallback(); + SparseArray<int[]> broadcastAllowList = new SparseArray<>(); + broadcastAllowList.put(0, new int[] {12345}); + + mPackageMonitorCallbackHelper.registerPackageMonitorCallback(callback, 0 /* userId */, + Process.SYSTEM_UID); + mPackageMonitorCallbackHelper.notifyPackageMonitor(Intent.ACTION_PACKAGE_ADDED, + FAKE_PACKAGE_NAME, createFakeBundle(), new int[]{0} /* userIds */, + null /* instantUserIds */, broadcastAllowList); + + verify(callback, after(WAIT_CALLBACK_CALLED_IN_MS).times(1)).sendResult(any()); + } + private IRemoteCallback createMockPackageMonitorCallback() { return spy(new IRemoteCallback.Stub() { @Override diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp index 4edb167a54c7..173c5f5dd40f 100644 --- a/services/tests/servicestests/Android.bp +++ b/services/tests/servicestests/Android.bp @@ -34,6 +34,7 @@ android_test { "services.core", "services.credentials", "services.devicepolicy", + "services.flags", "services.net", "services.people", "services.usage", diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java index 2d4bf144155c..32d0c98d4481 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java @@ -44,8 +44,10 @@ import android.annotation.NonNull; import android.graphics.PointF; import android.os.Handler; import android.os.Message; +import android.os.UserHandle; import android.os.VibrationEffect; import android.os.Vibrator; +import android.provider.Settings; import android.testing.TestableContext; import android.util.DebugUtils; import android.view.InputDevice; @@ -140,8 +142,6 @@ public class FullScreenMagnificationGestureHandlerTest { @Mock WindowMagnificationPromptController mWindowMagnificationPromptController; @Mock - AccessibilityManagerService mMockAccessibilityManagerService; - @Mock AccessibilityTraceManager mMockTraceManager; @Rule @@ -153,6 +153,8 @@ public class FullScreenMagnificationGestureHandlerTest { private long mLastDownTime = Integer.MIN_VALUE; + private float mOriginalMagnificationPersistedScale; + @Before public void setUp() { MockitoAnnotations.initMocks(this); @@ -166,6 +168,13 @@ public class FullScreenMagnificationGestureHandlerTest { when(mockController.newValueAnimator()).thenReturn(new ValueAnimator()); when(mockController.getAnimationDuration()).thenReturn(1000L); when(mockWindowManager.setMagnificationCallbacks(eq(DISPLAY_0), any())).thenReturn(true); + mOriginalMagnificationPersistedScale = Settings.Secure.getFloatForUser( + mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, 2.0f, + UserHandle.USER_SYSTEM); + Settings.Secure.putFloatForUser(mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, 2.0f, + UserHandle.USER_SYSTEM); mFullScreenMagnificationController = new FullScreenMagnificationController( mockController, new Object(), @@ -192,6 +201,10 @@ public class FullScreenMagnificationGestureHandlerTest { mMgh.onDestroy(); mFullScreenMagnificationController.unregister(DISPLAY_0); verify(mWindowMagnificationPromptController).onDestroy(); + Settings.Secure.putFloatForUser(mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, + mOriginalMagnificationPersistedScale, + UserHandle.USER_SYSTEM); } @NonNull @@ -525,10 +538,9 @@ public class FullScreenMagnificationGestureHandlerTest { final float threshold = FullScreenMagnificationGestureHandler.PanningScalingState .CHECK_DETECTING_PASS_PERSISTED_SCALE_THRESHOLD; final float persistedScale = (1.0f + threshold) * scale + 1.0f; - mFullScreenMagnificationController.setScale(DISPLAY_0, persistedScale, DEFAULT_X, - DEFAULT_Y, /* animate= */ false, - AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID); - mFullScreenMagnificationController.persistScale(DISPLAY_0); + Settings.Secure.putFloatForUser(mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, persistedScale, + UserHandle.USER_SYSTEM); mFullScreenMagnificationController.setScale(DISPLAY_0, scale, DEFAULT_X, DEFAULT_Y, /* animate= */ false, AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID); @@ -547,10 +559,9 @@ public class FullScreenMagnificationGestureHandlerTest { final float threshold = FullScreenMagnificationGestureHandler.PanningScalingState .CHECK_DETECTING_PASS_PERSISTED_SCALE_THRESHOLD; final float persistedScale = (1.0f + threshold) * scale - 0.1f; - mFullScreenMagnificationController.setScale(DISPLAY_0, persistedScale, DEFAULT_X, - DEFAULT_Y, /* animate= */ false, - AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID); - mFullScreenMagnificationController.persistScale(DISPLAY_0); + Settings.Secure.putFloatForUser(mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, persistedScale, + UserHandle.USER_SYSTEM); mFullScreenMagnificationController.setScale(DISPLAY_0, scale, DEFAULT_X, DEFAULT_Y, /* animate= */ false, AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID); diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationManagerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationManagerTest.java index 27e6ef199fe2..bbbab2129d22 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationManagerTest.java @@ -66,7 +66,6 @@ import com.android.server.accessibility.AccessibilityTraceManager; import com.android.server.statusbar.StatusBarManagerInternal; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.mockito.Mock; import org.mockito.Mockito; @@ -308,7 +307,6 @@ public class WindowMagnificationManagerTest { MagnificationScaleProvider.MAX_SCALE); } - @Ignore("b/278816260: We could refer to b/182561174#comment4 for solution.") @Test public void logTrackingTypingFocus_processScroll_logDuration() { WindowMagnificationManager spyWindowMagnificationManager = spy(mWindowMagnificationManager); diff --git a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java index 68217219e453..fc62e75b7d59 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java @@ -16,6 +16,7 @@ package com.android.server.biometrics; +import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE; import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT; import static android.hardware.biometrics.BiometricManager.Authenticators; import static android.view.DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS; @@ -113,6 +114,10 @@ public class BiometricServiceTest { private static final String ERROR_UNABLE_TO_PROCESS = "error_unable_to_process"; private static final String ERROR_USER_CANCELED = "error_user_canceled"; private static final String ERROR_LOCKOUT = "error_lockout"; + private static final String FACE_SUBTITLE = "face_subtitle"; + private static final String FINGERPRINT_SUBTITLE = "fingerprint_subtitle"; + private static final String DEFAULT_SUBTITLE = "default_subtitle"; + private static final String FINGERPRINT_ACQUIRED_SENSOR_DIRTY = "sensor_dirty"; @@ -191,6 +196,12 @@ public class BiometricServiceTest { .thenReturn(ERROR_NOT_RECOGNIZED); when(mResources.getString(R.string.biometric_error_user_canceled)) .thenReturn(ERROR_USER_CANCELED); + when(mContext.getString(R.string.biometric_dialog_face_subtitle)) + .thenReturn(FACE_SUBTITLE); + when(mContext.getString(R.string.biometric_dialog_fingerprint_subtitle)) + .thenReturn(FINGERPRINT_SUBTITLE); + when(mContext.getString(R.string.biometric_dialog_default_subtitle)) + .thenReturn(DEFAULT_SUBTITLE); when(mWindowManager.getDefaultDisplay()).thenReturn( new Display(DisplayManagerGlobal.getInstance(), Display.DEFAULT_DISPLAY, @@ -211,7 +222,7 @@ public class BiometricServiceTest { @Test public void testClientBinderDied_whenPaused() throws Exception { - setupAuthForOnly(BiometricAuthenticator.TYPE_FACE, Authenticators.BIOMETRIC_STRONG); + setupAuthForOnly(TYPE_FACE, Authenticators.BIOMETRIC_STRONG); invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, true /* requireConfirmation */, null /* authenticators */); @@ -238,7 +249,7 @@ public class BiometricServiceTest { @Test public void testClientBinderDied_whenAuthenticating() throws Exception { - setupAuthForOnly(BiometricAuthenticator.TYPE_FACE, Authenticators.BIOMETRIC_STRONG); + setupAuthForOnly(TYPE_FACE, Authenticators.BIOMETRIC_STRONG); invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, true /* requireConfirmation */, null /* authenticators */); @@ -374,7 +385,7 @@ public class BiometricServiceTest { final int[] modalities = new int[] { TYPE_FINGERPRINT, - BiometricAuthenticator.TYPE_FACE, + TYPE_FACE, }; final int[] strengths = new int[] { @@ -427,9 +438,56 @@ public class BiometricServiceTest { } @Test + public void testAuthenticateFace_shouldShowSubtitleForFace() throws Exception { + setupAuthForOnly(TYPE_FACE, Authenticators.BIOMETRIC_STRONG); + + invokeAuthenticate(mBiometricService.mImpl, mReceiver1, + false /* requireConfirmation */, + null); + waitForIdle(); + + assertEquals(FACE_SUBTITLE, mBiometricService.mAuthSession.mPromptInfo.getSubtitle()); + } + + @Test + public void testAuthenticateFingerprint_shouldShowSubtitleForFingerprint() throws Exception { + setupAuthForOnly(TYPE_FINGERPRINT, Authenticators.BIOMETRIC_STRONG); + + invokeAuthenticate(mBiometricService.mImpl, mReceiver1, + false /* requireConfirmation */, + null); + waitForIdle(); + + assertEquals(FINGERPRINT_SUBTITLE, + mBiometricService.mAuthSession.mPromptInfo.getSubtitle()); + } + + @Test + public void testAuthenticateBothFpAndFace_shouldShowDefaultSubtitle() throws Exception { + final int[] modalities = new int[] { + TYPE_FINGERPRINT, + TYPE_FACE, + }; + + final int[] strengths = new int[] { + Authenticators.BIOMETRIC_WEAK, + Authenticators.BIOMETRIC_STRONG, + }; + + setupAuthForMultiple(modalities, strengths); + + invokeAuthenticate(mBiometricService.mImpl, mReceiver1, + false /* requireConfirmation */, + null); + waitForIdle(); + + assertEquals(DEFAULT_SUBTITLE, mBiometricService.mAuthSession.mPromptInfo.getSubtitle()); + } + + @Test public void testAuthenticateFace_respectsUserSetting() throws Exception { - setupAuthForOnly(BiometricAuthenticator.TYPE_FACE, Authenticators.BIOMETRIC_STRONG); + setupAuthForOnly(TYPE_FACE, Authenticators.BIOMETRIC_STRONG); // Disabled in user settings receives onError when(mBiometricService.mSettingObserver.getEnabledForApps(anyInt())).thenReturn(false); @@ -568,7 +626,7 @@ public class BiometricServiceTest { @Test public void testAuthenticate_noBiometrics_credentialAllowed() throws Exception { - setupAuthForOnly(BiometricAuthenticator.TYPE_FACE, Authenticators.BIOMETRIC_STRONG); + setupAuthForOnly(TYPE_FACE, Authenticators.BIOMETRIC_STRONG); when(mFaceAuthenticator.hasEnrolledTemplates(anyInt(), any())).thenReturn(false); when(mTrustManager.isDeviceSecure(anyInt(), anyInt())) .thenReturn(true); @@ -595,13 +653,13 @@ public class BiometricServiceTest { @Test public void testAuthenticate_happyPathWithConfirmation_strongBiometric() throws Exception { - setupAuthForOnly(BiometricAuthenticator.TYPE_FACE, Authenticators.BIOMETRIC_STRONG); + setupAuthForOnly(TYPE_FACE, Authenticators.BIOMETRIC_STRONG); testAuthenticate_happyPathWithConfirmation(true /* isStrongBiometric */); } @Test public void testAuthenticate_happyPathWithConfirmation_weakBiometric() throws Exception { - setupAuthForOnly(BiometricAuthenticator.TYPE_FACE, Authenticators.BIOMETRIC_WEAK); + setupAuthForOnly(TYPE_FACE, Authenticators.BIOMETRIC_WEAK); testAuthenticate_happyPathWithConfirmation(false /* isStrongBiometric */); } @@ -637,7 +695,7 @@ public class BiometricServiceTest { @Test public void testAuthenticate_no_Biometrics_noCredential() throws Exception { - setupAuthForOnly(BiometricAuthenticator.TYPE_FACE, Authenticators.BIOMETRIC_STRONG); + setupAuthForOnly(TYPE_FACE, Authenticators.BIOMETRIC_STRONG); when(mFaceAuthenticator.hasEnrolledTemplates(anyInt(), any())).thenReturn(false); when(mTrustManager.isDeviceSecure(anyInt(), anyInt())) .thenReturn(false); @@ -655,7 +713,7 @@ public class BiometricServiceTest { @Test public void testRejectFace_whenAuthenticating_notifiesSystemUIAndClient_thenPaused() throws Exception { - setupAuthForOnly(BiometricAuthenticator.TYPE_FACE, Authenticators.BIOMETRIC_STRONG); + setupAuthForOnly(TYPE_FACE, Authenticators.BIOMETRIC_STRONG); invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */, null /* authenticators */); @@ -663,7 +721,7 @@ public class BiometricServiceTest { waitForIdle(); verify(mBiometricService.mStatusBarService).onBiometricError( - eq(BiometricAuthenticator.TYPE_FACE), + eq(TYPE_FACE), eq(BiometricConstants.BIOMETRIC_PAUSED_REJECTED), eq(0 /* vendorCode */)); verify(mReceiver1).onAuthenticationFailed(); @@ -691,7 +749,7 @@ public class BiometricServiceTest { @Test public void testRequestAuthentication_whenAlreadyAuthenticating() throws Exception { - setupAuthForOnly(BiometricAuthenticator.TYPE_FACE, Authenticators.BIOMETRIC_STRONG); + setupAuthForOnly(TYPE_FACE, Authenticators.BIOMETRIC_STRONG); invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */, null /* authenticators */); @@ -700,7 +758,7 @@ public class BiometricServiceTest { waitForIdle(); verify(mReceiver1).onError( - eq(BiometricAuthenticator.TYPE_FACE), + eq(TYPE_FACE), eq(BiometricPrompt.BIOMETRIC_ERROR_CANCELED), eq(0) /* vendorCode */); verify(mBiometricService.mStatusBarService).hideAuthenticationDialog(eq(TEST_REQUEST_ID)); @@ -710,7 +768,7 @@ public class BiometricServiceTest { @Test public void testErrorHalTimeout_whenAuthenticating_entersPausedState() throws Exception { - setupAuthForOnly(BiometricAuthenticator.TYPE_FACE, Authenticators.BIOMETRIC_STRONG); + setupAuthForOnly(TYPE_FACE, Authenticators.BIOMETRIC_STRONG); invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */, null /* authenticators */); @@ -723,7 +781,7 @@ public class BiometricServiceTest { assertEquals(STATE_AUTH_PAUSED, mBiometricService.mAuthSession.getState()); verify(mBiometricService.mStatusBarService).onBiometricError( - eq(BiometricAuthenticator.TYPE_FACE), + eq(TYPE_FACE), eq(BiometricConstants.BIOMETRIC_ERROR_TIMEOUT), eq(0 /* vendorCode */)); // Timeout does not count as fail as per BiometricPrompt documentation. @@ -759,7 +817,7 @@ public class BiometricServiceTest { @Test public void testErrorFromHal_whenPaused_notifiesSystemUIAndClient() throws Exception { - setupAuthForOnly(BiometricAuthenticator.TYPE_FACE, Authenticators.BIOMETRIC_STRONG); + setupAuthForOnly(TYPE_FACE, Authenticators.BIOMETRIC_STRONG); invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */, null /* authenticators */); @@ -777,7 +835,7 @@ public class BiometricServiceTest { // Client receives error immediately verify(mReceiver1).onError( - eq(BiometricAuthenticator.TYPE_FACE), + eq(TYPE_FACE), eq(BiometricConstants.BIOMETRIC_ERROR_CANCELED), eq(0 /* vendorCode */)); // Dialog is hidden immediately @@ -926,7 +984,7 @@ public class BiometricServiceTest { int biometricPromptError) throws Exception { final int[] modalities = new int[] { TYPE_FINGERPRINT, - BiometricAuthenticator.TYPE_FACE, + TYPE_FACE, }; final int[] strengths = new int[] { @@ -1123,7 +1181,7 @@ public class BiometricServiceTest { @Test public void testDismissedReasonNegative_whilePaused_invokeHalCancel() throws Exception { - setupAuthForOnly(BiometricAuthenticator.TYPE_FACE, Authenticators.BIOMETRIC_STRONG); + setupAuthForOnly(TYPE_FACE, Authenticators.BIOMETRIC_STRONG); invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */, null /* authenticators */); @@ -1142,7 +1200,7 @@ public class BiometricServiceTest { @Test public void testDismissedReasonUserCancel_whilePaused_invokesHalCancel() throws Exception { - setupAuthForOnly(BiometricAuthenticator.TYPE_FACE, Authenticators.BIOMETRIC_STRONG); + setupAuthForOnly(TYPE_FACE, Authenticators.BIOMETRIC_STRONG); invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, false /* requireConfirmation */, null /* authenticators */); @@ -1161,7 +1219,7 @@ public class BiometricServiceTest { @Test public void testDismissedReasonUserCancel_whenPendingConfirmation() throws Exception { - setupAuthForOnly(BiometricAuthenticator.TYPE_FACE, Authenticators.BIOMETRIC_STRONG); + setupAuthForOnly(TYPE_FACE, Authenticators.BIOMETRIC_STRONG); invokeAuthenticateAndStart(mBiometricService.mImpl, mReceiver1, true /* requireConfirmation */, null /* authenticators */); @@ -1175,7 +1233,7 @@ public class BiometricServiceTest { verify(mBiometricService.mSensors.get(0).impl) .cancelAuthenticationFromService(any(), any(), anyLong()); verify(mReceiver1).onError( - eq(BiometricAuthenticator.TYPE_FACE), + eq(TYPE_FACE), eq(BiometricConstants.BIOMETRIC_ERROR_USER_CANCELED), eq(0 /* vendorCode */)); verify(mBiometricService.mKeyStore, never()).addAuthToken(any(byte[].class)); @@ -1296,7 +1354,7 @@ public class BiometricServiceTest { @Test public void testCanAuthenticate_whenBiometricsNotEnabledForApps() throws Exception { - setupAuthForOnly(BiometricAuthenticator.TYPE_FACE, Authenticators.BIOMETRIC_STRONG); + setupAuthForOnly(TYPE_FACE, Authenticators.BIOMETRIC_STRONG); when(mBiometricService.mSettingObserver.getEnabledForApps(anyInt())).thenReturn(false); when(mTrustManager.isDeviceSecure(anyInt(), anyInt())) .thenReturn(true); @@ -1590,7 +1648,7 @@ public class BiometricServiceTest { @Test public void testWorkAuthentication_faceWorksIfNotDisabledByDevicePolicyManager() throws Exception { - setupAuthForOnly(BiometricAuthenticator.TYPE_FACE, Authenticators.BIOMETRIC_STRONG); + setupAuthForOnly(TYPE_FACE, Authenticators.BIOMETRIC_STRONG); when(mDevicePolicyManager .getKeyguardDisabledFeatures(any() /* admin*/, anyInt() /* userHandle */)) .thenReturn(~DevicePolicyManager.KEYGUARD_DISABLE_FACE); @@ -1683,7 +1741,7 @@ public class BiometricServiceTest { mFingerprintAuthenticator); } - if ((modality & BiometricAuthenticator.TYPE_FACE) != 0) { + if ((modality & TYPE_FACE) != 0) { when(mFaceAuthenticator.hasEnrolledTemplates(anyInt(), any())).thenReturn(enrolled); when(mFaceAuthenticator.isHardwareDetected(any())).thenReturn(true); when(mFaceAuthenticator.getLockoutModeForUser(anyInt())) @@ -1715,7 +1773,7 @@ public class BiometricServiceTest { strength, mFingerprintAuthenticator); } - if ((modality & BiometricAuthenticator.TYPE_FACE) != 0) { + if ((modality & TYPE_FACE) != 0) { when(mFaceAuthenticator.hasEnrolledTemplates(anyInt(), any())).thenReturn(true); when(mFaceAuthenticator.isHardwareDetected(any())).thenReturn(true); mBiometricService.mImpl.registerAuthenticator(SENSOR_ID_FACE, modality, @@ -1798,6 +1856,7 @@ public class BiometricServiceTest { boolean checkDevicePolicy) { final PromptInfo promptInfo = new PromptInfo(); promptInfo.setConfirmationRequested(requireConfirmation); + promptInfo.setUseDefaultSubtitle(true); if (authenticators != null) { promptInfo.setAuthenticators(authenticators); diff --git a/services/tests/servicestests/src/com/android/server/flags/FeatureFlagsServiceTest.java b/services/tests/servicestests/src/com/android/server/flags/FeatureFlagsServiceTest.java new file mode 100644 index 000000000000..df4731fb0bb7 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/flags/FeatureFlagsServiceTest.java @@ -0,0 +1,293 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.flags; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; + +import static org.junit.Assert.fail; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.flags.IFeatureFlagsCallback; +import android.flags.SyncableFlag; +import android.os.IBinder; +import android.os.RemoteException; +import android.platform.test.annotations.Presubmit; + +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.util.List; + +@Presubmit +@SmallTest +public class FeatureFlagsServiceTest { + private static final String NS = "ns"; + private static final String NAME = "name"; + private static final String PROP_NAME = FlagOverrideStore.getPropName(NS, NAME); + + @Rule + public final MockitoRule mockito = MockitoJUnit.rule(); + + @Mock + private FlagOverrideStore mFlagStore; + @Mock + private FlagsShellCommand mFlagCommand; + @Mock + private IFeatureFlagsCallback mIFeatureFlagsCallback; + @Mock + private IBinder mIFeatureFlagsCallbackAsBinder; + @Mock + private FeatureFlagsService.PermissionsChecker mPermissionsChecker; + + private FeatureFlagsBinder mFeatureFlagsService; + + @Before + public void setup() { + when(mIFeatureFlagsCallback.asBinder()).thenReturn(mIFeatureFlagsCallbackAsBinder); + mFeatureFlagsService = new FeatureFlagsBinder( + mFlagStore, mFlagCommand, mPermissionsChecker); + } + + @Test + public void testRegisterCallback() { + mFeatureFlagsService.registerCallback(mIFeatureFlagsCallback); + try { + verify(mIFeatureFlagsCallbackAsBinder).linkToDeath(any(), eq(0)); + } catch (RemoteException e) { + fail("Our mock threw a Remote Exception?"); + } + } + + @Test + public void testOverrideFlag_requiresWritePermission() { + SecurityException exc = new SecurityException("not allowed"); + doThrow(exc).when(mPermissionsChecker).assertWritePermission(); + + SyncableFlag f = new SyncableFlag(NS, "a", "false", false); + + try { + mFeatureFlagsService.overrideFlag(f); + fail("Should have thrown exception"); + } catch (SecurityException e) { + assertThat(exc).isEqualTo(e); + } catch (Exception e) { + fail("should have thrown a security exception"); + } + } + + @Test + public void testResetFlag_requiresWritePermission() { + SecurityException exc = new SecurityException("not allowed"); + doThrow(exc).when(mPermissionsChecker).assertWritePermission(); + + SyncableFlag f = new SyncableFlag(NS, "a", "false", false); + + try { + mFeatureFlagsService.resetFlag(f); + fail("Should have thrown exception"); + } catch (SecurityException e) { + assertThat(exc).isEqualTo(e); + } catch (Exception e) { + fail("should have thrown a security exception"); + } + } + + @Test + public void testSyncFlags_noOverrides() { + List<SyncableFlag> inputFlags = List.of( + new SyncableFlag(NS, "a", "false", false), + new SyncableFlag(NS, "b", "true", false), + new SyncableFlag(NS, "c", "false", false) + ); + + List<SyncableFlag> outputFlags = mFeatureFlagsService.syncFlags(inputFlags); + + assertThat(inputFlags.size()).isEqualTo(outputFlags.size()); + + for (SyncableFlag inpF: inputFlags) { + boolean found = false; + for (SyncableFlag outF : outputFlags) { + if (compareSyncableFlagsNames(inpF, outF)) { + found = true; + break; + } + } + assertWithMessage("Failed to find input flag " + inpF + " in the output") + .that(found).isTrue(); + } + } + + @Test + public void testSyncFlags_withSomeOverrides() { + List<SyncableFlag> inputFlags = List.of( + new SyncableFlag(NS, "a", "false", false), + new SyncableFlag(NS, "b", "true", false), + new SyncableFlag(NS, "c", "false", false) + ); + + assertThat(mFlagStore).isNotNull(); + when(mFlagStore.get(NS, "c")).thenReturn("true"); + List<SyncableFlag> outputFlags = mFeatureFlagsService.syncFlags(inputFlags); + + assertThat(inputFlags.size()).isEqualTo(outputFlags.size()); + + for (SyncableFlag inpF: inputFlags) { + boolean found = false; + for (SyncableFlag outF : outputFlags) { + if (compareSyncableFlagsNames(inpF, outF)) { + found = true; + + // Once we've found "c", do an extra check + if (outF.getName().equals("c")) { + assertWithMessage("Flag " + outF + "was not returned with an override") + .that(outF.getValue()).isEqualTo("true"); + } + break; + } + } + assertWithMessage("Failed to find input flag " + inpF + " in the output") + .that(found).isTrue(); + } + } + + @Test + public void testSyncFlags_twoCallsWithDifferentDefaults() { + List<SyncableFlag> inputFlagsFirst = List.of( + new SyncableFlag(NS, "a", "false", false) + ); + List<SyncableFlag> inputFlagsSecond = List.of( + new SyncableFlag(NS, "a", "true", false), + new SyncableFlag(NS, "b", "false", false) + ); + + List<SyncableFlag> outputFlagsFirst = mFeatureFlagsService.syncFlags(inputFlagsFirst); + List<SyncableFlag> outputFlagsSecond = mFeatureFlagsService.syncFlags(inputFlagsSecond); + + assertThat(inputFlagsFirst.size()).isEqualTo(outputFlagsFirst.size()); + assertThat(inputFlagsSecond.size()).isEqualTo(outputFlagsSecond.size()); + + // This test only cares that the "a" flag passed in the second time came out with the + // same value that was passed in the first time. + + boolean found = false; + for (SyncableFlag second : outputFlagsSecond) { + if (compareSyncableFlagsNames(second, inputFlagsFirst.get(0))) { + found = true; + assertThat(second.getValue()).isEqualTo(inputFlagsFirst.get(0).getValue()); + break; + } + } + + assertWithMessage( + "Failed to find flag " + inputFlagsFirst.get(0) + " in the second calls output") + .that(found).isTrue(); + } + + @Test + public void testQueryFlags_onlyOnce() { + List<SyncableFlag> inputFlags = List.of( + new SyncableFlag(NS, "a", "false", false), + new SyncableFlag(NS, "b", "true", false), + new SyncableFlag(NS, "c", "false", false) + ); + + List<SyncableFlag> outputFlags = mFeatureFlagsService.queryFlags(inputFlags); + + assertThat(inputFlags.size()).isEqualTo(outputFlags.size()); + + for (SyncableFlag inpF: inputFlags) { + boolean found = false; + for (SyncableFlag outF : outputFlags) { + if (compareSyncableFlagsNames(inpF, outF)) { + found = true; + break; + } + } + assertWithMessage("Failed to find input flag " + inpF + " in the output") + .that(found).isTrue(); + } + } + + @Test + public void testQueryFlags_twoCallsWithDifferentDefaults() { + List<SyncableFlag> inputFlagsFirst = List.of( + new SyncableFlag(NS, "a", "false", false) + ); + List<SyncableFlag> inputFlagsSecond = List.of( + new SyncableFlag(NS, "a", "true", false), + new SyncableFlag(NS, "b", "false", false) + ); + + List<SyncableFlag> outputFlagsFirst = mFeatureFlagsService.queryFlags(inputFlagsFirst); + List<SyncableFlag> outputFlagsSecond = mFeatureFlagsService.queryFlags(inputFlagsSecond); + + assertThat(inputFlagsFirst.size()).isEqualTo(outputFlagsFirst.size()); + assertThat(inputFlagsSecond.size()).isEqualTo(outputFlagsSecond.size()); + + // This test only cares that the "a" flag passed in the second time came out with the + // same value that was passed in (i.e. it wasn't cached). + + boolean found = false; + for (SyncableFlag second : outputFlagsSecond) { + if (compareSyncableFlagsNames(second, inputFlagsSecond.get(0))) { + found = true; + assertThat(second.getValue()).isEqualTo(inputFlagsSecond.get(0).getValue()); + break; + } + } + + assertWithMessage( + "Failed to find flag " + inputFlagsSecond.get(0) + " in the second calls output") + .that(found).isTrue(); + } + + @Test + public void testOverrideFlag() { + SyncableFlag f = new SyncableFlag(NS, "a", "false", false); + + mFeatureFlagsService.overrideFlag(f); + + verify(mFlagStore).set(f.getNamespace(), f.getName(), f.getValue()); + } + + @Test + public void testResetFlag() { + SyncableFlag f = new SyncableFlag(NS, "a", "false", false); + + mFeatureFlagsService.resetFlag(f); + + verify(mFlagStore).erase(f.getNamespace(), f.getName()); + } + + + private static boolean compareSyncableFlagsNames(SyncableFlag a, SyncableFlag b) { + return a.getNamespace().equals(b.getNamespace()) + && a.getName().equals(b.getName()) + && a.isDynamic() == b.isDynamic(); + } +} diff --git a/services/tests/servicestests/src/com/android/server/flags/FlagCacheTest.java b/services/tests/servicestests/src/com/android/server/flags/FlagCacheTest.java new file mode 100644 index 000000000000..c2cf540d1d62 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/flags/FlagCacheTest.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.flags; + +import static com.google.common.truth.Truth.assertThat; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class FlagCacheTest { + private static final String NS = "ns"; + private static final String NAME = "name"; + + FlagCache mFlagCache = new FlagCache(); + + @Test + public void testGetOrNull_unset() { + assertThat(mFlagCache.getOrNull(NS, NAME)).isNull(); + } + + @Test + public void testGetOrSet_unset() { + assertThat(mFlagCache.getOrSet(NS, NAME, "value")).isEqualTo("value"); + } + + @Test + public void testGetOrSet_alreadySet() { + mFlagCache.setIfChanged(NS, NAME, "value"); + assertThat(mFlagCache.getOrSet(NS, NAME, "newvalue")).isEqualTo("value"); + } + + @Test + public void testSetIfChanged_unset() { + assertThat(mFlagCache.setIfChanged(NS, NAME, "value")).isTrue(); + } + + @Test + public void testSetIfChanged_noChange() { + mFlagCache.setIfChanged(NS, NAME, "value"); + assertThat(mFlagCache.setIfChanged(NS, NAME, "value")).isFalse(); + } + + @Test + public void testSetIfChanged_changing() { + mFlagCache.setIfChanged(NS, NAME, "value"); + assertThat(mFlagCache.setIfChanged(NS, NAME, "newvalue")).isTrue(); + } + + @Test + public void testContainsNamespace_unset() { + assertThat(mFlagCache.containsNamespace(NS)).isFalse(); + } + + @Test + public void testContainsNamespace_set() { + mFlagCache.setIfChanged(NS, NAME, "value"); + assertThat(mFlagCache.containsNamespace(NS)).isTrue(); + } + + @Test + public void testContains_unset() { + assertThat(mFlagCache.contains(NS, NAME)).isFalse(); + } + + @Test + public void testContains_set() { + mFlagCache.setIfChanged(NS, NAME, "value"); + assertThat(mFlagCache.contains(NS, NAME)).isTrue(); + } +} diff --git a/services/tests/servicestests/src/com/android/server/flags/FlagOverrideStoreTest.java b/services/tests/servicestests/src/com/android/server/flags/FlagOverrideStoreTest.java new file mode 100644 index 000000000000..6cc3acfb6125 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/flags/FlagOverrideStoreTest.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.flags; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.platform.test.annotations.Presubmit; + +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +@Presubmit +@SmallTest +public class FlagOverrideStoreTest { + private static final String NS = "ns"; + private static final String NAME = "name"; + private static final String PROP_NAME = FlagOverrideStore.getPropName(NS, NAME); + + @Rule + public final MockitoRule mockito = MockitoJUnit.rule(); + + @Mock + private SettingsProxy mSettingsProxy; + @Mock + private FlagOverrideStore.FlagChangeCallback mCallback; + + private FlagOverrideStore mFlagStore; + + @Before + public void setup() { + mFlagStore = new FlagOverrideStore(mSettingsProxy); + mFlagStore.setChangeCallback(mCallback); + } + + @Test + public void testSet_unset() { + mFlagStore.set(NS, NAME, "value"); + verify(mSettingsProxy).putString(PROP_NAME, "value"); + } + + @Test + public void testSet_setTwice() { + mFlagStore.set(NS, NAME, "value"); + mFlagStore.set(NS, NAME, "newvalue"); + verify(mSettingsProxy).putString(PROP_NAME, "value"); + verify(mSettingsProxy).putString(PROP_NAME, "newvalue"); + } + + @Test + public void testGet_unset() { + assertThat(mFlagStore.get(NS, NAME)).isNull(); + } + + @Test + public void testGet_set() { + when(mSettingsProxy.getString(PROP_NAME)).thenReturn("value"); + assertThat(mFlagStore.get(NS, NAME)).isEqualTo("value"); + } + + @Test + public void testErase() { + mFlagStore.erase(NS, NAME); + verify(mSettingsProxy).putString(PROP_NAME, null); + } + + @Test + public void testContains_unset() { + assertThat(mFlagStore.contains(NS, NAME)).isFalse(); + } + + @Test + public void testContains_set() { + when(mSettingsProxy.getString(PROP_NAME)).thenReturn("value"); + assertThat(mFlagStore.contains(NS, NAME)).isTrue(); + } + + @Test + public void testCallback_onSet() { + mFlagStore.set(NS, NAME, "value"); + verify(mCallback).onFlagChanged(NS, NAME, "value"); + } + + @Test + public void testCallback_onErase() { + mFlagStore.erase(NS, NAME); + verify(mCallback).onFlagChanged(NS, NAME, null); + } +} diff --git a/services/tests/servicestests/src/com/android/server/flags/OWNERS b/services/tests/servicestests/src/com/android/server/flags/OWNERS new file mode 100644 index 000000000000..7ed369e37106 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/flags/OWNERS @@ -0,0 +1 @@ +include /services/flags/OWNERS diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index 3c882dc871fd..a109d5cddd21 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -73,6 +73,7 @@ import static android.service.notification.Adjustment.KEY_USER_SENTIMENT; import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ALERTING; import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_CONVERSATIONS; import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ONGOING; +import static android.service.notification.NotificationListenerService.REASON_CANCEL; import static android.service.notification.NotificationListenerService.REASON_LOCKDOWN; import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_NEGATIVE; import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_NEUTRAL; @@ -2009,10 +2010,10 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { final StatusBarNotification sbn = generateNotificationRecord(null).getSbn(); mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", sbn.getId(), sbn.getNotification(), sbn.getUserId()); - Thread.sleep(1); // make sure the system clock advances before the next step + mTestableLooper.moveTimeForward(1); // THEN it is canceled mBinderService.cancelNotificationWithTag(PKG, PKG, "tag", sbn.getId(), sbn.getUserId()); - Thread.sleep(1); // here too + mTestableLooper.moveTimeForward(1); // THEN it is posted again (before the cancel has a chance to finish) mBinderService.enqueueNotificationWithTag(PKG, PKG, "tag", sbn.getId(), sbn.getNotification(), sbn.getUserId()); @@ -2303,7 +2304,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { notif.getNotification().flags |= Notification.FLAG_NO_CLEAR; mService.addNotification(notif); mService.cancelAllNotificationsInt(mUid, 0, PKG, null, 0, 0, - notif.getUserId(), 0); + notif.getUserId(), REASON_CANCEL); waitForIdle(); StatusBarNotification[] notifs = mBinderService.getActiveNotifications(notif.getSbn().getPackageName()); @@ -3041,7 +3042,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { notif.getNotification().flags |= Notification.FLAG_NO_CLEAR; mService.addNotification(notif); mService.cancelAllNotificationsInt(mUid, 0, PKG, null, 0, - Notification.FLAG_ONGOING_EVENT, notif.getUserId(), 0); + Notification.FLAG_ONGOING_EVENT, notif.getUserId(), REASON_CANCEL); waitForIdle(); StatusBarNotification[] notifs = mBinderService.getActiveNotifications(notif.getSbn().getPackageName()); @@ -3069,7 +3070,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { notif.getNotification().flags |= Notification.FLAG_ONGOING_EVENT; mService.addNotification(notif); mService.cancelAllNotificationsInt(mUid, 0, PKG, null, 0, 0, - notif.getUserId(), 0); + notif.getUserId(), REASON_CANCEL); waitForIdle(); StatusBarNotification[] notifs = mBinderService.getActiveNotifications(notif.getSbn().getPackageName()); @@ -12208,7 +12209,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mOnPermissionChangeListener.onOpChanged( AppOpsManager.OPSTR_POST_NOTIFICATION, PKG, 0); waitForIdle(); - Thread.sleep(600); + mTestableLooper.moveTimeForward(500); waitForIdle(); ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class); @@ -12227,7 +12228,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mOnPermissionChangeListener.onOpChanged( AppOpsManager.OPSTR_POST_NOTIFICATION, PKG, 0); waitForIdle(); - Thread.sleep(600); + mTestableLooper.moveTimeForward(500); waitForIdle(); ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class); @@ -12261,7 +12262,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { assertThat(mService.mNotificationList).hasSize(0); - Thread.sleep(600); + mTestableLooper.moveTimeForward(500); waitForIdle(); verify(mContext).sendBroadcastAsUser(any(), eq(UserHandle.of(0)), eq(null)); } diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityOptionsTest.java b/services/tests/wmtests/src/com/android/server/wm/ActivityOptionsTest.java index fe1ea0d99eeb..f6f3f0324f9c 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityOptionsTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityOptionsTest.java @@ -22,11 +22,17 @@ import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_ROTATE; import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.verify; import android.app.Activity; import android.app.ActivityManager.RunningTaskInfo; @@ -41,6 +47,7 @@ import android.os.Binder; import android.os.Bundle; import android.os.IBinder; import android.platform.test.annotations.Presubmit; +import android.util.Log; import android.util.Rational; import android.view.SurfaceControl; import android.window.TaskOrganizer; @@ -48,7 +55,10 @@ import android.window.TaskOrganizer; import androidx.test.filters.MediumTest; import org.junit.Test; +import org.mockito.ArgumentCaptor; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -166,6 +176,122 @@ public class ActivityOptionsTest { } } + /** + * Tests if any unknown key is being used in the ActivityOptions bundle. If so, please review + * if the newly added bundle should be protected with permissions to avoid malicious attacks. + * + * @see SafeActivityOptionsTest#test_getOptions + */ + @Test + public void testActivityOptionsFromBundle() { + // Spy on a bundle that is generated from a basic ActivityOptions. + final ActivityOptions options = ActivityOptions.makeBasic(); + Bundle bundle = options.toBundle(); + spyOn(bundle); + + // Create a new ActivityOptions from the bundle + new ActivityOptions(bundle); + + // Verify the keys that are being used. + final ArgumentCaptor<String> stringCaptor = ArgumentCaptor.forClass(String.class); + verify(bundle, atLeastOnce()).getString(stringCaptor.capture()); + verify(bundle, atLeastOnce()).getBoolean(stringCaptor.capture()); + verify(bundle, atLeastOnce()).getParcelable(stringCaptor.capture(), any()); + verify(bundle, atLeastOnce()).getInt(stringCaptor.capture(), anyInt()); + verify(bundle, atLeastOnce()).getBinder(stringCaptor.capture()); + verify(bundle, atLeastOnce()).getBundle(stringCaptor.capture()); + final List<String> keys = stringCaptor.getAllValues(); + final List<String> unknownKeys = new ArrayList<>(); + for (String key : keys) { + switch (key) { + case ActivityOptions.KEY_PACKAGE_NAME: + case ActivityOptions.KEY_LAUNCH_BOUNDS: + case ActivityOptions.KEY_ANIM_TYPE: + case ActivityOptions.KEY_ANIM_ENTER_RES_ID: + case ActivityOptions.KEY_ANIM_EXIT_RES_ID: + case ActivityOptions.KEY_ANIM_IN_PLACE_RES_ID: + case ActivityOptions.KEY_ANIM_BACKGROUND_COLOR: + case ActivityOptions.KEY_ANIM_THUMBNAIL: + case ActivityOptions.KEY_ANIM_START_X: + case ActivityOptions.KEY_ANIM_START_Y: + case ActivityOptions.KEY_ANIM_WIDTH: + case ActivityOptions.KEY_ANIM_HEIGHT: + case ActivityOptions.KEY_ANIM_START_LISTENER: + case ActivityOptions.KEY_SPLASH_SCREEN_THEME: + case ActivityOptions.KEY_LEGACY_PERMISSION_PROMPT_ELIGIBLE: + case ActivityOptions.KEY_LAUNCH_ROOT_TASK_TOKEN: + case ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN: + case ActivityOptions.KEY_TRANSIENT_LAUNCH: + case "android:activity.animationFinishedListener": + // KEY_ANIMATION_FINISHED_LISTENER + case "android:activity.animSpecs": // KEY_ANIM_SPECS + case "android:activity.lockTaskMode": // KEY_LOCK_TASK_MODE + case "android:activity.shareIdentity": // KEY_SHARE_IDENTITY + case "android.activity.launchDisplayId": // KEY_LAUNCH_DISPLAY_ID + case "android.activity.callerDisplayId": // KEY_CALLER_DISPLAY_ID + case "android.activity.launchTaskDisplayAreaToken": + // KEY_LAUNCH_TASK_DISPLAY_AREA_TOKEN + case "android.activity.launchTaskDisplayAreaFeatureId": + // KEY_LAUNCH_TASK_DISPLAY_AREA_FEATURE_ID + case "android.activity.windowingMode": // KEY_LAUNCH_WINDOWING_MODE + case "android.activity.activityType": // KEY_LAUNCH_ACTIVITY_TYPE + case "android.activity.launchTaskId": // KEY_LAUNCH_TASK_ID + case "android.activity.disableStarting": // KEY_DISABLE_STARTING_WINDOW + case "android.activity.pendingIntentLaunchFlags": + // KEY_PENDING_INTENT_LAUNCH_FLAGS + case "android.activity.alwaysOnTop": // KEY_TASK_ALWAYS_ON_TOP + case "android.activity.taskOverlay": // KEY_TASK_OVERLAY + case "android.activity.taskOverlayCanResume": // KEY_TASK_OVERLAY_CAN_RESUME + case "android.activity.avoidMoveToFront": // KEY_AVOID_MOVE_TO_FRONT + case "android.activity.freezeRecentTasksReordering": + // KEY_FREEZE_RECENT_TASKS_REORDERING + case "android:activity.disallowEnterPictureInPictureWhileLaunching": + // KEY_DISALLOW_ENTER_PICTURE_IN_PICTURE_WHILE_LAUNCHING + case "android:activity.applyActivityFlagsForBubbles": + // KEY_APPLY_ACTIVITY_FLAGS_FOR_BUBBLES + case "android:activity.applyMultipleTaskFlagForShortcut": + // KEY_APPLY_MULTIPLE_TASK_FLAG_FOR_SHORTCUT + case "android:activity.applyNoUserActionFlagForShortcut": + // KEY_APPLY_NO_USER_ACTION_FLAG_FOR_SHORTCUT + case "android:activity.transitionCompleteListener": + // KEY_TRANSITION_COMPLETE_LISTENER + case "android:activity.transitionIsReturning": // KEY_TRANSITION_IS_RETURNING + case "android:activity.sharedElementNames": // KEY_TRANSITION_SHARED_ELEMENTS + case "android:activity.resultData": // KEY_RESULT_DATA + case "android:activity.resultCode": // KEY_RESULT_CODE + case "android:activity.exitCoordinatorIndex": // KEY_EXIT_COORDINATOR_INDEX + case "android.activity.sourceInfo": // KEY_SOURCE_INFO + case "android:activity.usageTimeReport": // KEY_USAGE_TIME_REPORT + case "android:activity.rotationAnimationHint": // KEY_ROTATION_ANIMATION_HINT + case "android:instantapps.installerbundle": // KEY_INSTANT_APP_VERIFICATION_BUNDLE + case "android:activity.specsFuture": // KEY_SPECS_FUTURE + case "android:activity.remoteAnimationAdapter": // KEY_REMOTE_ANIMATION_ADAPTER + case "android:activity.remoteTransition": // KEY_REMOTE_TRANSITION + case "android:activity.overrideTaskTransition": // KEY_OVERRIDE_TASK_TRANSITION + case "android.activity.removeWithTaskOrganizer": // KEY_REMOVE_WITH_TASK_ORGANIZER + case "android.activity.launchTypeBubble": // KEY_LAUNCHED_FROM_BUBBLE + case "android.activity.splashScreenStyle": // KEY_SPLASH_SCREEN_STYLE + case "android.activity.launchIntoPipParams": // KEY_LAUNCH_INTO_PIP_PARAMS + case "android.activity.dismissKeyguard": // KEY_DISMISS_KEYGUARD + case "android.activity.pendingIntentCreatorBackgroundActivityStartMode": + // KEY_PENDING_INTENT_CREATOR_BACKGROUND_ACTIVITY_START_MODE + case "android.activity.launchCookie": // KEY_LAUNCH_COOKIE + // Existing keys + break; + default: + unknownKeys.add(key); + break; + } + } + + // Report if any unknown key exists. + for (String key : unknownKeys) { + Log.e("ActivityOptionsTests", "Unknown key " + key + " is found. " + + "Please review if the given bundle should be protected with permissions."); + } + assertTrue(unknownKeys.isEmpty()); + } + public static class TrampolineActivity extends Activity { static int sTaskId; diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationPersisterTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationPersisterTest.java index 51a7e747afce..06033c7ebf75 100644 --- a/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationPersisterTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationPersisterTest.java @@ -20,7 +20,6 @@ import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentat import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT; import static com.android.server.wm.LetterboxConfiguration.LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP; -import static com.android.server.wm.LetterboxConfigurationPersister.LETTERBOX_CONFIGURATION_FILENAME; import android.annotation.NonNull; import android.annotation.Nullable; @@ -42,13 +41,26 @@ import org.junit.Test; import java.io.File; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; +import java.util.function.Supplier; +/** + * Tests for the {@link LetterboxConfigurationPersister} class. + * + * Build/Install/Run: + * atest WmTests:LetterboxConfigurationPersisterTest + */ @SmallTest @Presubmit public class LetterboxConfigurationPersisterTest { private static final long TIMEOUT = 2000L; // 2 secs + private static final int DEFAULT_REACHABILITY_TEST = -1; + private static final Supplier<Integer> DEFAULT_REACHABILITY_SUPPLIER_TEST = + () -> DEFAULT_REACHABILITY_TEST; + + private static final String LETTERBOX_CONFIGURATION_TEST_FILENAME = "letterbox_config_test"; + private LetterboxConfigurationPersister mLetterboxConfigurationPersister; private Context mContext; private PersisterQueue mPersisterQueue; @@ -62,7 +74,7 @@ public class LetterboxConfigurationPersisterTest { mConfigFolder = mContext.getFilesDir(); mPersisterQueue = new PersisterQueue(); mQueueState = new QueueState(); - mLetterboxConfigurationPersister = new LetterboxConfigurationPersister(mContext, + mLetterboxConfigurationPersister = new LetterboxConfigurationPersister( () -> mContext.getResources().getInteger( R.integer.config_letterboxDefaultPositionForHorizontalReachability), () -> mContext.getResources().getInteger( @@ -72,7 +84,8 @@ public class LetterboxConfigurationPersisterTest { () -> mContext.getResources().getInteger( R.integer.config_letterboxDefaultPositionForTabletopModeReachability ), - mConfigFolder, mPersisterQueue, mQueueState); + mConfigFolder, mPersisterQueue, mQueueState, + LETTERBOX_CONFIGURATION_TEST_FILENAME); mQueueListener = queueEmpty -> mQueueState.onItemAdded(); mPersisterQueue.addListener(mQueueListener); mLetterboxConfigurationPersister.start(); @@ -127,8 +140,10 @@ public class LetterboxConfigurationPersisterTest { public void test_whenUpdatedWithNewValues_valuesAreReadAfterRestart() { final PersisterQueue firstPersisterQueue = new PersisterQueue(); final LetterboxConfigurationPersister firstPersister = new LetterboxConfigurationPersister( - mContext, () -> -1, () -> -1, () -> -1, () -> -1, mContext.getFilesDir(), - firstPersisterQueue, mQueueState); + DEFAULT_REACHABILITY_SUPPLIER_TEST, DEFAULT_REACHABILITY_SUPPLIER_TEST, + DEFAULT_REACHABILITY_SUPPLIER_TEST, DEFAULT_REACHABILITY_SUPPLIER_TEST, + mContext.getFilesDir(), firstPersisterQueue, mQueueState, + LETTERBOX_CONFIGURATION_TEST_FILENAME); firstPersister.start(); firstPersister.setLetterboxPositionForHorizontalReachability(false, LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT); @@ -138,8 +153,10 @@ public class LetterboxConfigurationPersisterTest { stopPersisterSafe(firstPersisterQueue); final PersisterQueue secondPersisterQueue = new PersisterQueue(); final LetterboxConfigurationPersister secondPersister = new LetterboxConfigurationPersister( - mContext, () -> -1, () -> -1, () -> -1, () -> -1, mContext.getFilesDir(), - secondPersisterQueue, mQueueState); + DEFAULT_REACHABILITY_SUPPLIER_TEST, DEFAULT_REACHABILITY_SUPPLIER_TEST, + DEFAULT_REACHABILITY_SUPPLIER_TEST, DEFAULT_REACHABILITY_SUPPLIER_TEST, + mContext.getFilesDir(), secondPersisterQueue, mQueueState, + LETTERBOX_CONFIGURATION_TEST_FILENAME); secondPersister.start(); final int newPositionForHorizontalReachability = secondPersister.getLetterboxPositionForHorizontalReachability(false); @@ -156,37 +173,46 @@ public class LetterboxConfigurationPersisterTest { @Test public void test_whenUpdatedWithNewValuesAndDeleted_valuesAreDefaults() { - mLetterboxConfigurationPersister.setLetterboxPositionForHorizontalReachability(false, + final PersisterQueue firstPersisterQueue = new PersisterQueue(); + final LetterboxConfigurationPersister firstPersister = new LetterboxConfigurationPersister( + DEFAULT_REACHABILITY_SUPPLIER_TEST, DEFAULT_REACHABILITY_SUPPLIER_TEST, + DEFAULT_REACHABILITY_SUPPLIER_TEST, DEFAULT_REACHABILITY_SUPPLIER_TEST, + mContext.getFilesDir(), firstPersisterQueue, mQueueState, + LETTERBOX_CONFIGURATION_TEST_FILENAME); + firstPersister.start(); + firstPersister.setLetterboxPositionForHorizontalReachability(false, LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT); - mLetterboxConfigurationPersister.setLetterboxPositionForVerticalReachability(false, + firstPersister.setLetterboxPositionForVerticalReachability(false, LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP); waitForCompletion(mPersisterQueue); final int newPositionForHorizontalReachability = - mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability( - false); + firstPersister.getLetterboxPositionForHorizontalReachability(false); final int newPositionForVerticalReachability = - mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability(false); + firstPersister.getLetterboxPositionForVerticalReachability(false); Assert.assertEquals(LETTERBOX_HORIZONTAL_REACHABILITY_POSITION_LEFT, newPositionForHorizontalReachability); Assert.assertEquals(LETTERBOX_VERTICAL_REACHABILITY_POSITION_TOP, newPositionForVerticalReachability); - deleteConfiguration(mLetterboxConfigurationPersister, mPersisterQueue); - waitForCompletion(mPersisterQueue); + deleteConfiguration(firstPersister, firstPersisterQueue); + waitForCompletion(firstPersisterQueue); + stopPersisterSafe(firstPersisterQueue); + + final PersisterQueue secondPersisterQueue = new PersisterQueue(); + final LetterboxConfigurationPersister secondPersister = new LetterboxConfigurationPersister( + DEFAULT_REACHABILITY_SUPPLIER_TEST, DEFAULT_REACHABILITY_SUPPLIER_TEST, + DEFAULT_REACHABILITY_SUPPLIER_TEST, DEFAULT_REACHABILITY_SUPPLIER_TEST, + mContext.getFilesDir(), secondPersisterQueue, mQueueState, + LETTERBOX_CONFIGURATION_TEST_FILENAME); + secondPersister.start(); final int positionForHorizontalReachability = - mLetterboxConfigurationPersister.getLetterboxPositionForHorizontalReachability( - false); - final int defaultPositionForHorizontalReachability = - mContext.getResources().getInteger( - R.integer.config_letterboxDefaultPositionForHorizontalReachability); - Assert.assertEquals(defaultPositionForHorizontalReachability, - positionForHorizontalReachability); + secondPersister.getLetterboxPositionForHorizontalReachability(false); final int positionForVerticalReachability = - mLetterboxConfigurationPersister.getLetterboxPositionForVerticalReachability(false); - final int defaultPositionForVerticalReachability = - mContext.getResources().getInteger( - R.integer.config_letterboxDefaultPositionForVerticalReachability); - Assert.assertEquals(defaultPositionForVerticalReachability, - positionForVerticalReachability); + secondPersister.getLetterboxPositionForVerticalReachability(false); + Assert.assertEquals(DEFAULT_REACHABILITY_TEST, positionForHorizontalReachability); + Assert.assertEquals(DEFAULT_REACHABILITY_TEST, positionForVerticalReachability); + deleteConfiguration(secondPersister, secondPersisterQueue); + waitForCompletion(secondPersisterQueue); + stopPersisterSafe(secondPersisterQueue); } private void stopPersisterSafe(PersisterQueue persisterQueue) { @@ -222,7 +248,7 @@ public class LetterboxConfigurationPersisterTest { private void deleteConfiguration(LetterboxConfigurationPersister persister, PersisterQueue persisterQueue) { final AtomicFile fileToDelete = new AtomicFile( - new File(mConfigFolder, LETTERBOX_CONFIGURATION_FILENAME)); + new File(mConfigFolder, LETTERBOX_CONFIGURATION_TEST_FILENAME)); persisterQueue.addItem( new DeleteFileCommand(fileToDelete, mQueueState.andThen( s -> persister.useDefaultValue())), true); diff --git a/services/tests/wmtests/src/com/android/server/wm/SafeActivityOptionsTest.java b/services/tests/wmtests/src/com/android/server/wm/SafeActivityOptionsTest.java index 24e932f36f80..6c48a6961bc2 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SafeActivityOptionsTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/SafeActivityOptionsTest.java @@ -16,17 +16,36 @@ package com.android.server.wm; +import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; +import static android.content.pm.PackageManager.PERMISSION_DENIED; +import static android.view.Display.DEFAULT_DISPLAY; + +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.mock; import android.app.ActivityOptions; +import android.content.pm.ActivityInfo; +import android.os.Looper; import android.platform.test.annotations.Presubmit; +import android.view.RemoteAnimationAdapter; +import android.window.RemoteTransition; import android.window.WindowContainerToken; import androidx.test.filters.MediumTest; import org.junit.Test; +import org.mockito.MockitoSession; +import org.mockito.quality.Strictness; /** * Build/Install/Run: @@ -73,4 +92,119 @@ public class SafeActivityOptionsTest { assertSame(clone.getOriginalOptions().getLaunchRootTask(), token); } + + @Test + public void test_getOptions() { + // Mock everything necessary + MockitoSession mockingSession = mockitoSession() + .mockStatic(ActivityTaskManagerService.class) + .strictness(Strictness.LENIENT) + .startMocking(); + doReturn(PERMISSION_DENIED).when(() -> ActivityTaskManagerService.checkPermission( + any(), anyInt(), anyInt())); + + final LockTaskController lockTaskController = mock(LockTaskController.class); + doReturn(false).when(lockTaskController).isPackageAllowlisted(anyInt(), any()); + + final ActivityTaskManagerService atm = mock(ActivityTaskManagerService.class); + doReturn(lockTaskController).when(atm).getLockTaskController(); + + final ActivityTaskSupervisor taskSupervisor = + new ActivityTaskSupervisor(atm, mock(Looper.class)); + spyOn(taskSupervisor); + doReturn(false).when(taskSupervisor).isCallerAllowedToLaunchOnDisplay(anyInt(), + anyInt(), anyInt(), any()); + doReturn(false).when(taskSupervisor).isCallerAllowedToLaunchOnTaskDisplayArea(anyInt(), + anyInt(), any(), any()); + + taskSupervisor.mRecentTasks = mock(RecentTasks.class); + doReturn(false).when(taskSupervisor.mRecentTasks).isCallerRecents(anyInt()); + + // Ensure exceptions are thrown when lack of permissions. + ActivityOptions activityOptions = ActivityOptions.makeBasic(); + try { + activityOptions.setLaunchTaskId(100); + verifySecureExceptionThrown(activityOptions, taskSupervisor); + + activityOptions = ActivityOptions.makeBasic(); + activityOptions.setDisableStartingWindow(true); + verifySecureExceptionThrown(activityOptions, taskSupervisor); + + activityOptions = ActivityOptions.makeBasic(); + activityOptions.setTransientLaunch(); + verifySecureExceptionThrown(activityOptions, taskSupervisor); + + activityOptions = ActivityOptions.makeBasic(); + activityOptions.setDismissKeyguard(); + verifySecureExceptionThrown(activityOptions, taskSupervisor); + + activityOptions = ActivityOptions.makeBasic(); + activityOptions.setLaunchActivityType(ACTIVITY_TYPE_STANDARD); + verifySecureExceptionThrown(activityOptions, taskSupervisor); + + activityOptions = ActivityOptions.makeBasic(); + activityOptions.setLaunchedFromBubble(true); + verifySecureExceptionThrown(activityOptions, taskSupervisor); + + activityOptions = ActivityOptions.makeBasic(); + activityOptions.setLaunchDisplayId(DEFAULT_DISPLAY); + verifySecureExceptionThrown(activityOptions, taskSupervisor); + + activityOptions = ActivityOptions.makeBasic(); + activityOptions.setLockTaskEnabled(true); + verifySecureExceptionThrown(activityOptions, taskSupervisor); + + activityOptions = ActivityOptions.makeCustomTaskAnimation( + getInstrumentation().getContext(), 0, 0, null, null, null); + verifySecureExceptionThrown(activityOptions, taskSupervisor); + + RemoteAnimationAdapter remoteAnimationAdapter = mock(RemoteAnimationAdapter.class); + RemoteTransition remoteTransition = mock(RemoteTransition.class); + activityOptions = ActivityOptions.makeRemoteAnimation(remoteAnimationAdapter); + verifySecureExceptionThrown(activityOptions, taskSupervisor); + + activityOptions = ActivityOptions.makeRemoteAnimation(remoteAnimationAdapter, + remoteTransition); + verifySecureExceptionThrown(activityOptions, taskSupervisor); + + activityOptions = ActivityOptions.makeBasic(); + activityOptions.setRemoteAnimationAdapter(remoteAnimationAdapter); + verifySecureExceptionThrown(activityOptions, taskSupervisor); + + activityOptions = ActivityOptions.makeRemoteTransition(remoteTransition); + verifySecureExceptionThrown(activityOptions, taskSupervisor); + + activityOptions = ActivityOptions.makeBasic(); + activityOptions.setRemoteTransition(remoteTransition); + verifySecureExceptionThrown(activityOptions, taskSupervisor); + + verifySecureExceptionThrown(activityOptions, taskSupervisor, + mock(TaskDisplayArea.class)); + } finally { + mockingSession.finishMocking(); + } + } + + private void verifySecureExceptionThrown(ActivityOptions activityOptions, + ActivityTaskSupervisor taskSupervisor) { + verifySecureExceptionThrown(activityOptions, taskSupervisor, null /* mockTda */); + } + + private void verifySecureExceptionThrown(ActivityOptions activityOptions, + ActivityTaskSupervisor taskSupervisor, TaskDisplayArea mockTda) { + SafeActivityOptions safeActivityOptions = new SafeActivityOptions(activityOptions); + if (mockTda != null) { + spyOn(safeActivityOptions); + doReturn(mockTda).when(safeActivityOptions).getLaunchTaskDisplayArea(any(), any()); + } + + boolean isExceptionThrow = false; + final ActivityInfo aInfo = mock(ActivityInfo.class); + try { + safeActivityOptions.getOptions(null, aInfo, null, taskSupervisor); + } catch (SecurityException ex) { + isExceptionThrow = true; + } + assertTrue(isExceptionThrow); + } } diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java index 7351e81387bb..ffecafb7b4ed 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java @@ -61,6 +61,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; @@ -1429,6 +1430,15 @@ public class TransitionTests extends WindowTestsBase { // No need to wait for the activity in transient hide task. assertEquals(WindowContainer.SYNC_STATE_NONE, activity1.mSyncState); + // An active transient launch overrides idle state to avoid clearing power mode before the + // transition is finished. + spyOn(mRootWindowContainer.mTransitionController); + doAnswer(invocation -> controller.isTransientLaunch(invocation.getArgument(0))).when( + mRootWindowContainer.mTransitionController).isTransientLaunch(any()); + activity2.getTask().setResumedActivity(activity2, "test"); + activity2.idle = true; + assertFalse(mRootWindowContainer.allResumedActivitiesIdle()); + activity1.setVisibleRequested(false); activity2.setVisibleRequested(true); activity2.setVisible(true); diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java index 64e43568e4d6..c0f4008c653d 100644 --- a/telephony/java/android/telephony/SubscriptionManager.java +++ b/telephony/java/android/telephony/SubscriptionManager.java @@ -35,6 +35,9 @@ import android.annotation.SystemApi; import android.annotation.SystemService; import android.app.PendingIntent; import android.app.PropertyInvalidatedCache; +import android.compat.Compatibility; +import android.compat.annotation.ChangeId; +import android.compat.annotation.EnabledAfter; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; import android.content.Intent; @@ -85,6 +88,7 @@ import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; import java.util.function.Consumer; @@ -1322,41 +1326,61 @@ public class SubscriptionManager { * for #onSubscriptionsChanged to be invoked. */ public static class OnSubscriptionsChangedListener { - private class OnSubscriptionsChangedListenerHandler extends Handler { - OnSubscriptionsChangedListenerHandler() { - super(); - } - OnSubscriptionsChangedListenerHandler(Looper looper) { - super(looper); - } - } + /** + * After {@link Build.VERSION_CODES.Q}, it is no longer necessary to instantiate a + * Handler inside of the OnSubscriptionsChangedListener in all cases, so it will only + * be done for callers that do not supply an Executor. + */ + @ChangeId + @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.Q) + private static final long LAZY_INITIALIZE_SUBSCRIPTIONS_CHANGED_HANDLER = 278814050L; /** - * Posted executor callback on the handler associated with a given looper. - * The looper can be the calling thread's looper or the looper passed from the - * constructor {@link #OnSubscriptionsChangedListener(Looper)}. + * For backwards compatibility reasons, stashes the Looper associated with the thread + * context in which this listener was created. */ - private final HandlerExecutor mExecutor; + private final Looper mCreatorLooper; /** * @hide */ - public HandlerExecutor getHandlerExecutor() { - return mExecutor; + public Looper getCreatorLooper() { + return mCreatorLooper; } + /** + * Create an OnSubscriptionsChangedListener. + * + * For callers targeting {@link Build.VERSION_CODES.P} or earlier, this can only be called + * on a thread that already has a prepared Looper. Callers targeting Q or later should + * subsequently use {@link SubscriptionManager#addOnSubscriptionsChangedListener( + * Executor, OnSubscriptionsChangedListener)}. + * + * On OS versions prior to {@link Build.VERSION_CODES.V} callers should assume that this + * call will fail if invoked on a thread that does not already have a prepared looper. + */ public OnSubscriptionsChangedListener() { - mExecutor = new HandlerExecutor(new OnSubscriptionsChangedListenerHandler()); + mCreatorLooper = Looper.myLooper(); + if (mCreatorLooper == null + && !Compatibility.isChangeEnabled( + LAZY_INITIALIZE_SUBSCRIPTIONS_CHANGED_HANDLER)) { + // matches the implementation of Handler + throw new RuntimeException( + "Can't create handler inside thread " + + Thread.currentThread() + + " that has not called Looper.prepare()"); + } } /** * Allow a listener to be created with a custom looper - * @param looper the looper that the underlining handler should run on + * @param looper the non-null Looper that the underlining handler should run on * @hide */ - public OnSubscriptionsChangedListener(Looper looper) { - mExecutor = new HandlerExecutor(new OnSubscriptionsChangedListenerHandler(looper)); + public OnSubscriptionsChangedListener(@NonNull Looper looper) { + Objects.requireNonNull(looper); + mCreatorLooper = looper; } /** @@ -1423,7 +1447,9 @@ public class SubscriptionManager { @Deprecated public void addOnSubscriptionsChangedListener(OnSubscriptionsChangedListener listener) { if (listener == null) return; - addOnSubscriptionsChangedListener(listener.mExecutor, listener); + + addOnSubscriptionsChangedListener( + new HandlerExecutor(new Handler(listener.getCreatorLooper())), listener); } /** diff --git a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml index 68ae806f3c8b..92c5f17a5b94 100644 --- a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml +++ b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml @@ -102,6 +102,24 @@ <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> + <activity android:name=".LaunchTransparentActivity" + android:resizeableActivity="false" + android:screenOrientation="portrait" + android:theme="@android:style/Theme" + android:taskAffinity="com.android.server.wm.flicker.testapp.LaunchTransparentActivity" + android:label="LaunchTransparentActivity" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + <category android:name="android.intent.category.LAUNCHER"/> + </intent-filter> + </activity> + <activity android:name=".TransparentActivity" + android:theme="@style/TransparentTheme" + android:taskAffinity="com.android.server.wm.flicker.testapp.TransparentActivity" + android:label="TransparentActivity" + android:exported="false"> + </activity> <activity android:name=".LaunchNewActivity" android:taskAffinity="com.android.server.wm.flicker.testapp.LaunchNewActivity" android:theme="@style/CutoutShortEdges" diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_transparent.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_transparent.xml new file mode 100644 index 000000000000..0730ded66ce4 --- /dev/null +++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_transparent.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 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. + --> +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent"> + +</FrameLayout> diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_transparent_launch.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_transparent_launch.xml new file mode 100644 index 000000000000..ff4ead95f16e --- /dev/null +++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_transparent_launch.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 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. + --> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:background="@android:color/black"> + + <Button + android:id="@+id/button_launch_transparent" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_centerHorizontal="true" + android:layout_centerVertical="true" + android:text="Launch Transparent" /> + <Button + android:id="@+id/button_request_permission" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_centerHorizontal="true" + android:layout_centerVertical="true" + android:text="Request Permission" /> +</LinearLayout> diff --git a/tests/FlickerTests/test-apps/flickerapp/res/values/styles.xml b/tests/FlickerTests/test-apps/flickerapp/res/values/styles.xml index 1d21fd56a487..e51ed29adebf 100644 --- a/tests/FlickerTests/test-apps/flickerapp/res/values/styles.xml +++ b/tests/FlickerTests/test-apps/flickerapp/res/values/styles.xml @@ -43,6 +43,13 @@ <item name="android:windowSoftInputMode">stateUnchanged</item> </style> + <style name="TransparentTheme" parent="@android:style/Theme.DeviceDefault"> + <item name="android:windowIsTranslucent">true</item> + <item name="android:windowBackground">@android:color/transparent</item> + <item name="android:windowContentOverlay">@null</item> + <item name="android:backgroundDimEnabled">false</item> + </style> + <style name="no_starting_window" parent="@android:style/Theme.DeviceDefault"> <item name="android:windowDisablePreview">true</item> </style> diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java index 95c86acb9ee9..2795a6c43015 100644 --- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java @@ -73,6 +73,18 @@ public class ActivityOptions { FLICKER_APP_PACKAGE + ".NonResizeablePortraitActivity"); } + public static class TransparentActivity { + public static final String LABEL = "TransparentActivity"; + public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE, + FLICKER_APP_PACKAGE + ".TransparentActivity"); + } + + public static class LaunchTransparentActivity { + public static final String LABEL = "LaunchTransparentActivity"; + public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE, + FLICKER_APP_PACKAGE + ".LaunchTransparentActivity"); + } + public static class DialogThemedActivity { public static final String LABEL = "DialogThemedActivity"; public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE, diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/LaunchTransparentActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/LaunchTransparentActivity.java new file mode 100644 index 000000000000..7c161fd8e611 --- /dev/null +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/LaunchTransparentActivity.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.testapp; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; + +public class LaunchTransparentActivity extends Activity { + + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + setContentView(R.layout.activity_transparent_launch); + findViewById(R.id.button_launch_transparent) + .setOnClickListener(v -> launchTransparentActivity()); + } + + private void launchTransparentActivity() { + startActivity(new Intent(this, TransparentActivity.class)); + } +} diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/TransparentActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/TransparentActivity.java new file mode 100644 index 000000000000..1bac8bdb003a --- /dev/null +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/TransparentActivity.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.testapp; + +import android.app.Activity; +import android.os.Bundle; + +public class TransparentActivity extends Activity { + + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + setContentView(R.layout.activity_transparent); + } +} diff --git a/tests/Internal/src/android/service/wallpaper/OWNERS b/tests/Internal/src/android/service/wallpaper/OWNERS new file mode 100644 index 000000000000..5a26d0e1f62b --- /dev/null +++ b/tests/Internal/src/android/service/wallpaper/OWNERS @@ -0,0 +1,4 @@ +dupin@google.com +santie@google.com +pomini@google.com +poultney@google.com
\ No newline at end of file diff --git a/tests/Internal/src/android/service/wallpaper/WallpaperServiceTest.java b/tests/Internal/src/android/service/wallpaper/WallpaperServiceTest.java index 153ca79e346b..0c5e8d481131 100644 --- a/tests/Internal/src/android/service/wallpaper/WallpaperServiceTest.java +++ b/tests/Internal/src/android/service/wallpaper/WallpaperServiceTest.java @@ -85,4 +85,17 @@ public class WallpaperServiceTest { assertEquals("onAmbientModeChanged should have been called", 2, zoomChangedCount[0]); } + @Test + public void testNotifyColorsOfDestroyedEngine_doesntCrash() { + WallpaperService service = new WallpaperService() { + @Override + public Engine onCreateEngine() { + return new Engine(); + } + }; + WallpaperService.Engine engine = service.onCreateEngine(); + engine.detach(); + + engine.notifyColorsChanged(); + } } diff --git a/tests/testables/src/android/testing/TestableLooper.java b/tests/testables/src/android/testing/TestableLooper.java index edd6dd3468ef..82e40b1eee6b 100644 --- a/tests/testables/src/android/testing/TestableLooper.java +++ b/tests/testables/src/android/testing/TestableLooper.java @@ -32,6 +32,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Field; import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; /** * This is a wrapper around {@link TestLooperManager} to make it easier to manage @@ -55,7 +56,6 @@ public class TestableLooper { private MessageHandler mMessageHandler; private Handler mHandler; - private Runnable mEmptyMessage; private TestLooperManager mQueueWrapper; static { @@ -121,8 +121,12 @@ public class TestableLooper { * @param num Number of messages to parse */ public int processMessages(int num) { + return processMessagesInternal(num, null); + } + + private int processMessagesInternal(int num, Runnable barrierRunnable) { for (int i = 0; i < num; i++) { - if (!parseMessageInt()) { + if (!processSingleMessage(barrierRunnable)) { return i + 1; } } @@ -130,6 +134,27 @@ public class TestableLooper { } /** + * Process up to a certain number of messages, not blocking if the queue has less messages than + * that + * @param num the maximum number of messages to process + * @return the number of messages processed. This will be at most {@code num}. + */ + + public int processMessagesNonBlocking(int num) { + final AtomicBoolean reachedBarrier = new AtomicBoolean(false); + Runnable barrierRunnable = () -> { + reachedBarrier.set(true); + }; + mHandler.post(barrierRunnable); + waitForMessage(mQueueWrapper, mHandler, barrierRunnable); + try { + return processMessagesInternal(num, barrierRunnable) + (reachedBarrier.get() ? -1 : 0); + } finally { + mHandler.removeCallbacks(barrierRunnable); + } + } + + /** * Process messages in the queue until no more are found. */ public void processAllMessages() { @@ -165,19 +190,20 @@ public class TestableLooper { private int processQueuedMessages() { int count = 0; - mEmptyMessage = () -> { }; - mHandler.post(mEmptyMessage); - waitForMessage(mQueueWrapper, mHandler, mEmptyMessage); - while (parseMessageInt()) count++; + Runnable barrierRunnable = () -> { }; + mHandler.post(barrierRunnable); + waitForMessage(mQueueWrapper, mHandler, barrierRunnable); + while (processSingleMessage(barrierRunnable)) count++; return count; } - private boolean parseMessageInt() { + private boolean processSingleMessage(Runnable barrierRunnable) { try { Message result = mQueueWrapper.next(); if (result != null) { // This is a break message. - if (result.getCallback() == mEmptyMessage) { + if (result.getCallback() == barrierRunnable) { + mQueueWrapper.execute(result); mQueueWrapper.recycle(result); return false; } diff --git a/tests/testables/tests/src/android/testing/TestableLooperTest.java b/tests/testables/tests/src/android/testing/TestableLooperTest.java index 0f491b86626c..a02eb6b176dc 100644 --- a/tests/testables/tests/src/android/testing/TestableLooperTest.java +++ b/tests/testables/tests/src/android/testing/TestableLooperTest.java @@ -27,12 +27,6 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.InOrder; - import android.os.Handler; import android.os.Looper; import android.os.Message; @@ -40,6 +34,11 @@ import android.test.suitebuilder.annotation.SmallTest; import android.testing.TestableLooper.MessageHandler; import android.testing.TestableLooper.RunWithLooper; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InOrder; + @SmallTest @RunWith(AndroidTestingRunner.class) @RunWithLooper @@ -240,4 +239,33 @@ public class TestableLooperTest { inOrder.verify(handler).dispatchMessage(messageC); } + @Test + public void testProcessMessagesNonBlocking_onlyArgNumber() { + Handler h = new Handler(mTestableLooper.getLooper()); + Runnable r = mock(Runnable.class); + + h.post(r); + h.post(r); + h.post(r); + + int processed = mTestableLooper.processMessagesNonBlocking(2); + + verify(r, times(2)).run(); + assertEquals(2, processed); + } + + @Test + public void testProcessMessagesNonBlocking_lessMessagesThanArg() { + Handler h = new Handler(mTestableLooper.getLooper()); + Runnable r = mock(Runnable.class); + + h.post(r); + h.post(r); + h.post(r); + + int processed = mTestableLooper.processMessagesNonBlocking(5); + + verify(r, times(3)).run(); + assertEquals(3, processed); + } } |