diff options
72 files changed, 1932 insertions, 1301 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index 55e1aeccd0fd..af7d6f1a5da1 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -13225,8 +13225,8 @@ package android.content.pm { public abstract class PackageManager { ctor @Deprecated public PackageManager(); method @Deprecated public abstract void addPackageToPreferred(@NonNull String); - method public abstract boolean addPermission(@NonNull android.content.pm.PermissionInfo); - method public abstract boolean addPermissionAsync(@NonNull android.content.pm.PermissionInfo); + method @Deprecated @FlaggedApi("android.permission.flags.permission_tree_apis_deprecated") public abstract boolean addPermission(@NonNull android.content.pm.PermissionInfo); + method @Deprecated @FlaggedApi("android.permission.flags.permission_tree_apis_deprecated") public abstract boolean addPermissionAsync(@NonNull android.content.pm.PermissionInfo); method @Deprecated public abstract void addPreferredActivity(@NonNull android.content.IntentFilter, int, @Nullable android.content.ComponentName[], @NonNull android.content.ComponentName); method @RequiresPermission(value="android.permission.WHITELIST_RESTRICTED_PERMISSIONS", conditional=true) public boolean addWhitelistedRestrictedPermission(@NonNull String, @NonNull String, int); method public boolean canPackageQuery(@NonNull String, @NonNull String) throws android.content.pm.PackageManager.NameNotFoundException; @@ -13367,7 +13367,7 @@ package android.content.pm { method @NonNull public java.util.List<android.content.pm.PackageManager.Property> queryServiceProperty(@NonNull String); method public void relinquishUpdateOwnership(@NonNull String); method @Deprecated public abstract void removePackageFromPreferred(@NonNull String); - method public abstract void removePermission(@NonNull String); + method @Deprecated @FlaggedApi("android.permission.flags.permission_tree_apis_deprecated") public abstract void removePermission(@NonNull String); method @RequiresPermission(value="android.permission.WHITELIST_RESTRICTED_PERMISSIONS", conditional=true) public boolean removeWhitelistedRestrictedPermission(@NonNull String, @NonNull String, int); method public void requestChecksums(@NonNull String, boolean, int, @NonNull java.util.List<java.security.cert.Certificate>, @NonNull android.content.pm.PackageManager.OnChecksumsReadyListener) throws java.security.cert.CertificateEncodingException, android.content.pm.PackageManager.NameNotFoundException; method @Nullable public abstract android.content.pm.ResolveInfo resolveActivity(@NonNull android.content.Intent, int); diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 5de46b530620..83699ac30939 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -7637,10 +7637,11 @@ package android.media { method public boolean isActive(); method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public boolean isMuted(); method public boolean isSpatialized(); - field @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public static final int MUTED_BY_APP_OPS = 8; // 0x8 + field @Deprecated @FlaggedApi("android.media.audio.muted_by_port_volume_api") @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public static final int MUTED_BY_APP_OPS = 8; // 0x8 field @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public static final int MUTED_BY_CLIENT_VOLUME = 16; // 0x10 field @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public static final int MUTED_BY_MASTER = 1; // 0x1 field @FlaggedApi("android.media.audio.muted_by_port_volume_api") @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public static final int MUTED_BY_OP_CONTROL_AUDIO = 128; // 0x80 + field @FlaggedApi("android.media.audio.muted_by_port_volume_api") @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public static final int MUTED_BY_OP_PLAY_AUDIO = 8; // 0x8 field @FlaggedApi("android.media.audio.muted_by_port_volume_api") @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public static final int MUTED_BY_PORT_VOLUME = 64; // 0x40 field @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public static final int MUTED_BY_STREAM_MUTED = 4; // 0x4 field @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public static final int MUTED_BY_STREAM_VOLUME = 2; // 0x2 diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java index 7e0a9b69b7bd..3cbea87e135e 100644 --- a/core/java/android/app/ApplicationPackageManager.java +++ b/core/java/android/app/ApplicationPackageManager.java @@ -130,7 +130,6 @@ import android.util.Slog; import android.util.Xml; import com.android.internal.annotations.GuardedBy; -import com.android.internal.annotations.Immutable; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.SomeArgs; import com.android.internal.pm.RoSystemFeatures; @@ -1020,6 +1019,33 @@ public class ApplicationPackageManager extends PackageManager { } } + @Override + public void setPageSizeAppCompatFlagsSettingsOverride(String packageName, boolean enabled) { + try { + mPM.setPageSizeAppCompatFlagsSettingsOverride(packageName, enabled); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + @Override + public boolean isPageSizeCompatEnabled(String packageName) { + try { + return mPM.isPageSizeCompatEnabled(packageName); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + @Override + public String getPageSizeCompatWarningMessage(String packageName) { + try { + return mPM.getPageSizeCompatWarningMessage(packageName); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + private static List<byte[]> encodeCertificates(List<Certificate> certs) throws CertificateEncodingException { if (certs == null) { diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java index cccfdb0938e5..94784227049d 100644 --- a/core/java/android/content/pm/ApplicationInfo.java +++ b/core/java/android/content/pm/ApplicationInfo.java @@ -1449,6 +1449,97 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { } } + /** + * Use this to report any errors during alignment checks + * + * @hide + */ + public static final int PAGE_SIZE_APP_COMPAT_FLAG_ERROR = -1; + + /** + * Initial value for mPageSizeAppCompatFlags + * + * @hide + */ + public static final int PAGE_SIZE_APP_COMPAT_FLAG_UNDEFINED = 0; + + /** + * if set, extract libs forcefully for 16 KB device and show warning dialog. + * + * @hide + */ + public static final int PAGE_SIZE_APP_COMPAT_FLAG_UNCOMPRESSED_LIBS_NOT_ALIGNED = 1 << 1; + + /** + * if set, load 4 KB aligned ELFs on 16 KB device in compat mode and show warning dialog. + * + * @hide + */ + public static final int PAGE_SIZE_APP_COMPAT_FLAG_ELF_NOT_ALIGNED = 1 << 2; + + /** + * Run in 16 KB app compat mode. This flag will be set explicitly through settings. If set, 16 + * KB app compat warning dialogs will still show up. + * + * @hide + */ + public static final int PAGE_SIZE_APP_COMPAT_FLAG_SETTINGS_OVERRIDE_ENABLED = 1 << 3; + + /** + * Disable 16 KB app compat mode through settings. It should only affect ELF loading as app is + * already installed. + * + * @hide + */ + public static final int PAGE_SIZE_APP_COMPAT_FLAG_SETTINGS_OVERRIDE_DISABLED = 1 << 4; + + /** + * Run in 16 KB app compat mode. This flag will be set explicitly through manifest. If set, hide + * the 16 KB app compat warning dialogs. This has the highest priority to enable compat mode. + * + * @hide + */ + public static final int PAGE_SIZE_APP_COMPAT_FLAG_MANIFEST_OVERRIDE_ENABLED = 1 << 5; + + /** + * Disable 16 KB app compat mode. This has the highest priority to disable compat mode. + * + * @hide + */ + public static final int PAGE_SIZE_APP_COMPAT_FLAG_MANIFEST_OVERRIDE_DISABLED = 1 << 6; + + /** + * Max value for page size app compat + * + * @hide + */ + public static final int PAGE_SIZE_APP_COMPAT_FLAG_MAX = 1 << 7; + + /** + * 16 KB app compat status for the app. App can have native shared libs which are not page + * aligned, LOAD segments inside the shared libs have to be page aligned. Apps can specify the + * override in manifest file as well. + */ + private @PageSizeAppCompatFlags int mPageSizeAppCompatFlags = + ApplicationInfo.PAGE_SIZE_APP_COMPAT_FLAG_UNDEFINED; + + /** {@hide} */ + @IntDef( + prefix = {"PAGE_SIZE_APP_COMPAT_FLAG_"}, + value = { + PAGE_SIZE_APP_COMPAT_FLAG_ERROR, + PAGE_SIZE_APP_COMPAT_FLAG_UNDEFINED, + PAGE_SIZE_APP_COMPAT_FLAG_UNCOMPRESSED_LIBS_NOT_ALIGNED, + PAGE_SIZE_APP_COMPAT_FLAG_ELF_NOT_ALIGNED, + PAGE_SIZE_APP_COMPAT_FLAG_MANIFEST_OVERRIDE_ENABLED, + PAGE_SIZE_APP_COMPAT_FLAG_MANIFEST_OVERRIDE_DISABLED, + PAGE_SIZE_APP_COMPAT_FLAG_SETTINGS_OVERRIDE_ENABLED, + PAGE_SIZE_APP_COMPAT_FLAG_SETTINGS_OVERRIDE_DISABLED, + PAGE_SIZE_APP_COMPAT_FLAG_MAX, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface PageSizeAppCompatFlags {} + /** @hide */ public String classLoaderName; @@ -1777,7 +1868,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { pw.println(prefix + "enableOnBackInvokedCallback=" + isOnBackInvokedCallbackEnabled()); pw.println(prefix + "allowCrossUidActivitySwitchFromBelow=" + allowCrossUidActivitySwitchFromBelow); - + pw.println(prefix + "mPageSizeAppCompatFlags=" + mPageSizeAppCompatFlags); } pw.println(prefix + "createTimestamp=" + createTimestamp); if (mKnownActivityEmbeddingCerts != null) { @@ -1897,6 +1988,10 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { } proto.write(ApplicationInfoProto.Detail.ALLOW_CROSS_UID_ACTIVITY_SWITCH_FROM_BELOW, allowCrossUidActivitySwitchFromBelow); + + proto.write(ApplicationInfoProto.Detail.ENABLE_PAGE_SIZE_APP_COMPAT, + mPageSizeAppCompatFlags); + proto.end(detailToken); } if (!ArrayUtils.isEmpty(mKnownActivityEmbeddingCerts)) { @@ -2024,6 +2119,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { localeConfigRes = orig.localeConfigRes; allowCrossUidActivitySwitchFromBelow = orig.allowCrossUidActivitySwitchFromBelow; createTimestamp = SystemClock.uptimeMillis(); + mPageSizeAppCompatFlags = orig.mPageSizeAppCompatFlags; } public String toString() { @@ -2128,6 +2224,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { } dest.writeInt(localeConfigRes); dest.writeInt(allowCrossUidActivitySwitchFromBelow ? 1 : 0); + dest.writeInt(mPageSizeAppCompatFlags); sForStringSet.parcel(mKnownActivityEmbeddingCerts, dest, flags); } @@ -2228,6 +2325,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { } localeConfigRes = source.readInt(); allowCrossUidActivitySwitchFromBelow = source.readInt() != 0; + mPageSizeAppCompatFlags = source.readInt(); mKnownActivityEmbeddingCerts = sForStringSet.unparcel(source); if (mKnownActivityEmbeddingCerts.isEmpty()) { @@ -2765,6 +2863,11 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { requestRawExternalStorageAccess = value; } + /** {@hide} */ + public void setPageSizeAppCompatFlags(@PageSizeAppCompatFlags int value) { + mPageSizeAppCompatFlags |= value; + } + /** * Replaces {@link #mAppClassNamesByProcess}. This takes over the ownership of the passed map. * Do not modify the argument at the callsite. diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl index 5d4babb8a36d..57c12403dec8 100644 --- a/core/java/android/content/pm/IPackageManager.aidl +++ b/core/java/android/content/pm/IPackageManager.aidl @@ -848,4 +848,10 @@ interface IPackageManager { int getAppMetadataSource(String packageName, int userId); ComponentName getDomainVerificationAgent(int userId); + + void setPageSizeAppCompatFlagsSettingsOverride(in String packageName, boolean enabled); + + boolean isPageSizeCompatEnabled(in String packageName); + + String getPageSizeCompatWarningMessage(in String packageName); } diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index d2b43b9bd2b4..23d3693628e7 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -804,7 +804,6 @@ public abstract class PackageManager { @Deprecated private void __metadata() {} - //@formatter:on // End of generated code @@ -6729,6 +6728,11 @@ public abstract class PackageManager { * If the given permission already exists, the info you supply here * will be used to update it. * + * @deprecated Support for dynamic permissions is going to be removed, and apps that use dynamic + * permissions should declare their permissions statically inside their app manifest instead. + * This method will become a no-op in a future Android release and eventually be removed from + * the SDK. + * * @param info Description of the permission to be added. * * @return Returns true if a new permission was created, false if an @@ -6739,7 +6743,9 @@ public abstract class PackageManager { * * @see #removePermission(String) */ - //@Deprecated + @SuppressWarnings("HiddenAbstractMethod") + @FlaggedApi(android.permission.flags.Flags.FLAG_PERMISSION_TREE_APIS_DEPRECATED) + @Deprecated public abstract boolean addPermission(@NonNull PermissionInfo info); /** @@ -6748,8 +6754,15 @@ public abstract class PackageManager { * allowing it to return quicker and batch a series of adds at the * expense of no guarantee the added permission will be retained if * the device is rebooted before it is written. + * + * @deprecated Support for dynamic permissions is going to be removed, and apps that use dynamic + * permissions should declare their permissions statically inside their app manifest instead. + * This method will become a no-op in a future Android release and eventually be removed from + * the SDK. */ - //@Deprecated + @SuppressWarnings("HiddenAbstractMethod") + @FlaggedApi(android.permission.flags.Flags.FLAG_PERMISSION_TREE_APIS_DEPRECATED) + @Deprecated public abstract boolean addPermissionAsync(@NonNull PermissionInfo info); /** @@ -6758,6 +6771,11 @@ public abstract class PackageManager { * -- you are only allowed to remove permissions that you are allowed * to add. * + * @deprecated Support for dynamic permissions is going to be removed, and apps that use dynamic + * permissions should declare their permissions statically inside their app manifest instead. + * This method will become a no-op in a future Android release and eventually be removed from + * the SDK. + * * @param permName The name of the permission to remove. * * @throws SecurityException if you are not allowed to remove the @@ -6765,7 +6783,9 @@ public abstract class PackageManager { * * @see #addPermission(PermissionInfo) */ - //@Deprecated + @SuppressWarnings("HiddenAbstractMethod") + @FlaggedApi(android.permission.flags.Flags.FLAG_PERMISSION_TREE_APIS_DEPRECATED) + @Deprecated public abstract void removePermission(@NonNull String permName); /** @@ -10987,6 +11007,41 @@ public abstract class PackageManager { } /** + * Set the page compat mode override for given package + * + * @hide + */ + @FlaggedApi(android.content.pm.Flags.FLAG_APP_COMPAT_OPTION_16KB) + public void setPageSizeAppCompatFlagsSettingsOverride(@NonNull String packageName, + boolean enabled) { + throw new UnsupportedOperationException( + "setPageSizeAppCompatFlagsSettingsOverride not implemented in subclass"); + } + + /** + * Check whether page size app compat mode is enabled for given package + * + * @hide + */ + @FlaggedApi(android.content.pm.Flags.FLAG_APP_COMPAT_OPTION_16KB) + public boolean isPageSizeCompatEnabled(@NonNull String packageName) { + throw new UnsupportedOperationException( + "isPageSizeCompatEnabled not implemented in subclass"); + } + + /** + * Get the page size app compat warning dialog to show at app launch time + * + * @hide + */ + @Nullable + @FlaggedApi(android.content.pm.Flags.FLAG_APP_COMPAT_OPTION_16KB) + public String getPageSizeCompatWarningMessage(@NonNull String packageName) { + throw new UnsupportedOperationException( + "getPageSizeCompatWarningMessage not implemented in subclass"); + } + + /** * Returns the harmful app warning string for the given app, or null if there is none set. * * @param packageName The full name of the desired package. diff --git a/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java b/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java index 48d0d6c777de..5ec5762c0533 100644 --- a/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java +++ b/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java @@ -392,6 +392,10 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, private int memtagMode; @ApplicationInfo.NativeHeapZeroInitialized private int nativeHeapZeroInitialized; + + @ApplicationInfo.PageSizeAppCompatFlags private int mPageSizeAppCompatFlags = + ApplicationInfo.PAGE_SIZE_APP_COMPAT_FLAG_UNDEFINED; + @Nullable @DataClass.ParcelWith(Parcelling.BuiltIn.ForBoolean.class) private Boolean requestRawExternalStorageAccess; @@ -1118,6 +1122,12 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, return nativeHeapZeroInitialized; } + @ApplicationInfo.PageSizeAppCompatFlags + @Override + public int getPageSizeAppCompatFlags() { + return mPageSizeAppCompatFlags; + } + @Override public int getNetworkSecurityConfigResourceId() { return networkSecurityConfigRes; @@ -2221,6 +2231,12 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, } @Override + public PackageImpl setPageSizeAppCompatFlags(@ApplicationInfo.PageSizeAppCompatFlags int flag) { + mPageSizeAppCompatFlags = flag; + return this; + } + + @Override public PackageImpl setNetworkSecurityConfigResourceId(int value) { networkSecurityConfigRes = value; return this; @@ -2703,6 +2719,7 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, appInfo.setKnownActivityEmbeddingCerts(mKnownActivityEmbeddingCerts); } appInfo.allowCrossUidActivitySwitchFromBelow = mAllowCrossUidActivitySwitchFromBelow; + appInfo.setPageSizeAppCompatFlags(mPageSizeAppCompatFlags); return appInfo; } @@ -3305,6 +3322,7 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, dest.writeInt(this.mIntentMatchingFlags); dest.writeIntArray(this.mAlternateLauncherIconResIds); dest.writeIntArray(this.mAlternateLauncherLabelResIds); + dest.writeInt(this.mPageSizeAppCompatFlags); } private void writeFeatureFlagState(@NonNull Parcel dest) { @@ -3499,6 +3517,7 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, this.mIntentMatchingFlags = in.readInt(); this.mAlternateLauncherIconResIds = in.createIntArray(); this.mAlternateLauncherLabelResIds = in.createIntArray(); + this.mPageSizeAppCompatFlags = in.readInt(); assignDerivedFields(); assignDerivedFields2(); diff --git a/core/java/com/android/internal/pm/pkg/parsing/ParsingPackage.java b/core/java/com/android/internal/pm/pkg/parsing/ParsingPackage.java index 67b985a61455..5062d58d4dca 100644 --- a/core/java/com/android/internal/pm/pkg/parsing/ParsingPackage.java +++ b/core/java/com/android/internal/pm/pkg/parsing/ParsingPackage.java @@ -31,7 +31,6 @@ import android.util.ArraySet; import android.util.SparseArray; import android.util.SparseIntArray; -import com.android.internal.R; import com.android.internal.pm.parsing.pkg.ParsedPackage; import com.android.internal.pm.pkg.component.ParsedActivity; import com.android.internal.pm.pkg.component.ParsedApexSystemService; @@ -280,6 +279,9 @@ public interface ParsingPackage { ParsingPackage setNativeHeapZeroInitialized( @ApplicationInfo.NativeHeapZeroInitialized int nativeHeapZeroInitialized); + /** Manifest option pageSizeCompat will populate this field */ + ParsingPackage setPageSizeAppCompatFlags(@ApplicationInfo.PageSizeAppCompatFlags int value); + ParsingPackage setRequestRawExternalStorageAccess( @Nullable Boolean requestRawExternalStorageAccess); diff --git a/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java b/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java index 5fc1276dd9f9..0f93e6e8109b 100644 --- a/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java +++ b/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java @@ -541,6 +541,7 @@ public class ParsingPackageUtils { pkg.setGwpAsanMode(-1); pkg.setMemtagMode(-1); + pkg.setPageSizeAppCompatFlags(ApplicationInfo.PAGE_SIZE_APP_COMPAT_FLAG_UNDEFINED); afterParseBaseApplication(pkg); @@ -2182,6 +2183,13 @@ public class ParsingPackageUtils { pkg.setGwpAsanMode(sa.getInt(R.styleable.AndroidManifestApplication_gwpAsanMode, -1)); pkg.setMemtagMode(sa.getInt(R.styleable.AndroidManifestApplication_memtagMode, -1)); + + if (Flags.appCompatOption16kb()) { + pkg.setPageSizeAppCompatFlags( + sa.getInt(R.styleable.AndroidManifestApplication_pageSizeCompat, + ApplicationInfo.PAGE_SIZE_APP_COMPAT_FLAG_UNDEFINED)); + } + if (sa.hasValue(R.styleable.AndroidManifestApplication_nativeHeapZeroInitialized)) { final boolean v = sa.getBoolean( R.styleable.AndroidManifestApplication_nativeHeapZeroInitialized, false); diff --git a/core/java/com/android/server/pm/pkg/AndroidPackage.java b/core/java/com/android/server/pm/pkg/AndroidPackage.java index d05f5e3950b4..70dd10f2c371 100644 --- a/core/java/com/android/server/pm/pkg/AndroidPackage.java +++ b/core/java/com/android/server/pm/pkg/AndroidPackage.java @@ -875,6 +875,14 @@ public interface AndroidPackage { int getMemtagMode(); /** + * @see ApplicationInfo#getPageSizeAppCompatFlags() + * @see R.styleable#AndroidManifestApplication_pageSizeCompat + * @hide + */ + @ApplicationInfo.PageSizeAppCompatFlags + int getPageSizeAppCompatFlags(); + + /** * TODO(b/135203078): Make all the Bundles immutable (and non-null by shared empty reference?) * @see R.styleable#AndroidManifestMetaData * @hide diff --git a/core/proto/android/content/package_item_info.proto b/core/proto/android/content/package_item_info.proto index b7408a4da381..facadeedd1f8 100644 --- a/core/proto/android/content/package_item_info.proto +++ b/core/proto/android/content/package_item_info.proto @@ -114,6 +114,7 @@ message ApplicationInfoProto { optional int32 enable_memtag = 20; optional bool native_heap_zero_init = 21; optional bool allow_cross_uid_activity_switch_from_below = 22; + optional int32 enable_page_size_app_compat = 23; } optional Detail detail = 17; repeated string overlay_paths = 18; diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml index 7ef539492aa4..cdf184c9c944 100644 --- a/core/res/res/values/attrs_manifest.xml +++ b/core/res/res/values/attrs_manifest.xml @@ -1867,8 +1867,12 @@ 16 KB device. 4 KB natives libs will be loaded app-compat mode if they are eligible. @FlaggedApi(android.content.pm.Flags.FLAG_APP_COMPAT_OPTION_16KB) --> <attr name="pageSizeCompat"> - <enum name="enabled" value="5" /> - <enum name="disabled" value="6" /> + <!-- value for enabled must match with + ApplicationInfo.PAGE_SIZE_APP_COMPAT_FLAG_MANIFEST_OVERRIDE_ENABLED --> + <enum name="enabled" value="32" /> + <!-- value for disabled must match with + ApplicationInfo.PAGE_SIZE_APP_COMPAT_FLAG_MANIFEST_OVERRIDE_DISABLED --> + <enum name="disabled" value="64" /> </attr> diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 413f0c3e0c58..d498b9191559 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -199,6 +199,21 @@ <!-- Displayed to confirm to the user that caller ID will not be restricted on the next call or in general. --> <string name="CLIRDefaultOffNextCallOff">Caller ID defaults to not restricted. Next call: Not restricted</string> + <!-- Message displayed in dialog when APK is not 16 KB aligned. [CHAR LIMIT=NONE] --> + <string name="page_size_compat_apk_warning">This app isn’t 16 KB compatible. APK alignment check failed. + This app will be run using page size compatible mode. For best compatibility, please recompile the application with 16 KB support. + For more information, see <a href=\"https://developer.android.com/16kb-page-size\">https://developer.android.com/16kb-page-size</a> </string> + + <!-- Message displayed in dialog when ELF is not 16 KB aligned. [CHAR LIMIT=NONE] --> + <string name="page_size_compat_elf_warning">This app isn’t 16 KB compatible. ELF alignment check failed. + This app will be run using page size compatible mode. For best compatibility, please recompile the application with 16 KB support. + For more information, see <a href=\"https://developer.android.com/16kb-page-size\">https://developer.android.com/16kb-page-size</a></string> + + <!-- Message displayed in dialog when APK and ELF are not 16 KB aligned. [CHAR LIMIT=NONE] --> + <string name="page_size_compat_apk_and_elf_warning">This app isn’t 16 KB compatible. APK and ELF alignment checks failed. + This app will be run using page size compatible mode. For best compatibility, please recompile the application with 16 KB support. + For more information, see <a href=\"https://developer.android.com/16kb-page-size\">https://developer.android.com/16kb-page-size</a></string> + <!-- Displayed to tell the user that caller ID is not provisioned for their SIM. --> <string name="serviceNotProvisioned">Service not provisioned.</string> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 0622d7224411..2f82011726ec 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -3312,6 +3312,11 @@ <java-symbol type="string" name="language_selection_title" /> <java-symbol type="string" name="search_language_hint" /> + <!-- Strings for page size app compat dialog --> + <java-symbol type="string" name="page_size_compat_apk_warning" /> + <java-symbol type="string" name="page_size_compat_elf_warning" /> + <java-symbol type="string" name="page_size_compat_apk_and_elf_warning" /> + <!-- Work profile unlaunchable app alert dialog--> <java-symbol type="style" name="AlertDialogWithEmergencyButton"/> <java-symbol type="string" name="work_mode_emergency_call_button" /> diff --git a/errorprone/Android.bp b/errorprone/Android.bp index b559a15c3a60..1428b8965473 100644 --- a/errorprone/Android.bp +++ b/errorprone/Android.bp @@ -31,6 +31,14 @@ java_library_host { "//external/auto:auto_service_annotations", ], + javacflags: [ + // These exports are needed because this errorprone plugin access some private classes + // of the java compiler. + "--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED", + "--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED", + "--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED", + ], + plugins: [ "//external/auto:auto_service_plugin", ], diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java index 55e90e74a404..5276d9d6a4df 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java @@ -324,8 +324,6 @@ public class PipAnimationController { private final @AnimationType int mAnimationType; private final Rect mDestinationBounds = new Rect(); - private final Point mLeashOffset = new Point(); - private T mBaseValue; protected T mCurrentValue; protected T mStartValue; @@ -340,22 +338,13 @@ public class PipAnimationController { // Flag to avoid double-end private boolean mHasRequestedEnd; - private PipTransitionAnimator(@NonNull TaskInfo taskInfo, @NonNull SurfaceControl leash, - @AnimationType int animationType, @NonNull Rect destinationBounds, - @NonNull T baseValue, @NonNull T startValue, @NonNull T endValue) { - this(taskInfo, leash, animationType, destinationBounds, new Point(), baseValue, - startValue, endValue); - } - - private PipTransitionAnimator(@NonNull TaskInfo taskInfo, @NonNull SurfaceControl leash, - @AnimationType int animationType, @NonNull Rect destinationBounds, - @NonNull Point leashOffset, @NonNull T baseValue, @NonNull T startValue, - @NonNull T endValue) { + private PipTransitionAnimator(TaskInfo taskInfo, SurfaceControl leash, + @AnimationType int animationType, + Rect destinationBounds, T baseValue, T startValue, T endValue) { mTaskInfo = taskInfo; mLeash = leash; mAnimationType = animationType; mDestinationBounds.set(destinationBounds); - mLeashOffset.set(leashOffset); mBaseValue = baseValue; mStartValue = startValue; mEndValue = endValue; @@ -507,15 +496,6 @@ public class PipAnimationController { } } - /** - * Returns the offset of the {@link #mLeash}. - */ - @NonNull - Point getLeashOffset() { - // Use copy to prevent the leash to be modified unexpectedly. - return new Point(mLeashOffset); - } - void setCurrentValue(T value) { mCurrentValue = value; } @@ -712,8 +692,8 @@ public class PipAnimationController { final Rect zeroInsets = new Rect(0, 0, 0, 0); // construct new Rect instances in case they are recycled - return new PipTransitionAnimator<Rect>(taskInfo, leash, ANIM_TYPE_BOUNDS, endBounds, - leashOffset, new Rect(baseBounds), new Rect(startBounds), new Rect(endBounds)) { + return new PipTransitionAnimator<Rect>(taskInfo, leash, ANIM_TYPE_BOUNDS, + endBounds, new Rect(baseBounds), new Rect(startBounds), new Rect(endBounds)) { private final RectEvaluator mRectEvaluator = new RectEvaluator(new Rect()); private final RectEvaluator mInsetsEvaluator = new RectEvaluator(new Rect()); @@ -740,7 +720,6 @@ public class PipAnimationController { // Use the bounds relative to the task leash in case the leash does not // start from (0, 0). final Rect relativeEndBounds = new Rect(end); - final Point leashOffset = getLeashOffset(); relativeEndBounds.offset(-leashOffset.x, -leashOffset.y); getSurfaceTransactionHelper() .crop(tx, leash, relativeEndBounds) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java index 0042ec954f32..72c1ef06806a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java @@ -507,8 +507,8 @@ public class PipTransition extends PipTransitionController { } @Override - public void onFinishResize(@NonNull TaskInfo taskInfo, @NonNull Rect destinationBounds, - @NonNull Point leashOffset, @PipAnimationController.TransitionDirection int direction, + public void onFinishResize(TaskInfo taskInfo, Rect destinationBounds, + @PipAnimationController.TransitionDirection int direction, @NonNull SurfaceControl.Transaction tx) { final boolean enteringPip = isInPipDirection(direction); if (enteringPip) { @@ -531,16 +531,12 @@ public class PipTransition extends PipTransitionController { if (mFixedRotationState != FIXED_ROTATION_TRANSITION && mFinishTransaction != null) { mFinishTransaction.merge(tx); - // Set crop and position to destination bounds to avoid flickering. + // Set window crop and position to destination bounds to avoid flickering. if (hasValidLeash) { - final Rect relativeDestinationBounds = new Rect(destinationBounds); - relativeDestinationBounds.offset(-leashOffset.x, -leashOffset.y); - mFinishTransaction - .setCrop(leash, relativeDestinationBounds) - // Note that we should set the position to the start position of - // leash then the visible region will be at the same place even if - // the crop region doesn't start at (0, 0). - .setPosition(leash, leashOffset.x, leashOffset.y); + mFinishTransaction.setWindowCrop(leash, destinationBounds.width(), + destinationBounds.height()); + mFinishTransaction.setPosition(leash, destinationBounds.left, + destinationBounds.top); } } } else { @@ -1271,8 +1267,7 @@ public class PipTransition extends PipTransitionController { mPipBoundsState.setBounds(destinationBounds); final SurfaceControl.Transaction tx = new SurfaceControl.Transaction(); - onFinishResize(pipTaskInfo, destinationBounds, animator.getLeashOffset(), - TRANSITION_DIRECTION_TO_PIP, tx); + onFinishResize(pipTaskInfo, destinationBounds, TRANSITION_DIRECTION_TO_PIP, tx); sendOnPipTransitionFinished(TRANSITION_DIRECTION_TO_PIP); if (swipePipToHomeOverlay != null) { mPipOrganizer.fadeOutAndRemoveOverlay(swipePipToHomeOverlay, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java index 6129651dc271..a273822759f6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java @@ -31,7 +31,6 @@ import android.app.PictureInPictureUiState; import android.app.TaskInfo; import android.content.ComponentName; import android.content.pm.ActivityInfo; -import android.graphics.Point; import android.graphics.Rect; import android.os.IBinder; import android.os.RemoteException; @@ -93,8 +92,7 @@ public abstract class PipTransitionController implements Transitions.TransitionH mPipOrganizer.fadeOutAndRemoveOverlay(mPipOrganizer.mPipOverlay, null /* callback */, true /* withStartDelay*/); } - onFinishResize(taskInfo, animator.getDestinationBounds(), - animator.getLeashOffset(), direction, tx); + onFinishResize(taskInfo, animator.getDestinationBounds(), direction, tx); sendOnPipTransitionFinished(direction); } @@ -114,9 +112,9 @@ public abstract class PipTransitionController implements Transitions.TransitionH * Called when transition is about to finish. This is usually for performing tasks such as * applying WindowContainerTransaction to finalize the PiP bounds and send to the framework. */ - public void onFinishResize(@NonNull TaskInfo taskInfo, @NonNull Rect destinationBounds, - @NonNull Point leashOffset, @PipAnimationController.TransitionDirection int direction, - @NonNull SurfaceControl.Transaction tx) { + public void onFinishResize(TaskInfo taskInfo, Rect destinationBounds, + @PipAnimationController.TransitionDirection int direction, + SurfaceControl.Transaction tx) { } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java index 4f0f6760a951..6e0e696e15fe 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java @@ -582,6 +582,7 @@ public class SplitScreenController implements SplitDragPolicy.Starter, @Nullable WindowContainerToken hideTaskToken) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Legacy startTask does not support hide task token"); + if (isTaskInSplitScreenForeground(taskId)) return; final int[] result = new int[1]; IRemoteAnimationRunner wrapper = new IRemoteAnimationRunner.Stub() { @Override diff --git a/libs/WindowManager/Shell/tests/flicker/pip/Android.bp b/libs/WindowManager/Shell/tests/flicker/pip/Android.bp index f40edaebec5e..ddbc681f7cac 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/Android.bp +++ b/libs/WindowManager/Shell/tests/flicker/pip/Android.bp @@ -266,26 +266,5 @@ test_module_config { test_suites: ["device-tests"], } -test_module_config { - name: "WMShellFlickerTestsPip-nonMatchParent", - base: "WMShellFlickerTestsPip", - include_filters: ["com.android.wm.shell.flicker.pip.nonmatchparent.*"], - test_suites: ["device-tests"], -} - -test_module_config { - name: "WMShellFlickerTestsPip-BottomHalfExitPipToAppViaExpandButtonTest", - base: "WMShellFlickerTestsPip", - include_filters: ["com.android.wm.shell.flicker.pip.nonmatchparent.BottomHalfExitPipToAppViaExpandButtonTest"], - test_suites: ["device-tests"], -} - -test_module_config { - name: "WMShellFlickerTestsPip-BottomHalfExitPipToAppViaIntentTest", - base: "WMShellFlickerTestsPip", - include_filters: ["com.android.wm.shell.flicker.pip.nonmatchparent.BottomHalfExitPipToAppViaIntentTest"], - test_suites: ["device-tests"], -} - // End breakdowns for WMShellFlickerTestsPip module //////////////////////////////////////////////////////////////////////////////// diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/PipTransition.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/PipTransition.kt index 7861d201cd29..c37bf3579e93 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/PipTransition.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/PipTransition.kt @@ -40,6 +40,7 @@ abstract class PipTransition(flicker: LegacyFlickerTest) : BaseTest(flicker) { @Rule val checkFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() + protected val pipApp = PipAppHelper(instrumentation) protected val displayBounds = WindowUtils.getDisplayBounds(flicker.scenario.startRotation) protected val broadcastActionTrigger = BroadcastActionTrigger(instrumentation) @@ -62,11 +63,6 @@ abstract class PipTransition(flicker: LegacyFlickerTest) : BaseTest(flicker) { } } - /** - * Defines the test app to run PIP flicker test. - */ - protected open val pipApp = PipAppHelper(instrumentation) - /** Defines the transition used to run the test */ protected open val thisTransition: FlickerBuilder.() -> Unit = {} diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/nonmatchparent/BottomHalfExitPipToAppTransition.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/nonmatchparent/BottomHalfExitPipToAppTransition.kt deleted file mode 100644 index a6f29fc3452b..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/nonmatchparent/BottomHalfExitPipToAppTransition.kt +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.flicker.pip.nonmatchparent - -import android.platform.test.annotations.Presubmit -import android.platform.test.annotations.RequiresFlagsEnabled -import android.tools.flicker.legacy.LegacyFlickerTest -import android.tools.traces.component.ComponentNameMatcher -import com.android.server.wm.flicker.helpers.BottomHalfPipAppHelper -import com.android.server.wm.flicker.helpers.PipAppHelper -import com.android.window.flags.Flags -import com.android.wm.shell.flicker.pip.common.ExitPipToAppTransition -import org.junit.Test - -/** - * Base test class to verify PIP exit animation with an activity layout to the bottom half of - * the container. - */ -@RequiresFlagsEnabled(Flags.FLAG_BETTER_SUPPORT_NON_MATCH_PARENT_ACTIVITY) -abstract class BottomHalfExitPipToAppTransition(flicker: LegacyFlickerTest) : - ExitPipToAppTransition(flicker) { - - override val pipApp: PipAppHelper = BottomHalfPipAppHelper(instrumentation) - - @Presubmit - @Test - override fun showBothAppLayersThenHidePip() { - // Disabled since the BottomHalfPipActivity just covers half of the simple activity. - } - - @Presubmit - @Test - override fun showBothAppWindowsThenHidePip() { - // Disabled since the BottomHalfPipActivity just covers half of the simple activity. - } - - @Presubmit - @Test - override fun pipAppCoversFullScreenAtEnd() { - // Disabled since the BottomHalfPipActivity just covers half of the simple activity. - } - - /** - * Checks that the [testApp] and [pipApp] are always visible since the [pipApp] only covers - * half of screen. - */ - @Presubmit - @Test - fun showBothAppLayersDuringPipTransition() { - flicker.assertLayers { - isVisible(testApp) - .isVisible(pipApp.or(ComponentNameMatcher.TRANSITION_SNAPSHOT)) - } - } - - /** - * Checks that the [testApp] and [pipApp] are always visible since the [pipApp] only covers - * half of screen. - */ - @Presubmit - @Test - fun showBothAppWindowsDuringPipTransition() { - flicker.assertWm { - isAppWindowVisible(testApp) - .isAppWindowOnTop(pipApp) - .isAppWindowVisible(pipApp) - } - } - - /** - * Verify that the [testApp] and [pipApp] covers the entire screen at the end of PIP exit - * animation since the [pipApp] will use a bottom half layout. - */ - @Presubmit - @Test - fun testPlusPipAppCoversWindowFrameAtEnd() { - flicker.assertLayersEnd { - val pipRegion = visibleRegion(pipApp).region - visibleRegion(testApp).plus(pipRegion).coversExactly(displayBounds) - } - } -}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/nonmatchparent/BottomHalfExitPipToAppViaExpandButtonTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/nonmatchparent/BottomHalfExitPipToAppViaExpandButtonTest.kt deleted file mode 100644 index f492bd444702..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/nonmatchparent/BottomHalfExitPipToAppViaExpandButtonTest.kt +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - - -package com.android.wm.shell.flicker.pip.nonmatchparent - -import android.tools.flicker.junit.FlickerParametersRunnerFactory -import android.tools.flicker.legacy.FlickerBuilder -import android.tools.flicker.legacy.LegacyFlickerTest -import org.junit.FixMethodOrder -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -/** - * Test expanding a pip window back to bottom half layout via the expand button - * - * To run this test: `atest WMShellFlickerTestsPip:BottomHalfExitPipToAppViaExpandButtonTest` - * - * Actions: - * ``` - * Launch an app in pip mode [bottomHalfPipApp], - * Launch another full screen mode [testApp] - * Expand [bottomHalfPipApp] app to bottom half layout by clicking on the pip window and - * then on the expand button - * ``` - * - * Notes: - * ``` - * 1. Some default assertions (e.g., nav bar, status bar and screen covered) - * are inherited [PipTransition] - * 2. Part of the test setup occurs automatically via - * [android.tools.flicker.legacy.runner.TransitionRunner], - * including configuring navigation mode, initial orientation and ensuring no - * apps are running before setup - * ``` - */ -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -class BottomHalfExitPipToAppViaExpandButtonTest(flicker: LegacyFlickerTest) : - BottomHalfExitPipToAppTransition(flicker) -{ - override val thisTransition: FlickerBuilder.() -> Unit = { - setup { - // launch an app behind the pip one - testApp.launchViaIntent(wmHelper) - } - transitions { - // This will bring PipApp to fullscreen - pipApp.expandPipWindowToApp(wmHelper) - // Wait until the transition idle and test and pip app still shows. - wmHelper.StateSyncBuilder().withLayerVisible(testApp).withLayerVisible(pipApp) - .withAppTransitionIdle().waitForAndVerify() - } - } -}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/nonmatchparent/BottomHalfExitPipToAppViaIntentTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/nonmatchparent/BottomHalfExitPipToAppViaIntentTest.kt deleted file mode 100644 index a76a647348a1..000000000000 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/nonmatchparent/BottomHalfExitPipToAppViaIntentTest.kt +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.flicker.pip.nonmatchparent - -import android.tools.flicker.junit.FlickerParametersRunnerFactory -import android.tools.flicker.legacy.FlickerBuilder -import android.tools.flicker.legacy.LegacyFlickerTest -import org.junit.FixMethodOrder -import org.junit.runner.RunWith -import org.junit.runners.MethodSorters -import org.junit.runners.Parameterized - -/** - * Test expanding a pip window back to bottom half layout via an intent - * - * To run this test: `atest WMShellFlickerTestsPip:BottomHalfExitPipToAppViaIntentTest` - * - * Actions: - * ``` - * Launch an app in pip mode [bottomHalfPipApp], - * Launch another full screen mode [testApp] - * Expand [bottomHalfPipApp] app to bottom half layout via an intent - * ``` - * - * Notes: - * ``` - * 1. Some default assertions (e.g., nav bar, status bar and screen covered) - * are inherited from [PipTransition] - * 2. Part of the test setup occurs automatically via - * [android.tools.flicker.legacy.runner.TransitionRunner], - * including configuring navigation mode, initial orientation and ensuring no - * apps are running before setup - * ``` - */ -@RunWith(Parameterized::class) -@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -class BottomHalfExitPipToAppViaIntentTest(flicker: LegacyFlickerTest) : - BottomHalfExitPipToAppTransition(flicker) -{ - override val thisTransition: FlickerBuilder.() -> Unit = { - setup { - // launch an app behind the pip one - testApp.launchViaIntent(wmHelper) - } - transitions { - // This will bring PipApp to fullscreen - pipApp.exitPipToFullScreenViaIntent(wmHelper) - // Wait until the transition idle and test and pip app still shows. - wmHelper.StateSyncBuilder().withLayerVisible(testApp).withLayerVisible(pipApp) - .withAppTransitionIdle().waitForAndVerify() - } - } -}
\ No newline at end of file diff --git a/media/java/android/media/AudioPlaybackConfiguration.java b/media/java/android/media/AudioPlaybackConfiguration.java index 66da03144a7d..dba9cc95d902 100644 --- a/media/java/android/media/AudioPlaybackConfiguration.java +++ b/media/java/android/media/AudioPlaybackConfiguration.java @@ -283,8 +283,19 @@ public final class AudioPlaybackConfiguration implements Parcelable { * Flag used when playback is muted by AppOpsManager#OP_PLAY_AUDIO. */ @SystemApi + @FlaggedApi(FLAG_MUTED_BY_PORT_VOLUME_API) + @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) + public static final int MUTED_BY_OP_PLAY_AUDIO = (1 << 3); + /** + * @hide + * Flag used when playback is muted by AppOpsManager#OP_PLAY_AUDIO. + * @deprecated see {@link MUTED_BY_OP_PLAY_AUDIO} + */ + @SystemApi + @Deprecated + @FlaggedApi(FLAG_MUTED_BY_PORT_VOLUME_API) @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) - public static final int MUTED_BY_APP_OPS = (1 << 3); + public static final int MUTED_BY_APP_OPS = MUTED_BY_OP_PLAY_AUDIO; /** * @hide * Flag used when muted by client volume. @@ -324,7 +335,7 @@ public final class AudioPlaybackConfiguration implements Parcelable { @IntDef( flag = true, value = {MUTED_BY_MASTER, MUTED_BY_STREAM_VOLUME, MUTED_BY_STREAM_MUTED, - MUTED_BY_APP_OPS, MUTED_BY_CLIENT_VOLUME, MUTED_BY_VOLUME_SHAPER, + MUTED_BY_OP_PLAY_AUDIO, MUTED_BY_CLIENT_VOLUME, MUTED_BY_VOLUME_SHAPER, MUTED_BY_PORT_VOLUME, MUTED_BY_OP_CONTROL_AUDIO}) @Retention(RetentionPolicy.SOURCE) public @interface PlayerMuteEvent { @@ -770,7 +781,7 @@ public final class AudioPlaybackConfiguration implements Parcelable { private boolean isMuteAffectingActiveState() { return (mMutedState & MUTED_BY_CLIENT_VOLUME) != 0 || (mMutedState & MUTED_BY_VOLUME_SHAPER) != 0 - || (mMutedState & MUTED_BY_APP_OPS) != 0; + || (mMutedState & MUTED_BY_OP_PLAY_AUDIO) != 0; } /** @@ -911,8 +922,8 @@ public final class AudioPlaybackConfiguration implements Parcelable { if ((mMutedState & MUTED_BY_STREAM_MUTED) != 0) { apcToString.append("streamMute "); } - if ((mMutedState & MUTED_BY_APP_OPS) != 0) { - apcToString.append("appOps "); + if ((mMutedState & MUTED_BY_OP_PLAY_AUDIO) != 0) { + apcToString.append("opPlayAudio "); } if ((mMutedState & MUTED_BY_CLIENT_VOLUME) != 0) { apcToString.append("clientVolume "); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/activity/data/repository/ActivityManagerRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/activity/data/repository/ActivityManagerRepositoryTest.kt new file mode 100644 index 000000000000..d6ba98d65d15 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/activity/data/repository/ActivityManagerRepositoryTest.kt @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.activity.data.repository + +import android.app.ActivityManager +import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND +import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_GONE +import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_TOP_SLEEPING +import android.app.activityManager +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.log.core.Logger +import com.android.systemui.log.logcatLogBuffer +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@SmallTest +@RunWith(AndroidJUnit4::class) +class ActivityManagerRepositoryTest : SysuiTestCase() { + private val kosmos = testKosmos().useUnconfinedTestDispatcher() + private val logger = Logger(logcatLogBuffer("ActivityManagerRepositoryTest"), "tag") + + private val Kosmos.underTest by Kosmos.Fixture { realActivityManagerRepository } + + @Test + fun createIsAppVisibleFlow_fetchesInitialValue_true() = + kosmos.runTest { + whenever(activityManager.getUidImportance(THIS_UID)).thenReturn(IMPORTANCE_FOREGROUND) + + val latest by + collectLastValue(underTest.createIsAppVisibleFlow(THIS_UID, logger, LOG_TAG)) + + assertThat(latest).isTrue() + } + + @Test + fun createIsAppVisibleFlow_fetchesInitialValue_false() = + kosmos.runTest { + whenever(activityManager.getUidImportance(THIS_UID)).thenReturn(IMPORTANCE_GONE) + + val latest by + collectLastValue(underTest.createIsAppVisibleFlow(THIS_UID, logger, LOG_TAG)) + + assertThat(latest).isFalse() + } + + @Test + fun createIsAppVisibleFlow_getsImportanceUpdates() = + kosmos.runTest { + val latest by + collectLastValue(underTest.createIsAppVisibleFlow(THIS_UID, logger, LOG_TAG)) + + val listenerCaptor = argumentCaptor<ActivityManager.OnUidImportanceListener>() + verify(activityManager).addOnUidImportanceListener(listenerCaptor.capture(), any()) + val listener = listenerCaptor.firstValue + + listener.onUidImportance(THIS_UID, IMPORTANCE_GONE) + assertThat(latest).isFalse() + + listener.onUidImportance(THIS_UID, IMPORTANCE_FOREGROUND) + assertThat(latest).isTrue() + + listener.onUidImportance(THIS_UID, IMPORTANCE_TOP_SLEEPING) + assertThat(latest).isFalse() + } + + @Test + fun createIsAppVisibleFlow_ignoresUpdatesForOtherUids() = + kosmos.runTest { + val latest by + collectLastValue(underTest.createIsAppVisibleFlow(THIS_UID, logger, LOG_TAG)) + + val listenerCaptor = argumentCaptor<ActivityManager.OnUidImportanceListener>() + verify(activityManager).addOnUidImportanceListener(listenerCaptor.capture(), any()) + val listener = listenerCaptor.firstValue + + listener.onUidImportance(THIS_UID, IMPORTANCE_GONE) + assertThat(latest).isFalse() + + // WHEN another UID becomes foreground + listener.onUidImportance(THIS_UID + 2, IMPORTANCE_FOREGROUND) + + // THEN this UID still stays not visible + assertThat(latest).isFalse() + } + + @Test + fun createIsAppVisibleFlow_securityExceptionOnUidRegistration_ok() = + kosmos.runTest { + whenever(activityManager.getUidImportance(THIS_UID)).thenReturn(IMPORTANCE_GONE) + whenever(activityManager.addOnUidImportanceListener(any(), any())) + .thenThrow(SecurityException()) + + val latest by + collectLastValue(underTest.createIsAppVisibleFlow(THIS_UID, logger, LOG_TAG)) + + // Verify no crash, and we get a value emitted + assertThat(latest).isFalse() + } + + /** Regression test for b/216248574. */ + @Test + fun createIsAppVisibleFlow_getUidImportanceThrowsException_ok() = + kosmos.runTest { + whenever(activityManager.getUidImportance(any())).thenThrow(SecurityException()) + + val latest by + collectLastValue(underTest.createIsAppVisibleFlow(THIS_UID, logger, LOG_TAG)) + + // Verify no crash, and we get a value emitted + assertThat(latest).isFalse() + } + + companion object { + private const val THIS_UID = 558 + private const val LOG_TAG = "LogTag" + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt index dd8370231ef0..e288522ec212 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt @@ -25,6 +25,7 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.contextualeducation.GestureType import com.android.systemui.contextualeducation.GestureType.BACK +import com.android.systemui.contextualeducation.GestureType.HOME import com.android.systemui.education.data.repository.fakeEduClock import com.android.systemui.education.domain.interactor.KeyboardTouchpadEduInteractor import com.android.systemui.education.domain.interactor.contextualEducationInteractor @@ -52,6 +53,7 @@ import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock import org.mockito.junit.MockitoJUnit import org.mockito.kotlin.any +import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @@ -66,6 +68,7 @@ class ContextualEduUiCoordinatorTest : SysuiTestCase() { private val minDurationForNextEdu = KeyboardTouchpadEduInteractor.minIntervalBetweenEdu + 1.seconds private lateinit var underTest: ContextualEduUiCoordinator + private lateinit var previousDialog: Dialog @Mock private lateinit var dialog: Dialog @Mock private lateinit var notificationManager: NotificationManager @Mock private lateinit var accessibilityManagerWrapper: AccessibilityManagerWrapper @@ -95,9 +98,11 @@ class ContextualEduUiCoordinatorTest : SysuiTestCase() { kosmos.applicationCoroutineScope, viewModel, kosmos.applicationContext, - notificationManager + notificationManager, ) { model -> toastContent = model.message + previousDialog = dialog + dialog = mock<Dialog>() dialog } underTest.start() @@ -129,6 +134,14 @@ class ContextualEduUiCoordinatorTest : SysuiTestCase() { } @Test + fun dismissPreviousDialogOnNewDialog() = + testScope.runTest { + triggerEducation(BACK) + triggerEducation(HOME) + verify(previousDialog).dismiss() + } + + @Test fun verifyBackEduToastContent() = testScope.runTest { triggerEducation(BACK) @@ -149,14 +162,14 @@ class ContextualEduUiCoordinatorTest : SysuiTestCase() { verifyNotificationContent( R.string.back_edu_notification_title, R.string.back_edu_notification_content, - notificationCaptor.value + notificationCaptor.value, ) } private fun verifyNotificationContent( titleResId: Int, contentResId: Int, - notification: Notification + notification: Notification, ) { val expectedContent = context.getString(contentResId) val expectedTitle = context.getString(titleResId) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt new file mode 100644 index 000000000000..7fed47a4653e --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorTest.kt @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.chips.notification.domain.interactor + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.activity.data.repository.activityManagerRepository +import com.android.systemui.activity.data.repository.fake +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.statusbar.StatusBarIconView +import com.android.systemui.statusbar.notification.data.model.activeNotificationModel +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock + +@SmallTest +@RunWith(AndroidJUnit4::class) +class SingleNotificationChipInteractorTest : SysuiTestCase() { + private val kosmos = testKosmos().useUnconfinedTestDispatcher() + val factory = kosmos.singleNotificationChipInteractorFactory + + @Test + fun notificationChip_startsWithStartingModel() = + kosmos.runTest { + val icon = mock<StatusBarIconView>() + val startingNotif = activeNotificationModel(key = "notif1", statusBarChipIcon = icon) + + val underTest = factory.create(startingNotif) + + val latest by collectLastValue(underTest.notificationChip) + + assertThat(latest!!.key).isEqualTo("notif1") + assertThat(latest!!.statusBarChipIconView).isEqualTo(icon) + } + + @Test + fun notificationChip_updatesAfterSet() = + kosmos.runTest { + val originalIconView = mock<StatusBarIconView>() + val underTest = + factory.create( + activeNotificationModel(key = "notif1", statusBarChipIcon = originalIconView) + ) + + val latest by collectLastValue(underTest.notificationChip) + + val newIconView = mock<StatusBarIconView>() + underTest.setNotification( + activeNotificationModel(key = "notif1", statusBarChipIcon = newIconView) + ) + + assertThat(latest!!.key).isEqualTo("notif1") + assertThat(latest!!.statusBarChipIconView).isEqualTo(newIconView) + } + + @Test + fun notificationChip_ignoresSetWithDifferentKey() = + kosmos.runTest { + val originalIconView = mock<StatusBarIconView>() + val underTest = + factory.create( + activeNotificationModel(key = "notif1", statusBarChipIcon = originalIconView) + ) + + val latest by collectLastValue(underTest.notificationChip) + + val newIconView = mock<StatusBarIconView>() + underTest.setNotification( + activeNotificationModel(key = "other_notif", statusBarChipIcon = newIconView) + ) + + assertThat(latest!!.key).isEqualTo("notif1") + assertThat(latest!!.statusBarChipIconView).isEqualTo(originalIconView) + } + + @Test + fun notificationChip_missingStatusBarIconChipView_inConstructor_emitsNull() = + kosmos.runTest { + val underTest = + factory.create(activeNotificationModel(key = "notif1", statusBarChipIcon = null)) + + val latest by collectLastValue(underTest.notificationChip) + + assertThat(latest).isNull() + } + + @Test + fun notificationChip_missingStatusBarIconChipView_inSet_emitsNull() = + kosmos.runTest { + val startingNotif = activeNotificationModel(key = "notif1", statusBarChipIcon = mock()) + val underTest = factory.create(startingNotif) + val latest by collectLastValue(underTest.notificationChip) + assertThat(latest).isNotNull() + + underTest.setNotification( + activeNotificationModel(key = "notif1", statusBarChipIcon = null) + ) + + assertThat(latest).isNull() + } + + @Test + fun notificationChip_appIsVisibleOnCreation_emitsNull() = + kosmos.runTest { + activityManagerRepository.fake.startingIsAppVisibleValue = true + + val underTest = + factory.create( + activeNotificationModel(key = "notif", uid = UID, statusBarChipIcon = mock()) + ) + + val latest by collectLastValue(underTest.notificationChip) + + assertThat(latest).isNull() + } + + @Test + fun notificationChip_appNotVisibleOnCreation_emitsValue() = + kosmos.runTest { + activityManagerRepository.fake.startingIsAppVisibleValue = false + + val underTest = + factory.create( + activeNotificationModel(key = "notif", uid = UID, statusBarChipIcon = mock()) + ) + + val latest by collectLastValue(underTest.notificationChip) + + assertThat(latest).isNotNull() + } + + @Test + fun notificationChip_hidesWhenAppIsVisible() = + kosmos.runTest { + val underTest = + factory.create( + activeNotificationModel(key = "notif", uid = UID, statusBarChipIcon = mock()) + ) + + val latest by collectLastValue(underTest.notificationChip) + + activityManagerRepository.fake.setIsAppVisible(UID, false) + assertThat(latest).isNotNull() + + activityManagerRepository.fake.setIsAppVisible(UID, true) + assertThat(latest).isNull() + + activityManagerRepository.fake.setIsAppVisible(UID, false) + assertThat(latest).isNotNull() + } + + // Note: This test is theoretically impossible because the notification key should contain the + // UID, so if the UID changes then the key would also change and a new interactor would be + // created. But, test it just in case. + @Test + fun notificationChip_updatedUid_rechecksAppVisibility_oldObserverUnregistered() = + kosmos.runTest { + activityManagerRepository.fake.startingIsAppVisibleValue = false + + val hiddenUid = 100 + val shownUid = 101 + + val underTest = + factory.create( + activeNotificationModel( + key = "notif", + uid = hiddenUid, + statusBarChipIcon = mock(), + ) + ) + val latest by collectLastValue(underTest.notificationChip) + assertThat(latest).isNotNull() + + // WHEN the notif gets a new UID that starts as visible + activityManagerRepository.fake.startingIsAppVisibleValue = true + underTest.setNotification( + activeNotificationModel(key = "notif", uid = shownUid, statusBarChipIcon = mock()) + ) + + // THEN we re-fetch the app visibility state with the new UID, and since that UID is + // visible, we hide the chip + assertThat(latest).isNull() + } + + companion object { + private const val UID = 885 + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt index 19ed6a57d2f0..702e101d2d39 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorTest.kt @@ -16,30 +16,277 @@ package com.android.systemui.statusbar.chips.notification.domain.interactor +import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues import com.android.systemui.kosmos.testScope import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips +import com.android.systemui.statusbar.notification.data.model.activeNotificationModel +import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore +import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository +import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlin.test.Test import kotlinx.coroutines.test.runTest import org.junit.runner.RunWith +import org.mockito.kotlin.mock @SmallTest @RunWith(AndroidJUnit4::class) -@EnableFlags(StatusBarNotifChips.FLAG_NAME) class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { private val kosmos = testKosmos().useUnconfinedTestDispatcher() private val testScope = kosmos.testScope + private val activeNotificationListRepository = kosmos.activeNotificationListRepository - private val underTest = kosmos.statusBarNotificationChipsInteractor + private val underTest by lazy { + kosmos.statusBarNotificationChipsInteractor.also { it.start() } + } @Test + @DisableFlags(StatusBarNotifChips.FLAG_NAME) + fun notificationChips_flagOff_noNotifs() = + testScope.runTest { + val latest by collectLastValue(underTest.notificationChips) + + setNotifs( + listOf( + activeNotificationModel( + key = "notif", + statusBarChipIcon = mock<StatusBarIconView>(), + isPromoted = true, + ) + ) + ) + + assertThat(latest).isEmpty() + } + + @Test + @EnableFlags(StatusBarNotifChips.FLAG_NAME) + fun notificationChips_noNotifs_empty() = + testScope.runTest { + val latest by collectLastValue(underTest.notificationChips) + + setNotifs(emptyList()) + + assertThat(latest).isEmpty() + } + + @Test + @EnableFlags(StatusBarNotifChips.FLAG_NAME) + fun notificationChips_notifMissingStatusBarChipIconView_empty() = + testScope.runTest { + val latest by collectLastValue(underTest.notificationChips) + + setNotifs( + listOf( + activeNotificationModel( + key = "notif", + statusBarChipIcon = null, + isPromoted = true, + ) + ) + ) + + assertThat(latest).isEmpty() + } + + @Test + @EnableFlags(StatusBarNotifChips.FLAG_NAME) + fun notificationChips_onePromotedNotif_statusBarIconViewMatches() = + testScope.runTest { + val latest by collectLastValue(underTest.notificationChips) + + val icon = mock<StatusBarIconView>() + setNotifs( + listOf( + activeNotificationModel( + key = "notif", + statusBarChipIcon = icon, + isPromoted = true, + ) + ) + ) + + assertThat(latest).hasSize(1) + assertThat(latest!![0].key).isEqualTo("notif") + assertThat(latest!![0].statusBarChipIconView).isEqualTo(icon) + } + + @Test + @EnableFlags(StatusBarNotifChips.FLAG_NAME) + fun notificationChips_onlyForPromotedNotifs() = + testScope.runTest { + val latest by collectLastValue(underTest.notificationChips) + + val firstIcon = mock<StatusBarIconView>() + val secondIcon = mock<StatusBarIconView>() + setNotifs( + listOf( + activeNotificationModel( + key = "notif1", + statusBarChipIcon = firstIcon, + isPromoted = true, + ), + activeNotificationModel( + key = "notif2", + statusBarChipIcon = secondIcon, + isPromoted = true, + ), + activeNotificationModel( + key = "notif3", + statusBarChipIcon = mock<StatusBarIconView>(), + isPromoted = false, + ), + ) + ) + + assertThat(latest).hasSize(2) + assertThat(latest!![0].key).isEqualTo("notif1") + assertThat(latest!![0].statusBarChipIconView).isEqualTo(firstIcon) + assertThat(latest!![1].key).isEqualTo("notif2") + assertThat(latest!![1].statusBarChipIconView).isEqualTo(secondIcon) + } + + @Test + @EnableFlags(StatusBarNotifChips.FLAG_NAME) + fun notificationChips_notifUpdatesGoThrough() = + testScope.runTest { + val latest by collectLastValue(underTest.notificationChips) + + val firstIcon = mock<StatusBarIconView>() + val secondIcon = mock<StatusBarIconView>() + val thirdIcon = mock<StatusBarIconView>() + + setNotifs( + listOf( + activeNotificationModel( + key = "notif", + statusBarChipIcon = firstIcon, + isPromoted = true, + ) + ) + ) + assertThat(latest).hasSize(1) + assertThat(latest!![0].key).isEqualTo("notif") + assertThat(latest!![0].statusBarChipIconView).isEqualTo(firstIcon) + + setNotifs( + listOf( + activeNotificationModel( + key = "notif", + statusBarChipIcon = secondIcon, + isPromoted = true, + ) + ) + ) + assertThat(latest).hasSize(1) + assertThat(latest!![0].key).isEqualTo("notif") + assertThat(latest!![0].statusBarChipIconView).isEqualTo(secondIcon) + + setNotifs( + listOf( + activeNotificationModel( + key = "notif", + statusBarChipIcon = thirdIcon, + isPromoted = true, + ) + ) + ) + assertThat(latest).hasSize(1) + assertThat(latest!![0].key).isEqualTo("notif") + assertThat(latest!![0].statusBarChipIconView).isEqualTo(thirdIcon) + } + + @Test + @EnableFlags(StatusBarNotifChips.FLAG_NAME) + fun notificationChips_promotedNotifDisappearsThenReappears() = + testScope.runTest { + val latest by collectLastValue(underTest.notificationChips) + + setNotifs( + listOf( + activeNotificationModel( + key = "notif", + statusBarChipIcon = mock(), + isPromoted = true, + ) + ) + ) + assertThat(latest).hasSize(1) + assertThat(latest!![0].key).isEqualTo("notif") + + setNotifs( + listOf( + activeNotificationModel( + key = "notif", + statusBarChipIcon = mock(), + isPromoted = false, + ) + ) + ) + assertThat(latest).isEmpty() + + setNotifs( + listOf( + activeNotificationModel( + key = "notif", + statusBarChipIcon = mock(), + isPromoted = true, + ) + ) + ) + assertThat(latest).hasSize(1) + assertThat(latest!![0].key).isEqualTo("notif") + } + + @Test + @EnableFlags(StatusBarNotifChips.FLAG_NAME) + fun notificationChips_notifChangesKey() = + testScope.runTest { + val latest by collectLastValue(underTest.notificationChips) + + val firstIcon = mock<StatusBarIconView>() + val secondIcon = mock<StatusBarIconView>() + setNotifs( + listOf( + activeNotificationModel( + key = "notif|uid1", + statusBarChipIcon = firstIcon, + isPromoted = true, + ) + ) + ) + assertThat(latest).hasSize(1) + assertThat(latest!![0].key).isEqualTo("notif|uid1") + assertThat(latest!![0].statusBarChipIconView).isEqualTo(firstIcon) + + // WHEN a notification changes UID, which is a key change + setNotifs( + listOf( + activeNotificationModel( + key = "notif|uid2", + statusBarChipIcon = secondIcon, + isPromoted = true, + ) + ) + ) + + // THEN we correctly update + assertThat(latest).hasSize(1) + assertThat(latest!![0].key).isEqualTo("notif|uid2") + assertThat(latest!![0].statusBarChipIconView).isEqualTo(secondIcon) + } + + @Test + @EnableFlags(StatusBarNotifChips.FLAG_NAME) fun onPromotedNotificationChipTapped_emitsKeys() = testScope.runTest { val latest by collectValues(underTest.promotedNotificationChipTapEvent) @@ -56,6 +303,7 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { } @Test + @EnableFlags(StatusBarNotifChips.FLAG_NAME) fun onPromotedNotificationChipTapped_sameKeyTwice_emitsTwice() = testScope.runTest { val latest by collectValues(underTest.promotedNotificationChipTapEvent) @@ -67,4 +315,11 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { assertThat(latest[0]).isEqualTo("fakeKey") assertThat(latest[1]).isEqualTo("fakeKey") } + + private fun setNotifs(notifs: List<ActiveNotificationModel>) { + activeNotificationListRepository.activeNotifications.value = + ActiveNotificationsStore.Builder() + .apply { notifs.forEach { addIndividualNotif(it) } } + .build() + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt index 1b4132910555..16376c5b3850 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt @@ -22,7 +22,9 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips @@ -34,26 +36,28 @@ import com.android.systemui.statusbar.notification.shared.ActiveNotificationMode import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlin.test.Test -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest +import org.junit.Before import org.junit.runner.RunWith import org.mockito.kotlin.mock @SmallTest @RunWith(AndroidJUnit4::class) -@OptIn(ExperimentalCoroutinesApi::class) @EnableFlags(StatusBarNotifChips.FLAG_NAME) class NotifChipsViewModelTest : SysuiTestCase() { - private val kosmos = testKosmos() - private val testScope = kosmos.testScope + private val kosmos = testKosmos().useUnconfinedTestDispatcher() private val activeNotificationListRepository = kosmos.activeNotificationListRepository - private val underTest = kosmos.notifChipsViewModel + private val underTest by lazy { kosmos.notifChipsViewModel } + + @Before + fun setUp() { + kosmos.statusBarNotificationChipsInteractor.start() + } @Test fun chips_noNotifs_empty() = - testScope.runTest { + kosmos.runTest { val latest by collectLastValue(underTest.chips) setNotifs(emptyList()) @@ -63,7 +67,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { @Test fun chips_notifMissingStatusBarChipIconView_empty() = - testScope.runTest { + kosmos.runTest { val latest by collectLastValue(underTest.chips) setNotifs( @@ -81,7 +85,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { @Test fun chips_onePromotedNotif_statusBarIconViewMatches() = - testScope.runTest { + kosmos.runTest { val latest by collectLastValue(underTest.chips) val icon = mock<StatusBarIconView>() @@ -103,7 +107,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { @Test fun chips_onlyForPromotedNotifs() = - testScope.runTest { + kosmos.runTest { val latest by collectLastValue(underTest.chips) val firstIcon = mock<StatusBarIconView>() @@ -135,7 +139,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { @Test fun chips_clickingChipNotifiesInteractor() = - testScope.runTest { + kosmos.runTest { val latest by collectLastValue(underTest.chips) val latestChipTap by collectLastValue( @@ -163,7 +167,6 @@ class NotifChipsViewModelTest : SysuiTestCase() { ActiveNotificationsStore.Builder() .apply { notifs.forEach { addIndividualNotif(it) } } .build() - testScope.runCurrent() } companion object { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt index 25d5ce50e03f..eb0978eff24b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithNotifsViewModelTest.kt @@ -27,6 +27,7 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.mediaprojection.data.model.MediaProjectionState import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createTask @@ -38,6 +39,7 @@ import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.Me import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.setUpPackageManagerForMediaProjection import com.android.systemui.statusbar.chips.notification.demo.ui.viewmodel.DemoNotifChipViewModelTest.Companion.addDemoNotifChip import com.android.systemui.statusbar.chips.notification.demo.ui.viewmodel.demoNotifChipViewModel +import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips import com.android.systemui.statusbar.chips.notification.ui.viewmodel.NotifChipsViewModelTest.Companion.assertIsNotifChip import com.android.systemui.statusbar.chips.ui.model.MultipleOngoingActivityChipsModel @@ -67,6 +69,7 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.any @@ -79,7 +82,7 @@ import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) @EnableFlags(StatusBarNotifChips.FLAG_NAME) class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { - private val kosmos = testKosmos() + private val kosmos = testKosmos().useUnconfinedTestDispatcher() private val testScope = kosmos.testScope private val systemClock = kosmos.fakeSystemClock private val commandRegistry = kosmos.commandRegistry @@ -103,12 +106,13 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { .thenReturn(chipBackgroundView) } - private val underTest = kosmos.ongoingActivityChipsViewModel + private val underTest by lazy { kosmos.ongoingActivityChipsViewModel } @Before fun setUp() { setUpPackageManagerForMediaProjection(kosmos) kosmos.demoNotifChipViewModel.start() + kosmos.statusBarNotificationChipsInteractor.start() val icon = BitmapDrawable( context.resources, @@ -616,6 +620,7 @@ class OngoingActivityChipsWithNotifsViewModelTest : SysuiTestCase() { } @Test + @Ignore("b/364653005") // We'll need to re-do the animation story when we implement RON chips fun primaryChip_screenRecordStoppedViaDialog_chipHiddenWithoutAnimation() = testScope.runTest { screenRecordState.value = ScreenRecordModel.Recording diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt index e5d2cf65a389..1d7f25784327 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt @@ -83,7 +83,9 @@ import org.mockito.MockitoAnnotations class HeadsUpCoordinatorTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope - private val statusBarNotificationChipsInteractor = kosmos.statusBarNotificationChipsInteractor + private val statusBarNotificationChipsInteractor by lazy { + kosmos.statusBarNotificationChipsInteractor + } private val notifCollection = kosmos.mockNotifCollection private lateinit var coordinator: HeadsUpCoordinator diff --git a/packages/SystemUI/src/com/android/systemui/activity/ActivityManagerModule.kt b/packages/SystemUI/src/com/android/systemui/activity/ActivityManagerModule.kt new file mode 100644 index 000000000000..db315e4e0bf7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/activity/ActivityManagerModule.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.activity + +import com.android.systemui.activity.data.repository.ActivityManagerRepository +import com.android.systemui.activity.data.repository.ActivityManagerRepositoryImpl +import com.android.systemui.dagger.SysUISingleton +import dagger.Binds +import dagger.Module + +@Module +interface ActivityManagerModule { + @Binds + @SysUISingleton + fun activityManagerRepository(impl: ActivityManagerRepositoryImpl): ActivityManagerRepository +} diff --git a/packages/SystemUI/src/com/android/systemui/activity/data/repository/ActivityManagerRepository.kt b/packages/SystemUI/src/com/android/systemui/activity/data/repository/ActivityManagerRepository.kt new file mode 100644 index 000000000000..94614b70beda --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/activity/data/repository/ActivityManagerRepository.kt @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.activity.data.repository + +import android.app.ActivityManager +import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.log.core.Logger +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow +import javax.inject.Inject +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.onStart + +/** Repository for interfacing with [ActivityManager]. */ +interface ActivityManagerRepository { + /** + * Given a UID, creates a flow that emits true when the process with the given UID is visible to + * the user and false otherwise. + * + * @param identifyingLogTag a tag identifying who created this flow, used for logging. + */ + fun createIsAppVisibleFlow( + creationUid: Int, + logger: Logger, + identifyingLogTag: String, + ): Flow<Boolean> +} + +@SysUISingleton +class ActivityManagerRepositoryImpl +@Inject +constructor( + @Background private val backgroundContext: CoroutineContext, + private val activityManager: ActivityManager, +) : ActivityManagerRepository { + override fun createIsAppVisibleFlow( + creationUid: Int, + logger: Logger, + identifyingLogTag: String, + ): Flow<Boolean> { + return conflatedCallbackFlow { + val listener = + object : ActivityManager.OnUidImportanceListener { + override fun onUidImportance(uid: Int, importance: Int) { + if (uid != creationUid) { + return + } + val isAppVisible = isAppVisibleToUser(importance) + logger.d({ + "$str1: #onUidImportance. importance=$int1, isAppVisible=$bool1" + }) { + str1 = identifyingLogTag + int1 = importance + bool1 = isAppVisible + } + trySend(isAppVisible) + } + } + try { + // TODO(b/286258140): Replace this with the #addOnUidImportanceListener + // overload that filters to certain UIDs. + activityManager.addOnUidImportanceListener(listener, IMPORTANCE_CUTPOINT) + } catch (e: SecurityException) { + logger.e({ "$str1: Security exception on #addOnUidImportanceListener" }, e) { + str1 = identifyingLogTag + } + } + + awaitClose { activityManager.removeOnUidImportanceListener(listener) } + } + .distinctUntilChanged() + .onStart { + try { + val isVisibleOnStart = + isAppVisibleToUser(activityManager.getUidImportance(creationUid)) + logger.d({ "$str1: Starting UID observation. isAppVisible=$bool1" }) { + str1 = identifyingLogTag + bool1 = isVisibleOnStart + } + emit(isVisibleOnStart) + } catch (e: SecurityException) { + logger.e({ "$str1: Security exception on #getUidImportance" }, e) { + str1 = identifyingLogTag + } + emit(false) + } + } + .flowOn(backgroundContext) + } + + /** Returns true if the given [importance] represents an app that's visible to the user. */ + private fun isAppVisibleToUser(importance: Int): Boolean { + return importance <= IMPORTANCE_CUTPOINT + } + + companion object { + private const val IMPORTANCE_CUTPOINT = IMPORTANCE_FOREGROUND + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index 6a777ee7417b..d6f8957ace33 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -32,6 +32,7 @@ import com.android.systemui.BootCompleteCacheImpl; import com.android.systemui.CameraProtectionModule; import com.android.systemui.CoreStartable; import com.android.systemui.SystemUISecondaryUserService; +import com.android.systemui.activity.ActivityManagerModule; import com.android.systemui.ambient.dagger.AmbientModule; import com.android.systemui.appops.dagger.AppOpsModule; import com.android.systemui.assist.AssistModule; @@ -198,6 +199,7 @@ import javax.inject.Named; * may not appreciate that. */ @Module(includes = { + ActivityManagerModule.class, AmbientModule.class, AppOpsModule.class, AssistModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/education/ui/view/ContextualEduUiCoordinator.kt b/packages/SystemUI/src/com/android/systemui/education/ui/view/ContextualEduUiCoordinator.kt index 2a3729b1aeab..c49ba80c660b 100644 --- a/packages/SystemUI/src/com/android/systemui/education/ui/view/ContextualEduUiCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/education/ui/view/ContextualEduUiCoordinator.kt @@ -27,6 +27,7 @@ import android.os.Bundle import android.os.UserHandle import android.view.accessibility.AccessibilityManager import androidx.core.app.NotificationCompat +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application @@ -39,7 +40,6 @@ import com.android.systemui.inputdevice.tutorial.ui.view.KeyboardTouchpadTutoria import com.android.systemui.res.R import javax.inject.Inject import kotlinx.coroutines.CoroutineScope -import com.android.app.tracing.coroutines.launchTraced as launch /** * A class to show contextual education on UI based on the edu produced from @@ -107,6 +107,7 @@ constructor( } private fun showDialog(model: ContextualEduToastViewModel) { + dialog?.dismiss() dialog = createDialog(model) dialog?.show() } diff --git a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt index 49ceba834dd4..31780a56f7f0 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt @@ -37,9 +37,11 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.compose.theme.PlatformTheme import com.android.internal.annotations.VisibleForTesting import com.android.systemui.Flags +import com.android.systemui.Flags.communalHubOnMobile import com.android.systemui.ambient.touch.TouchMonitor import com.android.systemui.ambient.touch.dagger.AmbientTouchComponent import com.android.systemui.communal.dagger.Communal @@ -70,7 +72,6 @@ import java.util.function.Consumer import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine -import com.android.app.tracing.coroutines.launchTraced as launch /** * Controller that's responsible for the glanceable hub container view and its touch handling. @@ -513,14 +514,19 @@ constructor( val touchOnUmo = keyguardMediaController.isWithinMediaViewBounds(ev.x.toInt(), ev.y.toInt()) val touchOnSmartspace = lockscreenSmartspaceController.isWithinSmartspaceBounds(ev.x.toInt(), ev.y.toInt()) - if (!hubShowing && (touchOnNotifications || touchOnUmo || touchOnSmartspace)) { + val glanceableHubV2 = communalHubOnMobile() + if ( + !hubShowing && + (touchOnNotifications || touchOnUmo || touchOnSmartspace || glanceableHubV2) + ) { logger.d({ "Lockscreen touch ignored: touchOnNotifications: $bool1, touchOnUmo: $bool2, " + - "touchOnSmartspace: $bool3" + "touchOnSmartspace: $bool3, glanceableHubV2: $bool4" }) { bool1 = touchOnNotifications bool2 = touchOnUmo bool3 = touchOnSmartspace + bool4 = glanceableHubV2 } return false } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/StatusBarChipsModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/StatusBarChipsModule.kt index 8ce0dbf8e171..6db610bbc3a6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/StatusBarChipsModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/StatusBarChipsModule.kt @@ -21,7 +21,10 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.log.LogBuffer import com.android.systemui.log.LogBufferFactory import com.android.systemui.statusbar.chips.notification.demo.ui.viewmodel.DemoNotifChipViewModel +import com.android.systemui.statusbar.chips.notification.domain.interactor.StatusBarNotificationChipsInteractor +import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips import dagger.Binds +import dagger.Lazy import dagger.Module import dagger.Provides import dagger.multibindings.ClassKey @@ -41,5 +44,19 @@ abstract class StatusBarChipsModule { fun provideChipsLogBuffer(factory: LogBufferFactory): LogBuffer { return factory.create("StatusBarChips", 200) } + + @Provides + @SysUISingleton + @IntoMap + @ClassKey(StatusBarNotificationChipsInteractor::class) + fun statusBarNotificationChipsInteractorAsCoreStartable( + interactorLazy: Lazy<StatusBarNotificationChipsInteractor> + ): CoreStartable { + return if (StatusBarNotifChips.isEnabled) { + interactorLazy.get() + } else { + CoreStartable.NOP + } + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractor.kt new file mode 100644 index 000000000000..087b51032fcf --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractor.kt @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.chips.notification.domain.interactor + +import com.android.systemui.activity.data.repository.ActivityManagerRepository +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.core.Logger +import com.android.systemui.statusbar.chips.StatusBarChipLogTags.pad +import com.android.systemui.statusbar.chips.StatusBarChipsLog +import com.android.systemui.statusbar.chips.notification.domain.model.NotificationChipModel +import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map + +/** + * Interactor representing a single notification's status bar chip. + * + * [startingModel.key] dictates which notification this interactor corresponds to - all updates sent + * to this interactor via [setNotification] should only be for the notification with the same key. + * + * [StatusBarNotificationChipsInteractor] will collect all the individual instances of this + * interactor and send all the necessary information to the UI layer. + */ +@OptIn(ExperimentalCoroutinesApi::class) +class SingleNotificationChipInteractor +@AssistedInject +constructor( + @Assisted startingModel: ActiveNotificationModel, + private val activityManagerRepository: ActivityManagerRepository, + @StatusBarChipsLog private val logBuffer: LogBuffer, +) { + private val key = startingModel.key + private val logger = Logger(logBuffer, "Notif".pad()) + // [StatusBarChipLogTag] recommends a max tag length of 20, so [extraLogTag] should NOT be the + // top-level tag. It should instead be provided as the first string in each log message. + private val extraLogTag = "SingleChipInteractor[key=$key]" + + private val _notificationModel = MutableStateFlow(startingModel) + + /** + * Sets the new notification info corresponding to this interactor. The key on [model] *must* + * match the key on the original [startingModel], otherwise the update won't be processed. + */ + fun setNotification(model: ActiveNotificationModel) { + if (model.key != this.key) { + logger.w({ "$str1: received model for different key $str2" }) { + str1 = extraLogTag + str2 = model.key + } + return + } + _notificationModel.value = model + } + + private val uid: Flow<Int> = _notificationModel.map { it.uid } + + /** True if the application managing the notification is visible to the user. */ + private val isAppVisible: Flow<Boolean> = + uid.flatMapLatest { currentUid -> + activityManagerRepository.createIsAppVisibleFlow(currentUid, logger, extraLogTag) + } + + /** + * Emits this notification's status bar chip, or null if this notification shouldn't show a + * status bar chip. + */ + val notificationChip: Flow<NotificationChipModel?> = + combine(_notificationModel, isAppVisible) { notif, isAppVisible -> + if (isAppVisible) { + // If the app that posted this notification is visible, we want to hide the chip + // because information between the status bar chip and the app itself could be + // out-of-sync (like a timer that's slightly off) + null + } else { + notif.toNotificationChipModel() + } + } + + private fun ActiveNotificationModel.toNotificationChipModel(): NotificationChipModel? { + val statusBarChipIconView = this.statusBarChipIconView + if (statusBarChipIconView == null) { + logger.w({ "$str1: Can't show chip because status bar chip icon view is null" }) { + str1 = extraLogTag + } + return null + } + return NotificationChipModel(key, statusBarChipIconView) + } + + @AssistedFactory + fun interface Factory { + fun create(startingModel: ActiveNotificationModel): SingleNotificationChipInteractor + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractor.kt index 9e09671bc7bf..e8cb35b06999 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractor.kt @@ -17,16 +17,42 @@ package com.android.systemui.statusbar.chips.notification.domain.interactor import android.annotation.SuppressLint +import com.android.app.tracing.coroutines.launchTraced as launch +import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.core.Logger +import com.android.systemui.statusbar.chips.StatusBarChipLogTags.pad +import com.android.systemui.statusbar.chips.StatusBarChipsLog +import com.android.systemui.statusbar.chips.notification.domain.model.NotificationChipModel import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips +import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor +import com.android.systemui.util.kotlin.pairwise import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf /** An interactor for the notification chips shown in the status bar. */ @SysUISingleton -class StatusBarNotificationChipsInteractor @Inject constructor() { +@OptIn(ExperimentalCoroutinesApi::class) +class StatusBarNotificationChipsInteractor +@Inject +constructor( + @Background private val backgroundScope: CoroutineScope, + private val activeNotificationsInteractor: ActiveNotificationsInteractor, + private val singleNotificationChipInteractorFactory: SingleNotificationChipInteractor.Factory, + @StatusBarChipsLog private val logBuffer: LogBuffer, +) : CoreStartable { + private val logger = Logger(logBuffer, "AllNotifs".pad()) // Each chip tap is an individual event, *not* a state, which is why we're using SharedFlow not // StateFlow. There shouldn't be multiple updates per frame, which should avoid performance @@ -45,4 +71,79 @@ class StatusBarNotificationChipsInteractor @Inject constructor() { StatusBarNotifChips.assertInNewMode() _promotedNotificationChipTapEvent.emit(key) } + + /** + * A cache of interactors. Each currently-promoted notification should have a corresponding + * interactor in this map. + */ + private val promotedNotificationInteractorMap = + mutableMapOf<String, SingleNotificationChipInteractor>() + + /** + * A list of interactors. Each currently-promoted notification should have a corresponding + * interactor in this list. + */ + private val promotedNotificationInteractors = + MutableStateFlow<List<SingleNotificationChipInteractor>>(emptyList()) + + override fun start() { + if (!StatusBarNotifChips.isEnabled) { + return + } + + backgroundScope.launch("StatusBarNotificationChipsInteractor") { + activeNotificationsInteractor.promotedOngoingNotifications + .pairwise(initialValue = emptyList()) + .collect { (oldNotifs, currentNotifs) -> + val removedNotifs = oldNotifs.minus(currentNotifs.toSet()) + removedNotifs.forEach { removedNotif -> + val wasRemoved = promotedNotificationInteractorMap.remove(removedNotif.key) + if (wasRemoved == null) { + logger.w({ + "Attempted to remove $str1 from interactor map but it wasn't present" + }) { + str1 = removedNotif.key + } + } + } + currentNotifs.forEach { notif -> + val interactor = + promotedNotificationInteractorMap.computeIfAbsent(notif.key) { + singleNotificationChipInteractorFactory.create(notif) + } + interactor.setNotification(notif) + } + logger.d({ "Interactors: $str1" }) { + str1 = + promotedNotificationInteractorMap.keys.joinToString(separator = " /// ") + } + promotedNotificationInteractors.value = + promotedNotificationInteractorMap.values.toList() + } + } + } + + /** + * A flow modeling the notifications that should be shown as chips in the status bar. Emits an + * empty list if there are no notifications that should show a status bar chip. + */ + val notificationChips: Flow<List<NotificationChipModel>> = + if (StatusBarNotifChips.isEnabled) { + // For all our current interactors... + promotedNotificationInteractors.flatMapLatest { interactors -> + if (interactors.isNotEmpty()) { + // Combine each interactor's [notificationChip] flow... + val allNotificationChips: List<Flow<NotificationChipModel?>> = + interactors.map { interactor -> interactor.notificationChip } + combine(allNotificationChips) { + // ... and emit just the non-null chips + it.filterNotNull() + } + } else { + flowOf(emptyList()) + } + } + } else { + flowOf(emptyList()) + } } diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/BottomHalfPipLaunchingActivity.java b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/model/NotificationChipModel.kt index d9d4361411bb..5698ee6d1917 100644 --- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/BottomHalfPipLaunchingActivity.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/domain/model/NotificationChipModel.kt @@ -14,18 +14,9 @@ * limitations under the License. */ -package com.android.server.wm.flicker.testapp; +package com.android.systemui.statusbar.chips.notification.domain.model -import android.content.Intent; -import android.os.Bundle; +import com.android.systemui.statusbar.StatusBarIconView -public class BottomHalfPipLaunchingActivity extends SimpleActivity { - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - final Intent intent = new Intent(this, BottomHalfPipActivity.class); - startActivity(intent); - } -} +/** Modeling all the data needed to render a status bar notification chip. */ +data class NotificationChipModel(val key: String, val statusBarChipIconView: StatusBarIconView) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt index 752674854e2d..9eff627c8714 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt @@ -20,11 +20,10 @@ import android.view.View import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.statusbar.chips.notification.domain.interactor.StatusBarNotificationChipsInteractor +import com.android.systemui.statusbar.chips.notification.domain.model.NotificationChipModel import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips import com.android.systemui.statusbar.chips.ui.model.ColorsModel import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel -import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor -import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow @@ -37,7 +36,6 @@ class NotifChipsViewModel @Inject constructor( @Application private val applicationScope: CoroutineScope, - activeNotificationsInteractor: ActiveNotificationsInteractor, private val notifChipsInteractor: StatusBarNotificationChipsInteractor, ) { /** @@ -45,19 +43,14 @@ constructor( * no notifications that should show a status bar chip. */ val chips: Flow<List<OngoingActivityChipModel.Shown>> = - activeNotificationsInteractor.promotedOngoingNotifications.map { notifications -> - notifications.mapNotNull { it.toChipModel() } + notifChipsInteractor.notificationChips.map { notifications -> + notifications.map { it.toActivityChipModel() } } - /** - * Converts the notification to the [OngoingActivityChipModel] object. Returns null if the - * notification has invalid data such that it can't be displayed as a chip. - */ - private fun ActiveNotificationModel.toChipModel(): OngoingActivityChipModel.Shown? { + /** Converts the notification to the [OngoingActivityChipModel] object. */ + private fun NotificationChipModel.toActivityChipModel(): OngoingActivityChipModel.Shown { StatusBarNotifChips.assertInNewMode() - // TODO(b/364653005): Log error if there's no icon view. - val rawIcon = this.statusBarChipIconView ?: return null - val icon = OngoingActivityChipModel.ChipIcon.StatusBarView(rawIcon) + val icon = OngoingActivityChipModel.ChipIcon.StatusBarView(this.statusBarChipIconView) // TODO(b/364653005): Use the notification color if applicable. val colors = ColorsModel.Themed val onClickListener = @@ -65,7 +58,9 @@ constructor( // The notification pipeline needs everything to run on the main thread, so keep // this event on the main thread. applicationScope.launch { - notifChipsInteractor.onPromotedNotificationChipTapped(this@toChipModel.key) + notifChipsInteractor.onPromotedNotificationChipTapped( + this@toActivityChipModel.key + ) } } return OngoingActivityChipModel.Shown.IconOnly(icon, colors, onClickListener) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt index aac2cd1755d0..78926c78a368 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt @@ -25,6 +25,7 @@ import android.app.UidObserver import android.content.Context import android.view.View import androidx.annotation.VisibleForTesting +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.internal.jank.InteractionJankMonitor import com.android.systemui.CoreStartable import com.android.systemui.Dumpable @@ -58,7 +59,6 @@ import java.io.PrintWriter import java.util.concurrent.Executor import javax.inject.Inject import kotlinx.coroutines.CoroutineScope -import com.android.app.tracing.coroutines.launchTraced as launch /** A controller to handle the ongoing call chip in the collapsed status bar. */ @SysUISingleton @@ -122,9 +122,9 @@ constructor( entry.sbn.uid, entry.sbn.notification.extras.getInt( Notification.EXTRA_CALL_TYPE, - -1 + -1, ) == CALL_TYPE_ONGOING, - statusBarSwipedAway = callNotificationInfo?.statusBarSwipedAway ?: false + statusBarSwipedAway = callNotificationInfo?.statusBarSwipedAway ?: false, ) if (newOngoingCallInfo == callNotificationInfo) { return @@ -236,7 +236,7 @@ constructor( bool1 = Flags.statusBarCallChipNotificationIcon() bool2 = currentInfo.notificationIconView != null }, - { "Creating OngoingCallModel.InCall. notifIconFlag=$bool1 hasIcon=$bool2" } + { "Creating OngoingCallModel.InCall. notifIconFlag=$bool1 hasIcon=$bool2" }, ) val icon = if (Flags.statusBarCallChipNotificationIcon()) { @@ -288,7 +288,7 @@ constructor( str1 = notifModel.callType.name bool1 = notifModel.statusBarChipIconView != null }, - { "NotifInteractorCallModel: key=$str1 when=$long1 callType=$str2 hasIcon=$bool1" } + { "NotifInteractorCallModel: key=$str1 when=$long1 callType=$str2 hasIcon=$bool1" }, ) val newOngoingCallInfo = @@ -299,7 +299,7 @@ constructor( notifModel.contentIntent, notifModel.uid, isOngoing = true, - statusBarSwipedAway = callNotificationInfo?.statusBarSwipedAway ?: false + statusBarSwipedAway = callNotificationInfo?.statusBarSwipedAway ?: false, ) if (newOngoingCallInfo == callNotificationInfo) { return @@ -378,7 +378,7 @@ constructor( ActivityTransitionAnimator.Controller.fromView( backgroundView, InteractionJankMonitor.CUJ_STATUS_BAR_APP_LAUNCH_FROM_CALL_CHIP, - ) + ), ) } } @@ -455,7 +455,7 @@ constructor( /** True if the call is currently ongoing (as opposed to incoming, screening, etc.). */ val isOngoing: Boolean, /** True if the user has swiped away the status bar while in this phone call. */ - val statusBarSwipedAway: Boolean + val statusBarSwipedAway: Boolean, ) { /** * Returns true if the notification information has a valid call start time. See @@ -472,6 +472,9 @@ constructor( /** * Observer to tell us when the app that posted the ongoing call notification is visible so that * we don't show the call chip at the same time (since the timers could be out-of-sync). + * + * For a more recommended architecture implementation, see + * [com.android.systemui.activity.data.repository.ActivityManagerRepository]. */ inner class CallAppUidObserver : UidObserver() { /** True if the application managing the call is visible to the user. */ @@ -512,7 +515,7 @@ constructor( uidObserver, ActivityManager.UID_OBSERVER_PROCSTATE, ActivityManager.PROCESS_STATE_UNKNOWN, - context.opPackageName + context.opPackageName, ) isRegistered = true } catch (se: SecurityException) { @@ -537,7 +540,7 @@ constructor( uid: Int, procState: Int, procStateSeq: Long, - capability: Int + capability: Int, ) { val currentCallAppUid = callAppUid ?: return if (uid != currentCallAppUid) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt index 859f84edda39..e7fb470cfa76 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt @@ -33,6 +33,7 @@ import androidx.lifecycle.LifecycleOwner import androidx.test.filters.SmallTest import com.android.compose.animation.scene.SceneKey import com.android.systemui.Flags +import com.android.systemui.Flags.FLAG_COMMUNAL_HUB_ON_MOBILE import com.android.systemui.Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX import com.android.systemui.SysuiTestCase import com.android.systemui.ambient.touch.TouchHandler @@ -630,6 +631,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { } } + @DisableFlags(FLAG_COMMUNAL_HUB_ON_MOBILE) @Test fun onTouchEvent_shadeInteracting_movesNotDispatched() = with(kosmos) { @@ -686,6 +688,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { } } + @DisableFlags(FLAG_COMMUNAL_HUB_ON_MOBILE) @Test fun onTouchEvent_bouncerInteracting_movesNotDispatched() = with(kosmos) { @@ -718,6 +721,19 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { } } + @EnableFlags(FLAG_COMMUNAL_HUB_ON_MOBILE) + @Test + fun onTouchEvent_onLockscreenAndGlanceableHubV2_touchIgnored() = + with(kosmos) { + testScope.runTest { + // On lockscreen. + goToScene(CommunalScenes.Blank) + + assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse() + verify(containerView, never()).onTouchEvent(DOWN_EVENT) + } + } + @Test fun disposeView_destroysTouchMonitor() { clearInvocations(touchMonitor) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/activity/data/repository/ActivityManagerRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/activity/data/repository/ActivityManagerRepositoryKosmos.kt new file mode 100644 index 000000000000..a6e71333c816 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/activity/data/repository/ActivityManagerRepositoryKosmos.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.activity.data.repository + +import android.app.activityManager +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.log.core.Logger +import kotlinx.coroutines.flow.MutableStateFlow + +val Kosmos.activityManagerRepository by Kosmos.Fixture { FakeActivityManagerRepository() } + +val Kosmos.realActivityManagerRepository by + Kosmos.Fixture { ActivityManagerRepositoryImpl(testDispatcher, activityManager) } + +class FakeActivityManagerRepository : ActivityManagerRepository { + private val uidFlows = mutableMapOf<Int, MutableList<MutableStateFlow<Boolean>>>() + + var startingIsAppVisibleValue = false + + override fun createIsAppVisibleFlow( + creationUid: Int, + logger: Logger, + identifyingLogTag: String, + ): MutableStateFlow<Boolean> { + val newFlow = MutableStateFlow(startingIsAppVisibleValue) + uidFlows.computeIfAbsent(creationUid) { mutableListOf() }.add(newFlow) + return newFlow + } + + fun setIsAppVisible(uid: Int, isAppVisible: Boolean) { + uidFlows[uid]?.forEach { stateFlow -> stateFlow.value = isAppVisible } + } +} + +val ActivityManagerRepository.fake + get() = this as FakeActivityManagerRepository diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorFactoryKosmos.kt new file mode 100644 index 000000000000..1c095e11dffa --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/domain/interactor/SingleNotificationChipInteractorFactoryKosmos.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.chips.notification.domain.interactor + +import com.android.systemui.activity.data.repository.activityManagerRepository +import com.android.systemui.activity.data.repository.fake +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.statusbar.chips.statusBarChipsLogger + +val Kosmos.singleNotificationChipInteractorFactory: SingleNotificationChipInteractor.Factory by + Kosmos.Fixture { + SingleNotificationChipInteractor.Factory { startingModel -> + SingleNotificationChipInteractor( + startingModel, + activityManagerRepository.fake, + logBuffer = statusBarChipsLogger, + ) + } + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorKosmos.kt index 74c7611a6392..03e9f3d52ca3 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/domain/interactor/StatusBarNotificationChipsInteractorKosmos.kt @@ -17,6 +17,16 @@ package com.android.systemui.statusbar.chips.notification.domain.interactor import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testScope +import com.android.systemui.statusbar.chips.statusBarChipsLogger +import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor val Kosmos.statusBarNotificationChipsInteractor: StatusBarNotificationChipsInteractor by - Kosmos.Fixture { StatusBarNotificationChipsInteractor() } + Kosmos.Fixture { + StatusBarNotificationChipsInteractor( + testScope.backgroundScope, + activeNotificationsInteractor, + singleNotificationChipInteractorFactory, + logBuffer = statusBarChipsLogger, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelKosmos.kt index 68b28adb4b3a..4bcce8601d64 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelKosmos.kt @@ -19,13 +19,8 @@ package com.android.systemui.statusbar.chips.notification.ui.viewmodel import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.statusbar.chips.notification.domain.interactor.statusBarNotificationChipsInteractor -import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor val Kosmos.notifChipsViewModel: NotifChipsViewModel by Kosmos.Fixture { - NotifChipsViewModel( - applicationCoroutineScope, - activeNotificationsInteractor, - statusBarNotificationChipsInteractor, - ) + NotifChipsViewModel(applicationCoroutineScope, statusBarNotificationChipsInteractor) } diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRunnerState.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRunnerState.java index 4ab1fa1579e1..110de989b524 100644 --- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRunnerState.java +++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRunnerState.java @@ -15,22 +15,10 @@ */ package android.platform.test.ravenwood; -import static com.android.ravenwood.common.RavenwoodCommonUtils.ensureIsPublicMember; - -import static org.junit.Assert.fail; - -import android.annotation.Nullable; import android.util.Log; -import com.android.ravenwood.common.RavenwoodRuntimeException; - -import org.junit.ClassRule; -import org.junit.Rule; -import org.junit.rules.TestRule; import org.junit.runner.Description; -import java.lang.reflect.Field; - /** * Used to store various states associated with the current test runner that's inly needed * in junit-impl. @@ -52,30 +40,10 @@ public final class RavenwoodRunnerState { mRunner = runner; } - /** - * The RavenwoodConfig declared in the test class - */ - private RavenwoodConfig mConfig; - /** - * The RavenwoodRule currently in effect, declared in the test class - */ - private RavenwoodRule mRule; - private boolean mHasRavenwoodRule; private Description mMethodDescription; public void enterTestRunner() { Log.i(TAG, "enterTestRunner: " + mRunner); - - mHasRavenwoodRule = hasRavenwoodRule(mRunner.mTestJavaClass); - mConfig = extractConfiguration(mRunner.mTestJavaClass); - - if (mConfig != null) { - if (mHasRavenwoodRule) { - fail("RavenwoodConfig and RavenwoodRule cannot be used in the same class." - + " Suggest migrating to RavenwoodConfig."); - } - } - RavenwoodRuntimeEnvironmentController.initForRunner(); } @@ -85,12 +53,7 @@ public final class RavenwoodRunnerState { public void exitTestClass() { Log.i(TAG, "exitTestClass: " + mRunner.mTestJavaClass.getName()); - try { - RavenwoodRuntimeEnvironmentController.exitTestClass(); - } finally { - mConfig = null; - mRule = null; - } + RavenwoodRuntimeEnvironmentController.exitTestClass(); } public void enterTestMethod(Description description) { @@ -103,132 +66,9 @@ public final class RavenwoodRunnerState { } public void enterRavenwoodRule(RavenwoodRule rule) { - if (!mHasRavenwoodRule) { - fail("If you have a RavenwoodRule in your test, make sure the field type is" - + " RavenwoodRule so Ravenwood can detect it."); - } - if (mRule != null) { - fail("Multiple nesting RavenwoodRule's are detected in the same class," - + " which is not supported."); - } - mRule = rule; RavenwoodRuntimeEnvironmentController.setSystemProperties(rule.mSystemProperties); } public void exitRavenwoodRule(RavenwoodRule rule) { - if (mRule != rule) { - fail("RavenwoodRule did not take effect."); - } - mRule = null; - } - - /** - * @return a configuration from a test class, if any. - */ - @Nullable - private static RavenwoodConfig extractConfiguration(Class<?> testClass) { - var field = findConfigurationField(testClass); - if (field == null) { - return null; - } - - try { - return (RavenwoodConfig) field.get(null); - } catch (IllegalAccessException e) { - throw new RavenwoodRuntimeException("Failed to fetch from the configuration field", e); - } - } - - /** - * @return true if the current target class (or its super classes) has any @Rule / @ClassRule - * fields of type RavenwoodRule. - * - * Note, this check won't detect cases where a Rule is of type - * {@link TestRule} and still be a {@link RavenwoodRule}. But that'll be detected at runtime - * as a failure, in {@link #enterRavenwoodRule}. - */ - private static boolean hasRavenwoodRule(Class<?> testClass) { - for (var field : testClass.getDeclaredFields()) { - if (!field.isAnnotationPresent(Rule.class) - && !field.isAnnotationPresent(ClassRule.class)) { - continue; - } - if (field.getType().equals(RavenwoodRule.class)) { - return true; - } - } - // JUnit supports rules as methods, so we need to check them too. - for (var method : testClass.getDeclaredMethods()) { - if (!method.isAnnotationPresent(Rule.class) - && !method.isAnnotationPresent(ClassRule.class)) { - continue; - } - if (method.getReturnType().equals(RavenwoodRule.class)) { - return true; - } - } - // Look into the super class. - if (!testClass.getSuperclass().equals(Object.class)) { - return hasRavenwoodRule(testClass.getSuperclass()); - } - return false; - } - - /** - * Find and return a field with @RavenwoodConfig.Config, which must be of type - * RavenwoodConfig. - */ - @Nullable - private static Field findConfigurationField(Class<?> testClass) { - Field foundField = null; - - for (var field : testClass.getDeclaredFields()) { - final var hasAnot = field.isAnnotationPresent(RavenwoodConfig.Config.class); - final var isType = field.getType().equals(RavenwoodConfig.class); - - if (hasAnot) { - if (isType) { - // Good, use this field. - if (foundField != null) { - fail(String.format( - "Class %s has multiple fields with %s", - testClass.getCanonicalName(), - "@RavenwoodConfig.Config")); - } - // Make sure it's static public - ensureIsPublicMember(field, true); - - foundField = field; - } else { - fail(String.format( - "Field %s.%s has %s but type is not %s", - testClass.getCanonicalName(), - field.getName(), - "@RavenwoodConfig.Config", - "RavenwoodConfig")); - return null; // unreachable - } - } else { - if (isType) { - fail(String.format( - "Field %s.%s does not have %s but type is %s", - testClass.getCanonicalName(), - field.getName(), - "@RavenwoodConfig.Config", - "RavenwoodConfig")); - return null; // unreachable - } else { - // Unrelated field, ignore. - continue; - } - } - } - if (foundField != null) { - return foundField; - } - if (!testClass.getSuperclass().equals(Object.class)) { - return findConfigurationField(testClass.getSuperclass()); - } - return null; } } diff --git a/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodMultipleRuleTest.java b/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodMultipleRuleTest.java deleted file mode 100644 index c25d2b4cbc4d..000000000000 --- a/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodMultipleRuleTest.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.ravenwoodtest.bivalenttest; - -import android.platform.test.ravenwood.RavenwoodConfig; -import android.platform.test.ravenwood.RavenwoodRule; - -import androidx.test.ext.junit.runners.AndroidJUnit4; - -import org.junit.Assume; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.junit.runner.RunWith; - -/** - * Make sure having multiple RavenwoodRule's is detected. - * (But only when running on ravenwod. Otherwise it'll be ignored.) - */ -@RunWith(AndroidJUnit4.class) -public class RavenwoodMultipleRuleTest { - - @Rule(order = Integer.MIN_VALUE) - public final ExpectedException mExpectedException = ExpectedException.none(); - - @Rule - public final RavenwoodRule mRavenwood1 = new RavenwoodRule(); - - @Rule - public final RavenwoodRule mRavenwood2 = new RavenwoodRule(); - - public RavenwoodMultipleRuleTest() { - // We can't call it within the test method because the exception happens before - // calling the method, so set it up here. - if (RavenwoodConfig.isOnRavenwood()) { - mExpectedException.expectMessage("Multiple nesting RavenwoodRule"); - } - } - - @Test - public void testMultipleRulesNotAllowed() { - Assume.assumeTrue(RavenwoodConfig.isOnRavenwood()); - } -} diff --git a/ravenwood/tests/coretest/test/com/android/ravenwoodtest/runnercallbacktests/RavenwoodRuleValidationTest.java b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/runnercallbacktests/RavenwoodRuleValidationTest.java new file mode 100644 index 000000000000..f9e73db23740 --- /dev/null +++ b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/runnercallbacktests/RavenwoodRuleValidationTest.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.ravenwoodtest.runnercallbacktests; + +import static com.google.common.truth.Truth.assertThat; + +import android.os.SystemProperties; +import android.platform.test.annotations.NoRavenizer; +import android.platform.test.ravenwood.RavenwoodRule; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; +import org.junit.runner.RunWith; + +/** + * Test for RavenwoodRule. + */ +@NoRavenizer // This class shouldn't be executed with RavenwoodAwareTestRunner. +public class RavenwoodRuleValidationTest extends RavenwoodRunnerTestBase { + + public static class RuleInBaseClass { + static String PROPERTY_KEY = "debug.ravenwood.prop.in.base"; + static String PROPERTY_VAL = "ravenwood"; + @Rule + public final RavenwoodRule mRavenwood1 = new RavenwoodRule.Builder() + .setSystemPropertyImmutable(PROPERTY_KEY, PROPERTY_VAL).build(); + } + + /** + * Make sure that RavenwoodRule in a base class takes effect. + */ + @RunWith(AndroidJUnit4.class) + // CHECKSTYLE:OFF + @Expected(""" + testRunStarted: classes + testSuiteStarted: classes + testSuiteStarted: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRuleValidationTest$RuleInBaseClassSuccessTest + testStarted: testRuleInBaseClass(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRuleValidationTest$RuleInBaseClassSuccessTest) + testFinished: testRuleInBaseClass(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRuleValidationTest$RuleInBaseClassSuccessTest) + testSuiteFinished: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRuleValidationTest$RuleInBaseClassSuccessTest + testSuiteFinished: classes + testRunFinished: 1,0,0,0 + """) + // CHECKSTYLE:ON + public static class RuleInBaseClassSuccessTest extends RuleInBaseClass { + + @Test + public void testRuleInBaseClass() { + assertThat(SystemProperties.get(PROPERTY_KEY)).isEqualTo(PROPERTY_VAL); + } + } + + /** + * Same as {@link RuleInBaseClass}, but the type of the rule field is not {@link RavenwoodRule}. + */ + public abstract static class RuleWithDifferentTypeInBaseClass { + static String PROPERTY_KEY = "debug.ravenwood.prop.in.base.different.type"; + static String PROPERTY_VAL = "ravenwood"; + @Rule + public final TestRule mRavenwood1 = new RavenwoodRule.Builder() + .setSystemPropertyImmutable(PROPERTY_KEY, PROPERTY_VAL).build(); + } + + /** + * Make sure that RavenwoodRule in a base class takes effect, even if the field type is not + */ + @RunWith(AndroidJUnit4.class) + // CHECKSTYLE:OFF + @Expected(""" + testRunStarted: classes + testSuiteStarted: classes + testSuiteStarted: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRuleValidationTest$RuleWithDifferentTypeInBaseClassSuccessTest + testStarted: testRuleInBaseClass(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRuleValidationTest$RuleWithDifferentTypeInBaseClassSuccessTest) + testFinished: testRuleInBaseClass(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRuleValidationTest$RuleWithDifferentTypeInBaseClassSuccessTest) + testSuiteFinished: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRuleValidationTest$RuleWithDifferentTypeInBaseClassSuccessTest + testSuiteFinished: classes + testRunFinished: 1,0,0,0 + """) + // CHECKSTYLE:ON + public static class RuleWithDifferentTypeInBaseClassSuccessTest extends RuleWithDifferentTypeInBaseClass { + + @Test + public void testRuleInBaseClass() { + assertThat(SystemProperties.get(PROPERTY_KEY)).isEqualTo(PROPERTY_VAL); + } + } +} diff --git a/ravenwood/tests/coretest/test/com/android/ravenwoodtest/runnercallbacktests/RavenwoodRunnerConfigValidationTest.java b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/runnercallbacktests/RavenwoodRunnerConfigValidationTest.java deleted file mode 100644 index f94b98bc1fb8..000000000000 --- a/ravenwood/tests/coretest/test/com/android/ravenwoodtest/runnercallbacktests/RavenwoodRunnerConfigValidationTest.java +++ /dev/null @@ -1,484 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.ravenwoodtest.runnercallbacktests; - -import static com.google.common.truth.Truth.assertThat; - -import android.platform.test.annotations.NoRavenizer; -import android.platform.test.ravenwood.RavenwoodConfig; -import android.platform.test.ravenwood.RavenwoodRule; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.platform.app.InstrumentationRegistry; - -import org.junit.Ignore; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TestRule; -import org.junit.runner.RunWith; - - -/** - * Test for @Config field extraction and validation. - * - * TODO(b/377765941) Most of the tests here will be obsolete and deleted with b/377765941, but - * some of the tests may need to be re-implemented one way or another. (e.g. the package name - * test.) Until that happens, we'll keep all tests here but add an {@code @Ignore} instead. - */ -@NoRavenizer // This class shouldn't be executed with RavenwoodAwareTestRunner. -public class RavenwoodRunnerConfigValidationTest extends RavenwoodRunnerTestBase { - public abstract static class ConfigInBaseClass { - static String PACKAGE_NAME = "com.ConfigInBaseClass"; - - @RavenwoodConfig.Config - public static RavenwoodConfig sConfig = new RavenwoodConfig.Builder() - .setPackageName(PACKAGE_NAME).build(); - } - - /** - * Make sure a config in the base class is detected. - */ - @RunWith(AndroidJUnit4.class) - // CHECKSTYLE:OFF - @Expected(""" - testRunStarted: classes - testSuiteStarted: classes - testSuiteStarted: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ConfigInBaseClassTest - testStarted: test(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ConfigInBaseClassTest) - testFinished: test(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ConfigInBaseClassTest) - testSuiteFinished: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ConfigInBaseClassTest - testSuiteFinished: classes - testRunFinished: 1,0,0,0 - """) - // CHECKSTYLE:ON - @Ignore // Package name is no longer set via config. - public static class ConfigInBaseClassTest extends ConfigInBaseClass { - @Test - public void test() { - assertThat(InstrumentationRegistry.getInstrumentation().getContext().getPackageName()) - .isEqualTo(PACKAGE_NAME); - } - } - - /** - * Make sure a config in the base class is detected. - */ - @RunWith(AndroidJUnit4.class) - // CHECKSTYLE:OFF - @Expected(""" - testRunStarted: classes - testSuiteStarted: classes - testSuiteStarted: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ConfigOverridingTest - testStarted: test(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ConfigOverridingTest) - testFinished: test(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ConfigOverridingTest) - testSuiteFinished: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ConfigOverridingTest - testSuiteFinished: classes - testRunFinished: 1,0,0,0 - """) - // CHECKSTYLE:ON - @Ignore // Package name is no longer set via config. - public static class ConfigOverridingTest extends ConfigInBaseClass { - static String PACKAGE_NAME_OVERRIDE = "com.ConfigOverridingTest"; - - @RavenwoodConfig.Config - public static RavenwoodConfig sConfig = new RavenwoodConfig.Builder() - .setPackageName(PACKAGE_NAME_OVERRIDE).build(); - - @Test - public void test() { - assertThat(InstrumentationRegistry.getInstrumentation().getContext().getPackageName()) - .isEqualTo(PACKAGE_NAME_OVERRIDE); - } - } - - /** - * Test to make sure that if a test has a config error, the failure would be reported from - * each test method. - */ - @RunWith(AndroidJUnit4.class) - // CHECKSTYLE:OFF - @Expected(""" - testRunStarted: classes - testSuiteStarted: classes - testStarted: initializationError(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ErrorMustBeReportedFromEachTest) - testFailure: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ErrorMustBeReportedFromEachTest.sConfig expected to be public static - testFinished: initializationError(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ErrorMustBeReportedFromEachTest) - testSuiteFinished: classes - testRunFinished: 1,1,0,0 - """) - // CHECKSTYLE:ON - public static class ErrorMustBeReportedFromEachTest { - @RavenwoodConfig.Config - private static RavenwoodConfig sConfig = // Invalid because it's private. - new RavenwoodConfig.Builder().build(); - - @Test - public void testMethod1() { - } - - @Test - public void testMethod2() { - } - - @Test - public void testMethod3() { - } - } - - /** - * Invalid because there are two @Config's. - */ - @RunWith(AndroidJUnit4.class) - // CHECKSTYLE:OFF - @Expected(""" - testRunStarted: classes - testSuiteStarted: classes - testStarted: initializationError(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$DuplicateConfigTest) - testFailure: Class com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest.DuplicateConfigTest has multiple fields with @RavenwoodConfig.Config - testFinished: initializationError(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$DuplicateConfigTest) - testSuiteFinished: classes - testRunFinished: 1,1,0,0 - """) - // CHECKSTYLE:ON - public static class DuplicateConfigTest { - - @RavenwoodConfig.Config - public static RavenwoodConfig sConfig1 = - new RavenwoodConfig.Builder().build(); - - @RavenwoodConfig.Config - public static RavenwoodConfig sConfig2 = - new RavenwoodConfig.Builder().build(); - - @Test - public void testConfig() { - } - } - - /** - * @Config's must be static. - */ - @RunWith(AndroidJUnit4.class) - // CHECKSTYLE:OFF - @Expected(""" - testRunStarted: classes - testSuiteStarted: classes - testStarted: initializationError(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$NonStaticConfigTest) - testFailure: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$NonStaticConfigTest.sConfig expected to be public static - testFinished: initializationError(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$NonStaticConfigTest) - testSuiteFinished: classes - testRunFinished: 1,1,0,0 - """) - // CHECKSTYLE:ON - public static class NonStaticConfigTest { - - @RavenwoodConfig.Config - public RavenwoodConfig sConfig = - new RavenwoodConfig.Builder().build(); - - @Test - public void testConfig() { - } - } - - /** - * @Config's must be public. - */ - @RunWith(AndroidJUnit4.class) - // CHECKSTYLE:OFF - @Expected(""" - testRunStarted: classes - testSuiteStarted: classes - testStarted: initializationError(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$NonPublicConfigTest) - testFailure: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$NonPublicConfigTest.sConfig expected to be public static - testFinished: initializationError(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$NonPublicConfigTest) - testSuiteFinished: classes - testRunFinished: 1,1,0,0 - """) - // CHECKSTYLE:ON - public static class NonPublicConfigTest { - - @RavenwoodConfig.Config - RavenwoodConfig sConfig = - new RavenwoodConfig.Builder().build(); - - @Test - public void testConfig() { - } - } - - /** - * @Config's must be of type RavenwoodConfig. - */ - @RunWith(AndroidJUnit4.class) - // CHECKSTYLE:OFF - @Expected(""" - testRunStarted: classes - testSuiteStarted: classes - testStarted: initializationError(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$WrongTypeConfigTest) - testFailure: Field com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest.WrongTypeConfigTest.sConfig has @RavenwoodConfig.Config but type is not RavenwoodConfig - testFinished: initializationError(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$WrongTypeConfigTest) - testSuiteFinished: classes - testRunFinished: 1,1,0,0 - """) - // CHECKSTYLE:ON - public static class WrongTypeConfigTest { - - @RavenwoodConfig.Config - public static Object sConfig = - new RavenwoodConfig.Builder().build(); - - @Test - public void testConfig() { - } - - } - - /** - * @Rule must be of type RavenwoodRule. - */ - @RunWith(AndroidJUnit4.class) - // CHECKSTYLE:OFF - @Expected(""" - testRunStarted: classes - testSuiteStarted: classes - testSuiteStarted: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$WrongTypeRuleTest - testStarted: testConfig(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$WrongTypeRuleTest) - testFailure: If you have a RavenwoodRule in your test, make sure the field type is RavenwoodRule so Ravenwood can detect it. - testFinished: testConfig(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$WrongTypeRuleTest) - testSuiteFinished: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$WrongTypeRuleTest - testSuiteFinished: classes - testRunFinished: 1,1,0,0 - """) - // CHECKSTYLE:ON - public static class WrongTypeRuleTest { - - @Rule - public TestRule mRule = new RavenwoodRule.Builder().build(); - - @Test - public void testConfig() { - } - - } - - /** - * Config can't be used with a (instance) Rule. - */ - @RunWith(AndroidJUnit4.class) - // CHECKSTYLE:OFF - @Expected(""" - testRunStarted: classes - testSuiteStarted: classes - testStarted: initializationError(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$WithInstanceRuleTest) - testFailure: RavenwoodConfig and RavenwoodRule cannot be used in the same class. Suggest migrating to RavenwoodConfig. - testFinished: initializationError(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$WithInstanceRuleTest) - testSuiteFinished: classes - testRunFinished: 1,1,0,0 - """) - // CHECKSTYLE:ON - public static class WithInstanceRuleTest { - - @RavenwoodConfig.Config - public static RavenwoodConfig sConfig = - new RavenwoodConfig.Builder().build(); - - @Rule - public RavenwoodRule mRule = new RavenwoodRule.Builder().build(); - - @Test - public void testConfig() { - } - } - - /** - * Config can't be used with a (static) Rule. - */ - @RunWith(AndroidJUnit4.class) - // CHECKSTYLE:OFF - @Expected(""" - testRunStarted: classes - testSuiteStarted: classes - testStarted: initializationError(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$WithStaticRuleTest) - testFailure: Failed to instantiate class androidx.test.ext.junit.runners.AndroidJUnit4 - testFinished: initializationError(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$WithStaticRuleTest) - testSuiteFinished: classes - testRunFinished: 1,1,0,0 - """) - // CHECKSTYLE:ON - public static class WithStaticRuleTest { - - @RavenwoodConfig.Config - public static RavenwoodConfig sConfig = - new RavenwoodConfig.Builder().build(); - - @Rule - public static RavenwoodRule sRule = new RavenwoodRule.Builder().build(); - - @Test - public void testConfig() { - } - } - - @RunWith(AndroidJUnit4.class) - // CHECKSTYLE:OFF - @Expected(""" - testRunStarted: classes - testSuiteStarted: classes - testSuiteStarted: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$DuplicateRulesTest - testStarted: testMultipleRulesNotAllowed(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$DuplicateRulesTest) - testFailure: Multiple nesting RavenwoodRule's are detected in the same class, which is not supported. - testFinished: testMultipleRulesNotAllowed(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$DuplicateRulesTest) - testSuiteFinished: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$DuplicateRulesTest - testSuiteFinished: classes - testRunFinished: 1,1,0,0 - """) - // CHECKSTYLE:ON - public static class DuplicateRulesTest { - - @Rule - public final RavenwoodRule mRavenwood1 = new RavenwoodRule(); - - @Rule - public final RavenwoodRule mRavenwood2 = new RavenwoodRule(); - - @Test - public void testMultipleRulesNotAllowed() { - } - } - - public static class RuleInBaseClass { - static String PACKAGE_NAME = "com.RuleInBaseClass"; - @Rule - public final RavenwoodRule mRavenwood1 = new RavenwoodRule.Builder() - .setPackageName(PACKAGE_NAME).build(); - } - - /** - * Make sure that RavenwoodRule in a base class takes effect. - */ - @RunWith(AndroidJUnit4.class) - // CHECKSTYLE:OFF - @Expected(""" - testRunStarted: classes - testSuiteStarted: classes - testSuiteStarted: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$RuleInBaseClassSuccessTest - testStarted: testRuleInBaseClass(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$RuleInBaseClassSuccessTest) - testFinished: testRuleInBaseClass(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$RuleInBaseClassSuccessTest) - testSuiteFinished: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$RuleInBaseClassSuccessTest - testSuiteFinished: classes - testRunFinished: 1,0,0,0 - """) - // CHECKSTYLE:ON - @Ignore // Package name is no longer set via config. - public static class RuleInBaseClassSuccessTest extends RuleInBaseClass { - - @Test - public void testRuleInBaseClass() { - assertThat(InstrumentationRegistry.getInstrumentation().getContext().getPackageName()) - .isEqualTo(PACKAGE_NAME); - } - } - - /** - * Make sure that having a config and a rule in a base class should fail. - * RavenwoodRule. - */ - @RunWith(AndroidJUnit4.class) - // CHECKSTYLE:OFF - @Expected(""" - testRunStarted: classes - testSuiteStarted: classes - testStarted: initializationError(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ConfigWithRuleInBaseClassTest) - testFailure: RavenwoodConfig and RavenwoodRule cannot be used in the same class. Suggest migrating to RavenwoodConfig. - testFinished: initializationError(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ConfigWithRuleInBaseClassTest) - testSuiteFinished: classes - testRunFinished: 1,1,0,0 - """) - // CHECKSTYLE:ON - public static class ConfigWithRuleInBaseClassTest extends RuleInBaseClass { - @RavenwoodConfig.Config - public static RavenwoodConfig sConfig = new RavenwoodConfig.Builder().build(); - - @Test - public void test() { - } - } - - /** - * Same as {@link RuleInBaseClass}, but the type of the rule field is not {@link RavenwoodRule}. - */ - public abstract static class RuleWithDifferentTypeInBaseClass { - static String PACKAGE_NAME = "com.RuleWithDifferentTypeInBaseClass"; - @Rule - public final TestRule mRavenwood1 = new RavenwoodRule.Builder() - .setPackageName(PACKAGE_NAME).build(); - } - - /** - * Make sure that RavenwoodRule in a base class takes effect, even if the field type is not - */ - @RunWith(AndroidJUnit4.class) - // CHECKSTYLE:OFF - @Expected(""" - testRunStarted: classes - testSuiteStarted: classes - testSuiteStarted: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$RuleWithDifferentTypeInBaseClassSuccessTest - testStarted: testRuleInBaseClass(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$RuleWithDifferentTypeInBaseClassSuccessTest) - testFailure: If you have a RavenwoodRule in your test, make sure the field type is RavenwoodRule so Ravenwood can detect it. - testFinished: testRuleInBaseClass(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$RuleWithDifferentTypeInBaseClassSuccessTest) - testSuiteFinished: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$RuleWithDifferentTypeInBaseClassSuccessTest - testSuiteFinished: classes - testRunFinished: 1,1,0,0 - """) - // CHECKSTYLE:ON - @Ignore // Package name is no longer set via config. - public static class RuleWithDifferentTypeInBaseClassSuccessTest extends RuleWithDifferentTypeInBaseClass { - - @Test - public void testRuleInBaseClass() { - assertThat(InstrumentationRegistry.getInstrumentation().getContext().getPackageName()) - .isEqualTo(PACKAGE_NAME); - } - } - - /** - * Make sure that having a config and a rule in a base class should fail, even if the field type is not - * RavenwoodRule. - */ - @RunWith(AndroidJUnit4.class) - // CHECKSTYLE:OFF - @Expected(""" - testRunStarted: classes - testSuiteStarted: classes - testSuiteStarted: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ConfigWithRuleWithDifferentTypeInBaseClassTest - testStarted: test(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ConfigWithRuleWithDifferentTypeInBaseClassTest) - testFailure: If you have a RavenwoodRule in your test, make sure the field type is RavenwoodRule so Ravenwood can detect it. - testFinished: test(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ConfigWithRuleWithDifferentTypeInBaseClassTest) - testSuiteFinished: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ConfigWithRuleWithDifferentTypeInBaseClassTest - testSuiteFinished: classes - testRunFinished: 1,1,0,0 - """) - // CHECKSTYLE:ON - public static class ConfigWithRuleWithDifferentTypeInBaseClassTest extends RuleWithDifferentTypeInBaseClass { - @RavenwoodConfig.Config - public static RavenwoodConfig sConfig = new RavenwoodConfig.Builder().build(); - - @Test - public void test() { - } - } -} diff --git a/ravenwood/tests/runtime-test/test/com/android/ravenwoodtest/runtimetest/IdentityTest.java b/ravenwood/tests/runtime-test/test/com/android/ravenwoodtest/runtimetest/IdentityTest.java index 8e04b698c9d9..271c27f6ae93 100644 --- a/ravenwood/tests/runtime-test/test/com/android/ravenwoodtest/runtimetest/IdentityTest.java +++ b/ravenwood/tests/runtime-test/test/com/android/ravenwoodtest/runtimetest/IdentityTest.java @@ -15,7 +15,6 @@ */ package com.android.ravenwoodtest.runtimetest; -import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE; import static android.os.Process.FIRST_APPLICATION_UID; import static org.junit.Assert.assertEquals; @@ -23,7 +22,6 @@ import static org.junit.Assert.assertEquals; import android.os.Binder; import android.os.Build; import android.os.Process; -import android.platform.test.ravenwood.RavenwoodConfig; import android.system.Os; import com.android.ravenwood.RavenwoodRuntimeState; @@ -34,13 +32,6 @@ import org.junit.Test; public class IdentityTest { - @RavenwoodConfig.Config - public static final RavenwoodConfig sConfig = - new RavenwoodConfig.Builder() - .setTargetSdkLevel(UPSIDE_DOWN_CAKE) - .setProcessApp() - .build(); - @Test public void testUid() { assertEquals(FIRST_APPLICATION_UID, RavenwoodRuntimeState.sUid); @@ -60,7 +51,7 @@ public class IdentityTest { @Test public void testTargetSdkLevel() { assertEquals(Build.VERSION_CODES.CUR_DEVELOPMENT, RavenwoodRuntimeState.CUR_DEVELOPMENT); - assertEquals(UPSIDE_DOWN_CAKE, RavenwoodRuntimeState.sTargetSdkLevel); - assertEquals(UPSIDE_DOWN_CAKE, VMRuntime.getRuntime().getTargetSdkVersion()); + assertEquals(RavenwoodRuntimeState.sTargetSdkLevel, + VMRuntime.getRuntime().getTargetSdkVersion()); } } diff --git a/ravenwood/tests/services-test/test/com/android/ravenwoodtest/servicestest/RavenwoodServicesTest.java b/ravenwood/tests/services-test/test/com/android/ravenwoodtest/servicestest/RavenwoodServicesTest.java index 4aae1e11b72e..e83a247bd769 100644 --- a/ravenwood/tests/services-test/test/com/android/ravenwoodtest/servicestest/RavenwoodServicesTest.java +++ b/ravenwood/tests/services-test/test/com/android/ravenwoodtest/servicestest/RavenwoodServicesTest.java @@ -24,8 +24,6 @@ import static org.junit.Assert.fail; import android.content.Context; import android.hardware.SerialManager; import android.hardware.SerialManagerInternal; -import android.platform.test.ravenwood.RavenwoodConfig; -import android.platform.test.ravenwood.RavenwoodConfig.Config; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.platform.app.InstrumentationRegistry; @@ -42,12 +40,6 @@ import org.junit.runner.RunWith; public class RavenwoodServicesTest { private static final String TEST_VIRTUAL_PORT = "virtual:example"; - @Config - public static final RavenwoodConfig sRavenwood = new RavenwoodConfig.Builder() - .setProcessSystem() - .setServicesRequired(SerialManager.class) - .build(); - private Context mContext; @Before diff --git a/services/core/java/com/android/server/BatteryService.java b/services/core/java/com/android/server/BatteryService.java index 3dcca1433dec..4cf17ae3984d 100644 --- a/services/core/java/com/android/server/BatteryService.java +++ b/services/core/java/com/android/server/BatteryService.java @@ -69,6 +69,7 @@ import android.service.battery.BatteryServiceDumpProto; import android.sysprop.PowerProperties; import android.util.EventLog; import android.util.Slog; +import android.util.TimeUtils; import android.util.proto.ProtoOutputStream; import com.android.internal.annotations.VisibleForTesting; @@ -303,6 +304,17 @@ public final class BatteryService extends SystemService { */ @VisibleForTesting public long mLastBroadcastVoltageUpdateTime; + /** + * Time when the max charging current was updated last by HAL and we sent the + * {@link Intent#ACTION_BATTERY_CHANGED} broadcast. + * Note: This value is used to rate limit the {@link Intent#ACTION_BATTERY_CHANGED} broadcast + * so it is possible that max current was updated but we did not send the broadcast so in that + * case we do not update the time. + */ + @VisibleForTesting + public long mLastBroadcastMaxChargingCurrentUpdateTime; + + private boolean mIsFirstBatteryChangedUpdate = true; private Led mLed; @@ -350,16 +362,21 @@ public final class BatteryService extends SystemService { private static final int ABSOLUTE_DECI_CELSIUS_DIFF_FOR_TEMP_UPDATE = 10; /** * This value is used to rate limit the {@link Intent#ACTION_BATTERY_CHANGED} broadcast. We - * only send the broadcast if the last voltage was updated at least 20s seconds back and has a + * only send the broadcast if the last voltage was updated at least 20 seconds back and has a * fluctuation of at least 1%. */ private static final int TIME_DIFF_FOR_VOLTAGE_UPDATE_MS = 20000; /** * The value is used to rate limit the {@link Intent#ACTION_BATTERY_CHANGED} broadcast. We - * only send the broadcast if the last voltage was updated at least 20s seconds back and has a + * only send the broadcast if the last voltage was updated at least 20 seconds back and has a * fluctuation of at least 1%. */ private static final float BASE_POINT_DIFF_FOR_VOLTAGE_UPDATE = 0.01f; + /** + * This value is used to rate limit the {@link Intent#ACTION_BATTERY_CHANGED} broadcast. We + * only send the broadcast if the last max charging current was updated at least 5 seconds back. + */ + private static final int TIME_DIFF_FOR_MAX_CHARGING_CURRENT_UPDATE_MS = 5000; private final Handler.Callback mLocalCallback = msg -> { switch (msg.what) { @@ -1252,8 +1269,10 @@ public final class BatteryService extends SystemService { if (!com.android.server.flags.Flags.rateLimitBatteryChangedBroadcast()) { return false; } - if (mLastBroadcastBatteryVoltage == 0 || mLastBroadcastBatteryTemperature == 0) { + if (mIsFirstBatteryChangedUpdate) { mLastBroadcastVoltageUpdateTime = SystemClock.elapsedRealtime(); + mLastBroadcastMaxChargingCurrentUpdateTime = SystemClock.elapsedRealtime(); + mIsFirstBatteryChangedUpdate = false; return false; } @@ -1261,13 +1280,14 @@ public final class BatteryService extends SystemService { mLastBroadcastBatteryVoltage != mHealthInfo.batteryVoltageMillivolts; final boolean temperatureUpdated = mLastBroadcastBatteryTemperature != mHealthInfo.batteryTemperatureTenthsCelsius; + final boolean maxChargingCurrentUpdated = + mLastBroadcastMaxChargingCurrent != mHealthInfo.maxChargingCurrentMicroamps; final boolean otherStatesUpdated = forceUpdate || mHealthInfo.batteryStatus != mLastBroadcastBatteryStatus || mHealthInfo.batteryHealth != mLastBroadcastBatteryHealth || mHealthInfo.batteryPresent != mLastBroadcastBatteryPresent || mHealthInfo.batteryLevel != mLastBroadcastBatteryLevel || mPlugType != mLastBroadcastPlugType - || mHealthInfo.maxChargingCurrentMicroamps != mLastBroadcastMaxChargingCurrent || mHealthInfo.maxChargingVoltageMicrovolts != mLastBroadcastMaxChargingVoltage || mInvalidCharger != mLastBroadcastInvalidCharger || mHealthInfo.batteryCycleCount != mLastBroadcastBatteryCycleCount @@ -1280,6 +1300,9 @@ public final class BatteryService extends SystemService { if (voltageUpdated) { mLastBroadcastVoltageUpdateTime = SystemClock.elapsedRealtime(); } + if (maxChargingCurrentUpdated) { + mLastBroadcastMaxChargingCurrentUpdateTime = SystemClock.elapsedRealtime(); + } return false; } @@ -1295,6 +1318,9 @@ public final class BatteryService extends SystemService { >= TIME_DIFF_FOR_VOLTAGE_UPDATE_MS) { mLastBroadcastVoltageUpdateTime = SystemClock.elapsedRealtime(); + if (maxChargingCurrentUpdated) { + mLastBroadcastMaxChargingCurrentUpdateTime = SystemClock.elapsedRealtime(); + } return false; } @@ -1307,6 +1333,20 @@ public final class BatteryService extends SystemService { if (voltageUpdated) { mLastBroadcastVoltageUpdateTime = SystemClock.elapsedRealtime(); } + if (maxChargingCurrentUpdated) { + mLastBroadcastMaxChargingCurrentUpdateTime = SystemClock.elapsedRealtime(); + } + return false; + } + + if (maxChargingCurrentUpdated + && SystemClock.elapsedRealtime() - mLastBroadcastMaxChargingCurrentUpdateTime + >= TIME_DIFF_FOR_MAX_CHARGING_CURRENT_UPDATE_MS) { + mLastBroadcastMaxChargingCurrentUpdateTime = SystemClock.elapsedRealtime(); + + if (voltageUpdated) { + mLastBroadcastVoltageUpdateTime = SystemClock.elapsedRealtime(); + } return false; } @@ -1615,6 +1655,9 @@ public final class BatteryService extends SystemService { pw.println(" Wireless powered: " + mHealthInfo.chargerWirelessOnline); pw.println(" Dock powered: " + mHealthInfo.chargerDockOnline); pw.println(" Max charging current: " + mHealthInfo.maxChargingCurrentMicroamps); + pw.println(" Time when the latest updated value of the Max charging current was" + + " sent via battery changed broadcast: " + + TimeUtils.formatDuration(mLastBroadcastMaxChargingCurrentUpdateTime)); pw.println(" Max charging voltage: " + mHealthInfo.maxChargingVoltageMicrovolts); pw.println(" Charge counter: " + mHealthInfo.batteryChargeCounterUah); pw.println(" status: " + mHealthInfo.batteryStatus); @@ -1624,7 +1667,8 @@ public final class BatteryService extends SystemService { pw.println(" scale: " + BATTERY_SCALE); pw.println(" voltage: " + mHealthInfo.batteryVoltageMillivolts); pw.println(" Time when the latest updated value of the voltage was sent via " - + "battery changed broadcast: " + mLastBroadcastVoltageUpdateTime); + + "battery changed broadcast: " + + TimeUtils.formatDuration(mLastBroadcastVoltageUpdateTime)); pw.println(" The last voltage value sent via the battery changed broadcast: " + mLastBroadcastBatteryVoltage); pw.println(" temperature: " + mHealthInfo.batteryTemperatureTenthsCelsius); diff --git a/services/core/java/com/android/server/TEST_MAPPING b/services/core/java/com/android/server/TEST_MAPPING index ce66dc3c76cb..8da835896bd3 100644 --- a/services/core/java/com/android/server/TEST_MAPPING +++ b/services/core/java/com/android/server/TEST_MAPPING @@ -176,6 +176,10 @@ "include-filter": "com.android.server.wm.BackgroundActivityStart*" } ] + }, + { + "name": "FrameworksMockingServicesTests_service_batteryServiceTest", + "file_patterns": ["BatteryService\\.java"] } ] } diff --git a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java index a2200c9f8bf5..1c01fb9f19e0 100644 --- a/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java +++ b/services/core/java/com/android/server/audio/PlaybackActivityMonitor.java @@ -17,7 +17,7 @@ package com.android.server.audio; import static android.media.AudioPlaybackConfiguration.EXTRA_PLAYER_EVENT_MUTE; -import static android.media.AudioPlaybackConfiguration.MUTED_BY_APP_OPS; +import static android.media.AudioPlaybackConfiguration.MUTED_BY_OP_PLAY_AUDIO; import static android.media.AudioPlaybackConfiguration.MUTED_BY_CLIENT_VOLUME; import static android.media.AudioPlaybackConfiguration.MUTED_BY_MASTER; import static android.media.AudioPlaybackConfiguration.MUTED_BY_PORT_VOLUME; @@ -1377,8 +1377,8 @@ public final class PlaybackActivityMonitor if ((eventValue & MUTED_BY_STREAM_MUTED) != 0) { builder.append("streamMute "); } - if ((eventValue & MUTED_BY_APP_OPS) != 0) { - builder.append("appOps "); + if ((eventValue & MUTED_BY_OP_PLAY_AUDIO) != 0) { + builder.append("opPlayAudio "); } if ((eventValue & MUTED_BY_CLIENT_VOLUME) != 0) { builder.append("clientVolume "); diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 715633410575..442db10ab039 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -5875,6 +5875,67 @@ public class PackageManagerService implements PackageSender, TestUtilityService userId, callingPackage); } + @Override + public void setPageSizeAppCompatFlagsSettingsOverride(String packageName, boolean enabled) { + final int callingUid = Binder.getCallingUid(); + final int callingAppId = UserHandle.getAppId(callingUid); + + if (!PackageManagerServiceUtils.isSystemOrRoot(callingAppId)) { + throw new SecurityException("Caller must be the system or root."); + } + + int settingsMode = enabled + ? ApplicationInfo.PAGE_SIZE_APP_COMPAT_FLAG_SETTINGS_OVERRIDE_ENABLED + : ApplicationInfo.PAGE_SIZE_APP_COMPAT_FLAG_SETTINGS_OVERRIDE_DISABLED; + PackageStateMutator.Result result = + commitPackageStateMutation( + null, + packageName, + packageState -> + packageState + .setPageSizeAppCompatFlags(settingsMode)); + if (result.isSpecificPackageNull()) { + throw new IllegalArgumentException("Unknown package: " + packageName); + } + scheduleWriteSettings(); + } + + @Override + public boolean isPageSizeCompatEnabled(String packageName) { + final int callingUid = Binder.getCallingUid(); + final int callingAppId = UserHandle.getAppId(callingUid); + final int userId = UserHandle.getCallingUserId(); + + if (!PackageManagerServiceUtils.isSystemOrRoot(callingAppId)) { + throw new SecurityException("Caller must be the system or root."); + } + + PackageStateInternal packageState = + snapshotComputer().getPackageStateForInstalledAndFiltered( + packageName, callingUid, userId); + + return packageState == null ? false : packageState.isPageSizeAppCompatEnabled(); + } + + @Override + public String getPageSizeCompatWarningMessage(String packageName) { + final int callingUid = Binder.getCallingUid(); + final int callingAppId = UserHandle.getAppId(callingUid); + final int userId = UserHandle.getCallingUserId(); + + if (!PackageManagerServiceUtils.isSystemOrRoot(callingAppId)) { + throw new SecurityException("Caller must be the system or root."); + } + + PackageStateInternal packageState = + snapshotComputer().getPackageStateForInstalledAndFiltered( + packageName, callingUid, userId); + + return packageState == null + ? null + : packageState.getPageSizeCompatWarningMessage(mContext); + } + @android.annotation.EnforcePermission(android.Manifest.permission.MANAGE_USERS) @Override public boolean setApplicationHiddenSettingAsUser(String packageName, boolean hidden, diff --git a/services/core/java/com/android/server/pm/PackageSetting.java b/services/core/java/com/android/server/pm/PackageSetting.java index 9428de700385..fb16b862b275 100644 --- a/services/core/java/com/android/server/pm/PackageSetting.java +++ b/services/core/java/com/android/server/pm/PackageSetting.java @@ -27,6 +27,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; import android.content.ComponentName; +import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.SharedLibraryInfo; @@ -221,6 +222,8 @@ public class PackageSetting extends SettingBase implements PackageStateInternal /** @see PackageState#getCategoryOverride() */ private int categoryOverride = ApplicationInfo.CATEGORY_UNDEFINED; + private int mPageSizeAppCompatFlags = ApplicationInfo.PAGE_SIZE_APP_COMPAT_FLAG_UNDEFINED; + @NonNull private final PackageStateUnserialized pkgState = new PackageStateUnserialized(this); @@ -863,6 +866,8 @@ public class PackageSetting extends SettingBase implements PackageStateInternal } copyMimeGroups(other.mimeGroups); + mPageSizeAppCompatFlags = other.mPageSizeAppCompatFlags; + pkgState.updateFrom(other.pkgState); onChanged(); } @@ -1617,6 +1622,34 @@ public class PackageSetting extends SettingBase implements PackageStateInternal return this; } + /** + * @see Set page size app compat mode. + */ + public PackageSetting setPageSizeAppCompatFlags(int mode) { + if (mode < 0 || mode >= ApplicationInfo.PAGE_SIZE_APP_COMPAT_FLAG_MAX) { + throw new IllegalArgumentException("Invalid page size compat mode specified"); + } + + // OR assignment is used here to avoid overriding the mode set by the manifest. + this.mPageSizeAppCompatFlags |= mode; + + // Only one bit of the following can be set at same time. Both are needed to detect app + // compat 'disabled' state from settings vs bit was never set. + if (ApplicationInfo.PAGE_SIZE_APP_COMPAT_FLAG_SETTINGS_OVERRIDE_ENABLED == mode) { + this.mPageSizeAppCompatFlags &= + ~ApplicationInfo.PAGE_SIZE_APP_COMPAT_FLAG_SETTINGS_OVERRIDE_DISABLED; + } else if (ApplicationInfo.PAGE_SIZE_APP_COMPAT_FLAG_SETTINGS_OVERRIDE_DISABLED == mode) { + this.mPageSizeAppCompatFlags &= + ~ApplicationInfo.PAGE_SIZE_APP_COMPAT_FLAG_SETTINGS_OVERRIDE_ENABLED; + } + onChanged(); + return this; + } + + public int getPageSizeAppCompatFlags() { + return mPageSizeAppCompatFlags; + } + public PackageSetting setLegacyNativeLibraryPath( String legacyNativeLibraryPathString) { this.legacyNativeLibraryPath = legacyNativeLibraryPathString; @@ -1787,6 +1820,63 @@ public class PackageSetting extends SettingBase implements PackageStateInternal return getBoolean(Booleans.SCANNED_AS_STOPPED_SYSTEM_APP); } + /** Returns true if ELF files will be loaded in Page size compatibility mode */ + @Override + public boolean isPageSizeAppCompatEnabled() { + // If manifest or settings has disabled the compat mode, don't run app in compat mode. + boolean manifestOverrideDisabled = (mPageSizeAppCompatFlags + & ApplicationInfo.PAGE_SIZE_APP_COMPAT_FLAG_MANIFEST_OVERRIDE_DISABLED) != 0; + boolean settingsOverrideDisabled = (mPageSizeAppCompatFlags + & ApplicationInfo.PAGE_SIZE_APP_COMPAT_FLAG_SETTINGS_OVERRIDE_DISABLED) != 0; + if (manifestOverrideDisabled || settingsOverrideDisabled) { + return false; + } + + int mask = + ApplicationInfo.PAGE_SIZE_APP_COMPAT_FLAG_ELF_NOT_ALIGNED + | ApplicationInfo.PAGE_SIZE_APP_COMPAT_FLAG_MANIFEST_OVERRIDE_ENABLED + | ApplicationInfo.PAGE_SIZE_APP_COMPAT_FLAG_SETTINGS_OVERRIDE_ENABLED; + return (mPageSizeAppCompatFlags & mask) != 0; + } + + /** + * Returns dialog string based on alignment of uncompressed shared libs inside the APK and ELF + * alignment. + */ + @Override + public String getPageSizeCompatWarningMessage(Context context) { + boolean manifestOverrideEnabled = (mPageSizeAppCompatFlags + & ApplicationInfo.PAGE_SIZE_APP_COMPAT_FLAG_MANIFEST_OVERRIDE_ENABLED) != 0; + boolean settingsOverrideEnabled = (mPageSizeAppCompatFlags + & ApplicationInfo.PAGE_SIZE_APP_COMPAT_FLAG_MANIFEST_OVERRIDE_ENABLED) != 0; + if (manifestOverrideEnabled || settingsOverrideEnabled) { + return null; + } + + boolean uncompressedLibsNotAligned = (mPageSizeAppCompatFlags + & ApplicationInfo.PAGE_SIZE_APP_COMPAT_FLAG_UNCOMPRESSED_LIBS_NOT_ALIGNED) != 0; + boolean elfNotAligned = (mPageSizeAppCompatFlags + & ApplicationInfo.PAGE_SIZE_APP_COMPAT_FLAG_ELF_NOT_ALIGNED) != 0; + + if (uncompressedLibsNotAligned && elfNotAligned) { + return context.getText( + com.android.internal.R.string.page_size_compat_apk_and_elf_warning) + .toString(); + } + + if (uncompressedLibsNotAligned) { + return context.getText(com.android.internal.R.string.page_size_compat_apk_warning) + .toString(); + } + + if (elfNotAligned) { + return context.getText(com.android.internal.R.string.page_size_compat_elf_warning) + .toString(); + } + + return null; + } + // Code below generated by codegen v1.0.23. @@ -1952,7 +2042,6 @@ public class PackageSetting extends SettingBase implements PackageStateInternal @Deprecated private void __metadata() {} - //@formatter:on // End of generated code diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java index 1f672a093b38..485a28070bc5 100644 --- a/services/core/java/com/android/server/pm/Settings.java +++ b/services/core/java/com/android/server/pm/Settings.java @@ -3313,6 +3313,11 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile if (pkg.getBaseRevisionCode() != 0) { serializer.attributeInt(null, "baseRevisionCode", pkg.getBaseRevisionCode()); } + if (pkg.getPageSizeAppCompatFlags() + != ApplicationInfo.PAGE_SIZE_APP_COMPAT_FLAG_UNDEFINED) { + serializer.attributeInt(null, "pageSizeCompat", pkg.getPageSizeAppCompatFlags()); + } + serializer.attributeFloat(null, "loadingProgress", pkg.getLoadingProgress()); serializer.attributeLongHex(null, "loadingCompletedTime", pkg.getLoadingCompletedTime()); @@ -4129,6 +4134,7 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile boolean isScannedAsStoppedSystemApp = false; boolean isSdkLibrary = false; int baseRevisionCode = 0; + int PageSizeCompat = 0; try { name = parser.getAttributeValue(null, ATTR_NAME); realName = parser.getAttributeValue(null, "realName"); @@ -4175,6 +4181,8 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile appMetadataSource = parser.getAttributeInt(null, "appMetadataSource", PackageManager.APP_METADATA_SOURCE_UNKNOWN); baseRevisionCode = parser.getAttributeInt(null, "baseRevisionCode", 0); + PageSizeCompat = parser.getAttributeInt(null, "pageSizeCompat", + ApplicationInfo.PAGE_SIZE_APP_COMPAT_FLAG_UNDEFINED); isScannedAsStoppedSystemApp = parser.getAttributeBoolean(null, "scannedAsStoppedSystemApp", false); @@ -4330,7 +4338,8 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile .setTargetSdkVersion(targetSdkVersion) .setBaseRevisionCode(baseRevisionCode) .setRestrictUpdateHash(restrictUpdateHash) - .setScannedAsStoppedSystemApp(isScannedAsStoppedSystemApp); + .setScannedAsStoppedSystemApp(isScannedAsStoppedSystemApp) + .setPageSizeAppCompatFlags(PageSizeCompat); // Handle legacy string here for single-user mode final String enabledStr = parser.getAttributeValue(null, ATTR_ENABLED); if (enabledStr != null) { @@ -5211,6 +5220,10 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile pw.print(" (override=true)"); } pw.println(); + pw.print(prefix); + pw.print(" pageSizeCompat="); + pw.print(ps.getPageSizeAppCompatFlags()); + pw.println(); if (!ps.getPkg().getQueriesPackages().isEmpty()) { pw.append(prefix).append(" queriesPackages=") .println(ps.getPkg().getQueriesPackages()); diff --git a/services/core/java/com/android/server/pm/pkg/PackageState.java b/services/core/java/com/android/server/pm/pkg/PackageState.java index bbc17c83cfac..33fc066a62ee 100644 --- a/services/core/java/com/android/server/pm/pkg/PackageState.java +++ b/services/core/java/com/android/server/pm/pkg/PackageState.java @@ -22,6 +22,7 @@ import android.annotation.Nullable; import android.annotation.Size; import android.annotation.SystemApi; import android.annotation.UserIdInt; +import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; @@ -198,6 +199,21 @@ public interface PackageState { int getCategoryOverride(); /** + * Returns true if ELF files will be loaded in Page size compatibility mode + * + * @hide + */ + boolean isPageSizeAppCompatEnabled(); + + /** + * Returns dialog string based on alignment of uncompressed shared libs inside the APK and ELF + * alignment. + * + * @hide + */ + String getPageSizeCompatWarningMessage(Context context); + + /** * The install time CPU override, if any. This value is written at install time * and doesn't change during the life of an install. If non-null, * {@link #getPrimaryCpuAbiLegacy()} will also contain the same value. diff --git a/services/core/java/com/android/server/pm/pkg/mutate/PackageStateMutator.java b/services/core/java/com/android/server/pm/pkg/mutate/PackageStateMutator.java index 253eb4006122..a46c4a695d60 100644 --- a/services/core/java/com/android/server/pm/pkg/mutate/PackageStateMutator.java +++ b/services/core/java/com/android/server/pm/pkg/mutate/PackageStateMutator.java @@ -257,6 +257,16 @@ public class PackageStateMutator { @NonNull @Override + public PackageStateWrite setPageSizeAppCompatFlags( + @ApplicationInfo.PageSizeAppCompatFlags int mode) { + if (mState != null) { + mState.setPageSizeAppCompatFlags(mode); + } + return this; + } + + @NonNull + @Override public PackageStateWrite setUpdateAvailable(boolean updateAvailable) { if (mState != null) { mState.setUpdateAvailable(updateAvailable); diff --git a/services/core/java/com/android/server/pm/pkg/mutate/PackageStateWrite.java b/services/core/java/com/android/server/pm/pkg/mutate/PackageStateWrite.java index 55d96f3aee08..f8f8695b2832 100644 --- a/services/core/java/com/android/server/pm/pkg/mutate/PackageStateWrite.java +++ b/services/core/java/com/android/server/pm/pkg/mutate/PackageStateWrite.java @@ -46,6 +46,10 @@ public interface PackageStateWrite { @NonNull PackageStateWrite setCategoryOverride(@ApplicationInfo.Category int category); + /** set 16Kb App compat mode. @see ApplicationInfo.PageSizeAppCompatFlags */ + @NonNull + PackageStateWrite setPageSizeAppCompatFlags(@ApplicationInfo.PageSizeAppCompatFlags int mode); + @NonNull PackageStateWrite setUpdateAvailable(boolean updateAvailable); diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt index 3fdb53f5ab59..31f03704a756 100644 --- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt +++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt @@ -289,6 +289,7 @@ class AndroidPackageTest : ParcelableComponentTest(AndroidPackage::class, Packag AndroidPackage::getEmergencyInstaller, AndroidPackage::isAllowCrossUidActivitySwitchFromBelow, AndroidPackage::getIntentMatchingFlags, + AndroidPackage::getPageSizeAppCompatFlags, ) override fun extraParams() = listOf( diff --git a/services/tests/mockingservicestests/Android.bp b/services/tests/mockingservicestests/Android.bp index 993569fb17fe..0d25426700a6 100644 --- a/services/tests/mockingservicestests/Android.bp +++ b/services/tests/mockingservicestests/Android.bp @@ -392,3 +392,10 @@ test_module_config { ], include_filters: ["com.android.server.StorageManagerServiceTest"], } + +test_module_config { + name: "FrameworksMockingServicesTests_service_batteryServiceTest", + base: "FrameworksMockingServicesTests", + test_suites: ["device-tests"], + include_filters: ["com.android.server.BatteryServiceTest"], +} diff --git a/services/tests/mockingservicestests/src/com/android/server/BatteryServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/BatteryServiceTest.java index 5e2f80bf8311..1fbd53a27a4f 100644 --- a/services/tests/mockingservicestests/src/com/android/server/BatteryServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/BatteryServiceTest.java @@ -79,6 +79,8 @@ public class BatteryServiceTest { private static final int UPDATED_BATTERY_HEALTH = 3; private static final int CURRENT_CHARGE_COUNTER = 4680000; private static final int UPDATED_CHARGE_COUNTER = 4218000; + private static final int CURRENT_MAX_CHARGING_CURRENT = 298125; + private static final int UPDATED_MAX_CHARGING_CURRENT = 398125; private static final int HANDLER_IDLE_TIME_MS = 5000; @Rule public final ExtendedMockitoRule mExtendedMockitoRule = new ExtendedMockitoRule.Builder(this) @@ -143,7 +145,7 @@ public class BatteryServiceTest { @EnableFlags(Flags.FLAG_RATE_LIMIT_BATTERY_CHANGED_BROADCAST) public void onlyVoltageUpdated_lessThenOnePercent_broadcastNotSent() { mBatteryService.update(createHealthInfo(VOLTAGE_LESS_THEN_ONE_PERCENT, CURRENT_BATTERY_TEMP, - CURRENT_CHARGE_COUNTER, CURRENT_BATTERY_HEALTH)); + CURRENT_CHARGE_COUNTER, CURRENT_BATTERY_HEALTH, CURRENT_MAX_CHARGING_CURRENT)); waitForHandlerToExecute(); @@ -156,7 +158,8 @@ public class BatteryServiceTest { mBatteryService.update( createHealthInfo(VOLTAGE_MORE_THEN_ONE_PERCENT, CURRENT_BATTERY_TEMP, CURRENT_CHARGE_COUNTER, - CURRENT_BATTERY_HEALTH)); + CURRENT_BATTERY_HEALTH, + CURRENT_MAX_CHARGING_CURRENT)); waitForHandlerToExecute(); @@ -165,13 +168,17 @@ public class BatteryServiceTest { @Test @EnableFlags(Flags.FLAG_RATE_LIMIT_BATTERY_CHANGED_BROADCAST) - public void onlyVoltageUpdated_broadcastSent() { + public void voltageUpdated_withUpdateInChargingCurrent_broadcastSent() { mBatteryService.mLastBroadcastVoltageUpdateTime = SystemClock.elapsedRealtime() - 20000; + long lastChargingCurrentUpdateTime = + mBatteryService.mLastBroadcastMaxChargingCurrentUpdateTime; mBatteryService.update(createHealthInfo(VOLTAGE_MORE_THEN_ONE_PERCENT, CURRENT_BATTERY_TEMP, - CURRENT_CHARGE_COUNTER, CURRENT_BATTERY_HEALTH)); + CURRENT_CHARGE_COUNTER, CURRENT_BATTERY_HEALTH, UPDATED_MAX_CHARGING_CURRENT)); waitForHandlerToExecute(); + assertTrue(lastChargingCurrentUpdateTime + < mBatteryService.mLastBroadcastMaxChargingCurrentUpdateTime); verifyNumberOfTimesBroadcastSent(1); } @@ -180,7 +187,8 @@ public class BatteryServiceTest { public void onlyTempUpdated_lessThenOneDegreeCelsius_broadcastNotSent() { mBatteryService.update( createHealthInfo(CURRENT_BATTERY_VOLTAGE, TEMP_LESS_THEN_ONE_DEGREE_CELSIUS, - CURRENT_CHARGE_COUNTER, CURRENT_BATTERY_HEALTH)); + CURRENT_CHARGE_COUNTER, CURRENT_BATTERY_HEALTH, + CURRENT_MAX_CHARGING_CURRENT)); waitForHandlerToExecute(); @@ -191,23 +199,31 @@ public class BatteryServiceTest { @EnableFlags(Flags.FLAG_RATE_LIMIT_BATTERY_CHANGED_BROADCAST) public void tempUpdated_broadcastSent() { long lastVoltageUpdateTime = mBatteryService.mLastBroadcastVoltageUpdateTime; + long lastChargingCurrentUpdateTime = + mBatteryService.mLastBroadcastMaxChargingCurrentUpdateTime; mBatteryService.update( createHealthInfo(VOLTAGE_LESS_THEN_ONE_PERCENT, TEMP_MORE_THEN_ONE_DEGREE_CELSIUS, - UPDATED_CHARGE_COUNTER, CURRENT_BATTERY_HEALTH)); + UPDATED_CHARGE_COUNTER, CURRENT_BATTERY_HEALTH, + UPDATED_MAX_CHARGING_CURRENT)); waitForHandlerToExecute(); assertTrue(lastVoltageUpdateTime < mBatteryService.mLastBroadcastVoltageUpdateTime); + assertTrue(lastChargingCurrentUpdateTime + < mBatteryService.mLastBroadcastMaxChargingCurrentUpdateTime); verifyNumberOfTimesBroadcastSent(1); } @Test @EnableFlags(Flags.FLAG_RATE_LIMIT_BATTERY_CHANGED_BROADCAST) - public void batteryHealthUpdated_voltageAndTempConst_broadcastSent() { + public void batteryHealthUpdated_withOtherExtrasConstant_broadcastSent() { + long lastVoltageUpdateTime = mBatteryService.mLastBroadcastVoltageUpdateTime; + long lastChargingCurrentUpdateTime = + mBatteryService.mLastBroadcastMaxChargingCurrentUpdateTime; mBatteryService.update( - createHealthInfo(CURRENT_BATTERY_VOLTAGE, CURRENT_BATTERY_TEMP, + createHealthInfo(VOLTAGE_LESS_THEN_ONE_PERCENT, CURRENT_BATTERY_TEMP, CURRENT_CHARGE_COUNTER, - UPDATED_BATTERY_HEALTH)); + UPDATED_BATTERY_HEALTH, UPDATED_MAX_CHARGING_CURRENT)); waitForHandlerToExecute(); @@ -217,10 +233,13 @@ public class BatteryServiceTest { mBatteryService.update( createHealthInfo(CURRENT_BATTERY_VOLTAGE, CURRENT_BATTERY_TEMP, UPDATED_CHARGE_COUNTER, - UPDATED_BATTERY_HEALTH)); + UPDATED_BATTERY_HEALTH, CURRENT_MAX_CHARGING_CURRENT)); waitForHandlerToExecute(); + assertTrue(lastVoltageUpdateTime < mBatteryService.mLastBroadcastVoltageUpdateTime); + assertTrue(lastChargingCurrentUpdateTime + < mBatteryService.mLastBroadcastMaxChargingCurrentUpdateTime); verifyNumberOfTimesBroadcastSent(1); } @@ -228,7 +247,7 @@ public class BatteryServiceTest { @DisableFlags(Flags.FLAG_RATE_LIMIT_BATTERY_CHANGED_BROADCAST) public void voltageUpdated_lessThanOnePercent_flagDisabled_broadcastSent() { mBatteryService.update(createHealthInfo(VOLTAGE_LESS_THEN_ONE_PERCENT, CURRENT_BATTERY_TEMP, - CURRENT_CHARGE_COUNTER, CURRENT_BATTERY_HEALTH)); + CURRENT_CHARGE_COUNTER, CURRENT_BATTERY_HEALTH, CURRENT_MAX_CHARGING_CURRENT)); waitForHandlerToExecute(); @@ -241,7 +260,7 @@ public class BatteryServiceTest { mBatteryService.update( createHealthInfo(CURRENT_BATTERY_VOLTAGE, CURRENT_BATTERY_TEMP, UPDATED_CHARGE_COUNTER, - CURRENT_BATTERY_HEALTH)); + CURRENT_BATTERY_HEALTH, CURRENT_MAX_CHARGING_CURRENT)); waitForHandlerToExecute(); @@ -254,7 +273,7 @@ public class BatteryServiceTest { mBatteryService.update( createHealthInfo(CURRENT_BATTERY_VOLTAGE, TEMP_LESS_THEN_ONE_DEGREE_CELSIUS, UPDATED_CHARGE_COUNTER, - CURRENT_BATTERY_HEALTH)); + CURRENT_BATTERY_HEALTH, CURRENT_MAX_CHARGING_CURRENT)); waitForHandlerToExecute(); @@ -267,10 +286,42 @@ public class BatteryServiceTest { mBatteryService.update( createHealthInfo(CURRENT_BATTERY_VOLTAGE, CURRENT_BATTERY_TEMP, UPDATED_CHARGE_COUNTER, - CURRENT_BATTERY_HEALTH)); + CURRENT_BATTERY_HEALTH, CURRENT_MAX_CHARGING_CURRENT)); + + waitForHandlerToExecute(); + + verifyNumberOfTimesBroadcastSent(1); + } + + @Test + @EnableFlags(Flags.FLAG_RATE_LIMIT_BATTERY_CHANGED_BROADCAST) + public void onlyMaxChargingCurrentUpdated_beforeFiveSeconds_broadcastNotSent() { + mBatteryService.update( + createHealthInfo(CURRENT_BATTERY_VOLTAGE, CURRENT_BATTERY_TEMP, + CURRENT_CHARGE_COUNTER, + CURRENT_BATTERY_HEALTH, + UPDATED_MAX_CHARGING_CURRENT)); waitForHandlerToExecute(); + verifyNumberOfTimesBroadcastSent(0); + } + + @Test + @EnableFlags(Flags.FLAG_RATE_LIMIT_BATTERY_CHANGED_BROADCAST) + public void maxChargingCurrentUpdated_afterFiveSeconds_broadcastSent() { + mBatteryService.mLastBroadcastMaxChargingCurrentUpdateTime = + SystemClock.elapsedRealtime() - 5000; + long lastVoltageUpdateTime = mBatteryService.mLastBroadcastVoltageUpdateTime; + mBatteryService.update( + createHealthInfo(VOLTAGE_MORE_THEN_ONE_PERCENT, CURRENT_BATTERY_TEMP, + CURRENT_CHARGE_COUNTER, + CURRENT_BATTERY_HEALTH, + UPDATED_MAX_CHARGING_CURRENT)); + + waitForHandlerToExecute(); + + assertTrue(lastVoltageUpdateTime < mBatteryService.mLastBroadcastVoltageUpdateTime); verifyNumberOfTimesBroadcastSent(1); } @@ -278,7 +329,8 @@ public class BatteryServiceTest { int batteryVoltage, int batteryTemperature, int batteryChargeCounter, - int batteryHealth) { + int batteryHealth, + int maxChargingCurrent) { HealthInfo h = new HealthInfo(); h.batteryVoltageMillivolts = batteryVoltage; h.batteryTemperatureTenthsCelsius = batteryTemperature; @@ -287,7 +339,7 @@ public class BatteryServiceTest { h.batteryHealth = batteryHealth; h.batteryPresent = true; h.batteryLevel = 100; - h.maxChargingCurrentMicroamps = 298125; + h.maxChargingCurrentMicroamps = maxChargingCurrent; h.batteryCurrentAverageMicroamps = -2812; h.batteryCurrentMicroamps = 298125; h.maxChargingVoltageMicrovolts = 3000; @@ -308,7 +360,8 @@ public class BatteryServiceTest { mBatteryService.update( createHealthInfo(CURRENT_BATTERY_VOLTAGE, CURRENT_BATTERY_TEMP, CURRENT_CHARGE_COUNTER, - CURRENT_BATTERY_HEALTH)); + CURRENT_BATTERY_HEALTH, + CURRENT_MAX_CHARGING_CURRENT)); waitForHandlerToExecute(); } diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/BottomHalfPipAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/BottomHalfPipAppHelper.kt deleted file mode 100644 index 6573c2c83f20..000000000000 --- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/BottomHalfPipAppHelper.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server.wm.flicker.helpers - -import android.app.Instrumentation -import android.content.Intent -import android.tools.traces.parsers.toFlickerComponent -import com.android.server.wm.flicker.testapp.ActivityOptions - -class BottomHalfPipAppHelper( - instrumentation: Instrumentation, - private val useLaunchingActivity: Boolean = false, -) : PipAppHelper( - instrumentation, - appName = ActivityOptions.BottomHalfPip.LABEL, - componentNameMatcher = ActivityOptions.BottomHalfPip.COMPONENT - .toFlickerComponent() -) { - override val openAppIntent: Intent - get() = super.openAppIntent.apply { - component = if (useLaunchingActivity) { - ActivityOptions.BottomHalfPip.LAUNCHING_APP_COMPONENT - } else { - ActivityOptions.BottomHalfPip.COMPONENT - } - } -}
\ No newline at end of file diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt index 2fe84b00b040..db4838ee6092 100644 --- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt +++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/PipAppHelper.kt @@ -27,7 +27,6 @@ import android.tools.device.apphelpers.StandardAppHelper import android.tools.helpers.FIND_TIMEOUT import android.tools.helpers.SYSTEMUI_PACKAGE import android.tools.traces.ConditionsFactory -import android.tools.traces.component.ComponentNameMatcher import android.tools.traces.component.IComponentMatcher import android.tools.traces.parsers.WindowManagerStateHelper import android.tools.traces.parsers.toFlickerComponent @@ -36,11 +35,12 @@ import androidx.test.uiautomator.By import androidx.test.uiautomator.Until import com.android.server.wm.flicker.testapp.ActivityOptions -open class PipAppHelper( - instrumentation: Instrumentation, - appName: String = ActivityOptions.Pip.LABEL, - componentNameMatcher: ComponentNameMatcher = ActivityOptions.Pip.COMPONENT.toFlickerComponent(), -) : StandardAppHelper(instrumentation, appName, componentNameMatcher) { +open class PipAppHelper(instrumentation: Instrumentation) : + StandardAppHelper( + instrumentation, + ActivityOptions.Pip.LABEL, + ActivityOptions.Pip.COMPONENT.toFlickerComponent() + ) { private val mediaSessionManager: MediaSessionManager get() = context.getSystemService(MediaSessionManager::class.java) diff --git a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml index 7c24a4adca3d..9ce8e807f612 100644 --- a/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml +++ b/tests/FlickerTests/test-apps/flickerapp/AndroidManifest.xml @@ -347,27 +347,6 @@ <category android:name="android.intent.category.LEANBACK_LAUNCHER"/> </intent-filter> </activity> - <activity android:name=".BottomHalfPipLaunchingActivity" - android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation" - android:taskAffinity="com.android.server.wm.flicker.testapp.BottomHalfPipLaunchingActivity" - android:theme="@style/CutoutShortEdges" - android:label="BottomHalfPipLaunchingActivity" - 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=".BottomHalfPipActivity" - android:resizeableActivity="true" - android:supportsPictureInPicture="true" - android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation" - android:taskAffinity="com.android.server.wm.flicker.testapp.BottomHalfPipLaunchingActivity" - android:theme="@style/TranslucentTheme" - android:label="BottomHalfPipActivity" - android:exported="true"> - </activity> <activity android:name=".SplitScreenActivity" android:resizeableActivity="true" android:taskAffinity="com.android.server.wm.flicker.testapp.SplitScreenActivity" diff --git a/tests/FlickerTests/test-apps/flickerapp/res/values/styles.xml b/tests/FlickerTests/test-apps/flickerapp/res/values/styles.xml index 837d050b73ff..47d113717ae0 100644 --- a/tests/FlickerTests/test-apps/flickerapp/res/values/styles.xml +++ b/tests/FlickerTests/test-apps/flickerapp/res/values/styles.xml @@ -62,12 +62,6 @@ <item name="android:backgroundDimEnabled">false</item> </style> - <style name="TranslucentTheme" parent="@style/OptOutEdgeToEdge"> - <item name="android:windowIsTranslucent">true</item> - <item name="android:windowContentOverlay">@null</item> - <item name="android:backgroundDimEnabled">false</item> - </style> - <style name="no_starting_window" parent="@style/OptOutEdgeToEdge"> <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 0c1ac9951d32..73625da9dfa5 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 @@ -241,21 +241,6 @@ public class ActivityOptions { FLICKER_APP_PACKAGE + ".PipActivity"); } - public static class BottomHalfPip { - public static final String LAUNCHING_APP_LABEL = "BottomHalfPipLaunchingActivity"; - // Test App > Bottom Half PIP Activity - public static final String LABEL = "BottomHalfPipActivity"; - - // Use the bottom half layout for PIP Activity - public static final String EXTRA_BOTTOM_HALF_LAYOUT = "bottom_half"; - - public static final ComponentName LAUNCHING_APP_COMPONENT = new ComponentName( - FLICKER_APP_PACKAGE, FLICKER_APP_PACKAGE + ".BottomHalfPipLaunchingActivity"); - - public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE, - FLICKER_APP_PACKAGE + ".BottomHalfPipActivity"); - } - public static class SplitScreen { public static class Primary { public static final String LABEL = "SplitScreenPrimaryActivity"; diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/BottomHalfPipActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/BottomHalfPipActivity.java deleted file mode 100644 index 3d4865572486..000000000000 --- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/BottomHalfPipActivity.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server.wm.flicker.testapp; - -import android.app.Activity; -import android.content.res.Configuration; -import android.os.Bundle; -import android.view.ViewGroup.LayoutParams; -import android.view.WindowManager; - -import androidx.annotation.NonNull; - -public class BottomHalfPipActivity extends PipActivity { - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setTheme(R.style.TranslucentTheme); - updateLayout(); - } - - @Override - public void onConfigurationChanged(@NonNull Configuration newConfig) { - super.onConfigurationChanged(newConfig); - - updateLayout(); - } - - /** - * Sets to match parent layout if the activity is - * {@link Activity#isInPictureInPictureMode()}. Otherwise, set to bottom half - * layout. - * - * @see #setToBottomHalfMode(boolean) - */ - private void updateLayout() { - setToBottomHalfMode(!isInPictureInPictureMode()); - } - - /** - * Sets `useBottomHalfLayout` to `true` to use the bottom half layout. Use the - * [LayoutParams.MATCH_PARENT] layout. - */ - private void setToBottomHalfMode(boolean useBottomHalfLayout) { - final WindowManager.LayoutParams attrs = getWindow().getAttributes(); - if (useBottomHalfLayout) { - final int taskHeight = getWindowManager().getCurrentWindowMetrics().getBounds() - .height(); - attrs.y = taskHeight / 2; - attrs.height = taskHeight / 2; - } else { - attrs.y = 0; - attrs.height = LayoutParams.MATCH_PARENT; - } - getWindow().setAttributes(attrs); - } -} |