diff options
336 files changed, 10208 insertions, 3411 deletions
diff --git a/Android.bp b/Android.bp index d03128418eb4..2a6afe58163d 100644 --- a/Android.bp +++ b/Android.bp @@ -110,6 +110,7 @@ filegroup { ":android.security.maintenance-java-source", ":android.security.metrics-java-source", ":android.system.keystore2-V3-java-source", + ":android.hardware.cas-V1-java-source", ":credstore_aidl", ":dumpstate_aidl", ":framework_native_aidl", @@ -200,6 +201,7 @@ java_library { "updatable-driver-protos", "ota_metadata_proto_java", "android.hidl.base-V1.0-java", + "android.hardware.cas-V1-java", // AIDL "android.hardware.cas-V1.0-java", "android.hardware.cas-V1.1-java", "android.hardware.cas-V1.2-java", diff --git a/StubLibraries.bp b/StubLibraries.bp index 272b4f6e36e6..48c44c967ef0 100644 --- a/StubLibraries.bp +++ b/StubLibraries.bp @@ -378,6 +378,67 @@ java_library { }, } +java_library { + name: "android_stubs_private_jar", + defaults: ["android.jar_defaults"], + visibility: [ + "//visibility:override", + "//visibility:private", + ], + static_libs: [ + "stable.core.platform.api.stubs", + "core-lambda-stubs-for-system-modules", + "core-generated-annotation-stubs", + "framework", + "ext", + "framework-res-package-jar", + // The order of this matters, it has to be last to provide a + // package-private androidx.annotation.RecentlyNonNull without + // overriding the public android.annotation.Nullable in framework.jar + // with its own package-private android.annotation.Nullable. + "private-stub-annotations-jar", + ], +} + +java_genrule { + name: "android_stubs_private_hjar", + visibility: ["//visibility:private"], + srcs: [":android_stubs_private_jar{.hjar}"], + out: ["android_stubs_private.jar"], + cmd: "cp $(in) $(out)", +} + +java_library { + name: "android_stubs_private", + defaults: ["android_stubs_dists_default"], + visibility: ["//visibility:private"], + sdk_version: "none", + system_modules: "none", + static_libs: ["android_stubs_private_hjar"], + dist: { + dir: "apistubs/android/private", + }, +} + +java_genrule { + name: "android_stubs_private_framework_aidl", + visibility: ["//visibility:private"], + tools: ["sdkparcelables"], + srcs: [":android_stubs_private"], + out: ["framework.aidl"], + cmd: "rm -f $(genDir)/framework.aidl.merged && " + + "for i in $(in); do " + + " rm -f $(genDir)/framework.aidl.tmp && " + + " $(location sdkparcelables) $$i $(genDir)/framework.aidl.tmp && " + + " cat $(genDir)/framework.aidl.tmp >> $(genDir)/framework.aidl.merged; " + + "done && " + + "sort -u $(genDir)/framework.aidl.merged > $(out)", + dist: { + targets: ["sdk"], + dir: "apistubs/android/private", + }, +} + //////////////////////////////////////////////////////////////////////// // api-versions.xml generation, for public and system. This API database // also contains the android.test.* APIs. diff --git a/core/api/current.txt b/core/api/current.txt index 1b54cc9cc973..76bab1bcd99c 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -88,6 +88,7 @@ package android { field public static final String DIAGNOSTIC = "android.permission.DIAGNOSTIC"; field public static final String DISABLE_KEYGUARD = "android.permission.DISABLE_KEYGUARD"; field public static final String DUMP = "android.permission.DUMP"; + field public static final String ENFORCE_UPDATE_OWNERSHIP = "android.permission.ENFORCE_UPDATE_OWNERSHIP"; field public static final String EXPAND_STATUS_BAR = "android.permission.EXPAND_STATUS_BAR"; field public static final String FACTORY_TEST = "android.permission.FACTORY_TEST"; field public static final String FOREGROUND_SERVICE = "android.permission.FOREGROUND_SERVICE"; @@ -384,6 +385,7 @@ package android { field public static final int allowTaskReparenting = 16843268; // 0x1010204 field public static final int allowUndo = 16843999; // 0x10104df field public static final int allowUntrustedActivityEmbedding = 16844393; // 0x1010669 + field public static final int allowUpdateOwnership; field public static final int alpha = 16843551; // 0x101031f field public static final int alphabeticModifiers = 16844110; // 0x101054e field public static final int alphabeticShortcut = 16843235; // 0x10101e3 @@ -11691,6 +11693,7 @@ package android.content.pm { method @Nullable public String getInstallingPackageName(); method @Nullable public String getOriginatingPackageName(); method public int getPackageSource(); + method @Nullable public String getUpdateOwnerPackageName(); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.content.pm.InstallSourceInfo> CREATOR; } @@ -11981,6 +11984,7 @@ package android.content.pm { method public int getParentSessionId(); method public boolean isKeepApplicationEnabledSetting(); method public boolean isMultiPackage(); + method public boolean isRequestUpdateOwnership(); method public boolean isStaged(); method @NonNull public java.io.InputStream openRead(@NonNull String) throws java.io.IOException; method @NonNull public java.io.OutputStream openWrite(@NonNull String, long, long) throws java.io.IOException; @@ -12036,6 +12040,7 @@ package android.content.pm { method public boolean isCommitted(); method public boolean isKeepApplicationEnabledSetting(); method public boolean isMultiPackage(); + method public boolean isRequestUpdateOwnership(); method public boolean isSealed(); method public boolean isStaged(); method public boolean isStagedSessionActive(); @@ -12074,6 +12079,7 @@ package android.content.pm { method public void setOriginatingUri(@Nullable android.net.Uri); method public void setPackageSource(int); method public void setReferrerUri(@Nullable android.net.Uri); + method @RequiresPermission(android.Manifest.permission.ENFORCE_UPDATE_OWNERSHIP) public void setRequestUpdateOwnership(boolean); method public void setRequireUserAction(int); method public void setSize(long); method public void setWhitelistedRestrictedPermissions(@Nullable java.util.Set<java.lang.String>); @@ -12252,6 +12258,7 @@ package android.content.pm { method @NonNull public java.util.List<android.content.pm.PackageManager.Property> queryProviderProperty(@NonNull String); method @NonNull public java.util.List<android.content.pm.PackageManager.Property> queryReceiverProperty(@NonNull String); 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 @RequiresPermission(value="android.permission.WHITELIST_RESTRICTED_PERMISSIONS", conditional=true) public boolean removeWhitelistedRestrictedPermission(@NonNull String, @NonNull String, int); @@ -12686,7 +12693,7 @@ package android.content.pm { field public static final int FLAG_USE_APP_ZYGOTE = 8; // 0x8 field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_CAMERA}, anyOf={android.Manifest.permission.CAMERA}, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_CAMERA = 64; // 0x40 field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE}, anyOf={android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.CHANGE_NETWORK_STATE, android.Manifest.permission.CHANGE_WIFI_STATE, android.Manifest.permission.CHANGE_WIFI_MULTICAST_STATE, android.Manifest.permission.NFC, android.Manifest.permission.TRANSMIT_IR}, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE = 16; // 0x10 - field @Deprecated @RequiresPermission(value=android.Manifest.permission.FOREGROUND_SERVICE_DATA_SYNC, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_DATA_SYNC = 1; // 0x1 + field @RequiresPermission(value=android.Manifest.permission.FOREGROUND_SERVICE_DATA_SYNC, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_DATA_SYNC = 1; // 0x1 field @RequiresPermission(android.Manifest.permission.FOREGROUND_SERVICE_FILE_MANAGEMENT) public static final int FOREGROUND_SERVICE_TYPE_FILE_MANAGEMENT = 4096; // 0x1000 field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_HEALTH}, anyOf={android.Manifest.permission.ACTIVITY_RECOGNITION, android.Manifest.permission.BODY_SENSORS, android.Manifest.permission.HIGH_SAMPLING_RATE_SENSORS}) public static final int FOREGROUND_SERVICE_TYPE_HEALTH = 256; // 0x100 field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_LOCATION}, anyOf={android.Manifest.permission.ACCESS_COARSE_LOCATION, android.Manifest.permission.ACCESS_FINE_LOCATION}, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_LOCATION = 8; // 0x8 @@ -13251,6 +13258,7 @@ package android.credentials { ctor public ClearCredentialStateException(@NonNull String, @Nullable Throwable); ctor public ClearCredentialStateException(@NonNull String); method @NonNull public String getType(); + field @NonNull public static final String TYPE_UNKNOWN = "android.credentials.ClearCredentialStateException.TYPE_UNKNOWN"; } public final class ClearCredentialStateRequest implements android.os.Parcelable { @@ -13267,7 +13275,10 @@ package android.credentials { ctor public CreateCredentialException(@NonNull String, @Nullable Throwable); ctor public CreateCredentialException(@NonNull String); method @NonNull public String getType(); + field @NonNull public static final String TYPE_INTERRUPTED = "android.credentials.CreateCredentialException.TYPE_INTERRUPTED"; field @NonNull public static final String TYPE_NO_CREDENTIAL = "android.credentials.CreateCredentialException.TYPE_NO_CREDENTIAL"; + field @NonNull public static final String TYPE_UNKNOWN = "android.credentials.CreateCredentialException.TYPE_UNKNOWN"; + field @NonNull public static final String TYPE_USER_CANCELED = "android.credentials.CreateCredentialException.TYPE_USER_CANCELED"; } public final class CreateCredentialRequest implements android.os.Parcelable { @@ -13311,7 +13322,10 @@ package android.credentials { ctor public GetCredentialException(@NonNull String, @Nullable Throwable); ctor public GetCredentialException(@NonNull String); method @NonNull public String getType(); + field @NonNull public static final String TYPE_INTERRUPTED = "android.credentials.GetCredentialException.TYPE_INTERRUPTED"; field @NonNull public static final String TYPE_NO_CREDENTIAL = "android.credentials.GetCredentialException.TYPE_NO_CREDENTIAL"; + field @NonNull public static final String TYPE_UNKNOWN = "android.credentials.GetCredentialException.TYPE_UNKNOWN"; + field @NonNull public static final String TYPE_USER_CANCELED = "android.credentials.GetCredentialException.TYPE_USER_CANCELED"; } public final class GetCredentialOption implements android.os.Parcelable { @@ -21959,6 +21973,7 @@ package android.media { field public static final int PLUGIN_STATUS_PHYSICAL_MODULE_CHANGED = 0; // 0x0 field public static final int PLUGIN_STATUS_SESSION_NUMBER_CHANGED = 1; // 0x1 field public static final int SCRAMBLING_MODE_AES128 = 9; // 0x9 + field public static final int SCRAMBLING_MODE_AES_CBC = 14; // 0xe field public static final int SCRAMBLING_MODE_AES_ECB = 10; // 0xa field public static final int SCRAMBLING_MODE_AES_SCTE52 = 11; // 0xb field public static final int SCRAMBLING_MODE_DVB_CISSA_V1 = 6; // 0x6 diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 137751b17508..3ac828669e54 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -3592,6 +3592,9 @@ package android.content.pm { field public static final int LOCATION_DATA_APP = 0; // 0x0 field public static final int LOCATION_MEDIA_DATA = 2; // 0x2 field public static final int LOCATION_MEDIA_OBB = 1; // 0x1 + field public static final int REASON_CONFIRM_PACKAGE_CHANGE = 0; // 0x0 + field public static final int REASON_OWNERSHIP_CHANGED = 1; // 0x1 + field public static final int REASON_REMIND_OWNERSHIP = 2; // 0x2 } public static class PackageInstaller.InstallInfo { @@ -3621,6 +3624,7 @@ package android.content.pm { method public boolean getInstallAsFullApp(boolean); method public boolean getInstallAsInstantApp(boolean); method public boolean getInstallAsVirtualPreload(); + method public int getPendingUserActionReason(); method public boolean getRequestDowngrade(); method public int getRollbackDataPolicy(); method @NonNull public java.util.Set<java.lang.String> getWhitelistedRestrictedPermissions(); @@ -11769,6 +11773,7 @@ package android.service.euicc { method public int onEraseSubscriptions(int, @android.telephony.euicc.EuiccCardManager.ResetOption int); method public abstract android.service.euicc.GetDefaultDownloadableSubscriptionListResult onGetDefaultDownloadableSubscriptionList(int, boolean); method public abstract android.service.euicc.GetDownloadableSubscriptionMetadataResult onGetDownloadableSubscriptionMetadata(int, android.telephony.euicc.DownloadableSubscription, boolean); + method @NonNull public android.service.euicc.GetDownloadableSubscriptionMetadataResult onGetDownloadableSubscriptionMetadata(int, int, @NonNull android.telephony.euicc.DownloadableSubscription, boolean); method public abstract String onGetEid(int); method @NonNull public abstract android.telephony.euicc.EuiccInfo onGetEuiccInfo(int); method @NonNull public abstract android.service.euicc.GetEuiccProfileInfoListResult onGetEuiccProfileInfoList(int); @@ -16531,6 +16536,7 @@ package android.view.accessibility { public abstract class AccessibilityDisplayProxy { ctor public AccessibilityDisplayProxy(int, @NonNull java.util.concurrent.Executor, @NonNull java.util.List<android.accessibilityservice.AccessibilityServiceInfo>); + method @Nullable public android.view.accessibility.AccessibilityNodeInfo findFocus(int); method public int getDisplayId(); method @NonNull public final java.util.List<android.accessibilityservice.AccessibilityServiceInfo> getInstalledAndEnabledServices(); method @NonNull public java.util.List<android.view.accessibility.AccessibilityWindowInfo> getWindows(); diff --git a/core/api/test-current.txt b/core/api/test-current.txt index e3554a5aa043..3bc11facad4a 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -3664,14 +3664,13 @@ package android.window { ctor public WindowContainerTransaction(); method @NonNull public android.window.WindowContainerTransaction clearLaunchAdjacentFlagRoot(@NonNull android.window.WindowContainerToken); method @NonNull public android.window.WindowContainerTransaction createTaskFragment(@NonNull android.window.TaskFragmentCreationParams); - method @NonNull public android.window.WindowContainerTransaction deleteTaskFragment(@NonNull android.window.WindowContainerToken); + method @NonNull public android.window.WindowContainerTransaction deleteTaskFragment(@NonNull android.os.IBinder); method public int describeContents(); method @NonNull public android.window.WindowContainerTransaction finishActivity(@NonNull android.os.IBinder); method @NonNull public android.window.WindowContainerTransaction removeTask(@NonNull android.window.WindowContainerToken); method @NonNull public android.window.WindowContainerTransaction reorder(@NonNull android.window.WindowContainerToken, boolean); method @NonNull public android.window.WindowContainerTransaction reparent(@NonNull android.window.WindowContainerToken, @Nullable android.window.WindowContainerToken, boolean); method @NonNull public android.window.WindowContainerTransaction reparentActivityToTaskFragment(@NonNull android.os.IBinder, @NonNull android.os.IBinder); - method @NonNull public android.window.WindowContainerTransaction reparentChildren(@NonNull android.window.WindowContainerToken, @Nullable android.window.WindowContainerToken); method @NonNull public android.window.WindowContainerTransaction reparentTasks(@Nullable android.window.WindowContainerToken, @Nullable android.window.WindowContainerToken, @Nullable int[], @Nullable int[], boolean); method @NonNull public android.window.WindowContainerTransaction requestFocusOnTaskFragment(@NonNull android.os.IBinder); method @NonNull public android.window.WindowContainerTransaction scheduleFinishEnterPip(@NonNull android.window.WindowContainerToken, @NonNull android.graphics.Rect); diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java index a7a4b356c8d5..c11961e4c18e 100644 --- a/core/java/android/app/ApplicationPackageManager.java +++ b/core/java/android/app/ApplicationPackageManager.java @@ -3890,4 +3890,14 @@ public class ApplicationPackageManager extends PackageManager { return Settings.Global.getInt(mContext.getContentResolver(), Settings.Global.SHOW_NEW_APP_INSTALLED_NOTIFICATION_ENABLED, 0) == 1; } + + @Override + public void relinquishUpdateOwnership(String targetPackage) { + Objects.requireNonNull(targetPackage); + try { + mPM.relinquishUpdateOwnership(targetPackage); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } } diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index 240dbe1eea24..befe833a6a11 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -26,9 +26,7 @@ import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UiContext; -import android.companion.virtual.VirtualDevice; import android.companion.virtual.VirtualDeviceManager; -import android.companion.virtual.VirtualDeviceParams; import android.compat.annotation.UnsupportedAppUsage; import android.content.AttributionSource; import android.content.AutofillOptions; @@ -2742,9 +2740,12 @@ class ContextImpl extends Context { @Override public @NonNull Context createDeviceContext(int deviceId) { - if (!isValidDeviceId(deviceId)) { - throw new IllegalArgumentException( - "Not a valid ID of the default device or any virtual device: " + deviceId); + if (deviceId != VirtualDeviceManager.DEVICE_ID_DEFAULT) { + VirtualDeviceManager vdm = getSystemService(VirtualDeviceManager.class); + if (!vdm.isValidVirtualDeviceId(deviceId)) { + throw new IllegalArgumentException( + "Not a valid ID of the default device or any virtual device: " + deviceId); + } } ContextImpl context = new ContextImpl(this, mMainThread, mPackageInfo, mParams, @@ -2757,31 +2758,6 @@ class ContextImpl extends Context { return context; } - /** - * Checks whether the passed {@code deviceId} is valid or not. - * {@link VirtualDeviceManager#DEVICE_ID_DEFAULT} is valid as it is the ID of the default - * device when no additional virtual devices exist. If {@code deviceId} is the id of - * a virtual device, it should correspond to a virtual device created by - * {@link VirtualDeviceManager#createVirtualDevice(int, VirtualDeviceParams)}. - */ - private boolean isValidDeviceId(int deviceId) { - if (deviceId == VirtualDeviceManager.DEVICE_ID_DEFAULT) { - return true; - } - if (deviceId > VirtualDeviceManager.DEVICE_ID_DEFAULT) { - VirtualDeviceManager vdm = getSystemService(VirtualDeviceManager.class); - if (vdm != null) { - List<VirtualDevice> virtualDevices = vdm.getVirtualDevices(); - for (int i = 0; i < virtualDevices.size(); i++) { - if (virtualDevices.get(i).getDeviceId() == deviceId) { - return true; - } - } - } - } - return false; - } - @NonNull @Override public WindowContext createWindowContext(@WindowType int type, @@ -3044,10 +3020,13 @@ class ContextImpl extends Context { @Override public void updateDeviceId(int updatedDeviceId) { - if (!isValidDeviceId(updatedDeviceId)) { - throw new IllegalArgumentException( - "Not a valid ID of the default device or any virtual device: " - + updatedDeviceId); + if (updatedDeviceId != VirtualDeviceManager.DEVICE_ID_DEFAULT) { + VirtualDeviceManager vdm = getSystemService(VirtualDeviceManager.class); + if (!vdm.isValidVirtualDeviceId(updatedDeviceId)) { + throw new IllegalArgumentException( + "Not a valid ID of the default device or any virtual device: " + + updatedDeviceId); + } } if (mIsExplicitDeviceId) { throw new UnsupportedOperationException( diff --git a/core/java/android/app/ForegroundServiceTypePolicy.java b/core/java/android/app/ForegroundServiceTypePolicy.java index 877177cc8861..f0c39ab0dc26 100644 --- a/core/java/android/app/ForegroundServiceTypePolicy.java +++ b/core/java/android/app/ForegroundServiceTypePolicy.java @@ -117,14 +117,10 @@ public abstract class ForegroundServiceTypePolicy { * The FGS type enforcement: * deprecating the {@link android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_DATA_SYNC}. * - * <p>Starting a FGS with this type from apps with targetSdkVersion - * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} or later will - * result in a warning in the log.</p> - * * @hide */ @ChangeId - @EnabledAfter(targetSdkVersion = android.os.Build.VERSION_CODES.TIRAMISU) + @Disabled @Overridable public static final long FGS_TYPE_DATA_SYNC_DEPRECATION_CHANGE_ID = 255039210L; @@ -132,13 +128,8 @@ public abstract class ForegroundServiceTypePolicy { * The FGS type enforcement: * disabling the {@link android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_DATA_SYNC}. * - * <p>Starting a FGS with this type from apps with targetSdkVersion - * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} or later will - * result in an exception.</p> - * * @hide */ - // TODO (b/254661666): Change to @EnabledSince(U) in next OS release @ChangeId @Disabled @Overridable diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index ad17e0dea9c5..1633073d7cc8 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -2417,6 +2417,7 @@ public class DevicePolicyManager { * applied (cross profile intent filters updated). Only usesd for CTS tests. * @hide */ + @SuppressLint("ActionValue") @TestApi @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String ACTION_DATA_SHARING_RESTRICTION_APPLIED = @@ -2427,6 +2428,7 @@ public class DevicePolicyManager { * has been changed. * @hide */ + @SuppressLint("ActionValue") @TestApi @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) public static final String ACTION_DEVICE_POLICY_CONSTANTS_CHANGED = @@ -6979,6 +6981,8 @@ public class DevicePolicyManager { * {@link #ENCRYPTION_STATUS_UNSUPPORTED}, {@link #ENCRYPTION_STATUS_INACTIVE}, * {@link #ENCRYPTION_STATUS_ACTIVATING}, {@link #ENCRYPTION_STATUS_ACTIVE_DEFAULT_KEY}, * {@link #ENCRYPTION_STATUS_ACTIVE}, or {@link #ENCRYPTION_STATUS_ACTIVE_PER_USER}. + * + * @throws SecurityException if called on a parent instance. */ public int getStorageEncryptionStatus() { throwIfParentInstance("getStorageEncryptionStatus"); diff --git a/core/java/android/app/time/UnixEpochTime.java b/core/java/android/app/time/UnixEpochTime.java index 61cbc5ed7977..0b8f7ee45a96 100644 --- a/core/java/android/app/time/UnixEpochTime.java +++ b/core/java/android/app/time/UnixEpochTime.java @@ -124,7 +124,7 @@ public final class UnixEpochTime implements Parcelable { @Override public String toString() { return "UnixEpochTime{" - + "mElapsedRealtimeTimeMillis=" + mElapsedRealtimeMillis + + "mElapsedRealtimeMillis=" + mElapsedRealtimeMillis + ", mUnixEpochTimeMillis=" + mUnixEpochTimeMillis + '}'; } diff --git a/core/java/android/app/timedetector/TimeDetector.java b/core/java/android/app/timedetector/TimeDetector.java index f95d6d3bb056..50a7da1cede5 100644 --- a/core/java/android/app/timedetector/TimeDetector.java +++ b/core/java/android/app/timedetector/TimeDetector.java @@ -73,6 +73,18 @@ public interface TimeDetector { String SHELL_COMMAND_SUGGEST_NETWORK_TIME = "suggest_network_time"; /** + * A shell command that prints the current network time information. + * @hide + */ + String SHELL_COMMAND_GET_NETWORK_TIME = "get_network_time"; + + /** + * A shell command that clears the detector's network time information. + * @hide + */ + String SHELL_COMMAND_CLEAR_NETWORK_TIME = "clear_network_time"; + + /** * A shell command that injects a GNSS time suggestion. * @hide */ diff --git a/core/java/android/companion/virtual/IVirtualDeviceManager.aidl b/core/java/android/companion/virtual/IVirtualDeviceManager.aidl index f0d23ac8374f..e96a2c18037b 100644 --- a/core/java/android/companion/virtual/IVirtualDeviceManager.aidl +++ b/core/java/android/companion/virtual/IVirtualDeviceManager.aidl @@ -56,6 +56,14 @@ interface IVirtualDeviceManager { */ int getDeviceIdForDisplayId(int displayId); + /** + * Checks whether the passed {@code deviceId} is a valid virtual device ID or not. + * {@link VirtualDeviceManager#DEVICE_ID_DEFAULT} is not valid as it is the ID of the default + * device which is not a virtual device. {@code deviceId} must correspond to a virtual device + * created by {@link VirtualDeviceManager#createVirtualDevice(int, VirtualDeviceParams)}. + */ + boolean isValidVirtualDeviceId(int deviceId); + /** * Returns the device policy for the given virtual device and policy type. */ diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java index 6ad18d545a6d..3bc1628d3576 100644 --- a/core/java/android/companion/virtual/VirtualDeviceManager.java +++ b/core/java/android/companion/virtual/VirtualDeviceManager.java @@ -258,6 +258,26 @@ public final class VirtualDeviceManager { } /** + * Checks whether the passed {@code deviceId} is a valid virtual device ID or not. + * {@link VirtualDeviceManager#DEVICE_ID_DEFAULT} is not valid as it is the ID of the default + * device which is not a virtual device. {@code deviceId} must correspond to a virtual device + * created by {@link VirtualDeviceManager#createVirtualDevice(int, VirtualDeviceParams)}. + * + * @hide + */ + public boolean isValidVirtualDeviceId(int deviceId) { + if (mService == null) { + Log.w(TAG, "Failed to retrieve virtual devices; no virtual device manager service."); + return false; + } + try { + return mService.isValidVirtualDeviceId(deviceId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * Returns device-specific audio session id for audio playback. * * @param deviceId - id of the virtual audio device diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 12a2cae4c5c8..879c8bdeefcf 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -408,7 +408,12 @@ public abstract class Context { * will cause the isolated service to be co-located in the same shared isolated process. * * Note that the shared isolated process is scoped to the calling app; once created, only - * the calling app can bind additional isolated services into the shared process. + * the calling app can bind additional isolated services into the shared process. However, + * the services themselves can come from different APKs and therefore different vendors. + * + * Only services that set the {@link android.R.attr#allowSharedIsolatedProcess} attribute + * to {@code true} are allowed to be bound into a shared isolated process. + * */ public static final int BIND_SHARED_ISOLATED_PROCESS = 0x00002000; diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java index a9f55bc1ea4c..8fd905e5bd87 100644 --- a/core/java/android/content/pm/ActivityInfo.java +++ b/core/java/android/content/pm/ActivityInfo.java @@ -1102,6 +1102,41 @@ public class ActivityInfo extends ComponentInfo implements Parcelable { public static final long ALWAYS_SANDBOX_DISPLAY_APIS = 185004937L; // buganizer id /** + * This change id excludes the packages it is applied to from the camera compat force rotation + * treatment. See com.android.server.wm.DisplayRotationCompatPolicy for context. + * @hide + */ + @ChangeId + @Overridable + @Disabled + public static final long OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION = + 263959004L; // buganizer id + + /** + * This change id excludes the packages it is applied to from activity refresh after camera + * compat force rotation treatment. See com.android.server.wm.DisplayRotationCompatPolicy for + * context. + * @hide + */ + @ChangeId + @Overridable + @Disabled + public static final long OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH = 264304459L; // buganizer id + + /** + * This change id makes the packages it is applied to do activity refresh after camera compat + * force rotation treatment using "resumed -> paused -> resumed" cycle rather than "resumed -> + * ... -> stopped -> ... -> resumed" cycle. See + * com.android.server.wm.DisplayRotationCompatPolicy for context. + * @hide + */ + @ChangeId + @Overridable + @Disabled + public static final long OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE = + 264301586L; // buganizer id + + /** * This change id is the gatekeeper for all treatments that force a given min aspect ratio. * Enabling this change will allow the following min aspect ratio treatments to be applied: * OVERRIDE_MIN_ASPECT_RATIO_MEDIUM diff --git a/core/java/android/content/pm/IPackageInstallerSession.aidl b/core/java/android/content/pm/IPackageInstallerSession.aidl index 60a7b13ff6e9..c9a56324de79 100644 --- a/core/java/android/content/pm/IPackageInstallerSession.aidl +++ b/core/java/android/content/pm/IPackageInstallerSession.aidl @@ -63,6 +63,7 @@ interface IPackageInstallerSession { void requestUserPreapproval(in PackageInstaller.PreapprovalDetails details, in IntentSender statusReceiver); boolean isKeepApplicationEnabledSetting(); + boolean isRequestUpdateOwnership(); ParcelFileDescriptor getAppMetadataFd(); ParcelFileDescriptor openWriteAppMetadata(); diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl index 54ca1e59aafe..0e37c8798919 100644 --- a/core/java/android/content/pm/IPackageManager.aidl +++ b/core/java/android/content/pm/IPackageManager.aidl @@ -214,6 +214,8 @@ interface IPackageManager { @UnsupportedAppUsage void setInstallerPackageName(in String targetPackage, in String installerPackageName); + void relinquishUpdateOwnership(in String targetPackage); + void setApplicationCategoryHint(String packageName, int categoryHint, String callerPackageName); /** @deprecated rawr, don't call AIDL methods directly! */ diff --git a/core/java/android/content/pm/InstallSourceInfo.java b/core/java/android/content/pm/InstallSourceInfo.java index 88f1a16ec3ab..67123e87a265 100644 --- a/core/java/android/content/pm/InstallSourceInfo.java +++ b/core/java/android/content/pm/InstallSourceInfo.java @@ -35,6 +35,8 @@ public final class InstallSourceInfo implements Parcelable { @Nullable private final String mInstallingPackageName; + @Nullable private final String mUpdateOwnerPackageName; + @Nullable private final int mPackageSource; /** @hide */ @@ -42,18 +44,20 @@ public final class InstallSourceInfo implements Parcelable { @Nullable SigningInfo initiatingPackageSigningInfo, @Nullable String originatingPackageName, @Nullable String installingPackageName) { this(initiatingPackageName, initiatingPackageSigningInfo, originatingPackageName, - installingPackageName, PackageInstaller.PACKAGE_SOURCE_UNSPECIFIED); + installingPackageName, null /* updateOwnerPackageName */, + PackageInstaller.PACKAGE_SOURCE_UNSPECIFIED); } /** @hide */ public InstallSourceInfo(@Nullable String initiatingPackageName, @Nullable SigningInfo initiatingPackageSigningInfo, @Nullable String originatingPackageName, @Nullable String installingPackageName, - int packageSource) { + @Nullable String updateOwnerPackageName, int packageSource) { mInitiatingPackageName = initiatingPackageName; mInitiatingPackageSigningInfo = initiatingPackageSigningInfo; mOriginatingPackageName = originatingPackageName; mInstallingPackageName = installingPackageName; + mUpdateOwnerPackageName = updateOwnerPackageName; mPackageSource = packageSource; } @@ -69,6 +73,7 @@ public final class InstallSourceInfo implements Parcelable { dest.writeParcelable(mInitiatingPackageSigningInfo, flags); dest.writeString(mOriginatingPackageName); dest.writeString(mInstallingPackageName); + dest.writeString8(mUpdateOwnerPackageName); dest.writeInt(mPackageSource); } @@ -77,6 +82,7 @@ public final class InstallSourceInfo implements Parcelable { mInitiatingPackageSigningInfo = source.readParcelable(SigningInfo.class.getClassLoader(), android.content.pm.SigningInfo.class); mOriginatingPackageName = source.readString(); mInstallingPackageName = source.readString(); + mUpdateOwnerPackageName = source.readString8(); mPackageSource = source.readInt(); } @@ -137,6 +143,21 @@ public final class InstallSourceInfo implements Parcelable { } /** + * The name of the package that is the update owner, or null if not available. + * + * This indicates the update ownership enforcement is enabled for this app, + * and which package is the update owner. + * + * Returns null if the update ownership enforcement is disabled for the app. + * + * @see PackageInstaller.SessionParams#setRequestUpdateOwnership + */ + @Nullable + public String getUpdateOwnerPackageName() { + return mUpdateOwnerPackageName; + } + + /** * Information about the package source when installer installed this app. */ public @PackageInstaller.PackageSourceType int getPackageSource() { diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java index 703a92523cf5..812b5b314582 100644 --- a/core/java/android/content/pm/PackageInstaller.java +++ b/core/java/android/content/pm/PackageInstaller.java @@ -544,6 +544,46 @@ public class PackageInstaller { @Retention(RetentionPolicy.SOURCE) @interface PackageSourceType{} + /** + * Indicate the user intervention is required when the installer attempts to commit the session. + * This is the default case. + * + * @hide + */ + @SystemApi + public static final int REASON_CONFIRM_PACKAGE_CHANGE = 0; + + /** + * Indicate the user intervention is required because the update ownership enforcement is + * enabled, and the update owner will change. + * + * @see PackageInstaller.SessionParams#setRequestUpdateOwnership + * @see InstallSourceInfo#getUpdateOwnerPackageName + * @hide + */ + @SystemApi + public static final int REASON_OWNERSHIP_CHANGED = 1; + + /** + * Indicate the user intervention is required because the update ownership enforcement is + * enabled, and remind the update owner will retain. + * + * @see PackageInstaller.SessionParams#setRequestUpdateOwnership + * @see InstallSourceInfo#getUpdateOwnerPackageName + * @hide + */ + @SystemApi + public static final int REASON_REMIND_OWNERSHIP = 2; + + /** @hide */ + @IntDef(prefix = { "REASON_" }, value = { + REASON_CONFIRM_PACKAGE_CHANGE, + REASON_OWNERSHIP_CHANGED, + REASON_REMIND_OWNERSHIP, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface UserActionReason {} + /** Default set of checksums - includes all available checksums. * @see Session#requestChecksums */ private static final int DEFAULT_CHECKSUMS = @@ -1910,6 +1950,20 @@ public class PackageInstaller { throw e.rethrowFromSystemServer(); } } + + /** + * @return {@code true} if the installer requested the update ownership enforcement + * for the packages in this session. + * + * @see PackageInstaller.SessionParams#setRequestUpdateOwnership + */ + public boolean isRequestUpdateOwnership() { + try { + return mSession.isRequestUpdateOwnership(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } } /** @@ -2672,9 +2726,18 @@ public class PackageInstaller { * Android S ({@link android.os.Build.VERSION_CODES#S API 31})</li> * </ul> * </li> - * <li>The installer is the {@link InstallSourceInfo#getInstallingPackageName() - * installer of record} of an existing version of the app (in other words, this install - * session is an app update) or the installer is updating itself.</li> + * <li>The installer is: + * <ul> + * <li>The {@link InstallSourceInfo#getUpdateOwnerPackageName() update owner} + * of an existing version of the app (in other words, this install session is + * an app update) if the update ownership enforcement is enabled.</li> + * <li>The {@link InstallSourceInfo#getInstallingPackageName() installer of + * record} of an existing version of the app (in other words, this install + * session is an app update) if the update ownership enforcement isn't + * enabled.</li> + * <li>Updating itself.</li> + * </ul> + * </li>> * <li>The installer declares the * {@link android.Manifest.permission#UPDATE_PACKAGES_WITHOUT_USER_ACTION * UPDATE_PACKAGES_WITHOUT_USER_ACTION} permission.</li> @@ -2713,6 +2776,30 @@ public class PackageInstaller { this.keepApplicationEnabledSetting = true; } + /** + * Optionally indicate whether the package being installed needs the update ownership + * enforcement. Once the update ownership enforcement is enabled, the other installers + * will need the user action to update the package even if the installers have been + * granted the {@link android.Manifest.permission#INSTALL_PACKAGES INSTALL_PACKAGES} + * permission. Default to {@code false}. + * + * The update ownership enforcement can only be enabled on initial installation. Set + * this to {@code true} on package update indicates the installer package wants to be + * the update owner if the update ownership enforcement has enabled. + * + * Note: To enable the update ownership enforcement, the installer must have the + * {@link android.Manifest.permission#ENFORCE_UPDATE_OWNERSHIP ENFORCE_UPDATE_OWNERSHIP} + * permission. + */ + @RequiresPermission(Manifest.permission.ENFORCE_UPDATE_OWNERSHIP) + public void setRequestUpdateOwnership(boolean enable) { + if (enable) { + this.installFlags |= PackageManager.INSTALL_REQUEST_UPDATE_OWNERSHIP; + } else { + this.installFlags &= ~PackageManager.INSTALL_REQUEST_UPDATE_OWNERSHIP; + } + } + /** {@hide} */ public void dump(IndentingPrintWriter pw) { pw.printPair("mode", mode); @@ -2987,6 +3074,9 @@ public class PackageInstaller { /** @hide */ public boolean keepApplicationEnabledSetting; + /** @hide */ + public int pendingUserActionReason; + /** {@hide} */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public SessionInfo() { @@ -3041,6 +3131,7 @@ public class PackageInstaller { installerUid = source.readInt(); packageSource = source.readInt(); keepApplicationEnabledSetting = source.readBoolean(); + pendingUserActionReason = source.readInt(); } /** @@ -3585,6 +3676,25 @@ public class PackageInstaller { return isPreapprovalRequested; } + /** + * @return {@code true} if the installer requested the update ownership enforcement + * for the packages in this session. + * + * @see PackageInstaller.SessionParams#setRequestUpdateOwnership + */ + public boolean isRequestUpdateOwnership() { + return (installFlags & PackageManager.INSTALL_REQUEST_UPDATE_OWNERSHIP) != 0; + } + + /** + * Return the reason for requiring the user action. + * @hide + */ + @SystemApi + public @UserActionReason int getPendingUserActionReason() { + return pendingUserActionReason; + } + @Override public int describeContents() { return 0; @@ -3635,6 +3745,7 @@ public class PackageInstaller { dest.writeInt(installerUid); dest.writeInt(packageSource); dest.writeBoolean(keepApplicationEnabledSetting); + dest.writeInt(pendingUserActionReason); } public static final Parcelable.Creator<SessionInfo> diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index 4ad657e64fb3..fe063662db0d 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -1355,6 +1355,7 @@ public abstract class PackageManager { INSTALL_ENABLE_ROLLBACK, INSTALL_ALLOW_DOWNGRADE, INSTALL_STAGED, + INSTALL_REQUEST_UPDATE_OWNERSHIP, }) @Retention(RetentionPolicy.SOURCE) public @interface InstallFlags {} @@ -1545,6 +1546,21 @@ public abstract class PackageManager { */ public static final int INSTALL_DISABLE_ALLOWED_APEX_UPDATE_CHECK = 0x00800000; + /** + * Flag parameter for {@link #installPackage} to bypass the low targer sdk version block + * for this install. + * + * @hide + */ + public static final int INSTALL_BYPASS_LOW_TARGET_SDK_BLOCK = 0x00800000; + + /** + * Flag parameter for {@link PackageInstaller.SessionParams} to indicate that the + * update ownership enforcement is requested. + * @hide + */ + public static final int INSTALL_REQUEST_UPDATE_OWNERSHIP = 1 << 25; + /** @hide */ @IntDef(flag = true, value = { DONT_KILL_APP, @@ -2233,6 +2249,15 @@ public abstract class PackageManager { */ public static final int INSTALL_FAILED_PRE_APPROVAL_NOT_AVAILABLE = -129; + /** + * Installation return code: this is passed in the {@link PackageInstaller#EXTRA_LEGACY_STATUS} + * if the new package declares bad certificate digest for a shared library in the package + * manifest. + * + * @hide + */ + public static final int INSTALL_FAILED_SHARED_LIBRARY_BAD_CERTIFICATE_DIGEST = -130; + /** @hide */ @IntDef(flag = true, prefix = { "DELETE_" }, value = { DELETE_KEEP_DATA, @@ -9681,6 +9706,8 @@ public abstract class PackageManager { case INSTALL_FAILED_WRONG_INSTALLED_VERSION: return "INSTALL_FAILED_WRONG_INSTALLED_VERSION"; case INSTALL_FAILED_PROCESS_NOT_DEFINED: return "INSTALL_FAILED_PROCESS_NOT_DEFINED"; case INSTALL_FAILED_SESSION_INVALID: return "INSTALL_FAILED_SESSION_INVALID"; + case INSTALL_FAILED_SHARED_LIBRARY_BAD_CERTIFICATE_DIGEST: + return "INSTALL_FAILED_SHARED_LIBRARY_BAD_CERTIFICATE_DIGEST"; default: return Integer.toString(status); } } @@ -10850,4 +10877,16 @@ public abstract class PackageManager { throw new UnsupportedOperationException( "isShowNewAppInstalledNotificationEnabled not implemented in subclass"); } + + /** + * Attempt to relinquish the update ownership of the given package. Only the current + * update owner of the given package can use this API or a SecurityException will be + * thrown. + * + * @param targetPackage The installed package whose update owner will be changed. + */ + public void relinquishUpdateOwnership(@NonNull String targetPackage) { + throw new UnsupportedOperationException( + "relinquishUpdateOwnership not implemented in subclass"); + } } diff --git a/core/java/android/content/pm/ServiceInfo.java b/core/java/android/content/pm/ServiceInfo.java index 4ade8a8b87b6..4e2acc036386 100644 --- a/core/java/android/content/pm/ServiceInfo.java +++ b/core/java/android/content/pm/ServiceInfo.java @@ -133,20 +133,15 @@ public class ServiceInfo extends ComponentInfo * Data(photo, file, account) upload/download, backup/restore, import/export, fetch, * transfer over network between device and cloud. * - * <p>Apps targeting API level {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and - * later should NOT use this type: - * calling {@link android.app.Service#startForeground(int, android.app.Notification, int)} with - * this type on devices running {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} is still - * allowed, but calling it with this type on devices running future platform releases may get a - * {@link android.app.InvalidForegroundServiceTypeException}.</p> - * - * @deprecated Use {@link android.app.job.JobInfo.Builder} data transfer APIs instead. + * <p class="note"> + * Use the {@link android.app.job.JobInfo.Builder#setDataTransfer} API for data transfers + * that can be deferred until conditions are ideal for the app or device. + * </p> */ @RequiresPermission( value = Manifest.permission.FOREGROUND_SERVICE_DATA_SYNC, conditional = true ) - @Deprecated public static final int FOREGROUND_SERVICE_TYPE_DATA_SYNC = 1 << 0; /** diff --git a/core/java/android/credentials/ClearCredentialStateException.java b/core/java/android/credentials/ClearCredentialStateException.java index c518461f1a49..78fe203fb679 100644 --- a/core/java/android/credentials/ClearCredentialStateException.java +++ b/core/java/android/credentials/ClearCredentialStateException.java @@ -31,6 +31,12 @@ import java.util.concurrent.Executor; * CancellationSignal, Executor, OutcomeReceiver)} operation. */ public class ClearCredentialStateException extends Exception { + /** + * The error type value for when the given operation failed due to an unknown reason. + */ + @NonNull + public static final String TYPE_UNKNOWN = + "android.credentials.ClearCredentialStateException.TYPE_UNKNOWN"; @NonNull private final String mType; diff --git a/core/java/android/credentials/CreateCredentialException.java b/core/java/android/credentials/CreateCredentialException.java index cb326903a828..fefa60ae23ee 100644 --- a/core/java/android/credentials/CreateCredentialException.java +++ b/core/java/android/credentials/CreateCredentialException.java @@ -33,6 +33,13 @@ import java.util.concurrent.Executor; */ public class CreateCredentialException extends Exception { /** + * The error type value for when the given operation failed due to an unknown reason. + */ + @NonNull + public static final String TYPE_UNKNOWN = + "android.credentials.CreateCredentialException.TYPE_UNKNOWN"; + + /** * The error type value for when no credential is available for the given {@link * CredentialManager#executeCreateCredential(CreateCredentialRequest, Activity, * CancellationSignal, Executor, OutcomeReceiver)} request. @@ -40,6 +47,22 @@ public class CreateCredentialException extends Exception { @NonNull public static final String TYPE_NO_CREDENTIAL = "android.credentials.CreateCredentialException.TYPE_NO_CREDENTIAL"; + /** + * The error type value for when the user intentionally cancelled the request. + * + * <p>This is a strong indicator that your app should refrain from making the same api call for + * a certain amount of time to provide a better user experience. + */ + @NonNull + public static final String TYPE_USER_CANCELED = + "android.credentials.CreateCredentialException.TYPE_USER_CANCELED"; + /** + * The error type value for when the given operation failed due to internal interruption. + * Retrying the same operation should fix the error. + */ + @NonNull + public static final String TYPE_INTERRUPTED = + "android.credentials.CreateCredentialException.TYPE_INTERRUPTED"; @NonNull private final String mType; diff --git a/core/java/android/credentials/GetCredentialException.java b/core/java/android/credentials/GetCredentialException.java index 5d6e4dfa343c..478afff1fae1 100644 --- a/core/java/android/credentials/GetCredentialException.java +++ b/core/java/android/credentials/GetCredentialException.java @@ -33,6 +33,13 @@ import java.util.concurrent.Executor; */ public class GetCredentialException extends Exception { /** + * The error type value for when the given operation failed due to an unknown reason. + */ + @NonNull + public static final String TYPE_UNKNOWN = + "android.credentials.GetCredentialException.TYPE_UNKNOWN"; + + /** * The error type value for when no credential is found available for the given {@link * CredentialManager#executeGetCredential(GetCredentialRequest, Activity, CancellationSignal, * Executor, OutcomeReceiver)} request. @@ -40,6 +47,22 @@ public class GetCredentialException extends Exception { @NonNull public static final String TYPE_NO_CREDENTIAL = "android.credentials.GetCredentialException.TYPE_NO_CREDENTIAL"; + /** + * The error type value for when the user intentionally cancelled the request. + * + * <p>This is a strong indicator that your app should refrain from making the same api call for + * a certain amount of time to provide a better user experience. + */ + @NonNull + public static final String TYPE_USER_CANCELED = + "android.credentials.GetCredentialException.TYPE_USER_CANCELED"; + /** + * The error type value for when the given operation failed due to internal interruption. + * Retrying the same operation should fix the error. + */ + @NonNull + public static final String TYPE_INTERRUPTED = + "android.credentials.GetCredentialException.TYPE_INTERRUPTED"; @NonNull private final String mType; diff --git a/core/java/android/hardware/OverlayProperties.java b/core/java/android/hardware/OverlayProperties.java index 1ce1361cd4e7..8bfc2f7da25e 100644 --- a/core/java/android/hardware/OverlayProperties.java +++ b/core/java/android/hardware/OverlayProperties.java @@ -59,6 +59,16 @@ public final class OverlayProperties implements Parcelable { } /** + * @return True if the device can support mixed colorspaces, false otherwise. + */ + public boolean supportMixedColorSpaces() { + if (mNativeObject == 0) { + return false; + } + return nSupportMixedColorSpaces(mNativeObject); + } + + /** * Release the local reference. */ public void release() { @@ -106,6 +116,7 @@ public final class OverlayProperties implements Parcelable { private static native long nGetDestructor(); private static native boolean nSupportFp16ForHdr(long nativeObject); + private static native boolean nSupportMixedColorSpaces(long nativeObject); private static native void nWriteOverlayPropertiesToParcel(long nativeObject, Parcel dest); private static native long nReadOverlayPropertiesFromParcel(Parcel in); } diff --git a/core/java/android/service/credentials/CredentialProviderInfo.java b/core/java/android/service/credentials/CredentialProviderInfo.java index f89ad8e6e429..6a10a6ac891d 100644 --- a/core/java/android/service/credentials/CredentialProviderInfo.java +++ b/core/java/android/service/credentials/CredentialProviderInfo.java @@ -24,6 +24,7 @@ import android.app.AppGlobals; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; @@ -58,6 +59,7 @@ public final class CredentialProviderInfo { private final Drawable mIcon; @Nullable private final CharSequence mLabel; + private final boolean mIsSystemProvider; /** * Constructs an information instance of the credential provider. @@ -65,13 +67,14 @@ public final class CredentialProviderInfo { * @param context the context object * @param serviceComponent the serviceComponent of the provider service * @param userId the android userId for which the current process is running + * @param isSystemProvider whether this provider is a system provider * @throws PackageManager.NameNotFoundException If provider service is not found * @throws SecurityException If provider does not require the relevant permission */ public CredentialProviderInfo(@NonNull Context context, - @NonNull ComponentName serviceComponent, int userId) + @NonNull ComponentName serviceComponent, int userId, boolean isSystemProvider) throws PackageManager.NameNotFoundException { - this(context, getServiceInfoOrThrow(serviceComponent, userId)); + this(context, getServiceInfoOrThrow(serviceComponent, userId), isSystemProvider); } /** @@ -79,8 +82,11 @@ public final class CredentialProviderInfo { * @param context the context object * @param serviceInfo the service info for the provider app. This must be retrieved from the * {@code PackageManager} + * @param isSystemProvider whether the provider is a system app or not */ - public CredentialProviderInfo(@NonNull Context context, @NonNull ServiceInfo serviceInfo) { + public CredentialProviderInfo(@NonNull Context context, + @NonNull ServiceInfo serviceInfo, + boolean isSystemProvider) { if (!Manifest.permission.BIND_CREDENTIAL_PROVIDER_SERVICE.equals(serviceInfo.permission)) { Log.i(TAG, "Credential Provider Service from : " + serviceInfo.packageName + "does not require permission" @@ -95,6 +101,7 @@ public final class CredentialProviderInfo { mLabel = mServiceInfo.loadSafeLabel( mContext.getPackageManager(), 0 /* do not ellipsize */, TextUtils.SAFE_STRING_FLAG_FIRST_LINE | TextUtils.SAFE_STRING_FLAG_TRIM); + mIsSystemProvider = isSystemProvider; Log.i(TAG, "mLabel is : " + mLabel + ", for: " + mServiceInfo.getComponentName() .flattenToString()); populateProviderCapabilities(context, serviceInfo); @@ -147,6 +154,42 @@ public final class CredentialProviderInfo { } /** + * Returns the valid credential provider services available for the user with the + * given {@code userId}. + */ + @NonNull + public static List<CredentialProviderInfo> getAvailableSystemServices( + @NonNull Context context, + @UserIdInt int userId) { + final List<CredentialProviderInfo> services = new ArrayList<>(); + + final List<ResolveInfo> resolveInfos = + context.getPackageManager().queryIntentServicesAsUser( + new Intent(CredentialProviderService.SYSTEM_SERVICE_INTERFACE), + PackageManager.ResolveInfoFlags.of(PackageManager.GET_META_DATA), + userId); + for (ResolveInfo resolveInfo : resolveInfos) { + final ServiceInfo serviceInfo = resolveInfo.serviceInfo; + try { + ApplicationInfo appInfo = context.getPackageManager().getApplicationInfo( + serviceInfo.packageName, + PackageManager.ApplicationInfoFlags.of(PackageManager.MATCH_SYSTEM_ONLY)); + if (appInfo != null + && context.checkPermission(Manifest.permission.SYSTEM_CREDENTIAL_PROVIDER, + /*pId=*/-1, appInfo.uid) == PackageManager.PERMISSION_GRANTED) { + services.add(new CredentialProviderInfo(context, serviceInfo, + /*isSystemProvider=*/true)); + } + } catch (SecurityException e) { + Log.i(TAG, "Error getting info for " + serviceInfo + ": " + e); + } catch (PackageManager.NameNotFoundException e) { + Log.i(TAG, "Error getting info for " + serviceInfo + ": " + e); + } + } + return services; + } + + /** * Returns true if the service supports the given {@code credentialType}, false otherwise. */ @NonNull @@ -160,6 +203,10 @@ public final class CredentialProviderInfo { return mServiceInfo; } + public boolean isSystemProvider() { + return mIsSystemProvider; + } + /** Returns the service icon. */ @Nullable public Drawable getServiceIcon() { @@ -195,7 +242,8 @@ public final class CredentialProviderInfo { for (ResolveInfo resolveInfo : resolveInfos) { final ServiceInfo serviceInfo = resolveInfo.serviceInfo; try { - services.add(new CredentialProviderInfo(context, serviceInfo)); + services.add(new CredentialProviderInfo(context, + serviceInfo, false)); } catch (SecurityException e) { Log.w(TAG, "Error getting info for " + serviceInfo + ": " + e); } diff --git a/core/java/android/service/credentials/CredentialProviderService.java b/core/java/android/service/credentials/CredentialProviderService.java index 70dd16c5ca3a..0a3d95de722a 100644 --- a/core/java/android/service/credentials/CredentialProviderService.java +++ b/core/java/android/service/credentials/CredentialProviderService.java @@ -140,6 +140,20 @@ public abstract class CredentialProviderService extends Service { public static final String SERVICE_INTERFACE = "android.service.credentials.CredentialProviderService"; + /** + * The {@link Intent} that must be declared as handled by a system credential provider + * service. + * + * <p>The service must also require the + * {android.Manifest.permission#BIND_CREDENTIAL_PROVIDER_SERVICE} permission + * so that only the system can bind to it. + * + * @hide + */ + @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION) + public static final String SYSTEM_SERVICE_INTERFACE = + "android.service.credentials.system.CredentialProviderService"; + @CallSuper @Override public void onCreate() { diff --git a/core/java/android/service/dreams/DreamActivity.java b/core/java/android/service/dreams/DreamActivity.java index a2fa1392b079..a3892238f1e6 100644 --- a/core/java/android/service/dreams/DreamActivity.java +++ b/core/java/android/service/dreams/DreamActivity.java @@ -58,11 +58,13 @@ public class DreamActivity extends Activity { setTitle(title); } - final Bundle extras = getIntent().getExtras(); - mCallback = (DreamService.DreamActivityCallbacks) extras.getBinder(EXTRA_CALLBACK); - - if (mCallback != null) { + final Object callback = getIntent().getExtras().getBinder(EXTRA_CALLBACK); + if (callback instanceof DreamService.DreamActivityCallbacks) { + mCallback = (DreamService.DreamActivityCallbacks) callback; mCallback.onActivityCreated(this); + } else { + mCallback = null; + finishAndRemoveTask(); } } diff --git a/core/java/android/text/TextShaper.java b/core/java/android/text/TextShaper.java index a1d6cc8e283a..6da0b63dbc1f 100644 --- a/core/java/android/text/TextShaper.java +++ b/core/java/android/text/TextShaper.java @@ -173,7 +173,7 @@ public class TextShaper { private TextShaper() {} /** - * An consumer interface for accepting text shape result. + * A consumer interface for accepting text shape result. */ public interface GlyphsConsumer { /** diff --git a/core/java/android/view/InputDevice.java b/core/java/android/view/InputDevice.java index 5a9a2520d839..8683cc2a8009 100644 --- a/core/java/android/view/InputDevice.java +++ b/core/java/android/view/InputDevice.java @@ -555,6 +555,7 @@ public final class InputDevice implements Parcelable { private String mKeyboardLanguageTag = null; private String mKeyboardLayoutType = null; private boolean mSupportsUsi = false; + private List<MotionRange> mMotionRanges = new ArrayList<>(); /** @see InputDevice#getId() */ public Builder setId(int id) { @@ -670,12 +671,50 @@ public final class InputDevice implements Parcelable { return this; } + /** @see InputDevice#getMotionRanges() */ + public Builder addMotionRange(int axis, int source, + float min, float max, float flat, float fuzz, float resolution) { + mMotionRanges.add(new MotionRange(axis, source, min, max, flat, fuzz, resolution)); + return this; + } + /** Build {@link InputDevice}. */ public InputDevice build() { - return new InputDevice(mId, mGeneration, mControllerNumber, mName, mVendorId, - mProductId, mDescriptor, mIsExternal, mSources, mKeyboardType, mKeyCharacterMap, - mKeyboardLanguageTag, mKeyboardLayoutType, mHasVibrator, mHasMicrophone, - mHasButtonUnderPad, mHasSensor, mHasBattery, mSupportsUsi); + InputDevice device = new InputDevice( + mId, + mGeneration, + mControllerNumber, + mName, + mVendorId, + mProductId, + mDescriptor, + mIsExternal, + mSources, + mKeyboardType, + mKeyCharacterMap, + mKeyboardLanguageTag, + mKeyboardLayoutType, + mHasVibrator, + mHasMicrophone, + mHasButtonUnderPad, + mHasSensor, + mHasBattery, + mSupportsUsi); + + final int numRanges = mMotionRanges.size(); + for (int i = 0; i < numRanges; i++) { + final MotionRange range = mMotionRanges.get(i); + device.addMotionRange( + range.getAxis(), + range.getSource(), + range.getMin(), + range.getMax(), + range.getFlat(), + range.getFuzz(), + range.getResolution()); + } + + return device; } } @@ -1378,7 +1417,8 @@ public final class InputDevice implements Parcelable { out.writeInt(mHasBattery ? 1 : 0); out.writeInt(mSupportsUsi ? 1 : 0); - final int numRanges = mMotionRanges.size(); + int numRanges = mMotionRanges.size(); + numRanges = numRanges > MAX_RANGES ? MAX_RANGES : numRanges; out.writeInt(numRanges); for (int i = 0; i < numRanges; i++) { MotionRange range = mMotionRanges.get(i); diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 3502c34091a2..da9594baf6b8 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -1110,6 +1110,8 @@ public final class ViewRootImpl implements ViewParent, // Update the last resource config in case the resource configuration was changed while // activity relaunched. updateLastConfigurationFromResources(getConfiguration()); + // Make sure to report the completion of draw for relaunch with preserved window. + reportNextDraw("rebuilt"); } private Configuration getConfiguration() { diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index d0f0d4a41ec2..35f1787c0bb5 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -853,6 +853,143 @@ public interface WindowManager extends ViewManager { "android.window.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION"; /** + * Activity level {@link android.content.pm.PackageManager.Property PackageManager + * .Property} for an app to inform the system that the activity should be excluded from the + * camera compatibility force rotation treatment. + * + * <p>The camera compatibility treatment aligns orientations of portrait app window and natural + * orientation of the device and set opposite to natural orientation for a landscape app + * window. Mismatch between them can lead to camera issues like sideways or stretched + * viewfinder since this is one of the strongest assumptions that apps make when they implement + * camera previews. Since app and natural display orientations aren't guaranteed to match, the + * rotation can cause letterboxing. The forced rotation is triggered as soon as app opens to + * camera and is removed once camera is closed. + * + * <p>The camera compatibility can be enabled by device manufacturers on the displays that have + * ignoreOrientationRequest display setting enabled (enables compatibility mode for fixed + * orientation, see <a href="https://developer.android.com/guide/practices/enhanced-letterboxing">Enhanced letterboxing</a> + * for more details). + * + * <p>With this property set to {@code true} or unset, the system may apply the force rotation + * treatment to fixed orientation activities. Device manufacturers can exclude packages from the + * treatment using their discretion to improve display compatibility. + * + * <p>With this property set to {@code false}, the system will not apply the force rotation + * treatment. + * + * <p><b>Syntax:</b> + * <pre> + * <activity> + * <property + * android:name="android.window.PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION" + * android:value="true|false"/> + * </activity> + * </pre> + * + * @hide + */ + // TODO(b/263984287): Make this public API. + String PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION = + "android.window.PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION"; + + /** + * Activity level {@link android.content.pm.PackageManager.Property PackageManager + * .Property} for an app to inform the system that the activity should be excluded + * from the activity "refresh" after the camera compatibility force rotation treatment. + * + * <p>The camera compatibility treatment aligns orientations of portrait app window and natural + * orientation of the device and set opposite to natural orientation for a landscape app + * window. Mismatch between them can lead to camera issues like sideways or stretched + * viewfinder since this is one of the strongest assumptions that apps make when they implement + * camera previews. Since app and natural display orientations aren't guaranteed to match, the + * rotation can cause letterboxing. The forced rotation is triggered as soon as app opens to + * camera and is removed once camera is closed. + * + * <p>Force rotation is followed by the "Refresh" of the activity by going through "resumed -> + * ... -> stopped -> ... -> resumed" cycle (by default) or "resumed -> paused -> resumed" cycle + * (if overridden, see {@link #PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE} for context). + * This allows to clear cached values in apps (e.g. display or camera rotation) that influence + * camera preview and can lead to sideways or stretching issues persisting even after force + * rotation. + * + * <p>The camera compatibility can be enabled by device manufacturers on the displays that have + * ignoreOrientationRequest display setting enabled (enables compatibility mode for fixed + * orientation, see <a href="https://developer.android.com/guide/practices/enhanced-letterboxing">Enhanced letterboxing</a> + * for more details). + * + * <p>With this property set to {@code true} or unset, the system may "refresh" activity after + * the force rotation treatment. Device manufacturers can exclude packages from the "refresh" + * using their discretion to improve display compatibility. + * + * <p>With this property set to {@code false}, the system will not "refresh" activity after the + * force rotation treatment. + * + * <p><b>Syntax:</b> + * <pre> + * <activity> + * <property + * android:name="android.window.PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH" + * android:value="true|false"/> + * </activity> + * </pre> + * + * @hide + */ + // TODO(b/263984287): Make this public API. + String PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH = + "android.window.PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH"; + + /** + * Activity level {@link android.content.pm.PackageManager.Property PackageManager + * .Property} for an app to inform the system that the activity should be or shouldn't be + * "refreshed" after the camera compatibility force rotation treatment using "paused -> + * resumed" cycle rather than "stopped -> resumed". + * + * <p>The camera compatibility treatment aligns orientations of portrait app window and natural + * orientation of the device and set opposite to natural orientation for a landscape app + * window. Mismatch between them can lead to camera issues like sideways or stretched + * viewfinder since this is one of the strongest assumptions that apps make when they implement + * camera previews. Since app and natural display orientations aren't guaranteed to match, the + * rotation can cause letterboxing. The forced rotation is triggered as soon as app opens to + * camera and is removed once camera is closed. + * + * <p>Force rotation is followed by the "Refresh" of the activity by going through "resumed -> + * ... -> stopped -> ... -> resumed" cycle (by default) or "resumed -> paused -> resumed" cycle + * (if overridden by device manufacturers or using this property). This allows to clear cached + * values in apps (e.g., display or camera rotation) that influence camera preview and can lead + * to sideways or stretching issues persisting even after force rotation. + * + * <p>The camera compatibility can be enabled by device manufacturers on the displays that have + * ignoreOrientationRequest display setting enabled (enables compatibility mode for fixed + * orientation, see <a href="https://developer.android.com/guide/practices/enhanced-letterboxing">Enhanced letterboxing</a> + * for more details). + * + * <p>Device manufacturers can override packages to "refresh" via "resumed -> paused -> resumed" + * cycle using their discretion to improve display compatibility. + * + * <p>With this property set to {@code true}, the system will "refresh" activity after the + * force rotation treatment using "resumed -> paused -> resumed" cycle. + * + * <p>With this property set to {@code false}, the system will not "refresh" activity after the + * force rotation treatment using "resumed -> paused -> resumed" cycle even if the device + * manufacturer adds the corresponding override. + * + * <p><b>Syntax:</b> + * <pre> + * <activity> + * <property + * android:name="android.window.PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE" + * android:value="true|false"/> + * </activity> + * </pre> + * + * @hide + */ + // TODO(b/263984287): Make this public API. + String PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE = + "android.window.PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE"; + + /** * @hide */ public static final String PARCEL_KEY_SHORTCUTS_ARRAY = "shortcuts_array"; diff --git a/core/java/android/view/accessibility/AccessibilityDisplayProxy.java b/core/java/android/view/accessibility/AccessibilityDisplayProxy.java index a757236f92fd..dd320e196e8b 100644 --- a/core/java/android/view/accessibility/AccessibilityDisplayProxy.java +++ b/core/java/android/view/accessibility/AccessibilityDisplayProxy.java @@ -83,7 +83,7 @@ public abstract class AccessibilityDisplayProxy { * @param displayId the id of the display to proxy. * @param executor the executor used to execute proxy callbacks. * @param installedAndEnabledServices the list of infos representing the installed and - * enabled a11y services. + * enabled accessibility services. */ public AccessibilityDisplayProxy(int displayId, @NonNull Executor executor, @NonNull List<AccessibilityServiceInfo> installedAndEnabledServices) { @@ -147,19 +147,27 @@ public abstract class AccessibilityDisplayProxy { } /** - * Gets the focus of the window specified by {@code windowInfo}. + * Gets the node with focus, in this display. * - * @param windowInfo the window to search - * @param focus The focus to find. One of {@link AccessibilityNodeInfo#FOCUS_INPUT} or + * <p>For {@link AccessibilityNodeInfo#FOCUS_INPUT}, this returns the input-focused node in the + * proxy display if this display can receive unspecified input events (input that does not + * specify a target display.) + * + * <p>For {@link AccessibilityNodeInfo#FOCUS_ACCESSIBILITY}, this returns the + * accessibility-focused node in the proxy display if the display has accessibility focus. + * @param focusType The focus to find. One of {@link AccessibilityNodeInfo#FOCUS_INPUT} or * {@link AccessibilityNodeInfo#FOCUS_ACCESSIBILITY}. * @return The node info of the focused view or null. - * @hide - * TODO(254545943): Do not expose until support for accessibility focus and/or input is in place + */ @Nullable - public AccessibilityNodeInfo findFocus(@NonNull AccessibilityWindowInfo windowInfo, int focus) { - AccessibilityNodeInfo windowRoot = windowInfo.getRoot(); - return windowRoot != null ? windowRoot.findFocus(focus) : null; + public AccessibilityNodeInfo findFocus(int focusType) { + // TODO(264423198): Support querying the focused node of the proxy's display even if it is + // not the top-focused display and can't receive untargeted input events. + // TODO(254545943): Separate accessibility focus between proxy and phone state. + return AccessibilityInteractionClient.getInstance().findFocus(mConnectionId, + AccessibilityWindowInfo.ANY_WINDOW_ID, AccessibilityNodeInfo.ROOT_NODE_ID, + focusType); } /** @@ -177,10 +185,10 @@ public abstract class AccessibilityDisplayProxy { * Sets the list of {@link AccessibilityServiceInfo}s describing the services interested in the * {@link AccessibilityDisplayProxy}'s display. * - * <p>These represent a11y features and services that are installed and running. These should - * not include {@link AccessibilityService}s installed on the phone. + * <p>These represent accessibility features and services that are installed and running. These + * should not include {@link AccessibilityService}s installed on the phone. * - * @param installedAndEnabledServices the list of installed and running a11y services. + * @param installedAndEnabledServices the list of installed and running accessibility services. */ public void setInstalledAndEnabledServices( @NonNull List<AccessibilityServiceInfo> installedAndEnabledServices) { diff --git a/core/java/android/view/accessibility/AccessibilityInteractionClient.java b/core/java/android/view/accessibility/AccessibilityInteractionClient.java index 52eda0a19c55..83a6c5a2e8a6 100644 --- a/core/java/android/view/accessibility/AccessibilityInteractionClient.java +++ b/core/java/android/view/accessibility/AccessibilityInteractionClient.java @@ -921,8 +921,6 @@ public final class AccessibilityInteractionClient * * @param connectionId The id of a connection for interacting with the system. * @param accessibilityWindowId A unique window id. Use - * {@link AccessibilityWindowInfo#ACTIVE_WINDOW_ID} - * to query the currently active window. Use * {@link AccessibilityWindowInfo#ANY_WINDOW_ID} to query all * windows * @param accessibilityNodeId A unique view id or virtual descendant id from diff --git a/core/java/android/view/contentcapture/ContentCaptureManager.java b/core/java/android/view/contentcapture/ContentCaptureManager.java index d067d4bc366b..497f0668107f 100644 --- a/core/java/android/view/contentcapture/ContentCaptureManager.java +++ b/core/java/android/view/contentcapture/ContentCaptureManager.java @@ -66,8 +66,7 @@ import java.util.concurrent.Executor; import java.util.function.Consumer; /** - * <p>The {@link ContentCaptureManager} provides additional ways for for apps to - * integrate with the content capture subsystem. + * <p>Provides additional ways for apps to integrate with the content capture subsystem. * * <p>Content capture provides real-time, continuous capture of application activity, display and * events to an intelligence service that is provided by the Android system. The intelligence diff --git a/core/java/android/webkit/TEST_MAPPING b/core/java/android/webkit/TEST_MAPPING index bd25200ffc38..c1bc6d720ece 100644 --- a/core/java/android/webkit/TEST_MAPPING +++ b/core/java/android/webkit/TEST_MAPPING @@ -9,6 +9,14 @@ ] }, { + "name": "CtsSdkSandboxWebkitTestCases", + "options": [ + { + "exclude-annotation": "androidx.test.filters.FlakyTest" + } + ] + }, + { "name": "CtsHostsideWebViewTests", "options": [ { diff --git a/core/java/android/webkit/WebResourceError.java b/core/java/android/webkit/WebResourceError.java index 11f1b6f17566..4c874892d576 100644 --- a/core/java/android/webkit/WebResourceError.java +++ b/core/java/android/webkit/WebResourceError.java @@ -19,7 +19,7 @@ package android.webkit; import android.annotation.SystemApi; /** - * Encapsulates information about errors occured during loading of web resources. See + * Encapsulates information about errors that occurred during loading of web resources. See * {@link WebViewClient#onReceivedError(WebView, WebResourceRequest, WebResourceError) WebViewClient.onReceivedError(WebView, WebResourceRequest, WebResourceError)} */ public abstract class WebResourceError { diff --git a/core/java/android/widget/MediaController.java b/core/java/android/widget/MediaController.java index ff2e17548df4..2bd5c8859b9f 100644 --- a/core/java/android/widget/MediaController.java +++ b/core/java/android/widget/MediaController.java @@ -218,6 +218,7 @@ public class MediaController extends FrameLayout { p.width = mAnchor.getWidth(); p.x = anchorPos[0] + (mAnchor.getWidth() - p.width) / 2; p.y = anchorPos[1] + mAnchor.getHeight() - mDecor.getMeasuredHeight(); + p.token = mAnchor.getWindowToken(); } // This is called whenever mAnchor's layout bound changes diff --git a/core/java/android/window/ITaskOrganizerController.aidl b/core/java/android/window/ITaskOrganizerController.aidl index e6bb1f64ad86..0032b9ce0512 100644 --- a/core/java/android/window/ITaskOrganizerController.aidl +++ b/core/java/android/window/ITaskOrganizerController.aidl @@ -40,7 +40,8 @@ interface ITaskOrganizerController { void unregisterTaskOrganizer(ITaskOrganizer organizer); /** Creates a persistent root task in WM for a particular windowing-mode. */ - void createRootTask(int displayId, int windowingMode, IBinder launchCookie); + void createRootTask(int displayId, int windowingMode, IBinder launchCookie, + boolean removeWithTaskOrganizer); /** Deletes a persistent root task in WM */ boolean deleteRootTask(in WindowContainerToken task); diff --git a/core/java/android/window/TaskFragmentAnimationParams.aidl b/core/java/android/window/TaskFragmentAnimationParams.aidl index 04dee58089d4..8ae84c22dda1 100644 --- a/core/java/android/window/TaskFragmentAnimationParams.aidl +++ b/core/java/android/window/TaskFragmentAnimationParams.aidl @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/core/java/android/window/TaskFragmentAnimationParams.java b/core/java/android/window/TaskFragmentAnimationParams.java index a600a4db42b8..12ad91498626 100644 --- a/core/java/android/window/TaskFragmentAnimationParams.java +++ b/core/java/android/window/TaskFragmentAnimationParams.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/core/java/android/window/TaskFragmentOperation.aidl b/core/java/android/window/TaskFragmentOperation.aidl index c21700c6634b..c1ed0c7846e3 100644 --- a/core/java/android/window/TaskFragmentOperation.aidl +++ b/core/java/android/window/TaskFragmentOperation.aidl @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/core/java/android/window/TaskFragmentOperation.java b/core/java/android/window/TaskFragmentOperation.java index bec6c58e4c8a..3272c3bc0d09 100644 --- a/core/java/android/window/TaskFragmentOperation.java +++ b/core/java/android/window/TaskFragmentOperation.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,8 @@ package android.window; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.content.Intent; +import android.os.Bundle; import android.os.IBinder; import android.os.Parcel; import android.os.Parcelable; @@ -30,41 +32,108 @@ import java.util.Objects; /** * Data object of params for TaskFragment related {@link WindowContainerTransaction} operation. * - * @see WindowContainerTransaction#setTaskFragmentOperation(IBinder, TaskFragmentOperation). + * @see WindowContainerTransaction#addTaskFragmentOperation(IBinder, TaskFragmentOperation). * @hide */ -// TODO(b/263436063): move other TaskFragment related operation here. public final class TaskFragmentOperation implements Parcelable { + /** + * Type for tracking other {@link WindowContainerTransaction} to TaskFragment that is not set + * through {@link TaskFragmentOperation}, such as {@link WindowContainerTransaction#setBounds}. + */ + public static final int OP_TYPE_UNKNOWN = -1; + + /** Creates a new TaskFragment. */ + public static final int OP_TYPE_CREATE_TASK_FRAGMENT = 0; + + /** Deletes the given TaskFragment. */ + public static final int OP_TYPE_DELETE_TASK_FRAGMENT = 1; + + /** Starts an Activity in the given TaskFragment. */ + public static final int OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT = 2; + + /** Reparents the given Activity to the given TaskFragment. */ + public static final int OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT = 3; + + /** Sets two TaskFragments adjacent to each other. */ + public static final int OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS = 4; + + /** Requests focus on the top running Activity in the given TaskFragment. */ + public static final int OP_TYPE_REQUEST_FOCUS_ON_TASK_FRAGMENT = 5; + + /** Sets a given TaskFragment to have a companion TaskFragment. */ + public static final int OP_TYPE_SET_COMPANION_TASK_FRAGMENT = 6; + /** Sets the {@link TaskFragmentAnimationParams} for the given TaskFragment. */ - public static final int OP_TYPE_SET_ANIMATION_PARAMS = 0; + public static final int OP_TYPE_SET_ANIMATION_PARAMS = 7; @IntDef(prefix = { "OP_TYPE_" }, value = { + OP_TYPE_UNKNOWN, + OP_TYPE_CREATE_TASK_FRAGMENT, + OP_TYPE_DELETE_TASK_FRAGMENT, + OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT, + OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT, + OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS, + OP_TYPE_REQUEST_FOCUS_ON_TASK_FRAGMENT, + OP_TYPE_SET_COMPANION_TASK_FRAGMENT, OP_TYPE_SET_ANIMATION_PARAMS }) @Retention(RetentionPolicy.SOURCE) - @interface OperationType {} + public @interface OperationType {} @OperationType private final int mOpType; @Nullable + private final TaskFragmentCreationParams mTaskFragmentCreationParams; + + @Nullable + private final IBinder mActivityToken; + + @Nullable + private final Intent mActivityIntent; + + @Nullable + private final Bundle mBundle; + + @Nullable + private final IBinder mSecondaryFragmentToken; + + @Nullable private final TaskFragmentAnimationParams mAnimationParams; private TaskFragmentOperation(@OperationType int opType, + @Nullable TaskFragmentCreationParams taskFragmentCreationParams, + @Nullable IBinder activityToken, @Nullable Intent activityIntent, + @Nullable Bundle bundle, @Nullable IBinder secondaryFragmentToken, @Nullable TaskFragmentAnimationParams animationParams) { mOpType = opType; + mTaskFragmentCreationParams = taskFragmentCreationParams; + mActivityToken = activityToken; + mActivityIntent = activityIntent; + mBundle = bundle; + mSecondaryFragmentToken = secondaryFragmentToken; mAnimationParams = animationParams; } private TaskFragmentOperation(Parcel in) { mOpType = in.readInt(); + mTaskFragmentCreationParams = in.readTypedObject(TaskFragmentCreationParams.CREATOR); + mActivityToken = in.readStrongBinder(); + mActivityIntent = in.readTypedObject(Intent.CREATOR); + mBundle = in.readBundle(getClass().getClassLoader()); + mSecondaryFragmentToken = in.readStrongBinder(); mAnimationParams = in.readTypedObject(TaskFragmentAnimationParams.CREATOR); } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeInt(mOpType); + dest.writeTypedObject(mTaskFragmentCreationParams, flags); + dest.writeStrongBinder(mActivityToken); + dest.writeTypedObject(mActivityIntent, flags); + dest.writeBundle(mBundle); + dest.writeStrongBinder(mSecondaryFragmentToken); dest.writeTypedObject(mAnimationParams, flags); } @@ -91,6 +160,46 @@ public final class TaskFragmentOperation implements Parcelable { } /** + * Gets the options to create a new TaskFragment. + */ + @Nullable + public TaskFragmentCreationParams getTaskFragmentCreationParams() { + return mTaskFragmentCreationParams; + } + + /** + * Gets the Activity token set in this operation. + */ + @Nullable + public IBinder getActivityToken() { + return mActivityToken; + } + + /** + * Gets the Intent to start a new Activity. + */ + @Nullable + public Intent getActivityIntent() { + return mActivityIntent; + } + + /** + * Gets the Bundle set in this operation. + */ + @Nullable + public Bundle getBundle() { + return mBundle; + } + + /** + * Gets the fragment token of the secondary TaskFragment set in this operation. + */ + @Nullable + public IBinder getSecondaryFragmentToken() { + return mSecondaryFragmentToken; + } + + /** * Gets the animation related override of TaskFragment. */ @Nullable @@ -102,6 +211,21 @@ public final class TaskFragmentOperation implements Parcelable { public String toString() { final StringBuilder sb = new StringBuilder(); sb.append("TaskFragmentOperation{ opType=").append(mOpType); + if (mTaskFragmentCreationParams != null) { + sb.append(", taskFragmentCreationParams=").append(mTaskFragmentCreationParams); + } + if (mActivityToken != null) { + sb.append(", activityToken=").append(mActivityToken); + } + if (mActivityIntent != null) { + sb.append(", activityIntent=").append(mActivityIntent); + } + if (mBundle != null) { + sb.append(", bundle=").append(mBundle); + } + if (mSecondaryFragmentToken != null) { + sb.append(", secondaryFragmentToken=").append(mSecondaryFragmentToken); + } if (mAnimationParams != null) { sb.append(", animationParams=").append(mAnimationParams); } @@ -112,9 +236,8 @@ public final class TaskFragmentOperation implements Parcelable { @Override public int hashCode() { - int result = mOpType; - result = result * 31 + mAnimationParams.hashCode(); - return result; + return Objects.hash(mOpType, mTaskFragmentCreationParams, mActivityToken, mActivityIntent, + mBundle, mSecondaryFragmentToken, mAnimationParams); } @Override @@ -124,6 +247,11 @@ public final class TaskFragmentOperation implements Parcelable { } final TaskFragmentOperation other = (TaskFragmentOperation) obj; return mOpType == other.mOpType + && Objects.equals(mTaskFragmentCreationParams, other.mTaskFragmentCreationParams) + && Objects.equals(mActivityToken, other.mActivityToken) + && Objects.equals(mActivityIntent, other.mActivityIntent) + && Objects.equals(mBundle, other.mBundle) + && Objects.equals(mSecondaryFragmentToken, other.mSecondaryFragmentToken) && Objects.equals(mAnimationParams, other.mAnimationParams); } @@ -139,6 +267,21 @@ public final class TaskFragmentOperation implements Parcelable { private final int mOpType; @Nullable + private TaskFragmentCreationParams mTaskFragmentCreationParams; + + @Nullable + private IBinder mActivityToken; + + @Nullable + private Intent mActivityIntent; + + @Nullable + private Bundle mBundle; + + @Nullable + private IBinder mSecondaryFragmentToken; + + @Nullable private TaskFragmentAnimationParams mAnimationParams; /** @@ -149,6 +292,52 @@ public final class TaskFragmentOperation implements Parcelable { } /** + * Sets the {@link TaskFragmentCreationParams} for creating a new TaskFragment. + */ + @NonNull + public Builder setTaskFragmentCreationParams( + @Nullable TaskFragmentCreationParams taskFragmentCreationParams) { + mTaskFragmentCreationParams = taskFragmentCreationParams; + return this; + } + + /** + * Sets an Activity token to this operation. + */ + @NonNull + public Builder setActivityToken(@Nullable IBinder activityToken) { + mActivityToken = activityToken; + return this; + } + + /** + * Sets the Intent to start a new Activity. + */ + @NonNull + public Builder setActivityIntent(@Nullable Intent activityIntent) { + mActivityIntent = activityIntent; + return this; + } + + /** + * Sets a Bundle to this operation. + */ + @NonNull + public Builder setBundle(@Nullable Bundle bundle) { + mBundle = bundle; + return this; + } + + /** + * Sets the secondary fragment token to this operation. + */ + @NonNull + public Builder setSecondaryFragmentToken(@Nullable IBinder secondaryFragmentToken) { + mSecondaryFragmentToken = secondaryFragmentToken; + return this; + } + + /** * Sets the {@link TaskFragmentAnimationParams} for the given TaskFragment. */ @NonNull @@ -162,7 +351,8 @@ public final class TaskFragmentOperation implements Parcelable { */ @NonNull public TaskFragmentOperation build() { - return new TaskFragmentOperation(mOpType, mAnimationParams); + return new TaskFragmentOperation(mOpType, mTaskFragmentCreationParams, mActivityToken, + mActivityIntent, mBundle, mSecondaryFragmentToken, mAnimationParams); } } } diff --git a/core/java/android/window/TaskFragmentOrganizer.java b/core/java/android/window/TaskFragmentOrganizer.java index 283df7608806..f785a3d1514e 100644 --- a/core/java/android/window/TaskFragmentOrganizer.java +++ b/core/java/android/window/TaskFragmentOrganizer.java @@ -55,7 +55,7 @@ public class TaskFragmentOrganizer extends WindowOrganizer { public static final String KEY_ERROR_CALLBACK_TASK_FRAGMENT_INFO = "task_fragment_info"; /** - * Key to the {@link WindowContainerTransaction.HierarchyOp} in + * Key to the {@link TaskFragmentOperation.OperationType} in * {@link TaskFragmentTransaction.Change#getErrorBundle()}. */ public static final String KEY_ERROR_CALLBACK_OP_TYPE = "operation_type"; @@ -112,7 +112,7 @@ public class TaskFragmentOrganizer extends WindowOrganizer { * @hide */ public static @NonNull Bundle putErrorInfoInBundle(@NonNull Throwable exception, - @Nullable TaskFragmentInfo info, int opType) { + @Nullable TaskFragmentInfo info, @TaskFragmentOperation.OperationType int opType) { final Bundle errorBundle = new Bundle(); errorBundle.putSerializable(KEY_ERROR_CALLBACK_THROWABLE, exception); if (info != null) { diff --git a/core/java/android/window/TaskOrganizer.java b/core/java/android/window/TaskOrganizer.java index bffd4e437dfa..02878f8ae72b 100644 --- a/core/java/android/window/TaskOrganizer.java +++ b/core/java/android/window/TaskOrganizer.java @@ -152,17 +152,33 @@ public class TaskOrganizer extends WindowOrganizer { * @param windowingMode Windowing mode to put the root task in. * @param launchCookie Launch cookie to associate with the task so that is can be identified * when the {@link ITaskOrganizer#onTaskAppeared} callback is called. + * @param removeWithTaskOrganizer True if this task should be removed when organizer destroyed. + * @hide */ @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS) - @Nullable - public void createRootTask(int displayId, int windowingMode, @Nullable IBinder launchCookie) { + public void createRootTask(int displayId, int windowingMode, @Nullable IBinder launchCookie, + boolean removeWithTaskOrganizer) { try { - mTaskOrganizerController.createRootTask(displayId, windowingMode, launchCookie); + mTaskOrganizerController.createRootTask(displayId, windowingMode, launchCookie, + removeWithTaskOrganizer); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } + /** + * Creates a persistent root task in WM for a particular windowing-mode. + * @param displayId The display to create the root task on. + * @param windowingMode Windowing mode to put the root task in. + * @param launchCookie Launch cookie to associate with the task so that is can be identified + * when the {@link ITaskOrganizer#onTaskAppeared} callback is called. + */ + @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS) + @Nullable + public void createRootTask(int displayId, int windowingMode, @Nullable IBinder launchCookie) { + createRootTask(displayId, windowingMode, launchCookie, false /* removeWithTaskOrganizer */); + } + /** Deletes a persistent root task in WM */ @RequiresPermission(android.Manifest.permission.MANAGE_ACTIVITY_TASKS) public boolean deleteRootTask(@NonNull WindowContainerToken task) { diff --git a/core/java/android/window/WindowContainerTransaction.java b/core/java/android/window/WindowContainerTransaction.java index 647ccf51b5ef..80b2161edf09 100644 --- a/core/java/android/window/WindowContainerTransaction.java +++ b/core/java/android/window/WindowContainerTransaction.java @@ -16,6 +16,14 @@ package android.window; +import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_TASK_FRAGMENT; +import static android.window.TaskFragmentOperation.OP_TYPE_DELETE_TASK_FRAGMENT; +import static android.window.TaskFragmentOperation.OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT; +import static android.window.TaskFragmentOperation.OP_TYPE_REQUEST_FOCUS_ON_TASK_FRAGMENT; +import static android.window.TaskFragmentOperation.OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS; +import static android.window.TaskFragmentOperation.OP_TYPE_SET_COMPANION_TASK_FRAGMENT; +import static android.window.TaskFragmentOperation.OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT; + import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.TestApi; @@ -505,32 +513,29 @@ public final class WindowContainerTransaction implements Parcelable { /** * Creates a new TaskFragment with the given options. - * @param taskFragmentOptions the options used to create the TaskFragment. + * @param taskFragmentCreationParams the options used to create the TaskFragment. */ @NonNull public WindowContainerTransaction createTaskFragment( - @NonNull TaskFragmentCreationParams taskFragmentOptions) { - final HierarchyOp hierarchyOp = - new HierarchyOp.Builder(HierarchyOp.HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT) - .setTaskFragmentCreationOptions(taskFragmentOptions) - .build(); - mHierarchyOps.add(hierarchyOp); - return this; + @NonNull TaskFragmentCreationParams taskFragmentCreationParams) { + final TaskFragmentOperation operation = new TaskFragmentOperation.Builder( + OP_TYPE_CREATE_TASK_FRAGMENT) + .setTaskFragmentCreationParams(taskFragmentCreationParams) + .build(); + return addTaskFragmentOperation(taskFragmentCreationParams.getFragmentToken(), operation); } /** * Deletes an existing TaskFragment. Any remaining activities below it will be destroyed. - * @param taskFragment the TaskFragment to be removed. + * @param fragmentToken client assigned unique token to create TaskFragment with specified in + * {@link TaskFragmentCreationParams#getFragmentToken()}. */ @NonNull - public WindowContainerTransaction deleteTaskFragment( - @NonNull WindowContainerToken taskFragment) { - final HierarchyOp hierarchyOp = - new HierarchyOp.Builder(HierarchyOp.HIERARCHY_OP_TYPE_DELETE_TASK_FRAGMENT) - .setContainer(taskFragment.asBinder()) - .build(); - mHierarchyOps.add(hierarchyOp); - return this; + public WindowContainerTransaction deleteTaskFragment(@NonNull IBinder fragmentToken) { + final TaskFragmentOperation operation = new TaskFragmentOperation.Builder( + OP_TYPE_DELETE_TASK_FRAGMENT) + .build(); + return addTaskFragmentOperation(fragmentToken, operation); } /** @@ -546,16 +551,13 @@ public final class WindowContainerTransaction implements Parcelable { public WindowContainerTransaction startActivityInTaskFragment( @NonNull IBinder fragmentToken, @NonNull IBinder callerToken, @NonNull Intent activityIntent, @Nullable Bundle activityOptions) { - final HierarchyOp hierarchyOp = - new HierarchyOp.Builder( - HierarchyOp.HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT) - .setContainer(fragmentToken) - .setReparentContainer(callerToken) - .setActivityIntent(activityIntent) - .setLaunchOptions(activityOptions) - .build(); - mHierarchyOps.add(hierarchyOp); - return this; + final TaskFragmentOperation operation = new TaskFragmentOperation.Builder( + OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT) + .setActivityToken(callerToken) + .setActivityIntent(activityIntent) + .setBundle(activityOptions) + .build(); + return addTaskFragmentOperation(fragmentToken, operation); } /** @@ -567,33 +569,11 @@ public final class WindowContainerTransaction implements Parcelable { @NonNull public WindowContainerTransaction reparentActivityToTaskFragment( @NonNull IBinder fragmentToken, @NonNull IBinder activityToken) { - final HierarchyOp hierarchyOp = - new HierarchyOp.Builder( - HierarchyOp.HIERARCHY_OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT) - .setReparentContainer(fragmentToken) - .setContainer(activityToken) - .build(); - mHierarchyOps.add(hierarchyOp); - return this; - } - - /** - * Reparents all children of one TaskFragment to another. - * @param oldParent children of this TaskFragment will be reparented. - * @param newParent the new parent TaskFragment to move the children to. If {@code null}, the - * children will be moved to the leaf Task. - */ - @NonNull - public WindowContainerTransaction reparentChildren( - @NonNull WindowContainerToken oldParent, - @Nullable WindowContainerToken newParent) { - final HierarchyOp hierarchyOp = - new HierarchyOp.Builder(HierarchyOp.HIERARCHY_OP_TYPE_REPARENT_CHILDREN) - .setContainer(oldParent.asBinder()) - .setReparentContainer(newParent != null ? newParent.asBinder() : null) - .build(); - mHierarchyOps.add(hierarchyOp); - return this; + final TaskFragmentOperation operation = new TaskFragmentOperation.Builder( + OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT) + .setActivityToken(activityToken) + .build(); + return addTaskFragmentOperation(fragmentToken, operation); } /** @@ -613,14 +593,12 @@ public final class WindowContainerTransaction implements Parcelable { public WindowContainerTransaction setAdjacentTaskFragments( @NonNull IBinder fragmentToken1, @Nullable IBinder fragmentToken2, @Nullable TaskFragmentAdjacentParams params) { - final HierarchyOp hierarchyOp = - new HierarchyOp.Builder(HierarchyOp.HIERARCHY_OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS) - .setContainer(fragmentToken1) - .setReparentContainer(fragmentToken2) - .setLaunchOptions(params != null ? params.toBundle() : null) - .build(); - mHierarchyOps.add(hierarchyOp); - return this; + final TaskFragmentOperation operation = new TaskFragmentOperation.Builder( + OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS) + .setSecondaryFragmentToken(fragmentToken2) + .setBundle(params != null ? params.toBundle() : null) + .build(); + return addTaskFragmentOperation(fragmentToken1, operation); } /** @@ -700,14 +678,10 @@ public final class WindowContainerTransaction implements Parcelable { */ @NonNull public WindowContainerTransaction requestFocusOnTaskFragment(@NonNull IBinder fragmentToken) { - final HierarchyOp hierarchyOp = - new HierarchyOp.Builder( - HierarchyOp.HIERARCHY_OP_TYPE_REQUEST_FOCUS_ON_TASK_FRAGMENT) - .setContainer(fragmentToken) - .build(); - mHierarchyOps.add(hierarchyOp); - return this; - + final TaskFragmentOperation operation = new TaskFragmentOperation.Builder( + OP_TYPE_REQUEST_FOCUS_ON_TASK_FRAGMENT) + .build(); + return addTaskFragmentOperation(fragmentToken, operation); } /** @@ -728,30 +702,32 @@ public final class WindowContainerTransaction implements Parcelable { } /** - * Sets the TaskFragment {@code container} to have a companion TaskFragment {@code companion}. + * Sets the TaskFragment {@code fragmentToken} to have a companion TaskFragment + * {@code companionFragmentToken}. * This indicates that the organizer will remove the TaskFragment when the companion * TaskFragment is removed. * - * @param container the TaskFragment container - * @param companion the companion TaskFragment. If it is {@code null}, the transaction will - * reset the companion TaskFragment. + * @param fragmentToken client assigned unique token to create TaskFragment with specified + * in {@link TaskFragmentCreationParams#getFragmentToken()}. + * @param companionFragmentToken client assigned unique token to create TaskFragment with + * specified in + * {@link TaskFragmentCreationParams#getFragmentToken()}. + * If it is {@code null}, the transaction will reset the companion + * TaskFragment. * @hide */ @NonNull - public WindowContainerTransaction setCompanionTaskFragment(@NonNull IBinder container, - @Nullable IBinder companion) { - final HierarchyOp hierarchyOp = - new HierarchyOp.Builder( - HierarchyOp.HIERARCHY_OP_TYPE_SET_COMPANION_TASK_FRAGMENT) - .setContainer(container) - .setReparentContainer(companion) - .build(); - mHierarchyOps.add(hierarchyOp); - return this; + public WindowContainerTransaction setCompanionTaskFragment(@NonNull IBinder fragmentToken, + @Nullable IBinder companionFragmentToken) { + final TaskFragmentOperation operation = new TaskFragmentOperation.Builder( + OP_TYPE_SET_COMPANION_TASK_FRAGMENT) + .setSecondaryFragmentToken(companionFragmentToken) + .build(); + return addTaskFragmentOperation(fragmentToken, operation); } /** - * Sets the {@link TaskFragmentOperation} to apply to the given TaskFragment. + * Adds a {@link TaskFragmentOperation} to apply to the given TaskFragment. * * @param fragmentToken client assigned unique token to create TaskFragment with specified in * {@link TaskFragmentCreationParams#getFragmentToken()}. @@ -760,13 +736,13 @@ public final class WindowContainerTransaction implements Parcelable { * @hide */ @NonNull - public WindowContainerTransaction setTaskFragmentOperation(@NonNull IBinder fragmentToken, + public WindowContainerTransaction addTaskFragmentOperation(@NonNull IBinder fragmentToken, @NonNull TaskFragmentOperation taskFragmentOperation) { Objects.requireNonNull(fragmentToken); Objects.requireNonNull(taskFragmentOperation); final HierarchyOp hierarchyOp = new HierarchyOp.Builder( - HierarchyOp.HIERARCHY_OP_TYPE_SET_TASK_FRAGMENT_OPERATION) + HierarchyOp.HIERARCHY_OP_TYPE_ADD_TASK_FRAGMENT_OPERATION) .setContainer(fragmentToken) .setTaskFragmentOperation(taskFragmentOperation) .build(); @@ -1267,25 +1243,17 @@ public final class WindowContainerTransaction implements Parcelable { public static final int HIERARCHY_OP_TYPE_SET_ADJACENT_ROOTS = 4; public static final int HIERARCHY_OP_TYPE_LAUNCH_TASK = 5; public static final int HIERARCHY_OP_TYPE_SET_LAUNCH_ADJACENT_FLAG_ROOT = 6; - public static final int HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT = 7; - public static final int HIERARCHY_OP_TYPE_DELETE_TASK_FRAGMENT = 8; - public static final int HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT = 9; - public static final int HIERARCHY_OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT = 10; - public static final int HIERARCHY_OP_TYPE_REPARENT_CHILDREN = 11; - public static final int HIERARCHY_OP_TYPE_PENDING_INTENT = 12; - public static final int HIERARCHY_OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS = 13; - public static final int HIERARCHY_OP_TYPE_START_SHORTCUT = 14; - public static final int HIERARCHY_OP_TYPE_RESTORE_TRANSIENT_ORDER = 15; - public static final int HIERARCHY_OP_TYPE_ADD_RECT_INSETS_PROVIDER = 16; - public static final int HIERARCHY_OP_TYPE_REMOVE_INSETS_PROVIDER = 17; - public static final int HIERARCHY_OP_TYPE_REQUEST_FOCUS_ON_TASK_FRAGMENT = 18; - public static final int HIERARCHY_OP_TYPE_SET_ALWAYS_ON_TOP = 19; - public static final int HIERARCHY_OP_TYPE_REMOVE_TASK = 20; - public static final int HIERARCHY_OP_TYPE_FINISH_ACTIVITY = 21; - public static final int HIERARCHY_OP_TYPE_SET_COMPANION_TASK_FRAGMENT = 22; - public static final int HIERARCHY_OP_TYPE_CLEAR_ADJACENT_ROOTS = 23; - public static final int HIERARCHY_OP_TYPE_SET_REPARENT_LEAF_TASK_IF_RELAUNCH = 24; - public static final int HIERARCHY_OP_TYPE_SET_TASK_FRAGMENT_OPERATION = 25; + public static final int HIERARCHY_OP_TYPE_PENDING_INTENT = 7; + public static final int HIERARCHY_OP_TYPE_START_SHORTCUT = 8; + public static final int HIERARCHY_OP_TYPE_RESTORE_TRANSIENT_ORDER = 9; + public static final int HIERARCHY_OP_TYPE_ADD_RECT_INSETS_PROVIDER = 10; + public static final int HIERARCHY_OP_TYPE_REMOVE_INSETS_PROVIDER = 11; + public static final int HIERARCHY_OP_TYPE_SET_ALWAYS_ON_TOP = 12; + public static final int HIERARCHY_OP_TYPE_REMOVE_TASK = 13; + public static final int HIERARCHY_OP_TYPE_FINISH_ACTIVITY = 14; + public static final int HIERARCHY_OP_TYPE_CLEAR_ADJACENT_ROOTS = 15; + public static final int HIERARCHY_OP_TYPE_SET_REPARENT_LEAF_TASK_IF_RELAUNCH = 16; + public static final int HIERARCHY_OP_TYPE_ADD_TASK_FRAGMENT_OPERATION = 17; // The following key(s) are for use with mLaunchOptions: // When launching a task (eg. from recents), this is the taskId to be launched. @@ -1326,11 +1294,7 @@ public final class WindowContainerTransaction implements Parcelable { @Nullable private Intent mActivityIntent; - /** Used as options for {@link #createTaskFragment}. */ - @Nullable - private TaskFragmentCreationParams mTaskFragmentCreationOptions; - - /** Used as options for {@link #setTaskFragmentOperation}. */ + /** Used as options for {@link #addTaskFragmentOperation}. */ @Nullable private TaskFragmentOperation mTaskFragmentOperation; @@ -1452,7 +1416,6 @@ public final class WindowContainerTransaction implements Parcelable { mActivityTypes = copy.mActivityTypes; mLaunchOptions = copy.mLaunchOptions; mActivityIntent = copy.mActivityIntent; - mTaskFragmentCreationOptions = copy.mTaskFragmentCreationOptions; mTaskFragmentOperation = copy.mTaskFragmentOperation; mPendingIntent = copy.mPendingIntent; mShortcutInfo = copy.mShortcutInfo; @@ -1476,7 +1439,6 @@ public final class WindowContainerTransaction implements Parcelable { mActivityTypes = in.createIntArray(); mLaunchOptions = in.readBundle(); mActivityIntent = in.readTypedObject(Intent.CREATOR); - mTaskFragmentCreationOptions = in.readTypedObject(TaskFragmentCreationParams.CREATOR); mTaskFragmentOperation = in.readTypedObject(TaskFragmentOperation.CREATOR); mPendingIntent = in.readTypedObject(PendingIntent.CREATOR); mShortcutInfo = in.readTypedObject(ShortcutInfo.CREATOR); @@ -1516,16 +1478,6 @@ public final class WindowContainerTransaction implements Parcelable { return mReparent; } - @NonNull - public IBinder getCompanionContainer() { - return mReparent; - } - - @NonNull - public IBinder getCallingActivity() { - return mReparent; - } - public boolean getToTop() { return mToTop; } @@ -1561,11 +1513,6 @@ public final class WindowContainerTransaction implements Parcelable { } @Nullable - public TaskFragmentCreationParams getTaskFragmentCreationOptions() { - return mTaskFragmentCreationOptions; - } - - @Nullable public TaskFragmentOperation getTaskFragmentOperation() { return mTaskFragmentOperation; } @@ -1605,22 +1552,6 @@ public final class WindowContainerTransaction implements Parcelable { case HIERARCHY_OP_TYPE_SET_LAUNCH_ADJACENT_FLAG_ROOT: return "{SetAdjacentFlagRoot: container=" + mContainer + " clearRoot=" + mToTop + "}"; - case HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT: - return "{CreateTaskFragment: options=" + mTaskFragmentCreationOptions + "}"; - case HIERARCHY_OP_TYPE_DELETE_TASK_FRAGMENT: - return "{DeleteTaskFragment: taskFragment=" + mContainer + "}"; - case HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT: - return "{StartActivityInTaskFragment: fragmentToken=" + mContainer + " intent=" - + mActivityIntent + " options=" + mLaunchOptions + "}"; - case HIERARCHY_OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT: - return "{ReparentActivityToTaskFragment: fragmentToken=" + mReparent - + " activity=" + mContainer + "}"; - case HIERARCHY_OP_TYPE_REPARENT_CHILDREN: - return "{ReparentChildren: oldParent=" + mContainer + " newParent=" + mReparent - + "}"; - case HIERARCHY_OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS: - return "{SetAdjacentTaskFragments: container=" + mContainer - + " adjacentContainer=" + mReparent + "}"; case HIERARCHY_OP_TYPE_START_SHORTCUT: return "{StartShortcut: options=" + mLaunchOptions + " info=" + mShortcutInfo + "}"; @@ -1631,8 +1562,6 @@ public final class WindowContainerTransaction implements Parcelable { case HIERARCHY_OP_TYPE_REMOVE_INSETS_PROVIDER: return "{removeLocalInsetsProvider: container=" + mContainer + " insetsType=" + Arrays.toString(mInsetsTypes) + "}"; - case HIERARCHY_OP_TYPE_REQUEST_FOCUS_ON_TASK_FRAGMENT: - return "{requestFocusOnTaskFragment: container=" + mContainer + "}"; case HIERARCHY_OP_TYPE_SET_ALWAYS_ON_TOP: return "{setAlwaysOnTop: container=" + mContainer + " alwaysOnTop=" + mAlwaysOnTop + "}"; @@ -1640,16 +1569,13 @@ public final class WindowContainerTransaction implements Parcelable { return "{RemoveTask: task=" + mContainer + "}"; case HIERARCHY_OP_TYPE_FINISH_ACTIVITY: return "{finishActivity: activity=" + mContainer + "}"; - case HIERARCHY_OP_TYPE_SET_COMPANION_TASK_FRAGMENT: - return "{setCompanionTaskFragment: container = " + mContainer + " companion = " - + mReparent + "}"; case HIERARCHY_OP_TYPE_CLEAR_ADJACENT_ROOTS: return "{ClearAdjacentRoot: container=" + mContainer + "}"; case HIERARCHY_OP_TYPE_SET_REPARENT_LEAF_TASK_IF_RELAUNCH: return "{setReparentLeafTaskIfRelaunch: container= " + mContainer + " reparentLeafTaskIfRelaunch= " + mReparentLeafTaskIfRelaunch + "}"; - case HIERARCHY_OP_TYPE_SET_TASK_FRAGMENT_OPERATION: - return "{setTaskFragmentOperation: fragmentToken= " + mContainer + case HIERARCHY_OP_TYPE_ADD_TASK_FRAGMENT_OPERATION: + return "{addTaskFragmentOperation: fragmentToken= " + mContainer + " operation= " + mTaskFragmentOperation + "}"; default: return "{mType=" + mType + " container=" + mContainer + " reparent=" + mReparent @@ -1677,7 +1603,6 @@ public final class WindowContainerTransaction implements Parcelable { dest.writeIntArray(mActivityTypes); dest.writeBundle(mLaunchOptions); dest.writeTypedObject(mActivityIntent, flags); - dest.writeTypedObject(mTaskFragmentCreationOptions, flags); dest.writeTypedObject(mTaskFragmentOperation, flags); dest.writeTypedObject(mPendingIntent, flags); dest.writeTypedObject(mShortcutInfo, flags); @@ -1733,9 +1658,6 @@ public final class WindowContainerTransaction implements Parcelable { private Intent mActivityIntent; @Nullable - private TaskFragmentCreationParams mTaskFragmentCreationOptions; - - @Nullable private TaskFragmentOperation mTaskFragmentOperation; @Nullable @@ -1812,12 +1734,6 @@ public final class WindowContainerTransaction implements Parcelable { return this; } - Builder setTaskFragmentCreationOptions( - @Nullable TaskFragmentCreationParams taskFragmentCreationOptions) { - mTaskFragmentCreationOptions = taskFragmentCreationOptions; - return this; - } - Builder setTaskFragmentOperation( @Nullable TaskFragmentOperation taskFragmentOperation) { mTaskFragmentOperation = taskFragmentOperation; @@ -1852,7 +1768,6 @@ public final class WindowContainerTransaction implements Parcelable { hierarchyOp.mActivityIntent = mActivityIntent; hierarchyOp.mPendingIntent = mPendingIntent; hierarchyOp.mAlwaysOnTop = mAlwaysOnTop; - hierarchyOp.mTaskFragmentCreationOptions = mTaskFragmentCreationOptions; hierarchyOp.mTaskFragmentOperation = mTaskFragmentOperation; hierarchyOp.mShortcutInfo = mShortcutInfo; hierarchyOp.mReparentLeafTaskIfRelaunch = mReparentLeafTaskIfRelaunch; diff --git a/core/jni/android_hardware_OverlayProperties.cpp b/core/jni/android_hardware_OverlayProperties.cpp index a96af8628e62..9941ca427025 100644 --- a/core/jni/android_hardware_OverlayProperties.cpp +++ b/core/jni/android_hardware_OverlayProperties.cpp @@ -69,6 +69,16 @@ static jboolean android_hardware_OverlayProperties_supportFp16ForHdr(JNIEnv* env return false; } +static jboolean android_hardware_OverlayProperties_supportMixedColorSpaces(JNIEnv* env, + jobject thiz, + jlong nativeObject) { + gui::OverlayProperties* properties = reinterpret_cast<gui::OverlayProperties*>(nativeObject); + if (properties != nullptr && properties->supportMixedColorSpaces) { + return true; + } + return false; +} + // ---------------------------------------------------------------------------- // Serialization // ---------------------------------------------------------------------------- @@ -128,6 +138,8 @@ static const JNINativeMethod gMethods[] = { { "nGetDestructor", "()J", (void*) android_hardware_OverlayProperties_getDestructor }, { "nSupportFp16ForHdr", "(J)Z", (void*) android_hardware_OverlayProperties_supportFp16ForHdr }, + { "nSupportMixedColorSpaces", "(J)Z", + (void*) android_hardware_OverlayProperties_supportMixedColorSpaces }, { "nWriteOverlayPropertiesToParcel", "(JLandroid/os/Parcel;)V", (void*) android_hardware_OverlayProperties_write }, { "nReadOverlayPropertiesFromParcel", "(Landroid/os/Parcel;)J", diff --git a/core/proto/android/service/package.proto b/core/proto/android/service/package.proto index 1dedbb9362a3..0fe2a6bebb49 100644 --- a/core/proto/android/service/package.proto +++ b/core/proto/android/service/package.proto @@ -124,6 +124,9 @@ message PackageProto { // The package on behalf of which the initiiating package requested the install. optional string originating_package_name = 2; + + // The package that is the update owner. + optional string update_owner_package_name = 3; } message StatesProto { diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 5136fcbab7dd..81b3af0e0e60 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -3993,6 +3993,18 @@ <permission android:name="android.permission.LIST_ENABLED_CREDENTIAL_PROVIDERS" android:protectionLevel="signature|privileged" /> + <!-- Allows a system application to be registered with credential manager without + having to be enabled by the user. + @hide --> + <permission android:name="android.permission.SYSTEM_CREDENTIAL_PROVIDER" + android:protectionLevel="signature|privileged" /> + + <!-- Allows an application to be able to store and retrieve credentials from a remote + device. + @hide --> + <permission android:name="android.permission.HYBRID_CREDENTIAL_PROVIDER" + android:protectionLevel="signature|privileged" /> + <!-- ========================================= --> <!-- Permissions for special development tools --> <!-- ========================================= --> @@ -6815,6 +6827,16 @@ android:protectionLevel="normal" /> <uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION"/> + <!-- Allows an application to indicate via {@link + android.content.pm.PackageInstaller.SessionParams#setRequestUpdateOwnership} + that it has the intention of becoming the update owner. + <p>Protection level: normal + --> + <permission android:name="android.permission.ENFORCE_UPDATE_OWNERSHIP" + android:protectionLevel="normal" /> + <uses-permission android:name="android.permission.ENFORCE_UPDATE_OWNERSHIP" /> + + <!-- Allows an application to take screenshots of layers that normally would be blacked out when a screenshot is taken. Specifically, layers that have the flag {@link android.view.SurfaceControl#SECURE} will be screenshot if the caller requests to @@ -6885,7 +6907,7 @@ @hide --> <permission android:name="android.permission.ACCESS_AMBIENT_CONTEXT_EVENT" - android:protectionLevel="internal|role"/> + android:protectionLevel="signature|privileged|role"/> <!-- @SystemApi Required by a AmbientContextEventDetectionService to ensure that only the service with this permission can bind to it. diff --git a/core/res/res/values-television/themes_device_defaults.xml b/core/res/res/values-television/themes_device_defaults.xml index 9d0e52213f8b..7cda99a157d2 100644 --- a/core/res/res/values-television/themes_device_defaults.xml +++ b/core/res/res/values-television/themes_device_defaults.xml @@ -14,6 +14,7 @@ limitations under the License. --> <resources> + <style name="Theme.DeviceDefault.Dialog" parent="Theme.Leanback.Dialog" /> <style name="Theme.DeviceDefault.Dialog.Alert" parent="Theme.Leanback.Dialog.Alert" /> <style name="Theme.DeviceDefault.Dialog.AppError" parent="Theme.Leanback.Dialog.AppError" /> <style name="Theme.DeviceDefault.Light.Dialog.Alert" parent="Theme.Leanback.Dialog.Alert" /> diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml index 8ac13efba700..ef94484f8de6 100644 --- a/core/res/res/values/attrs_manifest.xml +++ b/core/res/res/values/attrs_manifest.xml @@ -1783,6 +1783,14 @@ --> <attr name="attributionTags" format="string" /> + <!-- Default value <code>true</code> allows an installer to enable update + ownership enforcement for this package via {@link + android.content.pm.PackageInstaller.SessionParams#setRequestUpdateOwnership} + during initial installation. This overrides the installer's use of {@link + android.content.pm.PackageInstaller.SessionParams#setRequestUpdateOwnership}. + --> + <attr name="allowUpdateOwnership" format="boolean" /> + <!-- The <code>manifest</code> tag is the root of an <code>AndroidManifest.xml</code> file, describing the contents of an Android package (.apk) file. One @@ -1820,6 +1828,7 @@ <attr name="isSplitRequired" /> <attr name="requiredSplitTypes" /> <attr name="splitTypes" /> + <attr name="allowUpdateOwnership" /> </declare-styleable> <!-- The <code>application</code> tag describes application-level components diff --git a/core/res/res/values/public-staging.xml b/core/res/res/values/public-staging.xml index dfd4d9a18271..a9c56f0f802b 100644 --- a/core/res/res/values/public-staging.xml +++ b/core/res/res/values/public-staging.xml @@ -124,6 +124,7 @@ <public name="allowSharedIsolatedProcess" /> <public name="keyboardLocale" /> <public name="keyboardLayoutType" /> + <public name="allowUpdateOwnership" /> </staging-public-group> <staging-public-group type="id" first-id="0x01cd0000"> diff --git a/data/etc/com.android.systemui.xml b/data/etc/com.android.systemui.xml index fc199440e782..0aad0a8e290d 100644 --- a/data/etc/com.android.systemui.xml +++ b/data/etc/com.android.systemui.xml @@ -83,5 +83,6 @@ <permission name="android.permission.READ_SAFETY_CENTER_STATUS" /> <permission name="android.permission.SET_UNRESTRICTED_KEEP_CLEAR_AREAS" /> <permission name="android.permission.READ_SEARCH_INDEXABLES" /> + <permission name="android.permission.ACCESS_AMBIENT_CONTEXT_EVENT"/> </privapp-permissions> </permissions> diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index 1070841b543e..5040a8668ae6 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -500,6 +500,8 @@ applications that come with the platform <permission name="android.permission.MODIFY_CELL_BROADCASTS" /> <!-- Permission required for CTS test - CtsBroadcastRadioTestCases --> <permission name="android.permission.ACCESS_BROADCAST_RADIO"/> + <!-- Permission required for CTS test - CtsAmbientContextServiceTestCases --> + <permission name="android.permission.ACCESS_AMBIENT_CONTEXT_EVENT"/> </privapp-permissions> <privapp-permissions package="com.android.statementservice"> diff --git a/graphics/java/android/graphics/HardwareRenderer.java b/graphics/java/android/graphics/HardwareRenderer.java index c7c97e0b82b9..89d63040b45a 100644 --- a/graphics/java/android/graphics/HardwareRenderer.java +++ b/graphics/java/android/graphics/HardwareRenderer.java @@ -1334,6 +1334,8 @@ public class HardwareRenderer { final OverlayProperties overlayProperties = defaultDisplay.getOverlaySupport(); boolean supportFp16ForHdr = overlayProperties != null ? overlayProperties.supportFp16ForHdr() : false; + boolean supportMixedColorSpaces = overlayProperties != null + ? overlayProperties.supportMixedColorSpaces() : false; for (int i = 0; i < allDisplays.length; i++) { final Display display = allDisplays[i]; @@ -1361,7 +1363,7 @@ public class HardwareRenderer { nInitDisplayInfo(largestWidth, largestHeight, defaultDisplay.getRefreshRate(), wideColorDataspace, defaultDisplay.getAppVsyncOffsetNanos(), defaultDisplay.getPresentationDeadlineNanos(), - supportFp16ForHdr); + supportFp16ForHdr, supportMixedColorSpaces); mDisplayInitialized = true; } @@ -1542,7 +1544,7 @@ public class HardwareRenderer { private static native void nInitDisplayInfo(int width, int height, float refreshRate, int wideColorDataspace, long appVsyncOffsetNanos, long presentationDeadlineNanos, - boolean supportsFp16ForHdr); + boolean supportsFp16ForHdr, boolean nInitDisplayInfo); private static native void nSetDrawingEnabled(boolean drawingEnabled); diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java index f0e496f3a178..d35dcab11f49 100644 --- a/graphics/java/android/graphics/Paint.java +++ b/graphics/java/android/graphics/Paint.java @@ -3151,10 +3151,10 @@ public class Paint { * @see #getRunAdvance(char[], int, int, int, int, boolean, int) for more details. * * @param text the text to measure. Cannot be null. - * @param start the index of the start of the range to measure - * @param end the index + 1 of the end of the range to measure - * @param contextStart the index of the start of the shaping context - * @param contextEnd the index + 1 of the end of the shaping context + * @param start the start index of the range to measure, inclusive + * @param end the end index of the range to measure, exclusive + * @param contextStart the start index of the shaping context, inclusive + * @param contextEnd the end index of the shaping context, exclusive * @param isRtl whether the run is in RTL direction * @param offset index of caret position * @param advances the array that receives the computed character advances diff --git a/graphics/java/android/graphics/drawable/LottieDrawable.java b/graphics/java/android/graphics/drawable/LottieDrawable.java new file mode 100644 index 000000000000..c1f1b50cf339 --- /dev/null +++ b/graphics/java/android/graphics/drawable/LottieDrawable.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics.drawable; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SuppressLint; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.PixelFormat; + +import dalvik.annotation.optimization.FastNative; + +import libcore.util.NativeAllocationRegistry; + +import java.io.IOException; + +/** + * {@link Drawable} for drawing Lottie files. + * + * <p>The framework handles decoding subsequent frames in another thread and + * updating when necessary. The drawable will only animate while it is being + * displayed.</p> + * + * @hide + */ +@SuppressLint("NotCloseable") +public class LottieDrawable extends Drawable implements Animatable { + private long mNativePtr; + + /** + * Create an animation from the provided JSON string + * @hide + */ + private LottieDrawable(@NonNull String animationJson) throws IOException { + mNativePtr = nCreate(animationJson); + if (mNativePtr == 0) { + throw new IOException("could not make LottieDrawable from json"); + } + + final long nativeSize = nNativeByteSize(mNativePtr); + NativeAllocationRegistry registry = new NativeAllocationRegistry( + LottieDrawable.class.getClassLoader(), nGetNativeFinalizer(), nativeSize); + registry.registerNativeAllocation(this, mNativePtr); + } + + /** + * Factory for LottieDrawable, throws IOException if native Skottie Builder fails to parse + */ + public static LottieDrawable makeLottieDrawable(@NonNull String animationJson) + throws IOException { + return new LottieDrawable(animationJson); + } + + + + /** + * Draw the current frame to the Canvas. + * @hide + */ + @Override + public void draw(@NonNull Canvas canvas) { + if (mNativePtr == 0) { + throw new IllegalStateException("called draw on empty LottieDrawable"); + } + + nDraw(mNativePtr, canvas.getNativeCanvasWrapper()); + } + + /** + * Start the animation. Needs to be called before draw calls. + * @hide + */ + @Override + public void start() { + if (mNativePtr == 0) { + throw new IllegalStateException("called start on empty LottieDrawable"); + } + + if (nStart(mNativePtr)) { + invalidateSelf(); + } + } + + /** + * Stops the animation playback. Does not release anything. + * @hide + */ + @Override + public void stop() { + if (mNativePtr == 0) { + throw new IllegalStateException("called stop on empty LottieDrawable"); + } + nStop(mNativePtr); + } + + /** + * Return whether the animation is currently running. + */ + @Override + public boolean isRunning() { + if (mNativePtr == 0) { + throw new IllegalStateException("called isRunning on empty LottieDrawable"); + } + return nIsRunning(mNativePtr); + } + + @Override + public int getOpacity() { + // We assume translucency to avoid checking each pixel. + return PixelFormat.TRANSLUCENT; + } + + @Override + public void setAlpha(int alpha) { + //TODO + } + + @Override + public void setColorFilter(@Nullable ColorFilter colorFilter) { + //TODO + } + + private static native long nCreate(String json); + private static native void nDraw(long nativeInstance, long nativeCanvas); + @FastNative + private static native long nGetNativeFinalizer(); + @FastNative + private static native long nNativeByteSize(long nativeInstance); + @FastNative + private static native boolean nIsRunning(long nativeInstance); + @FastNative + private static native boolean nStart(long nativeInstance); + @FastNative + private static native boolean nStop(long nativeInstance); + +} diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java index 00e13c94ea90..07d0d96a120b 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java @@ -310,16 +310,12 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { OP_TYPE_SET_ANIMATION_PARAMS) .setAnimationParams(animationParams) .build(); - wct.setTaskFragmentOperation(fragmentToken, operation); + wct.addTaskFragmentOperation(fragmentToken, operation); } void deleteTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken) { - if (!mFragmentInfos.containsKey(fragmentToken)) { - throw new IllegalArgumentException( - "Can't find an existing TaskFragment with fragmentToken=" + fragmentToken); - } - wct.deleteTaskFragment(mFragmentInfos.get(fragmentToken).getToken()); + wct.deleteTaskFragment(fragmentToken); } void updateTaskFragmentInfo(@NonNull TaskFragmentInfo taskFragmentInfo) { diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java index 868ced0e555e..b13c6724ed0e 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -20,6 +20,8 @@ import static android.app.ActivityManager.START_SUCCESS; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.view.Display.DEFAULT_DISPLAY; +import static android.window.TaskFragmentOperation.OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT; +import static android.window.TaskFragmentOperation.OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT; import static android.window.TaskFragmentOrganizer.KEY_ERROR_CALLBACK_OP_TYPE; import static android.window.TaskFragmentOrganizer.KEY_ERROR_CALLBACK_TASK_FRAGMENT_INFO; import static android.window.TaskFragmentOrganizer.KEY_ERROR_CALLBACK_THROWABLE; @@ -31,8 +33,6 @@ import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_ERROR; import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_INFO_CHANGED; import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED; import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_VANISHED; -import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT; -import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT; import static androidx.window.extensions.embedding.SplitContainer.getFinishPrimaryWithSecondaryBehavior; import static androidx.window.extensions.embedding.SplitContainer.getFinishSecondaryWithPrimaryBehavior; @@ -67,6 +67,7 @@ import android.util.SparseArray; import android.view.WindowMetrics; import android.window.TaskFragmentAnimationParams; import android.window.TaskFragmentInfo; +import android.window.TaskFragmentOperation; import android.window.TaskFragmentParentInfo; import android.window.TaskFragmentTransaction; import android.window.WindowContainerTransaction; @@ -592,11 +593,11 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @GuardedBy("mLock") void onTaskFragmentError(@NonNull WindowContainerTransaction wct, @Nullable IBinder errorCallbackToken, @Nullable TaskFragmentInfo taskFragmentInfo, - int opType, @NonNull Throwable exception) { + @TaskFragmentOperation.OperationType int opType, @NonNull Throwable exception) { Log.e(TAG, "onTaskFragmentError=" + exception.getMessage()); switch (opType) { - case HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT: - case HIERARCHY_OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT: { + case OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT: + case OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT: { final TaskFragmentContainer container; if (taskFragmentInfo != null) { container = getContainer(taskFragmentInfo.getFragmentToken()); diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java index d9abe8e040ba..85a00dfc010c 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -30,6 +30,7 @@ import android.content.res.Configuration; import android.graphics.Rect; import android.os.Bundle; import android.os.IBinder; +import android.util.DisplayMetrics; import android.util.LayoutDirection; import android.util.Pair; import android.util.Size; @@ -555,9 +556,9 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { final Function<SplitAttributesCalculatorParams, SplitAttributes> calculator = mController.getSplitAttributesCalculator(); final SplitAttributes defaultSplitAttributes = rule.getDefaultSplitAttributes(); - final boolean isDefaultMinSizeSatisfied = rule.checkParentMetrics(taskWindowMetrics); + final boolean areDefaultConstraintsSatisfied = rule.checkParentMetrics(taskWindowMetrics); if (calculator == null) { - if (!isDefaultMinSizeSatisfied) { + if (!areDefaultConstraintsSatisfied) { return EXPAND_CONTAINERS_ATTRIBUTES; } return sanitizeSplitAttributes(taskProperties, defaultSplitAttributes, @@ -567,8 +568,8 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { .getCurrentWindowLayoutInfo(taskProperties.getDisplayId(), taskConfiguration.windowConfiguration); final SplitAttributesCalculatorParams params = new SplitAttributesCalculatorParams( - taskWindowMetrics, taskConfiguration, defaultSplitAttributes, - isDefaultMinSizeSatisfied, windowLayoutInfo, rule.getTag()); + taskWindowMetrics, taskConfiguration, windowLayoutInfo, defaultSplitAttributes, + areDefaultConstraintsSatisfied, rule.getTag()); final SplitAttributes splitAttributes = calculator.apply(params); return sanitizeSplitAttributes(taskProperties, splitAttributes, minDimensionsPair); } @@ -972,6 +973,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { private static WindowMetrics getTaskWindowMetrics(@NonNull Configuration taskConfiguration) { final Rect taskBounds = taskConfiguration.windowConfiguration.getBounds(); // TODO(b/190433398): Supply correct insets. - return new WindowMetrics(taskBounds, WindowInsets.CONSUMED); + final float density = taskConfiguration.densityDpi * DisplayMetrics.DENSITY_DEFAULT_SCALE; + return new WindowMetrics(taskBounds, WindowInsets.CONSUMED, density); } } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java index 0bf0bc85b511..a26311efc23e 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java @@ -20,13 +20,13 @@ import static android.app.ActivityManager.START_CANCELED; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.view.Display.DEFAULT_DISPLAY; +import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_TASK_FRAGMENT; import static android.window.TaskFragmentTransaction.TYPE_ACTIVITY_REPARENTED_TO_TASK; import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_APPEARED; import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_ERROR; import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_INFO_CHANGED; import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED; import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_VANISHED; -import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT; import static androidx.window.extensions.embedding.EmbeddingTestUtils.DEFAULT_FINISH_PRIMARY_WITH_SECONDARY; import static androidx.window.extensions.embedding.EmbeddingTestUtils.DEFAULT_FINISH_SECONDARY_WITH_PRIMARY; @@ -1139,7 +1139,7 @@ public class SplitControllerTest { final TaskFragmentTransaction transaction = new TaskFragmentTransaction(); final IBinder errorToken = new Binder(); final TaskFragmentInfo info = mock(TaskFragmentInfo.class); - final int opType = HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT; + final int opType = OP_TYPE_CREATE_TASK_FRAGMENT; final Exception exception = new SecurityException("test"); final Bundle errorBundle = TaskFragmentOrganizer.putErrorInfoInBundle(exception, info, opType); diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java index ff1256b47429..07d01589be5a 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java @@ -66,8 +66,10 @@ import android.graphics.Color; import android.graphics.Rect; import android.os.IBinder; import android.platform.test.annotations.Presubmit; +import android.util.DisplayMetrics; import android.util.Pair; import android.util.Size; +import android.view.WindowMetrics; import android.window.TaskFragmentAnimationParams; import android.window.TaskFragmentInfo; import android.window.TaskFragmentOperation; @@ -101,7 +103,6 @@ import java.util.ArrayList; @RunWith(AndroidJUnit4.class) public class SplitPresenterTest { - @Mock private Activity mActivity; @Mock private Resources mActivityResources; @@ -193,7 +194,7 @@ public class SplitPresenterTest { OP_TYPE_SET_ANIMATION_PARAMS) .setAnimationParams(animationParams) .build(); - verify(mTransaction).setTaskFragmentOperation(container.getTaskFragmentToken(), + verify(mTransaction).addTaskFragmentOperation(container.getTaskFragmentToken(), expectedOperation); assertTrue(container.areLastRequestedAnimationParamsEqual(animationParams)); @@ -202,7 +203,7 @@ public class SplitPresenterTest { mPresenter.updateAnimationParams(mTransaction, container.getTaskFragmentToken(), animationParams); - verify(mTransaction, never()).setTaskFragmentOperation(any(), any()); + verify(mTransaction, never()).addTaskFragmentOperation(any(), any()); } @Test @@ -571,6 +572,21 @@ public class SplitPresenterTest { splitPairRule, null /* minDimensionsPair */)); } + @Test + public void testGetTaskWindowMetrics() { + final Configuration taskConfig = new Configuration(); + taskConfig.windowConfiguration.setBounds(TASK_BOUNDS); + taskConfig.densityDpi = 123; + final TaskContainer.TaskProperties taskProperties = new TaskContainer.TaskProperties( + DEFAULT_DISPLAY, taskConfig); + doReturn(taskProperties).when(mPresenter).getTaskProperties(mActivity); + + final WindowMetrics windowMetrics = mPresenter.getTaskWindowMetrics(mActivity); + assertEquals(TASK_BOUNDS, windowMetrics.getBounds()); + assertEquals(123 * DisplayMetrics.DENSITY_DEFAULT_SCALE, + windowMetrics.getDensity(), 0f); + } + private Activity createMockActivity() { final Activity activity = mock(Activity.class); final Configuration activityConfig = new Configuration(); diff --git a/libs/WindowManager/Jetpack/window-extensions-release.aar b/libs/WindowManager/Jetpack/window-extensions-release.aar Binary files differindex a939cd8a1745..5de5365e4d03 100644 --- a/libs/WindowManager/Jetpack/window-extensions-release.aar +++ b/libs/WindowManager/Jetpack/window-extensions-release.aar diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java index 065fd95b3ebc..b5ef72aec6aa 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java @@ -257,12 +257,30 @@ public class ShellTaskOrganizer extends TaskOrganizer implements } } + /** + * Creates a persistent root task in WM for a particular windowing-mode. + * @param displayId The display to create the root task on. + * @param windowingMode Windowing mode to put the root task in. + * @param listener The listener to get the created task callback. + */ public void createRootTask(int displayId, int windowingMode, TaskListener listener) { - ProtoLog.v(WM_SHELL_TASK_ORG, "createRootTask() displayId=%d winMode=%d listener=%s", + createRootTask(displayId, windowingMode, listener, false /* removeWithTaskOrganizer */); + } + + /** + * Creates a persistent root task in WM for a particular windowing-mode. + * @param displayId The display to create the root task on. + * @param windowingMode Windowing mode to put the root task in. + * @param listener The listener to get the created task callback. + * @param removeWithTaskOrganizer True if this task should be removed when organizer destroyed. + */ + public void createRootTask(int displayId, int windowingMode, TaskListener listener, + boolean removeWithTaskOrganizer) { + ProtoLog.v(WM_SHELL_TASK_ORG, "createRootTask() displayId=%d winMode=%d listener=%s" , displayId, windowingMode, listener.toString()); final IBinder cookie = new Binder(); setPendingLaunchCookieListener(cookie, listener); - super.createRootTask(displayId, windowingMode, cookie); + super.createRootTask(displayId, windowingMode, cookie, removeWithTaskOrganizer); } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index 9674b69baa00..360bfe78bf07 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -981,21 +981,59 @@ public class BubbleController implements ConfigurationChangeListener { } /** - * Adds and expands bubble for a specific intent. These bubbles are <b>not</b> backed by a n - * otification and remain until the user dismisses the bubble or bubble stack. Only one intent - * bubble is supported at a time. + * This method has different behavior depending on: + * - if an app bubble exists + * - if an app bubble is expanded + * + * If no app bubble exists, this will add and expand a bubble with the provided intent. The + * intent must be explicit (i.e. include a package name or fully qualified component class name) + * and the activity for it should be resizable. + * + * If an app bubble exists, this will toggle the visibility of it, i.e. if the app bubble is + * expanded, calling this method will collapse it. If the app bubble is not expanded, calling + * this method will expand it. + * + * These bubbles are <b>not</b> backed by a notification and remain until the user dismisses + * the bubble or bubble stack. + * + * Some notes: + * - Only one app bubble is supported at a time + * - Calling this method with a different intent than the existing app bubble will do nothing * * @param intent the intent to display in the bubble expanded view. */ - public void showAppBubble(Intent intent) { - if (intent == null || intent.getPackage() == null) return; + public void showOrHideAppBubble(Intent intent) { + if (intent == null || intent.getPackage() == null) { + Log.w(TAG, "App bubble failed to show, invalid intent: " + intent + + ((intent != null) ? " with package: " + intent.getPackage() : " ")); + return; + } PackageManager packageManager = getPackageManagerForUser(mContext, mCurrentUserId); if (!isResizableActivity(intent, packageManager, KEY_APP_BUBBLE)) return; - Bubble b = new Bubble(intent, UserHandle.of(mCurrentUserId), mMainExecutor); - b.setShouldAutoExpand(true); - inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false); + Bubble existingAppBubble = mBubbleData.getBubbleInStackWithKey(KEY_APP_BUBBLE); + if (existingAppBubble != null) { + BubbleViewProvider selectedBubble = mBubbleData.getSelectedBubble(); + if (isStackExpanded()) { + if (selectedBubble != null && KEY_APP_BUBBLE.equals(selectedBubble.getKey())) { + // App bubble is expanded, lets collapse + collapseStack(); + } else { + // App bubble is not selected, select it + mBubbleData.setSelectedBubble(existingAppBubble); + } + } else { + // App bubble is not selected, select it & expand + mBubbleData.setSelectedBubble(existingAppBubble); + mBubbleData.setExpanded(true); + } + } else { + // App bubble does not exist, lets add and expand it + Bubble b = new Bubble(intent, UserHandle.of(mCurrentUserId), mMainExecutor); + b.setShouldAutoExpand(true); + inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false); + } } /** @@ -1705,9 +1743,9 @@ public class BubbleController implements ConfigurationChangeListener { } @Override - public void showAppBubble(Intent intent) { + public void showOrHideAppBubble(Intent intent) { mMainExecutor.execute(() -> { - BubbleController.this.showAppBubble(intent); + BubbleController.this.showOrHideAppBubble(intent); }); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java index 465d1abe0a3d..df4325763a17 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java @@ -109,13 +109,28 @@ public interface Bubbles { void expandStackAndSelectBubble(Bubble bubble); /** - * Adds and expands bubble that is not notification based, but instead based on an intent from - * the app. The intent must be explicit (i.e. include a package name or fully qualified - * component class name) and the activity for it should be resizable. + * This method has different behavior depending on: + * - if an app bubble exists + * - if an app bubble is expanded * - * @param intent the intent to populate the bubble. + * If no app bubble exists, this will add and expand a bubble with the provided intent. The + * intent must be explicit (i.e. include a package name or fully qualified component class name) + * and the activity for it should be resizable. + * + * If an app bubble exists, this will toggle the visibility of it, i.e. if the app bubble is + * expanded, calling this method will collapse it. If the app bubble is not expanded, calling + * this method will expand it. + * + * These bubbles are <b>not</b> backed by a notification and remain until the user dismisses + * the bubble or bubble stack. + * + * Some notes: + * - Only one app bubble is supported at a time + * - Calling this method with a different intent than the existing app bubble will do nothing + * + * @param intent the intent to display in the bubble expanded view. */ - void showAppBubble(Intent intent); + void showOrHideAppBubble(Intent intent); /** * @return a bubble that matches the provided shortcutId, if one exists. 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 e6c7e101d078..83158ffafa7e 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 @@ -662,8 +662,8 @@ public class PipTransition extends PipTransitionController { } // Please file a bug to handle the unexpected transition type. - throw new IllegalStateException("Entering PIP with unexpected transition type=" - + transitTypeToString(transitType)); + android.util.Slog.e(TAG, "Found new PIP in transition with mis-matched type=" + + transitTypeToString(transitType), new Throwable()); } return false; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java index a0a8f9fb2cde..94e593b106a5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java @@ -333,6 +333,9 @@ public class PhonePipMenuController implements PipMenuController { mTmpDestinationRectF.set(destinationBounds); mMoveTransform.setRectToRect(mTmpSourceRectF, mTmpDestinationRectF, Matrix.ScaleToFit.FILL); final SurfaceControl surfaceControl = getSurfaceControl(); + if (surfaceControl == null) { + return; + } final SurfaceControl.Transaction menuTx = mSurfaceControlTransactionFactory.getTransaction(); menuTx.setMatrix(surfaceControl, mMoveTransform, mTmpTransform); @@ -359,6 +362,9 @@ public class PhonePipMenuController implements PipMenuController { } final SurfaceControl surfaceControl = getSurfaceControl(); + if (surfaceControl == null) { + return; + } final SurfaceControl.Transaction menuTx = mSurfaceControlTransactionFactory.getTransaction(); menuTx.setCrop(surfaceControl, destinationBounds); diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleFromLockScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleFromLockScreen.kt index 08ed91b3cab1..47734238fc23 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleFromLockScreen.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleFromLockScreen.kt @@ -94,9 +94,19 @@ class LaunchBubbleFromLockScreen(flicker: FlickerTest) : BaseBubbleScreen(flicke flicker.assertLayersEnd { this.isVisible(testApp) } } - @Postsubmit @Test fun navBarLayerIsVisibleAtEnd() = flicker.navBarLayerIsVisibleAtEnd() + @Postsubmit + @Test + fun navBarLayerIsVisibleAtEnd() { + Assume.assumeFalse(flicker.scenario.isTablet) + flicker.navBarLayerIsVisibleAtEnd() + } - @Postsubmit @Test fun navBarLayerPositionAtEnd() = flicker.navBarLayerPositionAtEnd() + @Postsubmit + @Test + fun navBarLayerPositionAtEnd() { + Assume.assumeFalse(flicker.scenario.isTablet) + flicker.navBarLayerPositionAtEnd() + } /** {@inheritDoc} */ @FlakyTest diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt index bc9fc7301541..8a694f770ab0 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt @@ -52,7 +52,7 @@ import org.junit.runners.Parameterized @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -class EnterPipOnUserLeaveHintTest(flicker: FlickerTest) : EnterPipTest(flicker) { +open class EnterPipOnUserLeaveHintTest(flicker: FlickerTest) : EnterPipTest(flicker) { /** Defines the transition used to run the test */ override val transition: FlickerBuilder.() -> Unit get() = { @@ -61,6 +61,8 @@ class EnterPipOnUserLeaveHintTest(flicker: FlickerTest) : EnterPipTest(flicker) pipApp.enableEnterPipOnUserLeaveHint() } teardown { + // close gracefully so that onActivityUnpinned() can be called before force exit + pipApp.closePipWindow(wmHelper) pipApp.exit(wmHelper) } transitions { tapl.goHome() } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTestCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTestCfArm.kt new file mode 100644 index 000000000000..e47805001cd0 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTestCfArm.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.pip + +import com.android.server.wm.flicker.FlickerTest +import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** This test will fail because of b/264261596 */ +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class EnterPipOnUserLeaveHintTestCfArm(flicker: FlickerTest) : EnterPipOnUserLeaveHintTest(flicker) diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTestCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTestCfArm.kt new file mode 100644 index 000000000000..d2e864587431 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipTestCfArm.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.pip + +import com.android.server.wm.flicker.FlickerTest +import com.android.server.wm.flicker.FlickerTestFactory +import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory +import com.android.server.wm.traces.common.service.PlatformConsts +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class EnterPipTestCfArm(flicker: FlickerTest) : EnterPipTest(flicker) { + companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestFactory.nonRotationTests] for configuring repetitions, screen orientation + * and navigation modes. + */ + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTest> { + return FlickerTestFactory.nonRotationTests( + supportedRotations = listOf(PlatformConsts.Rotation.ROTATION_0) + ) + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt index da162401cf79..ea6c14d7152e 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTest.kt @@ -67,7 +67,7 @@ import org.junit.runners.Parameterized @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -class EnterPipToOtherOrientationTest(flicker: FlickerTest) : PipTransition(flicker) { +open class EnterPipToOtherOrientationTest(flicker: FlickerTest) : PipTransition(flicker) { private val testApp = FixedOrientationAppHelper(instrumentation) private val startingBounds = WindowUtils.getDisplayBounds(PlatformConsts.Rotation.ROTATION_90) private val endingBounds = WindowUtils.getDisplayBounds(PlatformConsts.Rotation.ROTATION_0) diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTestCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTestCfArm.kt new file mode 100644 index 000000000000..39aab6ee49b7 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/EnterPipToOtherOrientationTestCfArm.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.pip + +import com.android.server.wm.flicker.FlickerTest +import com.android.server.wm.flicker.FlickerTestFactory +import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory +import com.android.server.wm.traces.common.service.PlatformConsts +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** This test fails because of b/264261596 */ +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +open class EnterPipToOtherOrientationTestCfArm(flicker: FlickerTest) : + EnterPipToOtherOrientationTest(flicker) { + companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and + * navigation modes. + */ + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<FlickerTest> { + return FlickerTestFactory.nonRotationTests( + supportedRotations = listOf(PlatformConsts.Rotation.ROTATION_0) + ) + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt index 1420f8ce653a..b5a5004aa553 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTest.kt @@ -56,7 +56,7 @@ import org.junit.runners.Parameterized @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) -class ExitPipViaExpandButtonClickTest(flicker: FlickerTest) : ExitPipToAppTransition(flicker) { +open class ExitPipViaExpandButtonClickTest(flicker: FlickerTest) : ExitPipToAppTransition(flicker) { /** Defines the transition used to run the test */ override val transition: FlickerBuilder.() -> Unit diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTestCfArm.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTestCfArm.kt new file mode 100644 index 000000000000..f77e335d8f52 --- /dev/null +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/ExitPipViaExpandButtonClickTestCfArm.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker.pip + +import com.android.server.wm.flicker.FlickerTest +import com.android.server.wm.flicker.FlickerTestFactory +import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory +import com.android.server.wm.traces.common.service.PlatformConsts +import org.junit.FixMethodOrder +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class ExitPipViaExpandButtonClickTestCfArm(flicker: FlickerTest) : + ExitPipViaExpandButtonClickTest(flicker) { + companion object { + /** + * Creates the test configurations. + * + * See [FlickerTestFactory.nonRotationTests] for configuring screen orientation and + * navigation modes. + */ + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): List<FlickerTest> { + return FlickerTestFactory.nonRotationTests( + supportedRotations = listOf(PlatformConsts.Rotation.ROTATION_0) + ) + } + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTransition.kt index a9fe93d15428..1bf1354f56aa 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTransition.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/PipTransition.kt @@ -18,6 +18,7 @@ package com.android.wm.shell.flicker.pip import android.app.Instrumentation import android.content.Intent +import android.platform.test.annotations.Postsubmit import com.android.server.wm.flicker.FlickerBuilder import com.android.server.wm.flicker.FlickerTest import com.android.server.wm.flicker.helpers.PipAppHelper @@ -25,8 +26,11 @@ import com.android.server.wm.flicker.helpers.WindowUtils import com.android.server.wm.flicker.helpers.setRotation import com.android.server.wm.flicker.rules.RemoveAllTasksButHomeRule.Companion.removeAllTasksButHome import com.android.server.wm.flicker.testapp.ActivityOptions +import com.android.server.wm.traces.common.ComponentNameMatcher import com.android.server.wm.traces.common.service.PlatformConsts import com.android.wm.shell.flicker.BaseTest +import com.google.common.truth.Truth +import org.junit.Test abstract class PipTransition(flicker: FlickerTest) : BaseTest(flicker) { protected val pipApp = PipAppHelper(instrumentation) @@ -56,7 +60,6 @@ abstract class PipTransition(flicker: FlickerTest) : BaseTest(flicker) { * Gets a configuration that handles basic setup and teardown of pip tests and that launches the * Pip app for test * - * @param eachRun If the pip app should be launched in each run (otherwise only 1x per test) * @param stringExtras Arguments to pass to the PIP launch intent * @param extraSpec Additional segment of flicker specification */ @@ -78,4 +81,21 @@ abstract class PipTransition(flicker: FlickerTest) : BaseTest(flicker) { extraSpec(this) } } + + @Postsubmit + @Test + fun hasAtMostOnePipDismissOverlayWindow() { + val matcher = ComponentNameMatcher("", "pip-dismiss-overlay") + flicker.assertWm { + val overlaysPerState = trace.entries.map { entry -> + entry.windowStates.count { window -> + matcher.windowMatchesAnyOf(window) + } <= 1 + } + + Truth.assertWithMessage("Number of dismiss overlays per state") + .that(overlaysPerState) + .doesNotContain(false) + } + } } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt index 7403aab7d4c0..0432a8497fbe 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt @@ -31,22 +31,26 @@ import org.junit.Test @RequiresDevice class TvPipMenuTests : TvPipTestBase() { - private val systemUiResources = + private val systemUiResources by lazy { packageManager.getResourcesForApplication(SYSTEM_UI_PACKAGE_NAME) - private val pipBoundsWhileInMenu: Rect = + } + private val pipBoundsWhileInMenu: Rect by lazy { systemUiResources.run { val bounds = getString(getIdentifier("pip_menu_bounds", "string", SYSTEM_UI_PACKAGE_NAME)) Rect.unflattenFromString(bounds) ?: error("Could not retrieve PiP menu bounds") } - private val playButtonDescription = + } + private val playButtonDescription by lazy { systemUiResources.run { getString(getIdentifier("pip_play", "string", SYSTEM_UI_PACKAGE_NAME)) } - private val pauseButtonDescription = + } + private val pauseButtonDescription by lazy { systemUiResources.run { getString(getIdentifier("pip_pause", "string", SYSTEM_UI_PACKAGE_NAME)) } + } @Before fun tvPipMenuTestsTestUp() { diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp index 59e4b7acdba7..23cf9136c475 100644 --- a/libs/hwui/Android.bp +++ b/libs/hwui/Android.bp @@ -78,6 +78,7 @@ cc_defaults { "external/skia/src/utils", "external/skia/src/gpu", "external/skia/src/shaders", + "external/skia/modules/skottie", ], }, host: { @@ -375,6 +376,7 @@ cc_defaults { "external/skia/src/effects", "external/skia/src/image", "external/skia/src/images", + "external/skia/modules/skottie", ], shared_libs: [ @@ -402,6 +404,7 @@ cc_defaults { "jni/BitmapRegionDecoder.cpp", "jni/GIFMovie.cpp", "jni/GraphicsStatsService.cpp", + "jni/LottieDrawable.cpp", "jni/Movie.cpp", "jni/MovieImpl.cpp", "jni/pdf/PdfDocument.cpp", @@ -509,6 +512,7 @@ cc_defaults { "hwui/BlurDrawLooper.cpp", "hwui/Canvas.cpp", "hwui/ImageDecoder.cpp", + "hwui/LottieDrawable.cpp", "hwui/MinikinSkia.cpp", "hwui/MinikinUtils.cpp", "hwui/PaintImpl.cpp", diff --git a/libs/hwui/DeviceInfo.cpp b/libs/hwui/DeviceInfo.cpp index 0240c86d5f45..32bc122fdc58 100644 --- a/libs/hwui/DeviceInfo.cpp +++ b/libs/hwui/DeviceInfo.cpp @@ -108,6 +108,10 @@ void DeviceInfo::setSupportFp16ForHdr(bool supportFp16ForHdr) { get()->mSupportFp16ForHdr = supportFp16ForHdr; } +void DeviceInfo::setSupportMixedColorSpaces(bool supportMixedColorSpaces) { + get()->mSupportMixedColorSpaces = supportMixedColorSpaces; +} + void DeviceInfo::onRefreshRateChanged(int64_t vsyncPeriod) { mVsyncPeriod = vsyncPeriod; } diff --git a/libs/hwui/DeviceInfo.h b/libs/hwui/DeviceInfo.h index 577780bbb5e0..d4af0872e31e 100644 --- a/libs/hwui/DeviceInfo.h +++ b/libs/hwui/DeviceInfo.h @@ -62,6 +62,9 @@ public: static void setSupportFp16ForHdr(bool supportFp16ForHdr); static bool isSupportFp16ForHdr() { return get()->mSupportFp16ForHdr; }; + static void setSupportMixedColorSpaces(bool supportMixedColorSpaces); + static bool isSupportMixedColorSpaces() { return get()->mSupportMixedColorSpaces; }; + // this value is only valid after the GPU has been initialized and there is a valid graphics // context or if you are using the HWUI_NULL_GPU int maxTextureSize() const; @@ -92,6 +95,7 @@ private: int mMaxTextureSize; sk_sp<SkColorSpace> mWideColorSpace = SkColorSpace::MakeSRGB(); bool mSupportFp16ForHdr = false; + bool mSupportMixedColorSpaces = false; SkColorType mWideColorType = SkColorType::kN32_SkColorType; int mDisplaysSize = 0; int mPhysicalDisplayIndex = -1; diff --git a/libs/hwui/MemoryPolicy.h b/libs/hwui/MemoryPolicy.h index 41ced8cebf83..2f0f7f506447 100644 --- a/libs/hwui/MemoryPolicy.h +++ b/libs/hwui/MemoryPolicy.h @@ -53,8 +53,8 @@ struct MemoryPolicy { // Whether or not to only purge scratch resources when triggering UI Hidden or background // collection bool purgeScratchOnly = true; - // EXPERIMENTAL: Whether or not to trigger releasing GPU context when all contexts are stopped - bool releaseContextOnStoppedOnly = false; + // Whether or not to trigger releasing GPU context when all contexts are stopped + bool releaseContextOnStoppedOnly = true; }; const MemoryPolicy& loadMemoryPolicy(); diff --git a/libs/hwui/Readback.cpp b/libs/hwui/Readback.cpp index 8dcd6dbe6421..045de35c1d97 100644 --- a/libs/hwui/Readback.cpp +++ b/libs/hwui/Readback.cpp @@ -28,6 +28,7 @@ #include <SkRefCnt.h> #include <SkSamplingOptions.h> #include <SkSurface.h> +#include "include/gpu/GpuTypes.h" // from Skia #include <gui/TraceUtils.h> #include <private/android/AHardwareBufferHelpers.h> #include <shaders/shaders.h> @@ -170,14 +171,15 @@ void Readback::copySurfaceInto(ANativeWindow* window, const std::shared_ptr<Copy SkBitmap skBitmap = request->getDestinationBitmap(srcRect.width(), srcRect.height()); SkBitmap* bitmap = &skBitmap; sk_sp<SkSurface> tmpSurface = - SkSurface::MakeRenderTarget(mRenderThread.getGrContext(), SkBudgeted::kYes, + SkSurface::MakeRenderTarget(mRenderThread.getGrContext(), skgpu::Budgeted::kYes, bitmap->info(), 0, kTopLeft_GrSurfaceOrigin, nullptr); // if we can't generate a GPU surface that matches the destination bitmap (e.g. 565) then we // attempt to do the intermediate rendering step in 8888 if (!tmpSurface.get()) { SkImageInfo tmpInfo = bitmap->info().makeColorType(SkColorType::kN32_SkColorType); - tmpSurface = SkSurface::MakeRenderTarget(mRenderThread.getGrContext(), SkBudgeted::kYes, + tmpSurface = SkSurface::MakeRenderTarget(mRenderThread.getGrContext(), + skgpu::Budgeted::kYes, tmpInfo, 0, kTopLeft_GrSurfaceOrigin, nullptr); if (!tmpSurface.get()) { ALOGW("Unable to generate GPU buffer in a format compatible with the provided bitmap"); @@ -345,14 +347,17 @@ bool Readback::copyLayerInto(Layer* layer, const SkRect* srcRect, const SkRect* * software buffer. */ sk_sp<SkSurface> tmpSurface = SkSurface::MakeRenderTarget(mRenderThread.getGrContext(), - SkBudgeted::kYes, bitmap->info(), 0, + skgpu::Budgeted::kYes, + bitmap->info(), + 0, kTopLeft_GrSurfaceOrigin, nullptr); // if we can't generate a GPU surface that matches the destination bitmap (e.g. 565) then we // attempt to do the intermediate rendering step in 8888 if (!tmpSurface.get()) { SkImageInfo tmpInfo = bitmap->info().makeColorType(SkColorType::kN32_SkColorType); - tmpSurface = SkSurface::MakeRenderTarget(mRenderThread.getGrContext(), SkBudgeted::kYes, + tmpSurface = SkSurface::MakeRenderTarget(mRenderThread.getGrContext(), + skgpu::Budgeted::kYes, tmpInfo, 0, kTopLeft_GrSurfaceOrigin, nullptr); if (!tmpSurface.get()) { ALOGW("Unable to generate GPU buffer in a format compatible with the provided bitmap"); diff --git a/libs/hwui/RecordingCanvas.cpp b/libs/hwui/RecordingCanvas.cpp index 5b6fff158f10..e1030b0faf8e 100644 --- a/libs/hwui/RecordingCanvas.cpp +++ b/libs/hwui/RecordingCanvas.cpp @@ -44,6 +44,7 @@ #include "SkTextBlob.h" #include "SkVertices.h" #include "VectorDrawable.h" +#include "include/gpu/GpuTypes.h" // from Skia #include "pipeline/skia/AnimatedDrawables.h" #include "pipeline/skia/FunctorDrawable.h" @@ -570,7 +571,7 @@ public: GrRecordingContext* directContext = c->recordingContext(); mLayerImageInfo = c->imageInfo().makeWH(deviceBounds.width(), deviceBounds.height()); - mLayerSurface = SkSurface::MakeRenderTarget(directContext, SkBudgeted::kYes, + mLayerSurface = SkSurface::MakeRenderTarget(directContext, skgpu::Budgeted::kYes, mLayerImageInfo, 0, kTopLeft_GrSurfaceOrigin, nullptr); } diff --git a/libs/hwui/SkiaCanvas.cpp b/libs/hwui/SkiaCanvas.cpp index d83d78f650aa..a1c4b49b6742 100644 --- a/libs/hwui/SkiaCanvas.cpp +++ b/libs/hwui/SkiaCanvas.cpp @@ -736,6 +736,10 @@ double SkiaCanvas::drawAnimatedImage(AnimatedImageDrawable* imgDrawable) { return imgDrawable->drawStaging(mCanvas); } +void SkiaCanvas::drawLottie(LottieDrawable* lottieDrawable) { + lottieDrawable->drawStaging(mCanvas); +} + void SkiaCanvas::drawVectorDrawable(VectorDrawableRoot* vectorDrawable) { vectorDrawable->drawStaging(this); } diff --git a/libs/hwui/SkiaCanvas.h b/libs/hwui/SkiaCanvas.h index 31e3b4c3c7e2..fd8b6cdb7ca0 100644 --- a/libs/hwui/SkiaCanvas.h +++ b/libs/hwui/SkiaCanvas.h @@ -145,6 +145,7 @@ public: float dstTop, float dstRight, float dstBottom, const Paint* paint) override; virtual double drawAnimatedImage(AnimatedImageDrawable* imgDrawable) override; + virtual void drawLottie(LottieDrawable* lottieDrawable) override; virtual void drawVectorDrawable(VectorDrawableRoot* vectorDrawable) override; diff --git a/libs/hwui/apex/jni_runtime.cpp b/libs/hwui/apex/jni_runtime.cpp index f57d80c496ad..b1aa19475518 100644 --- a/libs/hwui/apex/jni_runtime.cpp +++ b/libs/hwui/apex/jni_runtime.cpp @@ -37,6 +37,7 @@ extern int register_android_graphics_CreateJavaOutputStreamAdaptor(JNIEnv* env); extern int register_android_graphics_Graphics(JNIEnv* env); extern int register_android_graphics_ImageDecoder(JNIEnv*); extern int register_android_graphics_drawable_AnimatedImageDrawable(JNIEnv*); +extern int register_android_graphics_drawable_LottieDrawable(JNIEnv*); extern int register_android_graphics_Interpolator(JNIEnv* env); extern int register_android_graphics_MaskFilter(JNIEnv* env); extern int register_android_graphics_Movie(JNIEnv* env); @@ -117,6 +118,7 @@ extern int register_android_graphics_HardwareBufferRenderer(JNIEnv* env); REG_JNI(register_android_graphics_HardwareRendererObserver), REG_JNI(register_android_graphics_ImageDecoder), REG_JNI(register_android_graphics_drawable_AnimatedImageDrawable), + REG_JNI(register_android_graphics_drawable_LottieDrawable), REG_JNI(register_android_graphics_Interpolator), REG_JNI(register_android_graphics_MaskFilter), REG_JNI(register_android_graphics_Matrix), diff --git a/libs/hwui/hwui/Canvas.h b/libs/hwui/hwui/Canvas.h index 2a2019199bda..07e2fe24c939 100644 --- a/libs/hwui/hwui/Canvas.h +++ b/libs/hwui/hwui/Canvas.h @@ -60,6 +60,7 @@ typedef uirenderer::VectorDrawable::Tree VectorDrawableRoot; typedef std::function<void(uint16_t* text, float* positions)> ReadGlyphFunc; class AnimatedImageDrawable; +class LottieDrawable; class Bitmap; class Paint; struct Typeface; @@ -242,6 +243,7 @@ public: const Paint* paint) = 0; virtual double drawAnimatedImage(AnimatedImageDrawable* imgDrawable) = 0; + virtual void drawLottie(LottieDrawable* lottieDrawable) = 0; virtual void drawPicture(const SkPicture& picture) = 0; /** diff --git a/libs/hwui/hwui/LottieDrawable.cpp b/libs/hwui/hwui/LottieDrawable.cpp new file mode 100644 index 000000000000..92dc51e01a85 --- /dev/null +++ b/libs/hwui/hwui/LottieDrawable.cpp @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "LottieDrawable.h" + +#include <SkTime.h> +#include <log/log.h> +#include <pipeline/skia/SkiaUtils.h> + +namespace android { + +sk_sp<LottieDrawable> LottieDrawable::Make(sk_sp<skottie::Animation> animation, size_t bytesUsed) { + if (animation) { + return sk_sp<LottieDrawable>(new LottieDrawable(std::move(animation), bytesUsed)); + } + return nullptr; +} +LottieDrawable::LottieDrawable(sk_sp<skottie::Animation> animation, size_t bytesUsed) + : mAnimation(std::move(animation)), mBytesUsed(bytesUsed) {} + +bool LottieDrawable::start() { + if (mRunning) { + return false; + } + + mRunning = true; + return true; +} + +bool LottieDrawable::stop() { + bool wasRunning = mRunning; + mRunning = false; + return wasRunning; +} + +bool LottieDrawable::isRunning() { + return mRunning; +} + +// TODO: Check to see if drawable is actually dirty +bool LottieDrawable::isDirty() { + return true; +} + +void LottieDrawable::onDraw(SkCanvas* canvas) { + if (mRunning) { + const nsecs_t currentTime = systemTime(SYSTEM_TIME_MONOTONIC); + + nsecs_t t = 0; + if (mStartTime == 0) { + mStartTime = currentTime; + } else { + t = currentTime - mStartTime; + } + double seekTime = std::fmod((double)t * 1e-9, mAnimation->duration()); + mAnimation->seekFrameTime(seekTime); + mAnimation->render(canvas); + } +} + +void LottieDrawable::drawStaging(SkCanvas* canvas) { + onDraw(canvas); +} + +SkRect LottieDrawable::onGetBounds() { + // We do not actually know the bounds, so give a conservative answer. + return SkRectMakeLargest(); +} + +} // namespace android diff --git a/libs/hwui/hwui/LottieDrawable.h b/libs/hwui/hwui/LottieDrawable.h new file mode 100644 index 000000000000..9cc34bf12f4f --- /dev/null +++ b/libs/hwui/hwui/LottieDrawable.h @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <SkDrawable.h> +#include <Skottie.h> +#include <utils/Timers.h> + +class SkCanvas; + +namespace android { + +/** + * Native component of android.graphics.drawable.LottieDrawable.java. + * This class can be drawn into Canvas.h and maintains the state needed to drive + * the animation from the RenderThread. + */ +class LottieDrawable : public SkDrawable { +public: + static sk_sp<LottieDrawable> Make(sk_sp<skottie::Animation> animation, size_t bytes); + + // Draw to software canvas + void drawStaging(SkCanvas* canvas); + + // Returns true if the animation was started; false otherwise (e.g. it was + // already running) + bool start(); + // Returns true if the animation was stopped; false otherwise (e.g. it was + // already stopped) + bool stop(); + bool isRunning(); + + // TODO: Is dirty should take in a time til next frame to determine if it is dirty + bool isDirty(); + + SkRect onGetBounds() override; + + size_t byteSize() const { return sizeof(*this) + mBytesUsed; } + +protected: + void onDraw(SkCanvas* canvas) override; + +private: + LottieDrawable(sk_sp<skottie::Animation> animation, size_t bytes_used); + + sk_sp<skottie::Animation> mAnimation; + bool mRunning = false; + // The start time for the drawable itself. + nsecs_t mStartTime = 0; + const size_t mBytesUsed = 0; +}; + +} // namespace android diff --git a/libs/hwui/jni/LottieDrawable.cpp b/libs/hwui/jni/LottieDrawable.cpp new file mode 100644 index 000000000000..fb6eede213a8 --- /dev/null +++ b/libs/hwui/jni/LottieDrawable.cpp @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <SkRect.h> +#include <Skottie.h> +#include <hwui/Canvas.h> +#include <hwui/LottieDrawable.h> + +#include "GraphicsJNI.h" +#include "Utils.h" + +using namespace android; + +static jclass gLottieDrawableClass; + +static jlong LottieDrawable_nCreate(JNIEnv* env, jobject, jstring jjson) { + const ScopedUtfChars cstr(env, jjson); + // TODO(b/259267150) provide more accurate byteSize + size_t bytes = strlen(cstr.c_str()); + auto animation = skottie::Animation::Builder().make(cstr.c_str(), bytes); + sk_sp<LottieDrawable> drawable(LottieDrawable::Make(std::move(animation), bytes)); + if (!drawable) { + return 0; + } + return reinterpret_cast<jlong>(drawable.release()); +} + +static void LottieDrawable_destruct(LottieDrawable* drawable) { + SkSafeUnref(drawable); +} + +static jlong LottieDrawable_nGetNativeFinalizer(JNIEnv* /*env*/, jobject /*clazz*/) { + return static_cast<jlong>(reinterpret_cast<uintptr_t>(&LottieDrawable_destruct)); +} + +static void LottieDrawable_nDraw(JNIEnv* env, jobject /*clazz*/, jlong nativePtr, jlong canvasPtr) { + auto* drawable = reinterpret_cast<LottieDrawable*>(nativePtr); + auto* canvas = reinterpret_cast<Canvas*>(canvasPtr); + canvas->drawLottie(drawable); +} + +static jboolean LottieDrawable_nIsRunning(JNIEnv* env, jobject /*clazz*/, jlong nativePtr) { + auto* drawable = reinterpret_cast<LottieDrawable*>(nativePtr); + return drawable->isRunning(); +} + +static jboolean LottieDrawable_nStart(JNIEnv* env, jobject /*clazz*/, jlong nativePtr) { + auto* drawable = reinterpret_cast<LottieDrawable*>(nativePtr); + return drawable->start(); +} + +static jboolean LottieDrawable_nStop(JNIEnv* env, jobject /*clazz*/, jlong nativePtr) { + auto* drawable = reinterpret_cast<LottieDrawable*>(nativePtr); + return drawable->stop(); +} + +static jlong LottieDrawable_nNativeByteSize(JNIEnv* env, jobject /*clazz*/, jlong nativePtr) { + auto* drawable = reinterpret_cast<LottieDrawable*>(nativePtr); + return drawable->byteSize(); +} + +static const JNINativeMethod gLottieDrawableMethods[] = { + {"nCreate", "(Ljava/lang/String;)J", (void*)LottieDrawable_nCreate}, + {"nNativeByteSize", "(J)J", (void*)LottieDrawable_nNativeByteSize}, + {"nGetNativeFinalizer", "()J", (void*)LottieDrawable_nGetNativeFinalizer}, + {"nDraw", "(JJ)V", (void*)LottieDrawable_nDraw}, + {"nIsRunning", "(J)Z", (void*)LottieDrawable_nIsRunning}, + {"nStart", "(J)Z", (void*)LottieDrawable_nStart}, + {"nStop", "(J)Z", (void*)LottieDrawable_nStop}, +}; + +int register_android_graphics_drawable_LottieDrawable(JNIEnv* env) { + gLottieDrawableClass = reinterpret_cast<jclass>( + env->NewGlobalRef(FindClassOrDie(env, "android/graphics/drawable/LottieDrawable"))); + + return android::RegisterMethodsOrDie(env, "android/graphics/drawable/LottieDrawable", + gLottieDrawableMethods, NELEM(gLottieDrawableMethods)); +} diff --git a/libs/hwui/jni/android_graphics_HardwareRenderer.cpp b/libs/hwui/jni/android_graphics_HardwareRenderer.cpp index 47e2edb2ed0f..3f4d004ae8fa 100644 --- a/libs/hwui/jni/android_graphics_HardwareRenderer.cpp +++ b/libs/hwui/jni/android_graphics_HardwareRenderer.cpp @@ -829,12 +829,10 @@ static void android_view_ThreadedRenderer_setDisplayDensityDpi(JNIEnv*, jclass, DeviceInfo::setDensity(density); } -static void android_view_ThreadedRenderer_initDisplayInfo(JNIEnv* env, jclass, jint physicalWidth, - jint physicalHeight, jfloat refreshRate, - jint wideColorDataspace, - jlong appVsyncOffsetNanos, - jlong presentationDeadlineNanos, - jboolean supportFp16ForHdr) { +static void android_view_ThreadedRenderer_initDisplayInfo( + JNIEnv* env, jclass, jint physicalWidth, jint physicalHeight, jfloat refreshRate, + jint wideColorDataspace, jlong appVsyncOffsetNanos, jlong presentationDeadlineNanos, + jboolean supportFp16ForHdr, jboolean supportMixedColorSpaces) { DeviceInfo::setWidth(physicalWidth); DeviceInfo::setHeight(physicalHeight); DeviceInfo::setRefreshRate(refreshRate); @@ -842,6 +840,7 @@ static void android_view_ThreadedRenderer_initDisplayInfo(JNIEnv* env, jclass, j DeviceInfo::setAppVsyncOffsetNanos(appVsyncOffsetNanos); DeviceInfo::setPresentationDeadlineNanos(presentationDeadlineNanos); DeviceInfo::setSupportFp16ForHdr(supportFp16ForHdr); + DeviceInfo::setSupportMixedColorSpaces(supportMixedColorSpaces); } static void android_view_ThreadedRenderer_setDrawingEnabled(JNIEnv*, jclass, jboolean enabled) { @@ -991,7 +990,7 @@ static const JNINativeMethod gMethods[] = { {"nSetForceDark", "(JZ)V", (void*)android_view_ThreadedRenderer_setForceDark}, {"nSetDisplayDensityDpi", "(I)V", (void*)android_view_ThreadedRenderer_setDisplayDensityDpi}, - {"nInitDisplayInfo", "(IIFIJJZ)V", (void*)android_view_ThreadedRenderer_initDisplayInfo}, + {"nInitDisplayInfo", "(IIFIJJZZ)V", (void*)android_view_ThreadedRenderer_initDisplayInfo}, {"preload", "()V", (void*)android_view_ThreadedRenderer_preload}, {"isWebViewOverlaysEnabled", "()Z", (void*)android_view_ThreadedRenderer_isWebViewOverlaysEnabled}, diff --git a/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp b/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp index dc72aead4873..a4960ea17c79 100644 --- a/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp +++ b/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp @@ -24,6 +24,7 @@ #include "SkClipStack.h" #include "SkRect.h" #include "SkM44.h" +#include "include/gpu/GpuTypes.h" // from Skia #include "utils/GLUtils.h" namespace android { @@ -92,7 +93,7 @@ void GLFunctorDrawable::onDraw(SkCanvas* canvas) { SkImageInfo surfaceInfo = canvas->imageInfo().makeWH(clipBounds.width(), clipBounds.height()); tmpSurface = - SkSurface::MakeRenderTarget(directContext, SkBudgeted::kYes, surfaceInfo); + SkSurface::MakeRenderTarget(directContext, skgpu::Budgeted::kYes, surfaceInfo); tmpSurface->getCanvas()->clear(SK_ColorTRANSPARENT); GrGLFramebufferInfo fboInfo; diff --git a/libs/hwui/pipeline/skia/SkiaDisplayList.cpp b/libs/hwui/pipeline/skia/SkiaDisplayList.cpp index fcfc4f82abed..f0dc5eb4dd0e 100644 --- a/libs/hwui/pipeline/skia/SkiaDisplayList.cpp +++ b/libs/hwui/pipeline/skia/SkiaDisplayList.cpp @@ -146,6 +146,16 @@ bool SkiaDisplayList::prepareListAndChildren( } } + for (auto& lottie : mLotties) { + // If any animated image in the display list needs updated, then damage the node. + if (lottie->isDirty()) { + isDirty = true; + } + if (lottie->isRunning()) { + info.out.hasAnimations = true; + } + } + for (auto& [vectorDrawable, cachedMatrix] : mVectorDrawables) { // If any vector drawable in the display list needs update, damage the node. if (vectorDrawable->isDirty()) { diff --git a/libs/hwui/pipeline/skia/SkiaDisplayList.h b/libs/hwui/pipeline/skia/SkiaDisplayList.h index 2a677344b7b2..39217fcf1a56 100644 --- a/libs/hwui/pipeline/skia/SkiaDisplayList.h +++ b/libs/hwui/pipeline/skia/SkiaDisplayList.h @@ -22,6 +22,7 @@ #include "RenderNodeDrawable.h" #include "TreeInfo.h" #include "hwui/AnimatedImageDrawable.h" +#include "hwui/LottieDrawable.h" #include "utils/LinearAllocator.h" #include "utils/Pair.h" @@ -186,6 +187,8 @@ public: return mHasHolePunches; } + // TODO(b/257304231): create common base class for Lotties and AnimatedImages + std::vector<LottieDrawable*> mLotties; std::vector<AnimatedImageDrawable*> mAnimatedImages; DisplayListData mDisplayList; diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.cpp b/libs/hwui/pipeline/skia/SkiaPipeline.cpp index 1a336c5855d9..3692f0940b28 100644 --- a/libs/hwui/pipeline/skia/SkiaPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaPipeline.cpp @@ -36,6 +36,7 @@ #include <SkStream.h> #include <SkString.h> #include <SkTypeface.h> +#include "include/gpu/GpuTypes.h" // from Skia #include <android-base/properties.h> #include <unistd.h> @@ -187,7 +188,7 @@ bool SkiaPipeline::createOrUpdateLayer(RenderNode* node, const DamageAccumulator SkSurfaceProps props(0, kUnknown_SkPixelGeometry); SkASSERT(mRenderThread.getGrContext() != nullptr); node->setLayerSurface(SkSurface::MakeRenderTarget(mRenderThread.getGrContext(), - SkBudgeted::kYes, info, 0, + skgpu::Budgeted::kYes, info, 0, this->getSurfaceOrigin(), &props)); if (node->getLayerSurface()) { // update the transform in window of the layer to reset its origin wrt light source diff --git a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp index 1f87865f2672..db449d608c1f 100644 --- a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp +++ b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp @@ -188,6 +188,11 @@ void SkiaRecordingCanvas::drawWebViewFunctor(int functor) { #endif } +void SkiaRecordingCanvas::drawLottie(LottieDrawable* lottie) { + drawDrawable(lottie); + mDisplayList->mLotties.push_back(lottie); +} + void SkiaRecordingCanvas::drawVectorDrawable(VectorDrawableRoot* tree) { mRecorder.drawVectorDrawable(tree); SkMatrix mat; diff --git a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h index 7844e2cc2a73..c823d8d0a755 100644 --- a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h +++ b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h @@ -78,6 +78,7 @@ public: uirenderer::CanvasPropertyPaint* paint) override; virtual void drawRipple(const RippleDrawableParams& params) override; + virtual void drawLottie(LottieDrawable* lottieDrawable) override; virtual void drawVectorDrawable(VectorDrawableRoot* vectorDrawable) override; virtual void enableZ(bool enableZ) override; diff --git a/libs/hwui/pipeline/skia/StretchMask.cpp b/libs/hwui/pipeline/skia/StretchMask.cpp index b169c9200e88..cad3703d8d2b 100644 --- a/libs/hwui/pipeline/skia/StretchMask.cpp +++ b/libs/hwui/pipeline/skia/StretchMask.cpp @@ -18,6 +18,8 @@ #include "SkBlendMode.h" #include "SkCanvas.h" #include "SkSurface.h" +#include "include/gpu/GpuTypes.h" // from Skia + #include "TransformCanvas.h" #include "SkiaDisplayList.h" @@ -36,7 +38,7 @@ void StretchMask::draw(GrRecordingContext* context, // not match. mMaskSurface = SkSurface::MakeRenderTarget( context, - SkBudgeted::kYes, + skgpu::Budgeted::kYes, SkImageInfo::Make( width, height, diff --git a/libs/hwui/tests/unit/CacheManagerTests.cpp b/libs/hwui/tests/unit/CacheManagerTests.cpp index 508e1986b4e4..2b90bda87ecd 100644 --- a/libs/hwui/tests/unit/CacheManagerTests.cpp +++ b/libs/hwui/tests/unit/CacheManagerTests.cpp @@ -21,6 +21,7 @@ #include "tests/common/TestUtils.h" #include <SkImagePriv.h> +#include "include/gpu/GpuTypes.h" // from Skia using namespace android; using namespace android::uirenderer; @@ -45,7 +46,8 @@ RENDERTHREAD_SKIA_PIPELINE_TEST(CacheManager, DISABLED_trimMemory) { while (getCacheUsage(grContext) <= renderThread.cacheManager().getBackgroundCacheSize()) { SkImageInfo info = SkImageInfo::MakeA8(width, height); - sk_sp<SkSurface> surface = SkSurface::MakeRenderTarget(grContext, SkBudgeted::kYes, info); + sk_sp<SkSurface> surface = SkSurface::MakeRenderTarget(grContext, skgpu::Budgeted::kYes, + info); surface->getCanvas()->drawColor(SK_AlphaTRANSPARENT); grContext->flushAndSubmit(); diff --git a/media/java/android/media/MediaCas.java b/media/java/android/media/MediaCas.java index 582a28ee278e..015602e95533 100644 --- a/media/java/android/media/MediaCas.java +++ b/media/java/android/media/MediaCas.java @@ -21,11 +21,12 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.TestApi; import android.content.Context; +import android.hardware.cas.AidlCasPluginDescriptor; +import android.hardware.cas.ICas; +import android.hardware.cas.ICasListener; +import android.hardware.cas.IMediaCasService; +import android.hardware.cas.Status; import android.hardware.cas.V1_0.HidlCasPluginDescriptor; -import android.hardware.cas.V1_0.ICas; -import android.hardware.cas.V1_0.IMediaCasService; -import android.hardware.cas.V1_2.ICasListener; -import android.hardware.cas.V1_2.Status; import android.media.MediaCasException.*; import android.media.tv.TvInputService.PriorityHintUseCaseType; import android.media.tv.tunerresourcemanager.CasSessionRequest; @@ -39,6 +40,7 @@ import android.os.Looper; import android.os.Message; import android.os.Process; import android.os.RemoteException; +import android.os.ServiceManager; import android.util.Log; import android.util.Singleton; @@ -47,6 +49,7 @@ import com.android.internal.util.FrameworkStatsLog; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -114,9 +117,10 @@ import java.util.Objects; */ public final class MediaCas implements AutoCloseable { private static final String TAG = "MediaCas"; - private ICas mICas; - private android.hardware.cas.V1_1.ICas mICasV11; - private android.hardware.cas.V1_2.ICas mICasV12; + private ICas mICas = null; + private android.hardware.cas.V1_0.ICas mICasHidl = null; + private android.hardware.cas.V1_1.ICas mICasHidl11 = null; + private android.hardware.cas.V1_2.ICas mICasHidl12 = null; private EventListener mListener; private HandlerThread mHandlerThread; private EventHandler mEventHandler; @@ -133,88 +137,84 @@ public final class MediaCas implements AutoCloseable { * * @hide */ - @IntDef(prefix = "SCRAMBLING_MODE_", - value = {SCRAMBLING_MODE_RESERVED, SCRAMBLING_MODE_DVB_CSA1, SCRAMBLING_MODE_DVB_CSA2, - SCRAMBLING_MODE_DVB_CSA3_STANDARD, - SCRAMBLING_MODE_DVB_CSA3_MINIMAL, SCRAMBLING_MODE_DVB_CSA3_ENHANCE, - SCRAMBLING_MODE_DVB_CISSA_V1, SCRAMBLING_MODE_DVB_IDSA, - SCRAMBLING_MODE_MULTI2, SCRAMBLING_MODE_AES128, SCRAMBLING_MODE_AES_ECB, - SCRAMBLING_MODE_AES_SCTE52, SCRAMBLING_MODE_TDES_ECB, SCRAMBLING_MODE_TDES_SCTE52}) + @IntDef( + prefix = "SCRAMBLING_MODE_", + value = { + SCRAMBLING_MODE_RESERVED, + SCRAMBLING_MODE_DVB_CSA1, + SCRAMBLING_MODE_DVB_CSA2, + SCRAMBLING_MODE_DVB_CSA3_STANDARD, + SCRAMBLING_MODE_DVB_CSA3_MINIMAL, + SCRAMBLING_MODE_DVB_CSA3_ENHANCE, + SCRAMBLING_MODE_DVB_CISSA_V1, + SCRAMBLING_MODE_DVB_IDSA, + SCRAMBLING_MODE_MULTI2, + SCRAMBLING_MODE_AES128, + SCRAMBLING_MODE_AES_CBC, + SCRAMBLING_MODE_AES_ECB, + SCRAMBLING_MODE_AES_SCTE52, + SCRAMBLING_MODE_TDES_ECB, + SCRAMBLING_MODE_TDES_SCTE52 + }) @Retention(RetentionPolicy.SOURCE) public @interface ScramblingMode {} - /** - * DVB (Digital Video Broadcasting) reserved mode. - */ - public static final int SCRAMBLING_MODE_RESERVED = - android.hardware.cas.V1_2.ScramblingMode.RESERVED; - /** - * DVB (Digital Video Broadcasting) Common Scrambling Algorithm (CSA) 1. - */ - public static final int SCRAMBLING_MODE_DVB_CSA1 = - android.hardware.cas.V1_2.ScramblingMode.DVB_CSA1; - /** - * DVB CSA 2. - */ - public static final int SCRAMBLING_MODE_DVB_CSA2 = - android.hardware.cas.V1_2.ScramblingMode.DVB_CSA2; - /** - * DVB CSA 3 in standard mode. - */ + /** DVB (Digital Video Broadcasting) reserved mode. */ + public static final int SCRAMBLING_MODE_RESERVED = android.hardware.cas.ScramblingMode.RESERVED; + + /** DVB (Digital Video Broadcasting) Common Scrambling Algorithm (CSA) 1. */ + public static final int SCRAMBLING_MODE_DVB_CSA1 = android.hardware.cas.ScramblingMode.DVB_CSA1; + + /** DVB CSA 2. */ + public static final int SCRAMBLING_MODE_DVB_CSA2 = android.hardware.cas.ScramblingMode.DVB_CSA2; + + /** DVB CSA 3 in standard mode. */ public static final int SCRAMBLING_MODE_DVB_CSA3_STANDARD = - android.hardware.cas.V1_2.ScramblingMode.DVB_CSA3_STANDARD; - /** - * DVB CSA 3 in minimally enhanced mode. - */ + android.hardware.cas.ScramblingMode.DVB_CSA3_STANDARD; + + /** DVB CSA 3 in minimally enhanced mode. */ public static final int SCRAMBLING_MODE_DVB_CSA3_MINIMAL = - android.hardware.cas.V1_2.ScramblingMode.DVB_CSA3_MINIMAL; - /** - * DVB CSA 3 in fully enhanced mode. - */ + android.hardware.cas.ScramblingMode.DVB_CSA3_MINIMAL; + + /** DVB CSA 3 in fully enhanced mode. */ public static final int SCRAMBLING_MODE_DVB_CSA3_ENHANCE = - android.hardware.cas.V1_2.ScramblingMode.DVB_CSA3_ENHANCE; - /** - * DVB Common IPTV Software-oriented Scrambling Algorithm (CISSA) Version 1. - */ + android.hardware.cas.ScramblingMode.DVB_CSA3_ENHANCE; + + /** DVB Common IPTV Software-oriented Scrambling Algorithm (CISSA) Version 1. */ public static final int SCRAMBLING_MODE_DVB_CISSA_V1 = - android.hardware.cas.V1_2.ScramblingMode.DVB_CISSA_V1; - /** - * ATIS-0800006 IIF Default Scrambling Algorithm (IDSA). - */ - public static final int SCRAMBLING_MODE_DVB_IDSA = - android.hardware.cas.V1_2.ScramblingMode.DVB_IDSA; - /** - * A symmetric key algorithm. - */ - public static final int SCRAMBLING_MODE_MULTI2 = - android.hardware.cas.V1_2.ScramblingMode.MULTI2; - /** - * Advanced Encryption System (AES) 128-bit Encryption mode. - */ - public static final int SCRAMBLING_MODE_AES128 = - android.hardware.cas.V1_2.ScramblingMode.AES128; - /** - * Advanced Encryption System (AES) Electronic Code Book (ECB) mode. - */ - public static final int SCRAMBLING_MODE_AES_ECB = - android.hardware.cas.V1_2.ScramblingMode.AES_ECB; + android.hardware.cas.ScramblingMode.DVB_CISSA_V1; + + /** ATIS-0800006 IIF Default Scrambling Algorithm (IDSA). */ + public static final int SCRAMBLING_MODE_DVB_IDSA = android.hardware.cas.ScramblingMode.DVB_IDSA; + + /** A symmetric key algorithm. */ + public static final int SCRAMBLING_MODE_MULTI2 = android.hardware.cas.ScramblingMode.MULTI2; + + /** Advanced Encryption System (AES) 128-bit Encryption mode. */ + public static final int SCRAMBLING_MODE_AES128 = android.hardware.cas.ScramblingMode.AES128; + + /** Advanced Encryption System (AES) Cipher Block Chaining (CBC) mode. */ + public static final int SCRAMBLING_MODE_AES_CBC = android.hardware.cas.ScramblingMode.AES_CBC; + + /** Advanced Encryption System (AES) Electronic Code Book (ECB) mode. */ + public static final int SCRAMBLING_MODE_AES_ECB = android.hardware.cas.ScramblingMode.AES_ECB; + /** * Advanced Encryption System (AES) Society of Cable Telecommunications Engineers (SCTE) 52 * mode. */ public static final int SCRAMBLING_MODE_AES_SCTE52 = - android.hardware.cas.V1_2.ScramblingMode.AES_SCTE52; - /** - * Triple Data Encryption Algorithm (TDES) Electronic Code Book (ECB) mode. - */ - public static final int SCRAMBLING_MODE_TDES_ECB = - android.hardware.cas.V1_2.ScramblingMode.TDES_ECB; + android.hardware.cas.ScramblingMode.AES_SCTE52; + + /** Triple Data Encryption Algorithm (TDES) Electronic Code Book (ECB) mode. */ + public static final int SCRAMBLING_MODE_TDES_ECB = android.hardware.cas.ScramblingMode.TDES_ECB; + /** * Triple Data Encryption Algorithm (TDES) Society of Cable Telecommunications Engineers (SCTE) * 52 mode. */ public static final int SCRAMBLING_MODE_TDES_SCTE52 = - android.hardware.cas.V1_2.ScramblingMode.TDES_SCTE52; + android.hardware.cas.ScramblingMode.TDES_SCTE52; /** * Usages used to open cas sessions. @@ -226,25 +226,21 @@ public final class MediaCas implements AutoCloseable { SESSION_USAGE_TIMESHIFT}) @Retention(RetentionPolicy.SOURCE) public @interface SessionUsage {} - /** - * Cas session is used to descramble live streams. - */ - public static final int SESSION_USAGE_LIVE = android.hardware.cas.V1_2.SessionIntent.LIVE; - /** - * Cas session is used to descramble recoreded streams. - */ - public static final int SESSION_USAGE_PLAYBACK = - android.hardware.cas.V1_2.SessionIntent.PLAYBACK; - /** - * Cas session is used to descramble live streams and encrypt local recorded content - */ - public static final int SESSION_USAGE_RECORD = android.hardware.cas.V1_2.SessionIntent.RECORD; + + /** Cas session is used to descramble live streams. */ + public static final int SESSION_USAGE_LIVE = android.hardware.cas.SessionIntent.LIVE; + + /** Cas session is used to descramble recoreded streams. */ + public static final int SESSION_USAGE_PLAYBACK = android.hardware.cas.SessionIntent.PLAYBACK; + + /** Cas session is used to descramble live streams and encrypt local recorded content */ + public static final int SESSION_USAGE_RECORD = android.hardware.cas.SessionIntent.RECORD; + /** * Cas session is used to descramble live streams , encrypt local recorded content and playback * local encrypted content. */ - public static final int SESSION_USAGE_TIMESHIFT = - android.hardware.cas.V1_2.SessionIntent.TIMESHIFT; + public static final int SESSION_USAGE_TIMESHIFT = android.hardware.cas.SessionIntent.TIMESHIFT; /** * Plugin status events sent from cas system. @@ -261,63 +257,90 @@ public final class MediaCas implements AutoCloseable { * physical CAS modules. */ public static final int PLUGIN_STATUS_PHYSICAL_MODULE_CHANGED = - android.hardware.cas.V1_2.StatusEvent.PLUGIN_PHYSICAL_MODULE_CHANGED; - /** - * The event to indicate that the number of CAS system's session is changed. - */ - public static final int PLUGIN_STATUS_SESSION_NUMBER_CHANGED = - android.hardware.cas.V1_2.StatusEvent.PLUGIN_SESSION_NUMBER_CHANGED; + android.hardware.cas.StatusEvent.PLUGIN_PHYSICAL_MODULE_CHANGED; - private static final Singleton<IMediaCasService> sService = new Singleton<IMediaCasService>() { - @Override - protected IMediaCasService create() { - try { - Log.d(TAG, "Trying to get cas@1.2 service"); - android.hardware.cas.V1_2.IMediaCasService serviceV12 = - android.hardware.cas.V1_2.IMediaCasService.getService(true /*wait*/); - if (serviceV12 != null) { - return serviceV12; + /** The event to indicate that the number of CAS system's session is changed. */ + public static final int PLUGIN_STATUS_SESSION_NUMBER_CHANGED = + android.hardware.cas.StatusEvent.PLUGIN_SESSION_NUMBER_CHANGED; + + private static final Singleton<IMediaCasService> sService = + new Singleton<IMediaCasService>() { + @Override + protected IMediaCasService create() { + try { + Log.d(TAG, "Trying to get AIDL service"); + IMediaCasService serviceAidl = + IMediaCasService.Stub.asInterface( + ServiceManager.getService( + IMediaCasService.DESCRIPTOR + "/default")); + if (serviceAidl != null) { + return serviceAidl; + } + } catch (Exception eAidl) { + Log.d(TAG, "Failed to get cas AIDL service"); + } + return null; } - } catch (Exception eV1_2) { - Log.d(TAG, "Failed to get cas@1.2 service"); - } + }; + + private static final Singleton<android.hardware.cas.V1_0.IMediaCasService> sServiceHidl = + new Singleton<android.hardware.cas.V1_0.IMediaCasService>() { + @Override + protected android.hardware.cas.V1_0.IMediaCasService create() { + try { + Log.d(TAG, "Trying to get cas@1.2 service"); + android.hardware.cas.V1_2.IMediaCasService serviceV12 = + android.hardware.cas.V1_2.IMediaCasService.getService( + true /*wait*/); + if (serviceV12 != null) { + return serviceV12; + } + } catch (Exception eV1_2) { + Log.d(TAG, "Failed to get cas@1.2 service"); + } - try { - Log.d(TAG, "Trying to get cas@1.1 service"); - android.hardware.cas.V1_1.IMediaCasService serviceV11 = - android.hardware.cas.V1_1.IMediaCasService.getService(true /*wait*/); - if (serviceV11 != null) { - return serviceV11; + try { + Log.d(TAG, "Trying to get cas@1.1 service"); + android.hardware.cas.V1_1.IMediaCasService serviceV11 = + android.hardware.cas.V1_1.IMediaCasService.getService( + true /*wait*/); + if (serviceV11 != null) { + return serviceV11; + } + } catch (Exception eV1_1) { + Log.d(TAG, "Failed to get cas@1.1 service"); } - } catch (Exception eV1_1) { - Log.d(TAG, "Failed to get cas@1.1 service"); - } - try { - Log.d(TAG, "Trying to get cas@1.0 service"); - return IMediaCasService.getService(true /*wait*/); - } catch (Exception eV1_0) { - Log.d(TAG, "Failed to get cas@1.0 service"); - } + try { + Log.d(TAG, "Trying to get cas@1.0 service"); + return android.hardware.cas.V1_0.IMediaCasService.getService(true /*wait*/); + } catch (Exception eV1_0) { + Log.d(TAG, "Failed to get cas@1.0 service"); + } - return null; - } - }; + return null; + } + }; static IMediaCasService getService() { return sService.get(); } + static android.hardware.cas.V1_0.IMediaCasService getServiceHidl() { + return sServiceHidl.get(); + } + private void validateInternalStates() { - if (mICas == null) { + if (mICas == null && mICasHidl == null) { throw new IllegalStateException(); } } private void cleanupAndRethrowIllegalState() { mICas = null; - mICasV11 = null; - mICasV12 = null; + mICasHidl = null; + mICasHidl11 = null; + mICasHidl12 = null; throw new IllegalStateException(); } @@ -341,7 +364,7 @@ public final class MediaCas implements AutoCloseable { toBytes((ArrayList<Byte>) msg.obj)); } else if (msg.what == MSG_CAS_SESSION_EVENT) { Bundle bundle = msg.getData(); - ArrayList<Byte> sessionId = toByteArray(bundle.getByteArray(SESSION_KEY)); + byte[] sessionId = bundle.getByteArray(SESSION_KEY); mListener.onSessionEvent(MediaCas.this, createFromSessionId(sessionId), msg.arg1, msg.arg2, bundle.getByteArray(DATA_KEY)); @@ -357,40 +380,94 @@ public final class MediaCas implements AutoCloseable { } } - private final ICasListener.Stub mBinder = new ICasListener.Stub() { - @Override - public void onEvent(int event, int arg, @Nullable ArrayList<Byte> data) - throws RemoteException { - if (mEventHandler != null) { - mEventHandler.sendMessage(mEventHandler.obtainMessage( - EventHandler.MSG_CAS_EVENT, event, arg, data)); - } - } - @Override - public void onSessionEvent(@NonNull ArrayList<Byte> sessionId, - int event, int arg, @Nullable ArrayList<Byte> data) - throws RemoteException { - if (mEventHandler != null) { - Message msg = mEventHandler.obtainMessage(); - msg.what = EventHandler.MSG_CAS_SESSION_EVENT; - msg.arg1 = event; - msg.arg2 = arg; - Bundle bundle = new Bundle(); - bundle.putByteArray(EventHandler.SESSION_KEY, toBytes(sessionId)); - bundle.putByteArray(EventHandler.DATA_KEY, toBytes(data)); - msg.setData(bundle); - mEventHandler.sendMessage(msg); - } - } - @Override - public void onStatusUpdate(byte status, int arg) - throws RemoteException { - if (mEventHandler != null) { - mEventHandler.sendMessage(mEventHandler.obtainMessage( - EventHandler.MSG_CAS_STATUS_EVENT, status, arg)); - } - } - }; + private final ICasListener.Stub mBinder = + new ICasListener.Stub() { + @Override + public void onEvent(int event, int arg, byte[] data) throws RemoteException { + if (mEventHandler != null) { + mEventHandler.sendMessage( + mEventHandler.obtainMessage( + EventHandler.MSG_CAS_EVENT, event, arg, data)); + } + } + + @Override + public void onSessionEvent(byte[] sessionId, int event, int arg, byte[] data) + throws RemoteException { + if (mEventHandler != null) { + Message msg = mEventHandler.obtainMessage(); + msg.what = EventHandler.MSG_CAS_SESSION_EVENT; + msg.arg1 = event; + msg.arg2 = arg; + Bundle bundle = new Bundle(); + bundle.putByteArray(EventHandler.SESSION_KEY, sessionId); + bundle.putByteArray(EventHandler.DATA_KEY, data); + msg.setData(bundle); + mEventHandler.sendMessage(msg); + } + } + + @Override + public void onStatusUpdate(byte status, int arg) throws RemoteException { + if (mEventHandler != null) { + mEventHandler.sendMessage( + mEventHandler.obtainMessage( + EventHandler.MSG_CAS_STATUS_EVENT, status, arg)); + } + } + + @Override + public synchronized String getInterfaceHash() throws android.os.RemoteException { + return ICasListener.Stub.HASH; + } + + @Override + public int getInterfaceVersion() throws android.os.RemoteException { + return ICasListener.Stub.VERSION; + } + }; + + private final android.hardware.cas.V1_2.ICasListener.Stub mBinderHidl = + new android.hardware.cas.V1_2.ICasListener.Stub() { + @Override + public void onEvent(int event, int arg, @Nullable ArrayList<Byte> data) + throws RemoteException { + if (mEventHandler != null) { + mEventHandler.sendMessage( + mEventHandler.obtainMessage( + EventHandler.MSG_CAS_EVENT, event, arg, data)); + } + } + + @Override + public void onSessionEvent( + @NonNull ArrayList<Byte> sessionId, + int event, + int arg, + @Nullable ArrayList<Byte> data) + throws RemoteException { + if (mEventHandler != null) { + Message msg = mEventHandler.obtainMessage(); + msg.what = EventHandler.MSG_CAS_SESSION_EVENT; + msg.arg1 = event; + msg.arg2 = arg; + Bundle bundle = new Bundle(); + bundle.putByteArray(EventHandler.SESSION_KEY, toBytes(sessionId)); + bundle.putByteArray(EventHandler.DATA_KEY, toBytes(data)); + msg.setData(bundle); + mEventHandler.sendMessage(msg); + } + } + + @Override + public void onStatusUpdate(byte status, int arg) throws RemoteException { + if (mEventHandler != null) { + mEventHandler.sendMessage( + mEventHandler.obtainMessage( + EventHandler.MSG_CAS_STATUS_EVENT, status, arg)); + } + } + }; private final TunerResourceManager.ResourcesReclaimListener mResourceListener = new TunerResourceManager.ResourcesReclaimListener() { @@ -422,6 +499,11 @@ public final class MediaCas implements AutoCloseable { mName = null; } + PluginDescriptor(@NonNull AidlCasPluginDescriptor descriptor) { + mCASystemId = descriptor.caSystemId; + mName = descriptor.name; + } + PluginDescriptor(@NonNull HidlCasPluginDescriptor descriptor) { mCASystemId = descriptor.caSystemId; mName = descriptor.name; @@ -467,19 +549,20 @@ public final class MediaCas implements AutoCloseable { } return data; } + /** * Class for an open session with the CA system. */ public final class Session implements AutoCloseable { - final ArrayList<Byte> mSessionId; + final byte[] mSessionId; boolean mIsClosed = false; - Session(@NonNull ArrayList<Byte> sessionId) { - mSessionId = new ArrayList<Byte>(sessionId); + Session(@NonNull byte[] sessionId) { + mSessionId = sessionId; } private void validateSessionInternalStates() { - if (mICas == null) { + if (mICas == null && mICasHidl == null) { throw new IllegalStateException(); } if (mIsClosed) { @@ -496,7 +579,7 @@ public final class MediaCas implements AutoCloseable { */ public boolean equals(Object obj) { if (obj instanceof Session) { - return mSessionId.equals(((Session) obj).mSessionId); + return Arrays.equals(mSessionId, ((Session) obj).mSessionId); } return false; } @@ -515,8 +598,13 @@ public final class MediaCas implements AutoCloseable { validateSessionInternalStates(); try { - MediaCasException.throwExceptionIfNeeded( - mICas.setSessionPrivateData(mSessionId, toByteArray(data, 0, data.length))); + if (mICas != null) { + mICas.setSessionPrivateData(mSessionId, data); + } else { + MediaCasException.throwExceptionIfNeeded( + mICasHidl.setSessionPrivateData( + toByteArray(mSessionId), toByteArray(data, 0, data.length))); + } } catch (RemoteException e) { cleanupAndRethrowIllegalState(); } @@ -539,8 +627,13 @@ public final class MediaCas implements AutoCloseable { validateSessionInternalStates(); try { - MediaCasException.throwExceptionIfNeeded( - mICas.processEcm(mSessionId, toByteArray(data, offset, length))); + if (mICas != null) { + mICas.processEcm(mSessionId, data); + } else { + MediaCasException.throwExceptionIfNeeded( + mICasHidl.processEcm( + toByteArray(mSessionId), toByteArray(data, offset, length))); + } } catch (RemoteException e) { cleanupAndRethrowIllegalState(); } @@ -576,15 +669,23 @@ public final class MediaCas implements AutoCloseable { public void sendSessionEvent(int event, int arg, @Nullable byte[] data) throws MediaCasException { validateSessionInternalStates(); + if (mICas != null) { + try { + mICas.sendSessionEvent(mSessionId, event, arg, data); + } catch (RemoteException e) { + cleanupAndRethrowIllegalState(); + } + } - if (mICasV11 == null) { + if (mICasHidl11 == null) { Log.d(TAG, "Send Session Event isn't supported by cas@1.0 interface"); throw new UnsupportedCasException("Send Session Event is not supported"); } try { MediaCasException.throwExceptionIfNeeded( - mICasV11.sendSessionEvent(mSessionId, event, arg, toByteArray(data))); + mICasHidl11.sendSessionEvent( + toByteArray(mSessionId), event, arg, toByteArray(data))); } catch (RemoteException e) { cleanupAndRethrowIllegalState(); } @@ -600,7 +701,7 @@ public final class MediaCas implements AutoCloseable { @NonNull public byte[] getSessionId() { validateSessionInternalStates(); - return toBytes(mSessionId); + return mSessionId; } /** @@ -613,8 +714,12 @@ public final class MediaCas implements AutoCloseable { public void close() { validateSessionInternalStates(); try { - MediaCasStateException.throwExceptionIfNeeded( - mICas.closeSession(mSessionId)); + if (mICas != null) { + mICas.closeSession(mSessionId); + } else { + MediaCasStateException.throwExceptionIfNeeded( + mICasHidl.closeSession(toByteArray(mSessionId))); + } mIsClosed = true; removeSessionFromResourceMap(this); } catch (RemoteException e) { @@ -623,8 +728,8 @@ public final class MediaCas implements AutoCloseable { } } - Session createFromSessionId(@NonNull ArrayList<Byte> sessionId) { - if (sessionId == null || sessionId.size() == 0) { + Session createFromSessionId(byte[] sessionId) { + if (sessionId == null || sessionId.length == 0) { return null; } return new Session(sessionId); @@ -638,12 +743,20 @@ public final class MediaCas implements AutoCloseable { * @return Whether the specified CA system is supported on this device. */ public static boolean isSystemIdSupported(int CA_system_id) { - IMediaCasService service = getService(); - + IMediaCasService service = sService.get(); if (service != null) { try { return service.isSystemIdSupported(CA_system_id); } catch (RemoteException e) { + return false; + } + } + + android.hardware.cas.V1_0.IMediaCasService serviceHidl = sServiceHidl.get(); + if (serviceHidl != null) { + try { + return serviceHidl.isSystemIdSupported(CA_system_id); + } catch (RemoteException e) { } } return false; @@ -655,12 +768,26 @@ public final class MediaCas implements AutoCloseable { * @return an array of descriptors for the available CA plugins. */ public static PluginDescriptor[] enumeratePlugins() { - IMediaCasService service = getService(); - + IMediaCasService service = sService.get(); if (service != null) { try { - ArrayList<HidlCasPluginDescriptor> descriptors = - service.enumeratePlugins(); + AidlCasPluginDescriptor[] descriptors = service.enumeratePlugins(); + if (descriptors.length == 0) { + return null; + } + PluginDescriptor[] results = new PluginDescriptor[descriptors.length]; + for (int i = 0; i < results.length; i++) { + results[i] = new PluginDescriptor(descriptors[i]); + } + return results; + } catch (RemoteException e) { + } + } + + android.hardware.cas.V1_0.IMediaCasService serviceHidl = sServiceHidl.get(); + if (serviceHidl != null) { + try { + ArrayList<HidlCasPluginDescriptor> descriptors = serviceHidl.enumeratePlugins(); if (descriptors.size() == 0) { return null; } @@ -680,29 +807,40 @@ public final class MediaCas implements AutoCloseable { mCasSystemId = casSystemId; mUserId = Process.myUid(); IMediaCasService service = getService(); - android.hardware.cas.V1_2.IMediaCasService serviceV12 = - android.hardware.cas.V1_2.IMediaCasService.castFrom(service); - if (serviceV12 == null) { - android.hardware.cas.V1_1.IMediaCasService serviceV11 = - android.hardware.cas.V1_1.IMediaCasService.castFrom(service); - if (serviceV11 == null) { + if (service != null) { + Log.d(TAG, "Use CAS AIDL interface to create plugin"); + mICas = service.createPlugin(casSystemId, mBinder); + } else { + android.hardware.cas.V1_0.IMediaCasService serviceV10 = getServiceHidl(); + android.hardware.cas.V1_2.IMediaCasService serviceV12 = + android.hardware.cas.V1_2.IMediaCasService.castFrom(serviceV10); + if (serviceV12 == null) { + android.hardware.cas.V1_1.IMediaCasService serviceV11 = + android.hardware.cas.V1_1.IMediaCasService.castFrom(serviceV10); + if (serviceV11 == null) { Log.d(TAG, "Used cas@1_0 interface to create plugin"); - mICas = service.createPlugin(casSystemId, mBinder); - } else { + mICasHidl = serviceV10.createPlugin(casSystemId, mBinderHidl); + } else { Log.d(TAG, "Used cas@1.1 interface to create plugin"); - mICas = mICasV11 = serviceV11.createPluginExt(casSystemId, mBinder); + mICasHidl = + mICasHidl11 = serviceV11.createPluginExt(casSystemId, mBinderHidl); + } + } else { + Log.d(TAG, "Used cas@1.2 interface to create plugin"); + mICasHidl = + mICasHidl11 = + mICasHidl12 = + android.hardware.cas.V1_2.ICas.castFrom( + serviceV12.createPluginExt( + casSystemId, mBinderHidl)); } - } else { - Log.d(TAG, "Used cas@1.2 interface to create plugin"); - mICas = mICasV11 = mICasV12 = - android.hardware.cas.V1_2.ICas - .castFrom(serviceV12.createPluginExt(casSystemId, mBinder)); } } catch(Exception e) { Log.e(TAG, "Failed to create plugin: " + e); mICas = null; + mICasHidl = null; } finally { - if (mICas == null) { + if (mICas == null && mICasHidl == null) { throw new UnsupportedCasException( "Unsupported casSystemId " + casSystemId); } @@ -783,9 +921,22 @@ public final class MediaCas implements AutoCloseable { } IHwBinder getBinder() { + if (mICas != null) { + return null; // Return IHwBinder only for HIDL + } + validateInternalStates(); - return mICas.asBinder(); + return mICasHidl.asBinder(); + } + + /** + * Check if the HAL is an AIDL implementation + * + * @hide + */ + public boolean isAidlHal() { + return mICas != null; } /** @@ -886,8 +1037,12 @@ public final class MediaCas implements AutoCloseable { validateInternalStates(); try { - MediaCasException.throwExceptionIfNeeded( - mICas.setPrivateData(toByteArray(data, 0, data.length))); + if (mICas != null) { + mICas.setPrivateData(data); + } else { + MediaCasException.throwExceptionIfNeeded( + mICasHidl.setPrivateData(toByteArray(data, 0, data.length))); + } } catch (RemoteException e) { cleanupAndRethrowIllegalState(); } @@ -899,7 +1054,7 @@ public final class MediaCas implements AutoCloseable { @Override public void onValues(int status, ArrayList<Byte> sessionId) { mStatus = status; - mSession = createFromSessionId(sessionId); + mSession = createFromSessionId(toBytes(sessionId)); } } @@ -912,7 +1067,7 @@ public final class MediaCas implements AutoCloseable { @Override public void onValues(int status, ArrayList<Byte> sessionId) { mStatus = status; - mSession = createFromSessionId(sessionId); + mSession = createFromSessionId(toBytes(sessionId)); } } @@ -971,15 +1126,19 @@ public final class MediaCas implements AutoCloseable { int sessionResourceHandle = getSessionResourceHandle(); try { - OpenSessionCallback cb = new OpenSessionCallback(); - mICas.openSession(cb); - MediaCasException.throwExceptionIfNeeded(cb.mStatus); - addSessionToResourceMap(cb.mSession, sessionResourceHandle); - Log.d(TAG, "Write Stats Log for succeed to Open Session."); - FrameworkStatsLog - .write(FrameworkStatsLog.TV_CAS_SESSION_OPEN_STATUS, mUserId, mCasSystemId, + if (mICasHidl != null) { + OpenSessionCallback cb = new OpenSessionCallback(); + mICasHidl.openSession(cb); + MediaCasException.throwExceptionIfNeeded(cb.mStatus); + addSessionToResourceMap(cb.mSession, sessionResourceHandle); + Log.d(TAG, "Write Stats Log for succeed to Open Session."); + FrameworkStatsLog.write( + FrameworkStatsLog.TV_CAS_SESSION_OPEN_STATUS, + mUserId, + mCasSystemId, FrameworkStatsLog.TV_CAS_SESSION_OPEN_STATUS__STATE__SUCCEEDED); - return cb.mSession; + return cb.mSession; + } } catch (RemoteException e) { cleanupAndRethrowIllegalState(); } @@ -1012,14 +1171,30 @@ public final class MediaCas implements AutoCloseable { throws MediaCasException { int sessionResourceHandle = getSessionResourceHandle(); - if (mICasV12 == null) { + if (mICas != null) { + try { + byte[] sessionId = mICas.openSession(sessionUsage, scramblingMode); + Session session = createFromSessionId(sessionId); + addSessionToResourceMap(session, sessionResourceHandle); + Log.d(TAG, "Write Stats Log for succeed to Open Session."); + FrameworkStatsLog.write( + FrameworkStatsLog.TV_CAS_SESSION_OPEN_STATUS, + mUserId, + mCasSystemId, + FrameworkStatsLog.TV_CAS_SESSION_OPEN_STATUS__STATE__SUCCEEDED); + return session; + } catch (RemoteException e) { + cleanupAndRethrowIllegalState(); + } + } + if (mICasHidl12 == null) { Log.d(TAG, "Open Session with scrambling mode is only supported by cas@1.2+ interface"); throw new UnsupportedCasException("Open Session with scrambling mode is not supported"); } try { OpenSession_1_2_Callback cb = new OpenSession_1_2_Callback(); - mICasV12.openSession_1_2(sessionUsage, scramblingMode, cb); + mICasHidl12.openSession_1_2(sessionUsage, scramblingMode, cb); MediaCasException.throwExceptionIfNeeded(cb.mStatus); addSessionToResourceMap(cb.mSession, sessionResourceHandle); Log.d(TAG, "Write Stats Log for succeed to Open Session."); @@ -1053,8 +1228,12 @@ public final class MediaCas implements AutoCloseable { validateInternalStates(); try { - MediaCasException.throwExceptionIfNeeded( - mICas.processEmm(toByteArray(data, offset, length))); + if (mICas != null) { + mICas.processEmm(Arrays.copyOfRange(data, offset, length)); + } else { + MediaCasException.throwExceptionIfNeeded( + mICasHidl.processEmm(toByteArray(data, offset, length))); + } } catch (RemoteException e) { cleanupAndRethrowIllegalState(); } @@ -1092,8 +1271,12 @@ public final class MediaCas implements AutoCloseable { validateInternalStates(); try { - MediaCasException.throwExceptionIfNeeded( - mICas.sendEvent(event, arg, toByteArray(data))); + if (mICas != null) { + mICas.sendEvent(event, arg, data); + } else { + MediaCasException.throwExceptionIfNeeded( + mICasHidl.sendEvent(event, arg, toByteArray(data))); + } } catch (RemoteException e) { cleanupAndRethrowIllegalState(); } @@ -1114,8 +1297,11 @@ public final class MediaCas implements AutoCloseable { validateInternalStates(); try { - MediaCasException.throwExceptionIfNeeded( - mICas.provision(provisionString)); + if (mICas != null) { + mICas.provision(provisionString); + } else { + MediaCasException.throwExceptionIfNeeded(mICasHidl.provision(provisionString)); + } } catch (RemoteException e) { cleanupAndRethrowIllegalState(); } @@ -1136,8 +1322,12 @@ public final class MediaCas implements AutoCloseable { validateInternalStates(); try { - MediaCasException.throwExceptionIfNeeded( - mICas.refreshEntitlements(refreshType, toByteArray(refreshData))); + if (mICas != null) { + mICas.refreshEntitlements(refreshType, refreshData); + } else { + MediaCasException.throwExceptionIfNeeded( + mICasHidl.refreshEntitlements(refreshType, toByteArray(refreshData))); + } } catch (RemoteException e) { cleanupAndRethrowIllegalState(); } @@ -1163,6 +1353,13 @@ public final class MediaCas implements AutoCloseable { } finally { mICas = null; } + } else if (mICasHidl != null) { + try { + mICasHidl.release(); + } catch (RemoteException e) { + } finally { + mICasHidl = mICasHidl11 = mICasHidl12 = null; + } } if (mTunerResourceManager != null) { diff --git a/media/java/android/media/MediaDescrambler.java b/media/java/android/media/MediaDescrambler.java index 99bd2549cbc7..b4bdf93db3ab 100644 --- a/media/java/android/media/MediaDescrambler.java +++ b/media/java/android/media/MediaDescrambler.java @@ -17,14 +17,26 @@ package android.media; import android.annotation.NonNull; -import android.hardware.cas.V1_0.*; +import android.hardware.cas.DestinationBuffer; +import android.hardware.cas.IDescrambler; +import android.hardware.cas.ScramblingControl; +import android.hardware.cas.SharedBuffer; +import android.hardware.cas.SubSample; +import android.hardware.cas.V1_0.IDescramblerBase; +import android.hardware.common.Ashmem; +import android.hardware.common.NativeHandle; import android.media.MediaCasException.UnsupportedCasException; import android.os.IHwBinder; +import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.os.ServiceSpecificException; +import android.os.SharedMemory; +import android.system.ErrnoException; import android.util.Log; +import java.io.IOException; import java.nio.ByteBuffer; +import java.util.ArrayList; /** * MediaDescrambler class can be used in conjunction with {@link android.media.MediaCodec} @@ -39,7 +51,198 @@ import java.nio.ByteBuffer; */ public final class MediaDescrambler implements AutoCloseable { private static final String TAG = "MediaDescrambler"; - private IDescramblerBase mIDescrambler; + private DescramblerWrapper mIDescrambler; + + private interface DescramblerWrapper { + + IHwBinder asBinder(); + + int descramble( + @NonNull ByteBuffer srcBuf, + @NonNull ByteBuffer dstBuf, + @NonNull MediaCodec.CryptoInfo cryptoInfo) + throws RemoteException; + + boolean requiresSecureDecoderComponent(@NonNull String mime) throws RemoteException; + + void setMediaCasSession(byte[] sessionId) throws RemoteException; + + void release() throws RemoteException; + } + ; + + private long getSubsampleInfo( + int numSubSamples, + int[] numBytesOfClearData, + int[] numBytesOfEncryptedData, + SubSample[] subSamples) { + long totalSize = 0; + + for (int i = 0; i < numSubSamples; i++) { + totalSize += numBytesOfClearData[i]; + subSamples[i].numBytesOfClearData = numBytesOfClearData[i]; + totalSize += numBytesOfEncryptedData[i]; + subSamples[i].numBytesOfEncryptedData = numBytesOfEncryptedData[i]; + } + return totalSize; + } + + private ParcelFileDescriptor createSharedMemory(ByteBuffer buffer, String name) + throws RemoteException { + byte[] source = buffer.array(); + if (source.length == 0) { + return null; + } + ParcelFileDescriptor fd = null; + try { + SharedMemory ashmem = SharedMemory.create(name == null ? "" : name, source.length); + ByteBuffer ptr = ashmem.mapReadWrite(); + ptr.put(buffer); + ashmem.unmap(ptr); + fd = ashmem.getFdDup(); + return fd; + } catch (ErrnoException | IOException e) { + throw new RemoteException(e); + } + } + + private class AidlDescrambler implements DescramblerWrapper { + + IDescrambler mAidlDescrambler; + + AidlDescrambler(IDescrambler aidlDescrambler) { + mAidlDescrambler = aidlDescrambler; + } + + @Override + public IHwBinder asBinder() { + return null; + } + + @Override + public int descramble( + @NonNull ByteBuffer src, + @NonNull ByteBuffer dst, + @NonNull MediaCodec.CryptoInfo cryptoInfo) + throws RemoteException { + SubSample[] subSamples = new SubSample[cryptoInfo.numSubSamples]; + long totalLength = + getSubsampleInfo( + cryptoInfo.numSubSamples, + cryptoInfo.numBytesOfClearData, + cryptoInfo.numBytesOfEncryptedData, + subSamples); + SharedBuffer srcBuffer = new SharedBuffer(); + DestinationBuffer dstBuffer; + srcBuffer.heapBase = new Ashmem(); + srcBuffer.heapBase.fd = createSharedMemory(src, "Descrambler Source Buffer"); + srcBuffer.heapBase.size = src.array().length; + if (dst == null) { + dstBuffer = DestinationBuffer.nonsecureMemory(srcBuffer); + } else { + ParcelFileDescriptor pfd = + createSharedMemory(dst, "Descrambler Destination Buffer"); + NativeHandle nh = new NativeHandle(); + nh.fds = new ParcelFileDescriptor[] {pfd}; + nh.ints = new int[] {1}; // Mark 1 since source buffer also uses it? + dstBuffer = DestinationBuffer.secureMemory(nh); + } + @ScramblingControl int control = cryptoInfo.key[0]; + + return mAidlDescrambler.descramble( + (byte) control, + subSamples, + srcBuffer, + src.position(), + dstBuffer, + dst.position()); + } + + @Override + public boolean requiresSecureDecoderComponent(@NonNull String mime) throws RemoteException { + return mAidlDescrambler.requiresSecureDecoderComponent(mime); + } + + @Override + public void setMediaCasSession(byte[] sessionId) throws RemoteException { + mAidlDescrambler.setMediaCasSession(sessionId); + } + + @Override + public void release() throws RemoteException { + mAidlDescrambler.release(); + } + } + + private class HidlDescrambler implements DescramblerWrapper { + + IDescramblerBase mHidlDescrambler; + + HidlDescrambler(IDescramblerBase hidlDescrambler) { + mHidlDescrambler = hidlDescrambler; + native_setup(hidlDescrambler.asBinder()); + } + + @Override + public IHwBinder asBinder() { + return mHidlDescrambler.asBinder(); + } + + @Override + public int descramble( + @NonNull ByteBuffer srcBuf, + @NonNull ByteBuffer dstBuf, + @NonNull MediaCodec.CryptoInfo cryptoInfo) + throws RemoteException { + + try { + return native_descramble( + cryptoInfo.key[0], + cryptoInfo.key[1], + cryptoInfo.numSubSamples, + cryptoInfo.numBytesOfClearData, + cryptoInfo.numBytesOfEncryptedData, + srcBuf, + srcBuf.position(), + srcBuf.limit(), + dstBuf, + dstBuf.position(), + dstBuf.limit()); + } catch (ServiceSpecificException e) { + MediaCasStateException.throwExceptionIfNeeded(e.errorCode, e.getMessage()); + } catch (RemoteException e) { + cleanupAndRethrowIllegalState(); + } + return -1; + } + + @Override + public boolean requiresSecureDecoderComponent(@NonNull String mime) throws RemoteException { + return mHidlDescrambler.requiresSecureDecoderComponent(mime); + } + + @Override + public void setMediaCasSession(byte[] sessionId) throws RemoteException { + ArrayList<Byte> byteArray = new ArrayList<>(); + + if (sessionId != null) { + int length = sessionId.length; + byteArray = new ArrayList<Byte>(length); + for (int i = 0; i < length; i++) { + byteArray.add(Byte.valueOf(sessionId[i])); + } + } + + MediaCasStateException.throwExceptionIfNeeded( + mHidlDescrambler.setMediaCasSession(byteArray)); + } + + @Override + public void release() throws RemoteException { + mHidlDescrambler.release(); + native_release(); + } + } private final void validateInternalStates() { if (mIDescrambler == null) { @@ -61,7 +264,14 @@ public final class MediaDescrambler implements AutoCloseable { */ public MediaDescrambler(int CA_system_id) throws UnsupportedCasException { try { - mIDescrambler = MediaCas.getService().createDescrambler(CA_system_id); + if (MediaCas.getService() != null) { + mIDescrambler = + new AidlDescrambler(MediaCas.getService().createDescrambler(CA_system_id)); + } else if (MediaCas.getServiceHidl() != null) { + mIDescrambler = + new HidlDescrambler( + MediaCas.getServiceHidl().createDescrambler(CA_system_id)); + } } catch(Exception e) { Log.e(TAG, "Failed to create descrambler: " + e); mIDescrambler = null; @@ -70,7 +280,6 @@ public final class MediaDescrambler implements AutoCloseable { throw new UnsupportedCasException("Unsupported CA_system_id " + CA_system_id); } } - native_setup(mIDescrambler.asBinder()); } IHwBinder getBinder() { @@ -117,8 +326,7 @@ public final class MediaDescrambler implements AutoCloseable { validateInternalStates(); try { - MediaCasStateException.throwExceptionIfNeeded( - mIDescrambler.setMediaCasSession(session.mSessionId)); + mIDescrambler.setMediaCasSession(session.mSessionId); } catch (RemoteException e) { cleanupAndRethrowIllegalState(); } @@ -126,27 +334,31 @@ public final class MediaDescrambler implements AutoCloseable { /** * Scramble control value indicating that the samples are not scrambled. + * * @see #descramble(ByteBuffer, ByteBuffer, android.media.MediaCodec.CryptoInfo) */ - public static final byte SCRAMBLE_CONTROL_UNSCRAMBLED = 0; + public static final byte SCRAMBLE_CONTROL_UNSCRAMBLED = (byte) ScramblingControl.UNSCRAMBLED; /** * Scramble control value reserved and shouldn't be used currently. + * * @see #descramble(ByteBuffer, ByteBuffer, android.media.MediaCodec.CryptoInfo) */ - public static final byte SCRAMBLE_CONTROL_RESERVED = 1; + public static final byte SCRAMBLE_CONTROL_RESERVED = (byte) ScramblingControl.RESERVED; /** * Scramble control value indicating that the even key is used. + * * @see #descramble(ByteBuffer, ByteBuffer, android.media.MediaCodec.CryptoInfo) */ - public static final byte SCRAMBLE_CONTROL_EVEN_KEY = 2; + public static final byte SCRAMBLE_CONTROL_EVEN_KEY = (byte) ScramblingControl.EVENKEY; /** * Scramble control value indicating that the odd key is used. + * * @see #descramble(ByteBuffer, ByteBuffer, android.media.MediaCodec.CryptoInfo) */ - public static final byte SCRAMBLE_CONTROL_ODD_KEY = 3; + public static final byte SCRAMBLE_CONTROL_ODD_KEY = (byte) ScramblingControl.ODDKEY; /** * Scramble flag for a hint indicating that the descrambling request is for @@ -207,14 +419,7 @@ public final class MediaDescrambler implements AutoCloseable { } try { - return native_descramble( - cryptoInfo.key[0], - cryptoInfo.key[1], - cryptoInfo.numSubSamples, - cryptoInfo.numBytesOfClearData, - cryptoInfo.numBytesOfEncryptedData, - srcBuf, srcBuf.position(), srcBuf.limit(), - dstBuf, dstBuf.position(), dstBuf.limit()); + return mIDescrambler.descramble(srcBuf, dstBuf, cryptoInfo); } catch (ServiceSpecificException e) { MediaCasStateException.throwExceptionIfNeeded(e.errorCode, e.getMessage()); } catch (RemoteException e) { @@ -233,7 +438,6 @@ public final class MediaDescrambler implements AutoCloseable { mIDescrambler = null; } } - native_release(); } @Override @@ -256,4 +460,4 @@ public final class MediaDescrambler implements AutoCloseable { } private long mNativeContext; -}
\ No newline at end of file +} diff --git a/media/java/android/media/MediaExtractor.java b/media/java/android/media/MediaExtractor.java index dab188e40c1f..b11a81047bf8 100644 --- a/media/java/android/media/MediaExtractor.java +++ b/media/java/android/media/MediaExtractor.java @@ -36,7 +36,6 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.nio.ByteBuffer; import java.nio.ByteOrder; -import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; @@ -325,14 +324,6 @@ public final class MediaExtractor { } } - private ArrayList<Byte> toByteArray(@NonNull byte[] data) { - ArrayList<Byte> byteArray = new ArrayList<Byte>(data.length); - for (int i = 0; i < data.length; i++) { - byteArray.add(i, Byte.valueOf(data[i])); - } - return byteArray; - } - /** * Retrieves the information about the conditional access system used to scramble * a track. @@ -357,7 +348,7 @@ public final class MediaExtractor { buf.rewind(); final byte[] sessionId = new byte[buf.remaining()]; buf.get(sessionId); - session = mMediaCas.createFromSessionId(toByteArray(sessionId)); + session = mMediaCas.createFromSessionId(sessionId); } return new CasInfo(systemId, session, privateData); } diff --git a/media/tests/AudioPolicyTest/res/values/strings.xml b/media/tests/AudioPolicyTest/res/values/strings.xml index 036592770450..128c3c52aaff 100644 --- a/media/tests/AudioPolicyTest/res/values/strings.xml +++ b/media/tests/AudioPolicyTest/res/values/strings.xml @@ -2,4 +2,7 @@ <resources> <!-- name of the app [CHAR LIMIT=25]--> <string name="app_name">Audio Policy APIs Tests</string> + <string name="capture_duration_key">captureDurationMs</string> + <string name="callback_key">callback</string> + <string name="status_key">result</string> </resources> diff --git a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioPolicyDeathTest.java b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioPolicyDeathTest.java index 841804b02354..48c51af26d3a 100644 --- a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioPolicyDeathTest.java +++ b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioPolicyDeathTest.java @@ -20,6 +20,8 @@ import static androidx.test.core.app.ApplicationProvider.getApplicationContext; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeNoException; +import static org.junit.Assume.assumeTrue; import android.content.BroadcastReceiver; import android.content.Context; @@ -30,6 +32,8 @@ import android.media.AudioAttributes; import android.media.AudioFormat; import android.media.AudioManager; import android.media.AudioTrack; +import android.os.Bundle; +import android.os.RemoteCallback; import android.platform.test.annotations.Presubmit; import android.util.Log; @@ -39,33 +43,62 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + @Presubmit @RunWith(AndroidJUnit4.class) public class AudioPolicyDeathTest { private static final String TAG = "AudioPolicyDeathTest"; private static final int SAMPLE_RATE = 48000; - private static final int PLAYBACK_TIME_MS = 2000; + private static final int PLAYBACK_TIME_MS = 4000; + private static final int RECORD_TIME_MS = 1000; + private static final int ACTIVITY_TIMEOUT_SEC = 5; + private static final int BROADCAST_TIMEOUT_SEC = 10; + private static final int MAX_ATTEMPTS = 5; + private static final int DELAY_BETWEEN_ATTEMPTS_MS = 2000; private static final IntentFilter AUDIO_NOISY_INTENT_FILTER = new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY); private class MyBroadcastReceiver extends BroadcastReceiver { - private boolean mReceived = false; + private CountDownLatch mLatch = new CountDownLatch(1); + @Override public void onReceive(Context context, Intent intent) { if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())) { - synchronized (this) { - mReceived = true; - notify(); - } + mLatch.countDown(); } } - public synchronized boolean received() { - return mReceived; + public void reset() { + mLatch = new CountDownLatch(1); + } + + public boolean waitForBroadcast() { + boolean received = false; + long startTimeMs = System.currentTimeMillis(); + long elapsedTimeMs = 0; + + Log.i(TAG, "waiting for broadcast"); + + while (elapsedTimeMs < BROADCAST_TIMEOUT_SEC && !received) { + try { + received = mLatch.await(BROADCAST_TIMEOUT_SEC, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Log.w(TAG, "wait interrupted"); + } + elapsedTimeMs = System.currentTimeMillis() - startTimeMs; + } + Log.i(TAG, "broadcast " + (received ? "" : "NOT ") + "received"); + return received; } } + private final MyBroadcastReceiver mReceiver = new MyBroadcastReceiver(); private Context mContext; @@ -85,31 +118,55 @@ public class AudioPolicyDeathTest { public void testPolicyClientDeathSendBecomingNoisyIntent() { mContext.registerReceiver(mReceiver, AUDIO_NOISY_INTENT_FILTER); - // Launch process registering a dynamic auido policy and dying after PLAYBACK_TIME_MS/2 ms - Intent intent = new Intent(mContext, AudioPolicyDeathTestActivity.class); - intent.putExtra("captureDurationMs", PLAYBACK_TIME_MS / 2); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - mContext.startActivity(intent); - - AudioTrack track = createAudioTrack(); - track.play(); - synchronized (mReceiver) { - long startTimeMs = System.currentTimeMillis(); - long elapsedTimeMs = 0; - while (elapsedTimeMs < PLAYBACK_TIME_MS && !mReceiver.received()) { + boolean result = false; + for (int numAttempts = 1; numAttempts <= MAX_ATTEMPTS && !result; numAttempts++) { + mReceiver.reset(); + + CompletableFuture<Integer> callbackReturn = new CompletableFuture<>(); + RemoteCallback cb = new RemoteCallback((Bundle res) -> { + callbackReturn.complete( + res.getInt(mContext.getResources().getString(R.string.status_key))); + }); + + // Launch process registering a dynamic auido policy and dying after RECORD_TIME_MS ms + // RECORD_TIME_MS must be shorter than PLAYBACK_TIME_MS + Intent intent = new Intent(mContext, AudioPolicyDeathTestActivity.class); + intent.putExtra(mContext.getResources().getString(R.string.capture_duration_key), + RECORD_TIME_MS); + intent.putExtra(mContext.getResources().getString(R.string.callback_key), cb); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + + mContext.startActivity(intent); + + Integer status = AudioManager.ERROR; + try { + status = callbackReturn.get(ACTIVITY_TIMEOUT_SEC, TimeUnit.SECONDS); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + assumeNoException(e); + } + assumeTrue(status != null && status == AudioManager.SUCCESS); + + Log.i(TAG, "Activity started"); + AudioTrack track = null; + try { + track = createAudioTrack(); + track.play(); + result = mReceiver.waitForBroadcast(); + } finally { + if (track != null) { + track.stop(); + track.release(); + } + } + if (!result) { try { - mReceiver.wait(PLAYBACK_TIME_MS - elapsedTimeMs); + Log.i(TAG, "Retrying after attempt: " + numAttempts); + Thread.sleep(DELAY_BETWEEN_ATTEMPTS_MS); } catch (InterruptedException e) { - Log.w(TAG, "wait interrupted"); } - elapsedTimeMs = System.currentTimeMillis() - startTimeMs; } } - - track.stop(); - track.release(); - - assertTrue(mReceiver.received()); + assertTrue(result); } private AudioTrack createAudioTrack() { diff --git a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioPolicyDeathTestActivity.java b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioPolicyDeathTestActivity.java index 957e719ab71f..ce5f56c9e556 100644 --- a/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioPolicyDeathTestActivity.java +++ b/media/tests/AudioPolicyTest/src/com/android/audiopolicytest/AudioPolicyDeathTestActivity.java @@ -26,6 +26,7 @@ import android.media.audiopolicy.AudioMixingRule; import android.media.audiopolicy.AudioPolicy; import android.os.Bundle; import android.os.Looper; +import android.os.RemoteCallback; import android.util.Log; // This activity will register a dynamic audio policy to intercept media playback and launch @@ -71,19 +72,29 @@ public class AudioPolicyDeathTestActivity extends Activity { mAudioPolicy = audioPolicyBuilder.build(); int result = mAudioManager.registerAudioPolicy(mAudioPolicy); - if (result != AudioManager.SUCCESS) { + if (result == AudioManager.SUCCESS) { + AudioRecord audioRecord = mAudioPolicy.createAudioRecordSink(audioMix); + if (audioRecord != null && audioRecord.getState() != AudioRecord.STATE_UNINITIALIZED) { + int captureDurationMs = getIntent().getIntExtra( + getString(R.string.capture_duration_key), RECORD_TIME_MS); + AudioCapturingThread thread = + new AudioCapturingThread(audioRecord, captureDurationMs); + thread.start(); + } else { + Log.w(TAG, "AudioRecord creation failed"); + result = AudioManager.ERROR_NO_INIT; + } + } else { Log.w(TAG, "registerAudioPolicy failed, status: " + result); - return; - } - AudioRecord audioRecord = mAudioPolicy.createAudioRecordSink(audioMix); - if (audioRecord == null) { - Log.w(TAG, "AudioRecord creation failed"); - return; } - int captureDurationMs = getIntent().getIntExtra("captureDurationMs", RECORD_TIME_MS); - AudioCapturingThread thread = new AudioCapturingThread(audioRecord, captureDurationMs); - thread.start(); + RemoteCallback cb = + (RemoteCallback) getIntent().getExtras().get(getString(R.string.callback_key)); + Bundle res = new Bundle(); + res.putInt(getString(R.string.status_key), result); + Log.i(TAG, "policy " + (result == AudioManager.SUCCESS ? "" : "un") + + "successfully registered"); + cb.sendResult(res); } @Override diff --git a/native/webview/TEST_MAPPING b/native/webview/TEST_MAPPING index bd25200ffc38..c1bc6d720ece 100644 --- a/native/webview/TEST_MAPPING +++ b/native/webview/TEST_MAPPING @@ -9,6 +9,14 @@ ] }, { + "name": "CtsSdkSandboxWebkitTestCases", + "options": [ + { + "exclude-annotation": "androidx.test.filters.FlakyTest" + } + ] + }, + { "name": "CtsHostsideWebViewTests", "options": [ { diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt index aadbbc6deb6c..3e1137df345f 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt @@ -125,7 +125,9 @@ class CreateCredentialViewModel( fun onEntrySelectedFromMoreOptionScreen(activeEntry: ActiveEntry) { uiState = uiState.copy( - currentScreenState = CreateScreenState.MORE_OPTIONS_ROW_INTRO, + currentScreenState = if ( + activeEntry.activeProvider.id == UserConfigRepo.getInstance().getDefaultProviderId() + ) CreateScreenState.CREATION_OPTION_SELECTION else CreateScreenState.MORE_OPTIONS_ROW_INTRO, activeEntry = activeEntry ) } diff --git a/packages/PackageInstaller/res/values/strings.xml b/packages/PackageInstaller/res/values/strings.xml index b713c1420928..cb2baa974b0c 100644 --- a/packages/PackageInstaller/res/values/strings.xml +++ b/packages/PackageInstaller/res/values/strings.xml @@ -37,6 +37,11 @@ <string name="install_confirm_question">Do you want to install this app?</string> <!-- Message for updating an existing app [CHAR LIMIT=NONE] --> <string name="install_confirm_question_update">Do you want to update this app?</string> + <!-- TODO(b/244413073) Revise the description after getting UX input and UXR on this. --> + <!-- Message for updating an existing app when updating owner changed [CHAR LIMIT=NONE] --> + <string name="install_confirm_question_update_owner_changed">Updates to this app are currently managed by <xliff:g id="existing_update_owner">%1$s</xliff:g>.\n\nBy updating, you\'ll get future updates from <xliff:g id="new_update_owner">%2$s</xliff:g> instead.</string> + <!-- Message for updating an existing app with update owner reminder [CHAR LIMIT=NONE] --> + <string name="install_confirm_question_update_owner_reminder">Updates to this app are currently managed by <xliff:g id="existing_update_owner">%1$s</xliff:g>.\n\nDo you want to install this update from <xliff:g id="new_update_owner">%2$s</xliff:g>.</string> <!-- [CHAR LIMIT=100] --> <string name="install_failed">App not installed.</string> <!-- Reason displayed when installation fails because the package was blocked diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java index 313815839f53..49c9188a2cab 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java @@ -33,10 +33,12 @@ import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.ApplicationInfo; +import android.content.pm.InstallSourceInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageInstaller; import android.content.pm.PackageInstaller.SessionInfo; import android.content.pm.PackageManager; +import android.content.pm.PackageManager.ApplicationInfoFlags; import android.content.pm.PackageManager.NameNotFoundException; import android.graphics.drawable.BitmapDrawable; import android.net.Uri; @@ -47,9 +49,11 @@ import android.os.Process; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; +import android.text.TextUtils; import android.util.Log; import android.view.View; import android.widget.Button; +import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.StringRes; @@ -88,6 +92,7 @@ public class PackageInstallerActivity extends AlertActivity { private int mOriginatingUid = Process.INVALID_UID; private String mOriginatingPackage; // The package name corresponding to #mOriginatingUid private int mActivityResultCode = Activity.RESULT_CANCELED; + private int mPendingUserActionReason = -1; private final boolean mLocalLOGV = false; PackageManager mPm; @@ -132,10 +137,27 @@ public class PackageInstallerActivity extends AlertActivity { private boolean mEnableOk = false; private void startInstallConfirm() { - View viewToEnable; + TextView viewToEnable; if (mAppInfo != null) { viewToEnable = requireViewById(R.id.install_confirm_question_update); + + final CharSequence existingUpdateOwnerLabel = getExistingUpdateOwnerLabel(); + final CharSequence requestedUpdateOwnerLabel = getApplicationLabel(mCallingPackage); + if (!TextUtils.isEmpty(existingUpdateOwnerLabel)) { + if (mPendingUserActionReason == PackageInstaller.REASON_OWNERSHIP_CHANGED) { + viewToEnable.setText( + getString(R.string.install_confirm_question_update_owner_changed, + existingUpdateOwnerLabel, requestedUpdateOwnerLabel)); + } else if (mPendingUserActionReason == PackageInstaller.REASON_REMIND_OWNERSHIP + || mPendingUserActionReason + == PackageInstaller.REASON_CONFIRM_PACKAGE_CHANGE) { + viewToEnable.setText( + getString(R.string.install_confirm_question_update_owner_reminder, + existingUpdateOwnerLabel, requestedUpdateOwnerLabel)); + } + } + mOk.setText(R.string.update); } else { // This is a new application with no permissions. @@ -149,6 +171,27 @@ public class PackageInstallerActivity extends AlertActivity { mOk.setFilterTouchesWhenObscured(true); } + private CharSequence getExistingUpdateOwnerLabel() { + try { + final String packageName = mPkgInfo.packageName; + final InstallSourceInfo sourceInfo = mPm.getInstallSourceInfo(packageName); + final String existingUpdateOwner = sourceInfo.getUpdateOwnerPackageName(); + return getApplicationLabel(existingUpdateOwner); + } catch (NameNotFoundException e) { + return null; + } + } + + private CharSequence getApplicationLabel(String packageName) { + try { + final ApplicationInfo appInfo = mPm.getApplicationInfo(packageName, + ApplicationInfoFlags.of(0)); + return mPm.getApplicationLabel(appInfo); + } catch (NameNotFoundException e) { + return null; + } + } + /** * Replace any dialog shown by the dialog with the one for the given {@link #createDialog(int)}. * @@ -344,6 +387,7 @@ public class PackageInstallerActivity extends AlertActivity { packageSource = Uri.fromFile(new File(resolvedBaseCodePath)); mOriginatingURI = null; mReferrerURI = null; + mPendingUserActionReason = info.getPendingUserActionReason(); } else if (PackageInstaller.ACTION_CONFIRM_PRE_APPROVAL.equals(action)) { final int sessionId = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1 /* defaultValue */); @@ -358,11 +402,13 @@ public class PackageInstallerActivity extends AlertActivity { packageSource = info; mOriginatingURI = null; mReferrerURI = null; + mPendingUserActionReason = info.getPendingUserActionReason(); } else { mSessionId = -1; packageSource = intent.getData(); mOriginatingURI = intent.getParcelableExtra(Intent.EXTRA_ORIGINATING_URI); mReferrerURI = intent.getParcelableExtra(Intent.EXTRA_REFERRER); + mPendingUserActionReason = PackageInstaller.REASON_CONFIRM_PACKAGE_CHANGE; } // if there's nothing to do, quietly slip into the ether diff --git a/packages/SettingsLib/Spa/spa/build.gradle b/packages/SettingsLib/Spa/spa/build.gradle index 73c109994025..bf4ad758e465 100644 --- a/packages/SettingsLib/Spa/spa/build.gradle +++ b/packages/SettingsLib/Spa/spa/build.gradle @@ -88,6 +88,7 @@ dependencies { implementation "com.airbnb.android:lottie-compose:5.2.0" androidTestImplementation project(":testutils") + androidTestImplementation 'androidx.lifecycle:lifecycle-runtime-testing' androidTestImplementation "com.linkedin.dexmaker:dexmaker-mockito:2.28.1" } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/LifecycleEffect.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/LifecycleEffect.kt new file mode 100644 index 000000000000..e91fa65401a4 --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/LifecycleEffect.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spa.framework.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver + +@Composable +fun LifecycleEffect( + onStart: () -> Unit = {}, + onStop: () -> Unit = {}, +) { + val lifecycleOwner = LocalLifecycleOwner.current + DisposableEffect(lifecycleOwner) { + val observer = LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_START) { + onStart() + } else if (event == Lifecycle.Event.ON_STOP) { + onStop() + } + } + + lifecycleOwner.lifecycle.addObserver(observer) + + onDispose { + lifecycleOwner.lifecycle.removeObserver(observer) + } + } +}
\ No newline at end of file diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/PageLogger.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/PageLogger.kt index 271443e86dac..73eae07a4ba9 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/PageLogger.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/util/PageLogger.kt @@ -18,55 +18,41 @@ package com.android.settingslib.spa.framework.util import android.os.Bundle import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.remember -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.core.os.bundleOf -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleEventObserver import com.android.settingslib.spa.framework.common.LOG_DATA_DISPLAY_NAME import com.android.settingslib.spa.framework.common.LOG_DATA_SESSION_NAME import com.android.settingslib.spa.framework.common.LogCategory import com.android.settingslib.spa.framework.common.LogEvent +import com.android.settingslib.spa.framework.common.SettingsPage import com.android.settingslib.spa.framework.common.SettingsPageProvider import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory import com.android.settingslib.spa.framework.common.createSettingsPage +import com.android.settingslib.spa.framework.compose.LifecycleEffect import com.android.settingslib.spa.framework.compose.LocalNavController +import com.android.settingslib.spa.framework.compose.NavControllerWrapper @Composable internal fun SettingsPageProvider.PageEvent(arguments: Bundle? = null) { val page = remember(arguments) { createSettingsPage(arguments) } - val lifecycleOwner = LocalLifecycleOwner.current val navController = LocalNavController.current - DisposableEffect(lifecycleOwner) { - val observer = LifecycleEventObserver { _, event -> - val logPageEvent: (event: LogEvent) -> Unit = { - SpaEnvironmentFactory.instance.logger.event( - id = page.id, - event = it, - category = LogCategory.FRAMEWORK, - extraData = bundleOf( - LOG_DATA_DISPLAY_NAME to page.displayName, - LOG_DATA_SESSION_NAME to navController.sessionSourceName, - ).apply { - val normArguments = parameter.normalize(arguments) - if (normArguments != null) putAll(normArguments) - } - ) - } - if (event == Lifecycle.Event.ON_START) { - logPageEvent(LogEvent.PAGE_ENTER) - } else if (event == Lifecycle.Event.ON_STOP) { - logPageEvent(LogEvent.PAGE_LEAVE) - } - } - - // Add the observer to the lifecycle - lifecycleOwner.lifecycle.addObserver(observer) + LifecycleEffect( + onStart = { page.logPageEvent(LogEvent.PAGE_ENTER, navController) }, + onStop = { page.logPageEvent(LogEvent.PAGE_LEAVE, navController) }, + ) +} - // When the effect leaves the Composition, remove the observer - onDispose { - lifecycleOwner.lifecycle.removeObserver(observer) +private fun SettingsPage.logPageEvent(event: LogEvent, navController: NavControllerWrapper) { + SpaEnvironmentFactory.instance.logger.event( + id = id, + event = event, + category = LogCategory.FRAMEWORK, + extraData = bundleOf( + LOG_DATA_DISPLAY_NAME to displayName, + LOG_DATA_SESSION_NAME to navController.sessionSourceName, + ).apply { + val normArguments = parameter.normalize(arguments) + if (normArguments != null) putAll(normArguments) } - } -} + ) +}
\ No newline at end of file diff --git a/packages/SettingsLib/Spa/tests/Android.bp b/packages/SettingsLib/Spa/tests/Android.bp index f9e64aee1513..b4c67ccda6f2 100644 --- a/packages/SettingsLib/Spa/tests/Android.bp +++ b/packages/SettingsLib/Spa/tests/Android.bp @@ -31,6 +31,7 @@ android_test { "SpaLib", "SpaLibTestUtils", "androidx.compose.runtime_runtime", + "androidx.lifecycle_lifecycle-runtime-testing", "androidx.test.ext.junit", "androidx.test.runner", "mockito-target-minus-junit4", diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/compose/LifecycleEffectTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/compose/LifecycleEffectTest.kt new file mode 100644 index 000000000000..fe7baff43101 --- /dev/null +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/compose/LifecycleEffectTest.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spa.framework.compose + +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.testing.TestLifecycleOwner +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class LifecycleEffectTest { + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun onStart_isCalled() { + var onStartIsCalled = false + composeTestRule.setContent { + LifecycleEffect(onStart = { onStartIsCalled = true }) + } + + assertThat(onStartIsCalled).isTrue() + } + + @Test + fun onStop_isCalled() { + var onStopIsCalled = false + val testLifecycleOwner = TestLifecycleOwner() + + composeTestRule.setContent { + CompositionLocalProvider(LocalLifecycleOwner provides testLifecycleOwner) { + LifecycleEffect(onStop = { onStopIsCalled = true }) + } + LaunchedEffect(Unit) { + testLifecycleOwner.currentState = Lifecycle.State.CREATED + } + } + + assertThat(onStopIsCalled).isTrue() + } +}
\ No newline at end of file diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUser.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUser.kt index b2ea4a084e48..a2fb101e4e4c 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUser.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUser.kt @@ -22,11 +22,9 @@ import android.content.Intent import android.content.IntentFilter import android.os.UserHandle import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLifecycleOwner -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleEventObserver +import com.android.settingslib.spa.framework.compose.LifecycleEffect /** * A [BroadcastReceiver] which registered when on start and unregistered when on stop. @@ -39,28 +37,22 @@ fun DisposableBroadcastReceiverAsUser( onReceive: (Intent) -> Unit, ) { val context = LocalContext.current - val lifecycleOwner = LocalLifecycleOwner.current - DisposableEffect(lifecycleOwner) { - val broadcastReceiver = object : BroadcastReceiver() { + val broadcastReceiver = remember { + object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { onReceive(intent) } } - val observer = LifecycleEventObserver { _, event -> - if (event == Lifecycle.Event.ON_START) { - context.registerReceiverAsUser( - broadcastReceiver, userHandle, intentFilter, null, null - ) - onStart() - } else if (event == Lifecycle.Event.ON_STOP) { - context.unregisterReceiver(broadcastReceiver) - } - } - - lifecycleOwner.lifecycle.addObserver(observer) - - onDispose { - lifecycleOwner.lifecycle.removeObserver(observer) - } } + LifecycleEffect( + onStart = { + context.registerReceiverAsUser( + broadcastReceiver, userHandle, intentFilter, null, null + ) + onStart() + }, + onStop = { + context.unregisterReceiver(broadcastReceiver) + }, + ) } diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index d56300e6781a..d716b327f94d 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -170,6 +170,7 @@ <uses-permission android:name="android.permission.SET_ORIENTATION" /> <uses-permission android:name="android.permission.INSTALL_PACKAGES" /> <uses-permission android:name="android.permission.INSTALL_PACKAGE_UPDATES" /> + <uses-permission android:name="android.permission.ENFORCE_UPDATE_OWNERSHIP" /> <uses-permission android:name="android.permission.INSTALL_DPC_PACKAGES" /> <uses-permission android:name="com.android.permission.USE_INSTALLER_V2" /> <uses-permission android:name="android.permission.INSTALL_TEST_ONLY_PACKAGE" /> diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt index 462b90a10aee..86bd5f2bff5a 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt @@ -54,7 +54,6 @@ class AnimatableClockView @JvmOverloads constructor( defStyleAttr: Int = 0, defStyleRes: Int = 0 ) : TextView(context, attrs, defStyleAttr, defStyleRes) { - var tag: String = "UnnamedClockView" var logBuffer: LogBuffer? = null private val time = Calendar.getInstance() @@ -132,7 +131,7 @@ class AnimatableClockView @JvmOverloads constructor( override fun onAttachedToWindow() { super.onAttachedToWindow() - logBuffer?.log(tag, DEBUG, "onAttachedToWindow") + logBuffer?.log(TAG, DEBUG, "onAttachedToWindow") refreshFormat() } @@ -148,7 +147,7 @@ class AnimatableClockView @JvmOverloads constructor( time.timeInMillis = timeOverrideInMillis ?: System.currentTimeMillis() contentDescription = DateFormat.format(descFormat, time) val formattedText = DateFormat.format(format, time) - logBuffer?.log(tag, DEBUG, + logBuffer?.log(TAG, DEBUG, { str1 = formattedText?.toString() }, { "refreshTime: new formattedText=$str1" } ) @@ -157,7 +156,7 @@ class AnimatableClockView @JvmOverloads constructor( // relayout if the text didn't actually change. if (!TextUtils.equals(text, formattedText)) { text = formattedText - logBuffer?.log(tag, DEBUG, + logBuffer?.log(TAG, DEBUG, { str1 = formattedText?.toString() }, { "refreshTime: done setting new time text to: $str1" } ) @@ -167,17 +166,17 @@ class AnimatableClockView @JvmOverloads constructor( // without being notified TextInterpolator being notified. if (layout != null) { textAnimator?.updateLayout(layout) - logBuffer?.log(tag, DEBUG, "refreshTime: done updating textAnimator layout") + logBuffer?.log(TAG, DEBUG, "refreshTime: done updating textAnimator layout") } requestLayout() - logBuffer?.log(tag, DEBUG, "refreshTime: after requestLayout") + logBuffer?.log(TAG, DEBUG, "refreshTime: after requestLayout") } } fun onTimeZoneChanged(timeZone: TimeZone?) { time.timeZone = timeZone refreshFormat() - logBuffer?.log(tag, DEBUG, + logBuffer?.log(TAG, DEBUG, { str1 = timeZone?.toString() }, { "onTimeZoneChanged newTimeZone=$str1" } ) @@ -194,7 +193,7 @@ class AnimatableClockView @JvmOverloads constructor( } else { animator.updateLayout(layout) } - logBuffer?.log(tag, DEBUG, "onMeasure") + logBuffer?.log(TAG, DEBUG, "onMeasure") } override fun onDraw(canvas: Canvas) { @@ -206,12 +205,12 @@ class AnimatableClockView @JvmOverloads constructor( } else { super.onDraw(canvas) } - logBuffer?.log(tag, DEBUG, "onDraw lastDraw") + logBuffer?.log(TAG, DEBUG, "onDraw") } override fun invalidate() { super.invalidate() - logBuffer?.log(tag, DEBUG, "invalidate") + logBuffer?.log(TAG, DEBUG, "invalidate") } override fun onTextChanged( @@ -221,7 +220,7 @@ class AnimatableClockView @JvmOverloads constructor( lengthAfter: Int ) { super.onTextChanged(text, start, lengthBefore, lengthAfter) - logBuffer?.log(tag, DEBUG, + logBuffer?.log(TAG, DEBUG, { str1 = text.toString() }, { "onTextChanged text=$str1" } ) @@ -238,7 +237,7 @@ class AnimatableClockView @JvmOverloads constructor( } fun animateColorChange() { - logBuffer?.log(tag, DEBUG, "animateColorChange") + logBuffer?.log(TAG, DEBUG, "animateColorChange") setTextStyle( weight = lockScreenWeight, textSize = -1f, @@ -260,7 +259,7 @@ class AnimatableClockView @JvmOverloads constructor( } fun animateAppearOnLockscreen() { - logBuffer?.log(tag, DEBUG, "animateAppearOnLockscreen") + logBuffer?.log(TAG, DEBUG, "animateAppearOnLockscreen") setTextStyle( weight = dozingWeight, textSize = -1f, @@ -285,7 +284,7 @@ class AnimatableClockView @JvmOverloads constructor( if (isAnimationEnabled && textAnimator == null) { return } - logBuffer?.log(tag, DEBUG, "animateFoldAppear") + logBuffer?.log(TAG, DEBUG, "animateFoldAppear") setTextStyle( weight = lockScreenWeightInternal, textSize = -1f, @@ -312,7 +311,7 @@ class AnimatableClockView @JvmOverloads constructor( // Skip charge animation if dozing animation is already playing. return } - logBuffer?.log(tag, DEBUG, "animateCharge") + logBuffer?.log(TAG, DEBUG, "animateCharge") val startAnimPhase2 = Runnable { setTextStyle( weight = if (isDozing()) dozingWeight else lockScreenWeight, @@ -336,7 +335,7 @@ class AnimatableClockView @JvmOverloads constructor( } fun animateDoze(isDozing: Boolean, animate: Boolean) { - logBuffer?.log(tag, DEBUG, "animateDoze") + logBuffer?.log(TAG, DEBUG, "animateDoze") setTextStyle( weight = if (isDozing) dozingWeight else lockScreenWeight, textSize = -1f, @@ -455,7 +454,7 @@ class AnimatableClockView @JvmOverloads constructor( isSingleLineInternal && !use24HourFormat -> Patterns.sClockView12 else -> DOUBLE_LINE_FORMAT_12_HOUR } - logBuffer?.log(tag, DEBUG, + logBuffer?.log(TAG, DEBUG, { str1 = format?.toString() }, { "refreshFormat format=$str1" } ) @@ -466,6 +465,7 @@ class AnimatableClockView @JvmOverloads constructor( fun dump(pw: PrintWriter) { pw.println("$this") + pw.println(" alpha=$alpha") pw.println(" measuredWidth=$measuredWidth") pw.println(" measuredHeight=$measuredHeight") pw.println(" singleLineInternal=$isSingleLineInternal") @@ -626,7 +626,7 @@ class AnimatableClockView @JvmOverloads constructor( } companion object { - private val TAG = AnimatableClockView::class.simpleName + private val TAG = AnimatableClockView::class.simpleName!! const val ANIMATION_DURATION_FOLD_TO_AOD: Int = 600 private const val DOUBLE_LINE_FORMAT_12_HOUR = "hh\nmm" private const val DOUBLE_LINE_FORMAT_24_HOUR = "HH\nmm" diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt index e138ef8a1ea8..7645decfde24 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt @@ -88,13 +88,6 @@ class DefaultClockController( events.onTimeTick() } - override fun setLogBuffer(logBuffer: LogBuffer) { - smallClock.view.tag = "smallClockView" - largeClock.view.tag = "largeClockView" - smallClock.view.logBuffer = logBuffer - largeClock.view.logBuffer = logBuffer - } - open inner class DefaultClockFaceController( override val view: AnimatableClockView, ) : ClockFaceController { @@ -104,6 +97,12 @@ class DefaultClockController( private var isRegionDark = false protected var targetRegion: Rect? = null + override var logBuffer: LogBuffer? + get() = view.logBuffer + set(value) { + view.logBuffer = value + } + init { view.setColors(currentColor, currentColor) } diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt index 66e44b9005de..a2a07095c16c 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt @@ -71,9 +71,6 @@ interface ClockController { /** Optional method for dumping debug information */ fun dump(pw: PrintWriter) {} - - /** Optional method for debug logging */ - fun setLogBuffer(logBuffer: LogBuffer) {} } /** Interface for a specific clock face version rendered by the clock */ @@ -83,6 +80,9 @@ interface ClockFaceController { /** Events specific to this clock face */ val events: ClockFaceEvents + + /** Some clocks may log debug information */ + var logBuffer: LogBuffer? } /** Events that should call when various rendering parameters change */ diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogBuffer.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogBuffer.kt index 6436dcb5f613..e99b2149bc1d 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogBuffer.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/log/LogBuffer.kt @@ -159,8 +159,13 @@ constructor( * bug report more actionable, so using the [log] with a messagePrinter to add more detail to * every log may do more to improve overall logging than adding more logs with this method. */ - fun log(tag: String, level: LogLevel, @CompileTimeConstant message: String) = - log(tag, level, { str1 = message }, { str1!! }) + @JvmOverloads + fun log( + tag: String, + level: LogLevel, + @CompileTimeConstant message: String, + exception: Throwable? = null, + ) = log(tag, level, { str1 = message }, { str1!! }, exception) /** * You should call [log] instead of this method. diff --git a/packages/SystemUI/res/drawable/ic_media_explicit_indicator.xml b/packages/SystemUI/res/drawable/ic_media_explicit_indicator.xml new file mode 100644 index 000000000000..08c5aaf56bf7 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_media_explicit_indicator.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2022 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="13dp" + android:height="13dp" + android:viewportWidth="48" + android:viewportHeight="48" + android:tint="?attr/colorControlNormal"> + <path + android:fillColor="@android:color/white" + android:pathData="M18.3,34H29.65V31H21.3V25.7H29.65V22.7H21.3V17.35H29.65V14.35H18.3ZM9,42Q7.8,42 6.9,41.1Q6,40.2 6,39V9Q6,7.8 6.9,6.9Q7.8,6 9,6H39Q40.2,6 41.1,6.9Q42,7.8 42,9V39Q42,40.2 41.1,41.1Q40.2,42 39,42ZM9,39H39Q39,39 39,39Q39,39 39,39V9Q39,9 39,9Q39,9 39,9H9Q9,9 9,9Q9,9 9,9V39Q9,39 9,39Q9,39 9,39ZM9,9Q9,9 9,9Q9,9 9,9V39Q9,39 9,39Q9,39 9,39Q9,39 9,39Q9,39 9,39V9Q9,9 9,9Q9,9 9,9Z"/> +</vector> diff --git a/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml b/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml index 9add32c6ee0a..885e5e2d4441 100644 --- a/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml +++ b/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml @@ -57,6 +57,7 @@ android:layout_width="@dimen/dream_overlay_status_bar_icon_size" android:layout_height="match_parent" android:layout_marginStart="@dimen/dream_overlay_status_icon_margin" + android:layout_marginTop="@dimen/dream_overlay_status_bar_marginTop" android:src="@drawable/ic_alarm" android:tint="@android:color/white" android:visibility="gone" @@ -67,6 +68,7 @@ android:layout_width="@dimen/dream_overlay_status_bar_icon_size" android:layout_height="match_parent" android:layout_marginStart="@dimen/dream_overlay_status_icon_margin" + android:layout_marginTop="@dimen/dream_overlay_status_bar_marginTop" android:src="@drawable/ic_qs_dnd_on" android:tint="@android:color/white" android:visibility="gone" @@ -77,6 +79,7 @@ android:layout_width="@dimen/dream_overlay_status_bar_icon_size" android:layout_height="match_parent" android:layout_marginStart="@dimen/dream_overlay_status_icon_margin" + android:layout_marginTop="@dimen/dream_overlay_status_bar_marginTop" android:src="@drawable/ic_signal_wifi_off" android:visibility="gone" android:contentDescription="@string/dream_overlay_status_bar_wifi_off" /> diff --git a/packages/SystemUI/res/layout/media_session_view.xml b/packages/SystemUI/res/layout/media_session_view.xml index 95aefab328df..abc83379950a 100644 --- a/packages/SystemUI/res/layout/media_session_view.xml +++ b/packages/SystemUI/res/layout/media_session_view.xml @@ -147,6 +147,14 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" /> + <!-- Explicit Indicator --> + <com.android.internal.widget.CachingIconView + android:id="@+id/media_explicit_indicator" + android:layout_width="@dimen/qs_media_explicit_indicator_icon_size" + android:layout_height="@dimen/qs_media_explicit_indicator_icon_size" + android:src="@drawable/ic_media_explicit_indicator" + /> + <!-- Artist name --> <TextView android:id="@+id/header_artist" diff --git a/packages/SystemUI/res/layout/user_switcher_fullscreen.xml b/packages/SystemUI/res/layout/user_switcher_fullscreen.xml index fa9d7390dcf8..7eaed4356f46 100644 --- a/packages/SystemUI/res/layout/user_switcher_fullscreen.xml +++ b/packages/SystemUI/res/layout/user_switcher_fullscreen.xml @@ -46,7 +46,7 @@ app:layout_constraintEnd_toEndOf="parent" app:flow_horizontalBias="0.5" app:flow_verticalAlign="center" - app:flow_wrapMode="chain" + app:flow_wrapMode="chain2" app:flow_horizontalGap="@dimen/user_switcher_fullscreen_horizontal_gap" app:flow_verticalGap="44dp" app:flow_horizontalStyle="packed"/> diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 7f45e5eb047f..3c2453e4c59c 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -671,6 +671,16 @@ <item>17</item> <!-- WAKE_REASON_BIOMETRIC --> </integer-array> + <!-- Whether to support posture listening for face auth, default is 0(DEVICE_POSTURE_UNKNOWN) + means systemui will try listening on all postures. + 0 : DEVICE_POSTURE_UNKNOWN + 1 : DEVICE_POSTURE_CLOSED + 2 : DEVICE_POSTURE_HALF_OPENED + 3 : DEVICE_POSTURE_OPENED + 4 : DEVICE_POSTURE_FLIPPED + --> + <integer name="config_face_auth_supported_posture">0</integer> + <!-- Whether the communal service should be enabled --> <bool name="config_communalServiceEnabled">false</bool> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index ae7ab9e199e4..59bcb707c79e 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1050,6 +1050,7 @@ <dimen name="qs_media_disabled_seekbar_height">1dp</dimen> <dimen name="qs_media_enabled_seekbar_height">2dp</dimen> <dimen name="qs_media_app_icon_size">24dp</dimen> + <dimen name="qs_media_explicit_indicator_icon_size">13dp</dimen> <dimen name="qs_media_session_enabled_seekbar_vertical_padding">15dp</dimen> <dimen name="qs_media_session_disabled_seekbar_vertical_padding">16dp</dimen> @@ -1646,6 +1647,8 @@ <dimen name="dream_overlay_status_bar_ambient_text_shadow_dx">0.5dp</dimen> <dimen name="dream_overlay_status_bar_ambient_text_shadow_dy">0.5dp</dimen> <dimen name="dream_overlay_status_bar_ambient_text_shadow_radius">2dp</dimen> + <dimen name="dream_overlay_icon_inset_dimen">0dp</dimen> + <dimen name="dream_overlay_status_bar_marginTop">22dp</dimen> <!-- Default device corner radius, used for assist UI --> <dimen name="config_rounded_mask_size">0px</dimen> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 61a6e9d5df19..e4f339af9bcb 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2470,6 +2470,8 @@ <string name="media_transfer_failed">Something went wrong. Try again.</string> <!-- Text to indicate that a media transfer is currently in-progress, aka loading. [CHAR LIMIT=NONE] --> <string name="media_transfer_loading">Loading</string> + <!-- Default name of the device. [CHAR LIMIT=30] --> + <string name="media_ttt_default_device_type">tablet</string> <!-- Error message indicating that a control timed out while waiting for an update [CHAR_LIMIT=30] --> <string name="controls_error_timeout">Inactive, check app</string> @@ -2518,6 +2520,8 @@ <string name="media_output_dialog_volume_percentage"><xliff:g id="percentage" example="10">%1$d</xliff:g>%%</string> <!-- Title for Speakers and Displays group. [CHAR LIMIT=NONE] --> <string name="media_output_group_title_speakers_and_displays">Speakers & Displays</string> + <!-- Title for Suggested Devices group. [CHAR LIMIT=NONE] --> + <string name="media_output_group_title_suggested_device">Suggested Devices</string> <!-- Media Output Broadcast Dialog --> <!-- Title for Broadcast First Notify Dialog [CHAR LIMIT=60] --> @@ -2887,6 +2891,9 @@ <!-- Text for education page content description for unfolded animation. [CHAR_LIMIT=NONE] --> <string name="rear_display_accessibility_unfolded_animation">Foldable device being flipped around</string> - <!-- Title for notification of low stylus battery. [CHAR_LIMIT=NONE] --> - <string name="stylus_battery_low">Stylus battery low</string> + <!-- Title for notification of low stylus battery with percentage. "percentage" is + the value of the battery capacity remaining [CHAR LIMIT=none]--> + <string name="stylus_battery_low_percentage"><xliff:g id="percentage" example="16%">%s</xliff:g> battery remaining</string> + <!-- Subtitle for the notification sent when a stylus battery is low. [CHAR LIMIT=none]--> + <string name="stylus_battery_low_subtitle">Connect your stylus to a charger</string> </resources> diff --git a/packages/SystemUI/res/xml/media_session_collapsed.xml b/packages/SystemUI/res/xml/media_session_collapsed.xml index 1eb621e0368b..d9c81af54a12 100644 --- a/packages/SystemUI/res/xml/media_session_collapsed.xml +++ b/packages/SystemUI/res/xml/media_session_collapsed.xml @@ -66,6 +66,21 @@ app:layout_constraintTop_toBottomOf="@id/icon" app:layout_constraintStart_toStartOf="parent" app:layout_constraintHorizontal_bias="0" /> + + <Constraint + android:id="@+id/media_explicit_indicator" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="@dimen/qs_media_info_spacing" + android:layout_marginBottom="@dimen/qs_media_padding" + android:layout_marginTop="0dp" + app:layout_constraintStart_toStartOf="@id/header_title" + app:layout_constraintEnd_toStartOf="@id/header_artist" + app:layout_constraintTop_toTopOf="@id/header_artist" + app:layout_constraintBottom_toTopOf="@id/media_action_barrier_top" + app:layout_constraintHorizontal_bias="0" + app:layout_constraintHorizontal_chainStyle="packed" /> + <Constraint android:id="@+id/header_artist" android:layout_width="wrap_content" @@ -75,9 +90,8 @@ app:layout_constraintEnd_toStartOf="@id/action_button_guideline" app:layout_constrainedWidth="true" app:layout_constraintTop_toBottomOf="@id/header_title" - app:layout_constraintStart_toStartOf="@id/header_title" - app:layout_constraintVertical_bias="0" - app:layout_constraintHorizontal_bias="0" /> + app:layout_constraintStart_toEndOf="@id/media_explicit_indicator" + app:layout_constraintVertical_bias="0" /> <Constraint android:id="@+id/actionPlayPause" diff --git a/packages/SystemUI/res/xml/media_session_expanded.xml b/packages/SystemUI/res/xml/media_session_expanded.xml index 7de0a5e0e8c4..0cdc0f9505bc 100644 --- a/packages/SystemUI/res/xml/media_session_expanded.xml +++ b/packages/SystemUI/res/xml/media_session_expanded.xml @@ -58,6 +58,21 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintBottom_toTopOf="@id/header_artist" app:layout_constraintHorizontal_bias="0" /> + + <Constraint + android:id="@+id/media_explicit_indicator" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="@dimen/qs_media_info_spacing" + android:layout_marginBottom="@dimen/qs_media_padding" + android:layout_marginTop="0dp" + app:layout_constraintStart_toStartOf="@id/header_title" + app:layout_constraintEnd_toStartOf="@id/header_artist" + app:layout_constraintTop_toTopOf="@id/header_artist" + app:layout_constraintBottom_toTopOf="@id/media_action_barrier_top" + app:layout_constraintHorizontal_bias="0" + app:layout_constraintHorizontal_chainStyle="packed"/> + <Constraint android:id="@+id/header_artist" android:layout_width="wrap_content" @@ -67,10 +82,9 @@ android:layout_marginTop="0dp" app:layout_constrainedWidth="true" app:layout_constraintEnd_toStartOf="@id/actionPlayPause" - app:layout_constraintStart_toStartOf="@id/header_title" + app:layout_constraintStart_toEndOf="@id/media_explicit_indicator" app:layout_constraintBottom_toTopOf="@id/media_action_barrier_top" - app:layout_constraintVertical_bias="0" - app:layout_constraintHorizontal_bias="0" /> + app:layout_constraintVertical_bias="0" /> <Constraint android:id="@+id/actionPlayPause" diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowTextView.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowTextView.kt index 25d272185bc0..9b73cc3ea9f8 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowTextView.kt +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowTextView.kt @@ -48,48 +48,28 @@ constructor( val drawableInsetSize: Int try { val keyShadowBlur = - attributes.getDimensionPixelSize(R.styleable.DoubleShadowTextView_keyShadowBlur, 0) + attributes.getDimension(R.styleable.DoubleShadowTextView_keyShadowBlur, 0f) val keyShadowOffsetX = - attributes.getDimensionPixelSize( - R.styleable.DoubleShadowTextView_keyShadowOffsetX, - 0 - ) + attributes.getDimension(R.styleable.DoubleShadowTextView_keyShadowOffsetX, 0f) val keyShadowOffsetY = - attributes.getDimensionPixelSize( - R.styleable.DoubleShadowTextView_keyShadowOffsetY, - 0 - ) + attributes.getDimension(R.styleable.DoubleShadowTextView_keyShadowOffsetY, 0f) val keyShadowAlpha = attributes.getFloat(R.styleable.DoubleShadowTextView_keyShadowAlpha, 0f) mKeyShadowInfo = - ShadowInfo( - keyShadowBlur.toFloat(), - keyShadowOffsetX.toFloat(), - keyShadowOffsetY.toFloat(), - keyShadowAlpha - ) + ShadowInfo(keyShadowBlur, keyShadowOffsetX, keyShadowOffsetY, keyShadowAlpha) val ambientShadowBlur = - attributes.getDimensionPixelSize( - R.styleable.DoubleShadowTextView_ambientShadowBlur, - 0 - ) + attributes.getDimension(R.styleable.DoubleShadowTextView_ambientShadowBlur, 0f) val ambientShadowOffsetX = - attributes.getDimensionPixelSize( - R.styleable.DoubleShadowTextView_ambientShadowOffsetX, - 0 - ) + attributes.getDimension(R.styleable.DoubleShadowTextView_ambientShadowOffsetX, 0f) val ambientShadowOffsetY = - attributes.getDimensionPixelSize( - R.styleable.DoubleShadowTextView_ambientShadowOffsetY, - 0 - ) + attributes.getDimension(R.styleable.DoubleShadowTextView_ambientShadowOffsetY, 0f) val ambientShadowAlpha = attributes.getFloat(R.styleable.DoubleShadowTextView_ambientShadowAlpha, 0f) mAmbientShadowInfo = ShadowInfo( - ambientShadowBlur.toFloat(), - ambientShadowOffsetX.toFloat(), - ambientShadowOffsetY.toFloat(), + ambientShadowBlur, + ambientShadowOffsetX, + ambientShadowOffsetY, ambientShadowAlpha ) drawableSize = diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt index 8f38e5800015..a45ce422dca5 100644 --- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt +++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt @@ -38,9 +38,11 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.lifecycle.repeatWhenAttached -import com.android.systemui.log.dagger.KeyguardClockLog +import com.android.systemui.log.dagger.KeyguardSmallClockLog +import com.android.systemui.log.dagger.KeyguardLargeClockLog import com.android.systemui.plugins.ClockController import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel.DEBUG import com.android.systemui.shared.regionsampling.RegionSampler import com.android.systemui.statusbar.policy.BatteryController import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback @@ -73,16 +75,18 @@ open class ClockEventController @Inject constructor( private val context: Context, @Main private val mainExecutor: Executor, @Background private val bgExecutor: Executor, - @KeyguardClockLog private val logBuffer: LogBuffer?, + @KeyguardSmallClockLog private val smallLogBuffer: LogBuffer?, + @KeyguardLargeClockLog private val largeLogBuffer: LogBuffer?, private val featureFlags: FeatureFlags ) { var clock: ClockController? = null set(value) { field = value if (value != null) { - if (logBuffer != null) { - value.setLogBuffer(logBuffer) - } + smallLogBuffer?.log(TAG, DEBUG, {}, { "New Clock" }) + value.smallClock.logBuffer = smallLogBuffer + largeLogBuffer?.log(TAG, DEBUG, {}, { "New Clock" }) + value.largeClock.logBuffer = largeLogBuffer value.initialize(resources, dozeAmount, 0f) updateRegionSamplers(value) @@ -325,4 +329,8 @@ open class ClockEventController @Inject constructor( } } } + + companion object { + private val TAG = ClockEventController::class.simpleName!! + } } diff --git a/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt b/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt index 5bb9367fa4a5..e0cf7b6a2bc4 100644 --- a/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt +++ b/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt @@ -50,6 +50,7 @@ import com.android.keyguard.InternalFaceAuthReasons.KEYGUARD_RESET import com.android.keyguard.InternalFaceAuthReasons.KEYGUARD_VISIBILITY_CHANGED import com.android.keyguard.InternalFaceAuthReasons.NON_STRONG_BIOMETRIC_ALLOWED_CHANGED import com.android.keyguard.InternalFaceAuthReasons.OCCLUDING_APP_REQUESTED +import com.android.keyguard.InternalFaceAuthReasons.POSTURE_CHANGED import com.android.keyguard.InternalFaceAuthReasons.PRIMARY_BOUNCER_SHOWN import com.android.keyguard.InternalFaceAuthReasons.PRIMARY_BOUNCER_SHOWN_OR_WILL_BE_SHOWN import com.android.keyguard.InternalFaceAuthReasons.RETRY_AFTER_HW_UNAVAILABLE @@ -126,6 +127,7 @@ private object InternalFaceAuthReasons { const val STRONG_AUTH_ALLOWED_CHANGED = "Face auth stopped because strong auth allowed changed" const val NON_STRONG_BIOMETRIC_ALLOWED_CHANGED = "Face auth stopped because non strong biometric allowed changed" + const val POSTURE_CHANGED = "Face auth started/stopped due to device posture changed." } /** @@ -173,6 +175,7 @@ constructor(private val id: Int, val reason: String, var extraInfo: Int = 0) : return PowerManager.wakeReasonToString(extraInfo) } }, + @UiEvent(doc = POSTURE_CHANGED) FACE_AUTH_UPDATED_POSTURE_CHANGED(1265, POSTURE_CHANGED), @Deprecated( "Not a face auth trigger.", ReplaceWith( diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java index 62babadc45d8..4acbb0aaf1d8 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java @@ -7,7 +7,6 @@ import android.animation.ObjectAnimator; import android.content.Context; import android.graphics.Rect; import android.util.AttributeSet; -import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; @@ -20,11 +19,15 @@ import com.android.keyguard.dagger.KeyguardStatusViewScope; import com.android.systemui.R; import com.android.systemui.animation.Interpolators; import com.android.systemui.plugins.ClockController; +import com.android.systemui.plugins.log.LogBuffer; +import com.android.systemui.plugins.log.LogLevel; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import kotlin.Unit; + /** * Switch to show plugin clock when plugin is connected, otherwise it will show default clock. */ @@ -87,6 +90,7 @@ public class KeyguardClockSwitch extends RelativeLayout { private int mClockSwitchYAmount; @VisibleForTesting boolean mChildrenAreLaidOut = false; @VisibleForTesting boolean mAnimateOnLayout = true; + private LogBuffer mLogBuffer = null; public KeyguardClockSwitch(Context context, AttributeSet attrs) { super(context, attrs); @@ -113,6 +117,14 @@ public class KeyguardClockSwitch extends RelativeLayout { onDensityOrFontScaleChanged(); } + public void setLogBuffer(LogBuffer logBuffer) { + mLogBuffer = logBuffer; + } + + public LogBuffer getLogBuffer() { + return mLogBuffer; + } + void setClock(ClockController clock, int statusBarState) { mClock = clock; @@ -121,12 +133,16 @@ public class KeyguardClockSwitch extends RelativeLayout { mLargeClockFrame.removeAllViews(); if (clock == null) { - Log.e(TAG, "No clock being shown"); + if (mLogBuffer != null) { + mLogBuffer.log(TAG, LogLevel.ERROR, "No clock being shown"); + } return; } // Attach small and big clock views to hierarchy. - Log.i(TAG, "Attached new clock views to switch"); + if (mLogBuffer != null) { + mLogBuffer.log(TAG, LogLevel.INFO, "Attached new clock views to switch"); + } mSmallClockFrame.addView(clock.getSmallClock().getView()); mLargeClockFrame.addView(clock.getLargeClock().getView()); updateClockTargetRegions(); @@ -152,8 +168,18 @@ public class KeyguardClockSwitch extends RelativeLayout { } private void updateClockViews(boolean useLargeClock, boolean animate) { - Log.i(TAG, "updateClockViews; useLargeClock=" + useLargeClock + "; animate=" + animate - + "; mChildrenAreLaidOut=" + mChildrenAreLaidOut); + if (mLogBuffer != null) { + mLogBuffer.log(TAG, LogLevel.DEBUG, (msg) -> { + msg.setBool1(useLargeClock); + msg.setBool2(animate); + msg.setBool3(mChildrenAreLaidOut); + return Unit.INSTANCE; + }, (msg) -> "updateClockViews" + + "; useLargeClock=" + msg.getBool1() + + "; animate=" + msg.getBool2() + + "; mChildrenAreLaidOut=" + msg.getBool3()); + } + if (mClockInAnim != null) mClockInAnim.cancel(); if (mClockOutAnim != null) mClockOutAnim.cancel(); if (mStatusAreaAnim != null) mStatusAreaAnim.cancel(); @@ -183,6 +209,7 @@ public class KeyguardClockSwitch extends RelativeLayout { if (!animate) { out.setAlpha(0f); + out.setVisibility(INVISIBLE); in.setAlpha(1f); in.setVisibility(VISIBLE); mStatusArea.setTranslationY(statusAreaYTranslation); @@ -198,7 +225,10 @@ public class KeyguardClockSwitch extends RelativeLayout { direction * -mClockSwitchYAmount)); mClockOutAnim.addListener(new AnimatorListenerAdapter() { public void onAnimationEnd(Animator animation) { - mClockOutAnim = null; + if (mClockOutAnim == animation) { + out.setVisibility(INVISIBLE); + mClockOutAnim = null; + } } }); @@ -212,7 +242,9 @@ public class KeyguardClockSwitch extends RelativeLayout { mClockInAnim.setStartDelay(CLOCK_OUT_MILLIS / 2); mClockInAnim.addListener(new AnimatorListenerAdapter() { public void onAnimationEnd(Animator animation) { - mClockInAnim = null; + if (mClockInAnim == animation) { + mClockInAnim = null; + } } }); @@ -225,7 +257,9 @@ public class KeyguardClockSwitch extends RelativeLayout { mStatusAreaAnim.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); mStatusAreaAnim.addListener(new AnimatorListenerAdapter() { public void onAnimationEnd(Animator animation) { - mStatusAreaAnim = null; + if (mStatusAreaAnim == animation) { + mStatusAreaAnim = null; + } } }); mStatusAreaAnim.start(); @@ -269,7 +303,9 @@ public class KeyguardClockSwitch extends RelativeLayout { public void dump(PrintWriter pw, String[] args) { pw.println("KeyguardClockSwitch:"); pw.println(" mSmallClockFrame: " + mSmallClockFrame); + pw.println(" mSmallClockFrame.alpha: " + mSmallClockFrame.getAlpha()); pw.println(" mLargeClockFrame: " + mLargeClockFrame); + pw.println(" mLargeClockFrame.alpha: " + mLargeClockFrame.getAlpha()); pw.println(" mStatusArea: " + mStatusArea); pw.println(" mDisplayedClockSize: " + mDisplayedClockSize); } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java index 6ce84a94cc87..08567a76f741 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java @@ -38,8 +38,11 @@ import com.android.systemui.R; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; +import com.android.systemui.log.dagger.KeyguardClockLog; import com.android.systemui.plugins.ClockAnimations; import com.android.systemui.plugins.ClockController; +import com.android.systemui.plugins.log.LogBuffer; +import com.android.systemui.plugins.log.LogLevel; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.shared.clocks.ClockRegistry; import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController; @@ -62,6 +65,8 @@ import javax.inject.Inject; */ public class KeyguardClockSwitchController extends ViewController<KeyguardClockSwitch> implements Dumpable { + private static final String TAG = "KeyguardClockSwitchController"; + private final StatusBarStateController mStatusBarStateController; private final ClockRegistry mClockRegistry; private final KeyguardSliceViewController mKeyguardSliceViewController; @@ -70,6 +75,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS private final SecureSettings mSecureSettings; private final DumpManager mDumpManager; private final ClockEventController mClockEventController; + private final LogBuffer mLogBuffer; private FrameLayout mSmallClockFrame; // top aligned clock private FrameLayout mLargeClockFrame; // centered clock @@ -119,7 +125,8 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS SecureSettings secureSettings, @Main Executor uiExecutor, DumpManager dumpManager, - ClockEventController clockEventController) { + ClockEventController clockEventController, + @KeyguardClockLog LogBuffer logBuffer) { super(keyguardClockSwitch); mStatusBarStateController = statusBarStateController; mClockRegistry = clockRegistry; @@ -131,6 +138,8 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS mKeyguardUnlockAnimationController = keyguardUnlockAnimationController; mDumpManager = dumpManager; mClockEventController = clockEventController; + mLogBuffer = logBuffer; + mView.setLogBuffer(mLogBuffer); mClockChangedListener = () -> { setClock(mClockRegistry.createCurrentClock()); @@ -337,10 +346,6 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS int clockHeight = clock.getLargeClock().getView().getHeight(); return frameHeight / 2 + clockHeight / 2 + mKeyguardLargeClockTopMargin / -2; } else { - // This is only called if we've never shown the large clock as the frame is inflated - // with 'gone', but then the visibility is never set when it is animated away by - // KeyguardClockSwitch, instead it is removed from the view hierarchy. - // TODO(b/261755021): Cleanup Large Frame Visibility int clockHeight = clock.getSmallClock().getView().getHeight(); return clockHeight + statusBarHeaderHeight + mKeyguardSmallClockTopMargin; } @@ -358,15 +363,11 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS if (mLargeClockFrame.getVisibility() == View.VISIBLE) { return clock.getLargeClock().getView().getHeight(); } else { - // Is not called except in certain edge cases, see comment in getClockBottom - // TODO(b/261755021): Cleanup Large Frame Visibility return clock.getSmallClock().getView().getHeight(); } } boolean isClockTopAligned() { - // Returns false except certain edge cases, see comment in getClockBottom - // TODO(b/261755021): Cleanup Large Frame Visibility return mLargeClockFrame.getVisibility() != View.VISIBLE; } @@ -378,6 +379,10 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS } private void setClock(ClockController clock) { + if (clock != null && mLogBuffer != null) { + mLogBuffer.log(TAG, LogLevel.INFO, "New Clock"); + } + mClockEventController.setClock(clock); mView.setClock(clock, mStatusBarStateController.getState()); } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardFaceListenModel.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardFaceListenModel.kt index deead1959b8a..1a06b5f1c767 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardFaceListenModel.kt +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardFaceListenModel.kt @@ -39,6 +39,7 @@ data class KeyguardFaceListenModel( var keyguardGoingAway: Boolean = false, var listeningForFaceAssistant: Boolean = false, var occludingAppRequestingFaceAuth: Boolean = false, + val postureAllowsListening: Boolean = false, var primaryUser: Boolean = false, var secureCameraLaunched: Boolean = false, var supportsDetect: Boolean = false, diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index 4e10bffc381d..9d6bb087288b 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -63,11 +63,13 @@ import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_UPDATED_KEYGUARD_RE import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_UPDATED_KEYGUARD_VISIBILITY_CHANGED; import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_UPDATED_ON_FACE_AUTHENTICATED; import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_UPDATED_ON_KEYGUARD_INIT; +import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_UPDATED_POSTURE_CHANGED; import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_UPDATED_PRIMARY_BOUNCER_SHOWN; import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_UPDATED_STARTED_WAKING_UP; import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_UPDATED_STRONG_AUTH_CHANGED; import static com.android.keyguard.FaceAuthUiEvent.FACE_AUTH_UPDATED_USER_SWITCHING; import static com.android.systemui.DejankUtils.whitelistIpcs; +import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_UNKNOWN; import android.annotation.AnyThread; import android.annotation.MainThread; @@ -154,6 +156,7 @@ import com.android.systemui.shared.system.TaskStackChangeListener; import com.android.systemui.shared.system.TaskStackChangeListeners; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.phone.KeyguardBypassController; +import com.android.systemui.statusbar.policy.DevicePostureController; import com.android.systemui.telephony.TelephonyListenerManager; import com.android.systemui.util.Assert; import com.android.systemui.util.settings.SecureSettings; @@ -341,6 +344,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private final TrustManager mTrustManager; private final UserManager mUserManager; private final DevicePolicyManager mDevicePolicyManager; + private final DevicePostureController mPostureController; private final BroadcastDispatcher mBroadcastDispatcher; private final SecureSettings mSecureSettings; private final InteractionJankMonitor mInteractionJankMonitor; @@ -358,6 +362,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private final FaceManager mFaceManager; private final LockPatternUtils mLockPatternUtils; private final boolean mWakeOnFingerprintAcquiredStart; + @VisibleForTesting + @DevicePostureController.DevicePostureInt + protected int mConfigFaceAuthSupportedPosture; private KeyguardBypassController mKeyguardBypassController; private List<SubscriptionInfo> mSubscriptionInfo; @@ -368,6 +375,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private boolean mLogoutEnabled; private boolean mIsFaceEnrolled; private int mActiveMobileDataSubscription = SubscriptionManager.INVALID_SUBSCRIPTION_ID; + private int mPostureState = DEVICE_POSTURE_UNKNOWN; private FingerprintInteractiveToAuthProvider mFingerprintInteractiveToAuthProvider; /** @@ -696,8 +704,11 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab */ public void setKeyguardGoingAway(boolean goingAway) { mKeyguardGoingAway = goingAway; - // This is set specifically to stop face authentication from running. - updateBiometricListeningState(BIOMETRIC_ACTION_STOP, FACE_AUTH_STOPPED_KEYGUARD_GOING_AWAY); + if (mKeyguardGoingAway) { + updateFaceListeningState(BIOMETRIC_ACTION_STOP, + FACE_AUTH_STOPPED_KEYGUARD_GOING_AWAY); + } + updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE); } /** @@ -1776,6 +1787,17 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab }; @VisibleForTesting + final DevicePostureController.Callback mPostureCallback = + new DevicePostureController.Callback() { + @Override + public void onPostureChanged(int posture) { + mPostureState = posture; + updateFaceListeningState(BIOMETRIC_ACTION_UPDATE, + FACE_AUTH_UPDATED_POSTURE_CHANGED); + } + }; + + @VisibleForTesting CancellationSignal mFingerprintCancelSignal; @VisibleForTesting CancellationSignal mFaceCancelSignal; @@ -1935,9 +1957,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab cb.onFinishedGoingToSleep(arg1); } } - // This is set specifically to stop face authentication from running. - updateBiometricListeningState(BIOMETRIC_ACTION_STOP, + updateFaceListeningState(BIOMETRIC_ACTION_STOP, FACE_AUTH_STOPPED_FINISHED_GOING_TO_SLEEP); + updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE); } private void handleScreenTurnedOff() { @@ -2041,6 +2063,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab @Nullable FingerprintManager fingerprintManager, @Nullable BiometricManager biometricManager, FaceWakeUpTriggersConfig faceWakeUpTriggersConfig, + DevicePostureController devicePostureController, Optional<FingerprintInteractiveToAuthProvider> interactiveToAuthProvider) { mContext = context; mSubscriptionManager = subscriptionManager; @@ -2070,6 +2093,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab mDreamManager = dreamManager; mTelephonyManager = telephonyManager; mDevicePolicyManager = devicePolicyManager; + mPostureController = devicePostureController; mPackageManager = packageManager; mFpm = fingerprintManager; mFaceManager = faceManager; @@ -2081,6 +2105,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab R.array.config_face_acquire_device_entry_ignorelist)) .boxed() .collect(Collectors.toSet()); + mConfigFaceAuthSupportedPosture = mContext.getResources().getInteger( + R.integer.config_face_auth_supported_posture); mFaceWakeUpTriggersConfig = faceWakeUpTriggersConfig; mHandler = new Handler(mainLooper) { @@ -2272,6 +2298,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab FACE_AUTH_TRIGGERED_ENROLLMENTS_CHANGED)); } }); + if (mConfigFaceAuthSupportedPosture != DEVICE_POSTURE_UNKNOWN) { + mPostureController.addCallback(mPostureCallback); + } updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE, FACE_AUTH_UPDATED_ON_KEYGUARD_INIT); TaskStackChangeListeners.getInstance().registerTaskStackListener(mTaskStackListener); @@ -2704,7 +2733,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab mFingerprintInteractiveToAuthProvider != null && mFingerprintInteractiveToAuthProvider.isEnabled(getCurrentUser()); shouldListenSideFpsState = - interactiveToAuthEnabled ? isDeviceInteractive() : true; + interactiveToAuthEnabled ? isDeviceInteractive() && !mGoingToSleep : true; } boolean shouldListen = shouldListenKeyguardState && shouldListenUserState @@ -2716,7 +2745,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab user, shouldListen, biometricEnabledForUser, - mPrimaryBouncerIsOrWillBeShowing, + mPrimaryBouncerIsOrWillBeShowing, userCanSkipBouncer, mCredentialAttempted, mDeviceInteractive, @@ -2776,6 +2805,9 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab final boolean biometricEnabledForUser = mBiometricEnabledForUser.get(user); final boolean shouldListenForFaceAssistant = shouldListenForFaceAssistant(); final boolean isUdfpsFingerDown = mAuthController.isUdfpsFingerDown(); + final boolean isPostureAllowedForFaceAuth = + mConfigFaceAuthSupportedPosture == 0 /* DEVICE_POSTURE_UNKNOWN */ ? true + : (mPostureState == mConfigFaceAuthSupportedPosture); // Only listen if this KeyguardUpdateMonitor belongs to the primary user. There is an // instance of KeyguardUpdateMonitor for each user but KeyguardUpdateMonitor is user-aware. @@ -2792,7 +2824,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab && faceAuthAllowedOrDetectionIsNeeded && mIsPrimaryUser && (!mSecureCameraLaunched || mOccludingAppRequestingFace) && faceAndFpNotAuthenticated - && !mGoingToSleep; + && !mGoingToSleep + && isPostureAllowedForFaceAuth; // Aggregate relevant fields for debug logging. logListenerModelData( @@ -2812,6 +2845,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab mKeyguardGoingAway, shouldListenForFaceAssistant, mOccludingAppRequestingFace, + isPostureAllowedForFaceAuth, mIsPrimaryUser, mSecureCameraLaunched, supportsDetect, @@ -2897,7 +2931,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab getKeyguardSessionId(), faceAuthUiEvent.getExtraInfo() ); - + mLogger.logFaceUnlockPossible(unlockPossible); if (unlockPossible) { mFaceCancelSignal = new CancellationSignal(); diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt index b106fec11eb5..2c7eceba48a2 100644 --- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt +++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt @@ -17,36 +17,46 @@ package com.android.keyguard.logging import com.android.systemui.log.dagger.KeyguardLog -import com.android.systemui.plugins.log.ConstantStringsLogger -import com.android.systemui.plugins.log.ConstantStringsLoggerImpl import com.android.systemui.plugins.log.LogBuffer -import com.android.systemui.plugins.log.LogLevel.DEBUG -import com.android.systemui.plugins.log.LogLevel.ERROR -import com.android.systemui.plugins.log.LogLevel.INFO -import com.android.systemui.plugins.log.LogLevel.VERBOSE +import com.android.systemui.plugins.log.LogLevel import com.google.errorprone.annotations.CompileTimeConstant import javax.inject.Inject -private const val TAG = "KeyguardLog" +private const val BIO_TAG = "KeyguardLog" /** * Generic logger for keyguard that's wrapping [LogBuffer]. This class should be used for adding * temporary logs or logs for smaller classes when creating whole new [LogBuffer] wrapper might be * an overkill. */ -class KeyguardLogger @Inject constructor(@KeyguardLog val buffer: LogBuffer) : - ConstantStringsLogger by ConstantStringsLoggerImpl(buffer, TAG) { - - fun logException(ex: Exception, @CompileTimeConstant logMsg: String) { - buffer.log(TAG, ERROR, {}, { logMsg }, exception = ex) - } - - fun v(msg: String, arg: Any) { - buffer.log(TAG, VERBOSE, { str1 = arg.toString() }, { "$msg: $str1" }) - } +class KeyguardLogger +@Inject +constructor( + @KeyguardLog val buffer: LogBuffer, +) { + @JvmOverloads + fun log( + tag: String, + level: LogLevel, + @CompileTimeConstant msg: String, + ex: Throwable? = null, + ) = buffer.log(tag, level, msg, ex) - fun i(msg: String, arg: Any) { - buffer.log(TAG, INFO, { str1 = arg.toString() }, { "$msg: $str1" }) + fun log( + tag: String, + level: LogLevel, + @CompileTimeConstant msg: String, + arg: Any, + ) { + buffer.log( + tag, + level, + { + str1 = msg + str2 = arg.toString() + }, + { "$str1: $str2" } + ) } @JvmOverloads @@ -56,8 +66,8 @@ class KeyguardLogger @Inject constructor(@KeyguardLog val buffer: LogBuffer) : msg: String? = null ) { buffer.log( - TAG, - DEBUG, + BIO_TAG, + LogLevel.DEBUG, { str1 = context str2 = "$msgId" diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt index 21d3b24174b6..5b4245595be9 100644 --- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt +++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt @@ -132,6 +132,12 @@ class KeyguardUpdateMonitorLogger @Inject constructor( logBuffer.log(TAG, DEBUG, { int1 = faceRunningState }, { "faceRunningState: $int1" }) } + fun logFaceUnlockPossible(isFaceUnlockPossible: Boolean) { + logBuffer.log(TAG, DEBUG, + { bool1 = isFaceUnlockPossible }, + {"isUnlockWithFacePossible: $bool1"}) + } + fun logFingerprintAuthForWrongUser(authUserId: Int) { logBuffer.log(TAG, DEBUG, { int1 = authUserId }, diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt index 338bf66d197e..693f64a1f93d 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessor.kt @@ -27,6 +27,8 @@ import com.android.systemui.biometrics.udfps.TouchProcessorResult.ProcessedTouch import com.android.systemui.dagger.SysUISingleton import javax.inject.Inject +private val SUPPORTED_ROTATIONS = setOf(Surface.ROTATION_90, Surface.ROTATION_270) + /** * TODO(b/259140693): Consider using an object pool of TouchProcessorResult to avoid allocations. */ @@ -129,19 +131,27 @@ private fun MotionEvent.normalize( val nativeY = naturalTouch.y / overlayParams.scaleFactor val nativeMinor: Float = getTouchMinor(pointerIndex) / overlayParams.scaleFactor val nativeMajor: Float = getTouchMajor(pointerIndex) / overlayParams.scaleFactor + var nativeOrientation: Float = getOrientation(pointerIndex) + if (SUPPORTED_ROTATIONS.contains(overlayParams.rotation)) { + nativeOrientation = toRadVerticalFromRotated(nativeOrientation.toDouble()).toFloat() + } return NormalizedTouchData( pointerId = getPointerId(pointerIndex), x = nativeX, y = nativeY, minor = nativeMinor, major = nativeMajor, - // TODO(b/259311354): touch orientation should be reported relative to Surface.ROTATION_O. - orientation = getOrientation(pointerIndex), + orientation = nativeOrientation, time = eventTime, gestureStart = downTime, ) } +private fun toRadVerticalFromRotated(rad: Double): Double { + val piBound = ((rad % Math.PI) + Math.PI / 2) % Math.PI + return if (piBound < Math.PI / 2.0) piBound else piBound - Math.PI +} + /** * Returns the [MotionEvent.getRawX] and [MotionEvent.getRawY] of the given pointer as if the device * is in the [Surface.ROTATION_0] orientation. @@ -152,7 +162,7 @@ private fun MotionEvent.rotateToNaturalOrientation( ): PointF { val touchPoint = PointF(getRawX(pointerIndex), getRawY(pointerIndex)) val rot = overlayParams.rotation - if (rot == Surface.ROTATION_90 || rot == Surface.ROTATION_270) { + if (SUPPORTED_ROTATIONS.contains(rot)) { RotationUtils.rotatePointF( touchPoint, RotationUtils.deltaRotation(rot, Surface.ROTATION_0), diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java index f244cb009ba4..96bce4cd3cd9 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java @@ -19,6 +19,7 @@ package com.android.systemui.dreams; import android.annotation.IntDef; import android.annotation.Nullable; import android.content.Context; +import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; @@ -26,6 +27,9 @@ import android.view.ViewGroup; import androidx.constraintlayout.widget.ConstraintLayout; import com.android.systemui.R; +import com.android.systemui.shared.shadow.DoubleShadowIconDrawable; +import com.android.systemui.shared.shadow.DoubleShadowTextHelper.ShadowInfo; +import com.android.systemui.statusbar.AlphaOptimizedImageView; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -60,8 +64,15 @@ public class DreamOverlayStatusBarView extends ConstraintLayout { public static final int STATUS_ICON_PRIORITY_MODE_ON = 6; private final Map<Integer, View> mStatusIcons = new HashMap<>(); + private Context mContext; private ViewGroup mSystemStatusViewGroup; private ViewGroup mExtraSystemStatusViewGroup; + private ShadowInfo mKeyShadowInfo; + private ShadowInfo mAmbientShadowInfo; + private int mDrawableSize; + private int mDrawableInsetSize; + private static final float KEY_SHADOW_ALPHA = 0.35f; + private static final float AMBIENT_SHADOW_ALPHA = 0.4f; public DreamOverlayStatusBarView(Context context) { this(context, null); @@ -73,6 +84,7 @@ public class DreamOverlayStatusBarView extends ConstraintLayout { public DreamOverlayStatusBarView(Context context, AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); + mContext = context; } public DreamOverlayStatusBarView( @@ -80,14 +92,36 @@ public class DreamOverlayStatusBarView extends ConstraintLayout { super(context, attrs, defStyleAttr, defStyleRes); } + @Override protected void onFinishInflate() { super.onFinishInflate(); + mKeyShadowInfo = createShadowInfo( + R.dimen.dream_overlay_status_bar_key_text_shadow_radius, + R.dimen.dream_overlay_status_bar_key_text_shadow_dx, + R.dimen.dream_overlay_status_bar_key_text_shadow_dy, + KEY_SHADOW_ALPHA + ); + + mAmbientShadowInfo = createShadowInfo( + R.dimen.dream_overlay_status_bar_ambient_text_shadow_radius, + R.dimen.dream_overlay_status_bar_ambient_text_shadow_dx, + R.dimen.dream_overlay_status_bar_ambient_text_shadow_dy, + AMBIENT_SHADOW_ALPHA + ); + + mDrawableSize = mContext + .getResources() + .getDimensionPixelSize(R.dimen.dream_overlay_status_bar_icon_size); + mDrawableInsetSize = mContext + .getResources() + .getDimensionPixelSize(R.dimen.dream_overlay_icon_inset_dimen); + mStatusIcons.put(STATUS_ICON_WIFI_UNAVAILABLE, - fetchStatusIconForResId(R.id.dream_overlay_wifi_status)); + addDoubleShadow(fetchStatusIconForResId(R.id.dream_overlay_wifi_status))); mStatusIcons.put(STATUS_ICON_ALARM_SET, - fetchStatusIconForResId(R.id.dream_overlay_alarm_set)); + addDoubleShadow(fetchStatusIconForResId(R.id.dream_overlay_alarm_set))); mStatusIcons.put(STATUS_ICON_CAMERA_DISABLED, fetchStatusIconForResId(R.id.dream_overlay_camera_off)); mStatusIcons.put(STATUS_ICON_MIC_DISABLED, @@ -97,7 +131,7 @@ public class DreamOverlayStatusBarView extends ConstraintLayout { mStatusIcons.put(STATUS_ICON_NOTIFICATIONS, fetchStatusIconForResId(R.id.dream_overlay_notification_indicator)); mStatusIcons.put(STATUS_ICON_PRIORITY_MODE_ON, - fetchStatusIconForResId(R.id.dream_overlay_priority_mode)); + addDoubleShadow(fetchStatusIconForResId(R.id.dream_overlay_priority_mode))); mSystemStatusViewGroup = findViewById(R.id.dream_overlay_system_status); mExtraSystemStatusViewGroup = findViewById(R.id.dream_overlay_extra_items); @@ -137,4 +171,34 @@ public class DreamOverlayStatusBarView extends ConstraintLayout { } return false; } + + private View addDoubleShadow(View icon) { + if (icon instanceof AlphaOptimizedImageView) { + AlphaOptimizedImageView i = (AlphaOptimizedImageView) icon; + Drawable drawableIcon = i.getDrawable(); + i.setImageDrawable(new DoubleShadowIconDrawable( + mKeyShadowInfo, + mAmbientShadowInfo, + drawableIcon, + mDrawableSize, + mDrawableInsetSize + )); + } + return icon; + } + + private ShadowInfo createShadowInfo(int blurId, int offsetXId, int offsetYId, float alpha) { + return new ShadowInfo( + fetchDimensionForResId(blurId), + fetchDimensionForResId(offsetXId), + fetchDimensionForResId(offsetYId), + alpha + ); + } + + private Float fetchDimensionForResId(int resId) { + return mContext + .getResources() + .getDimension(resId); + } } diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index e4e8d59df066..25e824b04d64 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -114,8 +114,6 @@ object Flags { // ** Flag retired ** // public static final BooleanFlag KEYGUARD_LAYOUT = // new BooleanFlag(200, true); - // TODO(b/254512713): Tracking Bug - @JvmField val LOCKSCREEN_ANIMATIONS = releasedFlag(201, "lockscreen_animations") // TODO(b/254512750): Tracking Bug val NEW_UNLOCK_SWIPE_ANIMATION = releasedFlag(202, "new_unlock_swipe_animation") @@ -332,13 +330,17 @@ object Flags { val MEDIA_TTT_RECEIVER_SUCCESS_RIPPLE = unreleasedFlag(910, "media_ttt_receiver_success_ripple", teamfood = true) + // TODO(b/263512203): Tracking Bug + val MEDIA_EXPLICIT_INDICATOR = unreleasedFlag(911, "media_explicit_indicator", teamfood = true) + // 1000 - dock val SIMULATE_DOCK_THROUGH_CHARGING = releasedFlag(1000, "simulate_dock_through_charging") // TODO(b/254512758): Tracking Bug @JvmField val ROUNDED_BOX_RIPPLE = releasedFlag(1002, "rounded_box_ripple") - val SHOW_LOWLIGHT_ON_DIRECT_BOOT = unreleasedFlag(1003, "show_lowlight_on_direct_boot") + // TODO(b/265045965): Tracking Bug + val SHOW_LOWLIGHT_ON_DIRECT_BOOT = releasedFlag(1003, "show_lowlight_on_direct_boot") // 1100 - windowing @Keep diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/WakefulnessLifecycle.java b/packages/SystemUI/src/com/android/systemui/keyguard/WakefulnessLifecycle.java index 017b65acd1d2..ffd8a0244a86 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/WakefulnessLifecycle.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/WakefulnessLifecycle.java @@ -33,6 +33,7 @@ import com.android.systemui.Dumpable; import com.android.systemui.R; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dump.DumpManager; +import com.android.systemui.util.time.SystemClock; import java.io.PrintWriter; import java.lang.annotation.Retention; @@ -63,6 +64,7 @@ public class WakefulnessLifecycle extends Lifecycle<WakefulnessLifecycle.Observe private final Context mContext; private final DisplayMetrics mDisplayMetrics; + private final SystemClock mSystemClock; @Nullable private final IWallpaperManager mWallpaperManagerService; @@ -71,6 +73,9 @@ public class WakefulnessLifecycle extends Lifecycle<WakefulnessLifecycle.Observe private @PowerManager.WakeReason int mLastWakeReason = PowerManager.WAKE_REASON_UNKNOWN; + public static final long UNKNOWN_LAST_WAKE_TIME = -1; + private long mLastWakeTime = UNKNOWN_LAST_WAKE_TIME; + @Nullable private Point mLastWakeOriginLocation = null; @@ -84,10 +89,12 @@ public class WakefulnessLifecycle extends Lifecycle<WakefulnessLifecycle.Observe public WakefulnessLifecycle( Context context, @Nullable IWallpaperManager wallpaperManagerService, + SystemClock systemClock, DumpManager dumpManager) { mContext = context; mDisplayMetrics = context.getResources().getDisplayMetrics(); mWallpaperManagerService = wallpaperManagerService; + mSystemClock = systemClock; dumpManager.registerDumpable(getClass().getSimpleName(), this); } @@ -104,6 +111,14 @@ public class WakefulnessLifecycle extends Lifecycle<WakefulnessLifecycle.Observe } /** + * Returns the most recent time (in device uptimeMillis) the display woke up. + * Returns {@link UNKNOWN_LAST_WAKE_TIME} if there hasn't been a wakeup yet. + */ + public long getLastWakeTime() { + return mLastWakeTime; + } + + /** * Returns the most recent reason the device went to sleep up. This is one of * PowerManager.GO_TO_SLEEP_REASON_*. */ @@ -117,6 +132,7 @@ public class WakefulnessLifecycle extends Lifecycle<WakefulnessLifecycle.Observe } setWakefulness(WAKEFULNESS_WAKING); mLastWakeReason = pmWakeReason; + mLastWakeTime = mSystemClock.uptimeMillis(); updateLastWakeOriginLocation(); if (mWallpaperManagerService != null) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt index a4fd087a24b1..d99af90ab6dc 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt @@ -40,6 +40,7 @@ import com.android.systemui.keyguard.shared.model.WakefulnessModel import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.phone.BiometricUnlockController import com.android.systemui.statusbar.phone.BiometricUnlockController.WakeAndUnlockMode +import com.android.systemui.statusbar.phone.DozeParameters import com.android.systemui.statusbar.policy.KeyguardStateController import javax.inject.Inject import kotlinx.coroutines.channels.awaitClose @@ -88,6 +89,9 @@ interface KeyguardRepository { /** Observable for whether the bouncer is showing. */ val isBouncerShowing: Flow<Boolean> + /** Is the always-on display available to be used? */ + val isAodAvailable: Flow<Boolean> + /** * Observable for whether we are in doze state. * @@ -182,6 +186,7 @@ constructor( private val keyguardStateController: KeyguardStateController, private val keyguardUpdateMonitor: KeyguardUpdateMonitor, private val dozeTransitionListener: DozeTransitionListener, + private val dozeParameters: DozeParameters, private val authController: AuthController, private val dreamOverlayCallbackController: DreamOverlayCallbackController, ) : KeyguardRepository { @@ -220,6 +225,31 @@ constructor( } .distinctUntilChanged() + override val isAodAvailable: Flow<Boolean> = + conflatedCallbackFlow { + val callback = + object : DozeParameters.Callback { + override fun onAlwaysOnChange() { + trySendWithFailureLogging( + dozeParameters.getAlwaysOn(), + TAG, + "updated isAodAvailable" + ) + } + } + + dozeParameters.addCallback(callback) + // Adding the callback does not send an initial update. + trySendWithFailureLogging( + dozeParameters.getAlwaysOn(), + TAG, + "initial isAodAvailable" + ) + + awaitClose { dozeParameters.removeCallback(callback) } + } + .distinctUntilChanged() + override val isKeyguardOccluded: Flow<Boolean> = conflatedCallbackFlow { val callback = diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt index fd2d271e40f9..ce61f2fec92f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt @@ -21,9 +21,9 @@ import com.android.systemui.animation.Interpolators import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository -import com.android.systemui.keyguard.shared.model.DozeStateModel.Companion.isDozeOff import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionInfo +import com.android.systemui.keyguard.shared.model.WakefulnessModel.Companion.isWakingOrStartingToWake import com.android.systemui.util.kotlin.sample import javax.inject.Inject import kotlin.time.Duration @@ -48,12 +48,11 @@ constructor( private fun listenForDozingToLockscreen() { scope.launch { - keyguardInteractor.dozeTransitionModel + keyguardInteractor.wakefulnessModel .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair) - .collect { pair -> - val (dozeTransitionModel, lastStartedTransition) = pair + .collect { (wakefulnessModel, lastStartedTransition) -> if ( - isDozeOff(dozeTransitionModel.to) && + isWakingOrStartingToWake(wakefulnessModel) && lastStartedTransition.to == KeyguardState.DOZING ) { keyguardTransitionRepository.startTransition( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt index 553fafeb92c3..9203a9b924a7 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt @@ -26,7 +26,10 @@ import com.android.systemui.keyguard.shared.model.TransitionInfo import com.android.systemui.keyguard.shared.model.WakefulnessState import com.android.systemui.util.kotlin.sample import javax.inject.Inject +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch @SysUISingleton @@ -40,7 +43,7 @@ constructor( ) : TransitionInteractor(FromGoneTransitionInteractor::class.simpleName!!) { override fun start() { - listenForGoneToAod() + listenForGoneToAodOrDozing() listenForGoneToDreaming() } @@ -56,7 +59,7 @@ constructor( name, KeyguardState.GONE, KeyguardState.DREAMING, - getAnimator(), + getAnimator(TO_DREAMING_DURATION), ) ) } @@ -64,12 +67,18 @@ constructor( } } - private fun listenForGoneToAod() { + private fun listenForGoneToAodOrDozing() { scope.launch { keyguardInteractor.wakefulnessModel - .sample(keyguardTransitionInteractor.finishedKeyguardState, ::Pair) - .collect { pair -> - val (wakefulnessState, keyguardState) = pair + .sample( + combine( + keyguardTransitionInteractor.finishedKeyguardState, + keyguardInteractor.isAodAvailable, + ::Pair + ), + ::toTriple + ) + .collect { (wakefulnessState, keyguardState, isAodAvailable) -> if ( keyguardState == KeyguardState.GONE && wakefulnessState.state == WakefulnessState.STARTING_TO_SLEEP @@ -78,7 +87,11 @@ constructor( TransitionInfo( name, KeyguardState.GONE, - KeyguardState.AOD, + if (isAodAvailable) { + KeyguardState.AOD + } else { + KeyguardState.DOZING + }, getAnimator(), ) ) @@ -87,14 +100,15 @@ constructor( } } - private fun getAnimator(): ValueAnimator { + private fun getAnimator(duration: Duration = DEFAULT_DURATION): ValueAnimator { return ValueAnimator().apply { setInterpolator(Interpolators.LINEAR) - setDuration(TRANSITION_DURATION_MS) + setDuration(duration.inWholeMilliseconds) } } companion object { - private const val TRANSITION_DURATION_MS = 500L + private val DEFAULT_DURATION = 500.milliseconds + val TO_DREAMING_DURATION = 933.milliseconds } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt index 20c6531d580b..64028ceb2fbe 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt @@ -21,11 +21,11 @@ import com.android.systemui.animation.Interpolators import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository -import com.android.systemui.keyguard.shared.model.DozeStateModel import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.StatusBarState.KEYGUARD import com.android.systemui.keyguard.shared.model.TransitionInfo import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.WakefulnessState import com.android.systemui.shade.data.repository.ShadeRepository import com.android.systemui.util.kotlin.sample import java.util.UUID @@ -54,7 +54,7 @@ constructor( listenForLockscreenToGone() listenForLockscreenToOccluded() listenForLockscreenToCamera() - listenForLockscreenToAod() + listenForLockscreenToAodOrDozing() listenForLockscreenToBouncer() listenForLockscreenToDreaming() listenForLockscreenToBouncerDragging() @@ -230,19 +230,31 @@ constructor( } } - private fun listenForLockscreenToAod() { + private fun listenForLockscreenToAodOrDozing() { scope.launch { - keyguardInteractor - .dozeTransitionTo(DozeStateModel.DOZE_AOD) - .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair) - .collect { pair -> - val (dozeToAod, lastStartedStep) = pair - if (lastStartedStep.to == KeyguardState.LOCKSCREEN) { + keyguardInteractor.wakefulnessModel + .sample( + combine( + keyguardTransitionInteractor.startedKeyguardTransitionStep, + keyguardInteractor.isAodAvailable, + ::Pair + ), + ::toTriple + ) + .collect { (wakefulnessState, lastStartedStep, isAodAvailable) -> + if ( + lastStartedStep.to == KeyguardState.LOCKSCREEN && + wakefulnessState.state == WakefulnessState.STARTING_TO_SLEEP + ) { keyguardTransitionRepository.startTransition( TransitionInfo( name, KeyguardState.LOCKSCREEN, - KeyguardState.AOD, + if (isAodAvailable) { + KeyguardState.AOD + } else { + KeyguardState.DOZING + }, getAnimator(), ) ) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt index 88789019b10f..2dc8fee25379 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt @@ -23,12 +23,14 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionInfo +import com.android.systemui.keyguard.shared.model.WakefulnessState import com.android.systemui.util.kotlin.sample import javax.inject.Inject import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch @SysUISingleton @@ -44,6 +46,7 @@ constructor( override fun start() { listenForOccludedToLockscreen() listenForOccludedToDreaming() + listenForOccludedToAodOrDozing() } private fun listenForOccludedToDreaming() { @@ -70,8 +73,7 @@ constructor( scope.launch { keyguardInteractor.isKeyguardOccluded .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair) - .collect { pair -> - val (isOccluded, lastStartedKeyguardState) = pair + .collect { (isOccluded, lastStartedKeyguardState) -> // Occlusion signals come from the framework, and should interrupt any // existing transition if (!isOccluded && lastStartedKeyguardState.to == KeyguardState.OCCLUDED) { @@ -88,6 +90,39 @@ constructor( } } + private fun listenForOccludedToAodOrDozing() { + scope.launch { + keyguardInteractor.wakefulnessModel + .sample( + combine( + keyguardTransitionInteractor.startedKeyguardTransitionStep, + keyguardInteractor.isAodAvailable, + ::Pair + ), + ::toTriple + ) + .collect { (wakefulnessState, lastStartedStep, isAodAvailable) -> + if ( + lastStartedStep.to == KeyguardState.OCCLUDED && + wakefulnessState.state == WakefulnessState.STARTING_TO_SLEEP + ) { + keyguardTransitionRepository.startTransition( + TransitionInfo( + name, + KeyguardState.OCCLUDED, + if (isAodAvailable) { + KeyguardState.AOD + } else { + KeyguardState.DOZING + }, + getAnimator(), + ) + ) + } + } + } + } + private fun getAnimator(duration: Duration = DEFAULT_DURATION): ValueAnimator { return ValueAnimator().apply { setInterpolator(Interpolators.LINEAR) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt index ac2d230ee605..490d22eb0820 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt @@ -57,6 +57,8 @@ constructor( val dozeAmount: Flow<Float> = repository.linearDozeAmount /** Whether the system is in doze mode. */ val isDozing: Flow<Boolean> = repository.isDozing + /** Whether Always-on Display mode is available. */ + val isAodAvailable: Flow<Boolean> = repository.isAodAvailable /** Doze transition information. */ val dozeTransitionModel: Flow<DozeTransitionModel> = repository.dozeTransitionModel /** diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt index a2661d76d90d..d4e2349907bc 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt @@ -19,11 +19,14 @@ package com.android.systemui.keyguard.domain.interactor import com.android.keyguard.logging.KeyguardLogger import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.plugins.log.LogLevel.VERBOSE import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch +private val TAG = KeyguardTransitionAuditLogger::class.simpleName!! + /** Collect flows of interest for auditing keyguard transitions. */ @SysUISingleton class KeyguardTransitionAuditLogger @@ -37,35 +40,47 @@ constructor( fun start() { scope.launch { - keyguardInteractor.wakefulnessModel.collect { logger.v("WakefulnessModel", it) } + keyguardInteractor.wakefulnessModel.collect { + logger.log(TAG, VERBOSE, "WakefulnessModel", it) + } } scope.launch { - keyguardInteractor.isBouncerShowing.collect { logger.v("Bouncer showing", it) } + keyguardInteractor.isBouncerShowing.collect { + logger.log(TAG, VERBOSE, "Bouncer showing", it) + } } - scope.launch { keyguardInteractor.isDozing.collect { logger.v("isDozing", it) } } + scope.launch { + keyguardInteractor.isDozing.collect { logger.log(TAG, VERBOSE, "isDozing", it) } + } - scope.launch { keyguardInteractor.isDreaming.collect { logger.v("isDreaming", it) } } + scope.launch { + keyguardInteractor.isDreaming.collect { logger.log(TAG, VERBOSE, "isDreaming", it) } + } scope.launch { interactor.finishedKeyguardTransitionStep.collect { - logger.i("Finished transition", it) + logger.log(TAG, VERBOSE, "Finished transition", it) } } scope.launch { interactor.canceledKeyguardTransitionStep.collect { - logger.i("Canceled transition", it) + logger.log(TAG, VERBOSE, "Canceled transition", it) } } scope.launch { - interactor.startedKeyguardTransitionStep.collect { logger.i("Started transition", it) } + interactor.startedKeyguardTransitionStep.collect { + logger.log(TAG, VERBOSE, "Started transition", it) + } } scope.launch { - keyguardInteractor.dozeTransitionModel.collect { logger.i("Doze transition", it) } + keyguardInteractor.dozeTransitionModel.collect { + logger.log(TAG, VERBOSE, "Doze transition", it) + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardClockLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardClockLog.kt index 0645236226bd..9f563fe4eae5 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardClockLog.kt +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardClockLog.kt @@ -23,3 +23,15 @@ import javax.inject.Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class KeyguardClockLog + +/** A [com.android.systemui.plugins.log.LogBuffer] for small keyguard clock logs. */ +@Qualifier +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +annotation class KeyguardSmallClockLog + +/** A [com.android.systemui.plugins.log.LogBuffer] for large keyguard clock logs. */ +@Qualifier +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +annotation class KeyguardLargeClockLog diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java index 711bca06d985..afbd8ed9bf5d 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java @@ -335,13 +335,33 @@ public class LogModule { } /** - * Provides a {@link LogBuffer} for keyguard clock logs. + * Provides a {@link LogBuffer} for general keyguard clock logs. */ @Provides @SysUISingleton @KeyguardClockLog public static LogBuffer provideKeyguardClockLog(LogBufferFactory factory) { - return factory.create("KeyguardClockLog", 500); + return factory.create("KeyguardClockLog", 100); + } + + /** + * Provides a {@link LogBuffer} for keyguard small clock logs. + */ + @Provides + @SysUISingleton + @KeyguardSmallClockLog + public static LogBuffer provideKeyguardSmallClockLog(LogBufferFactory factory) { + return factory.create("KeyguardSmallClockLog", 100); + } + + /** + * Provides a {@link LogBuffer} for keyguard large clock logs. + */ + @Provides + @SysUISingleton + @KeyguardLargeClockLog + public static LogBuffer provideKeyguardLargeClockLog(LogBufferFactory factory) { + return factory.create("KeyguardLargeClockLog", 100); } /** diff --git a/packages/SystemUI/src/com/android/systemui/log/table/TableLogBufferFactory.kt b/packages/SystemUI/src/com/android/systemui/log/table/TableLogBufferFactory.kt index 7a90a7470cd2..7ccc43ce62c2 100644 --- a/packages/SystemUI/src/com/android/systemui/log/table/TableLogBufferFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/log/table/TableLogBufferFactory.kt @@ -29,6 +29,18 @@ constructor( private val dumpManager: DumpManager, private val systemClock: SystemClock, ) { + private val existingBuffers = mutableMapOf<String, TableLogBuffer>() + + /** + * Creates a new [TableLogBuffer]. This method should only be called from static contexts, where + * it is guaranteed only to be created one time. See [getOrCreate] for a cache-aware method of + * obtaining a buffer. + * + * @param name a unique table name + * @param maxSize the buffer max size. See [adjustMaxSize] + * + * @return a new [TableLogBuffer] registered with [DumpManager] + */ fun create( name: String, maxSize: Int, @@ -37,4 +49,23 @@ constructor( dumpManager.registerNormalDumpable(name, tableBuffer) return tableBuffer } + + /** + * Log buffers are retained indefinitely by [DumpManager], so that they can be represented in + * bugreports. Because of this, many of them are created statically in the Dagger graph. + * + * In the case where you have to create a logbuffer with a name only known at runtime, this + * method can be used to lazily create a table log buffer which is then cached for reuse. + * + * @return a [TableLogBuffer] suitable for reuse + */ + fun getOrCreate( + name: String, + maxSize: Int, + ): TableLogBuffer = + existingBuffers.getOrElse(name) { + val buffer = create(name, maxSize) + existingBuffers[name] = buffer + buffer + } } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt index f006442906e7..be18cbec7163 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaData.kt @@ -88,7 +88,10 @@ data class MediaData( val instanceId: InstanceId, /** The UID of the app, used for logging */ - val appUid: Int + val appUid: Int, + + /** Whether explicit indicator exists */ + val isExplicit: Boolean = false, ) { companion object { /** Media is playing on the local device */ diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaViewHolder.kt index a8f39fa9a456..1c8bfd1fc468 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaViewHolder.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/models/player/MediaViewHolder.kt @@ -24,6 +24,7 @@ import android.widget.ImageView import android.widget.SeekBar import android.widget.TextView import androidx.constraintlayout.widget.Barrier +import com.android.internal.widget.CachingIconView import com.android.systemui.R import com.android.systemui.media.controls.models.GutsViewHolder import com.android.systemui.surfaceeffects.ripple.MultiRippleView @@ -44,6 +45,7 @@ class MediaViewHolder constructor(itemView: View) { val appIcon = itemView.requireViewById<ImageView>(R.id.icon) val titleText = itemView.requireViewById<TextView>(R.id.header_title) val artistText = itemView.requireViewById<TextView>(R.id.header_artist) + val explicitIndicator = itemView.requireViewById<CachingIconView>(R.id.media_explicit_indicator) // Output switcher val seamless = itemView.requireViewById<ViewGroup>(R.id.media_seamless) @@ -123,6 +125,7 @@ class MediaViewHolder constructor(itemView: View) { R.id.app_name, R.id.header_title, R.id.header_artist, + R.id.media_explicit_indicator, R.id.media_seamless, R.id.media_progress_bar, R.id.actionPlayPause, diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt index 1cc8a1353a34..9f28d4607ab5 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt @@ -46,6 +46,7 @@ import android.os.Process import android.os.UserHandle import android.provider.Settings import android.service.notification.StatusBarNotification +import android.support.v4.media.MediaMetadataCompat import android.text.TextUtils import android.util.Log import androidx.media.utils.MediaConstants @@ -661,6 +662,10 @@ class MediaDataManager( val currentEntry = mediaEntries.get(packageName) val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId() val appUid = currentEntry?.appUid ?: Process.INVALID_UID + val isExplicit = + desc.extras?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) == + MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT && + mediaFlags.isExplicitIndicatorEnabled() val mediaAction = getResumeMediaAction(resumeAction) val lastActive = systemClock.elapsedRealtime() @@ -690,7 +695,8 @@ class MediaDataManager( hasCheckedForResume = true, lastActive = lastActive, instanceId = instanceId, - appUid = appUid + appUid = appUid, + isExplicit = isExplicit, ) ) } @@ -751,6 +757,15 @@ class MediaDataManager( song = HybridGroupManager.resolveTitle(notif) } + // Explicit Indicator + var isExplicit = false + if (mediaFlags.isExplicitIndicatorEnabled()) { + val mediaMetadataCompat = MediaMetadataCompat.fromMediaMetadata(metadata) + isExplicit = + mediaMetadataCompat?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) == + MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT + } + // Artist name var artist: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_ARTIST) if (artist == null) { @@ -852,7 +867,8 @@ class MediaDataManager( isClearable = sbn.isClearable(), lastActive = lastActive, instanceId = instanceId, - appUid = appUid + appUid = appUid, + isExplicit = isExplicit, ) ) } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java index ee0147f55536..9d1ebb664c10 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java @@ -51,7 +51,6 @@ import android.os.Process; import android.os.Trace; import android.text.TextUtils; import android.util.Log; -import android.util.Pair; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; @@ -69,6 +68,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.graphics.ColorUtils; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.InstanceId; +import com.android.internal.widget.CachingIconView; import com.android.settingslib.widget.AdaptiveIcon; import com.android.systemui.ActivityIntentHelper; import com.android.systemui.R; @@ -123,6 +123,7 @@ import java.util.concurrent.Executor; import javax.inject.Inject; +import kotlin.Triple; import kotlin.Unit; /** @@ -400,10 +401,11 @@ public class MediaControlPanel { TextView titleText = mMediaViewHolder.getTitleText(); TextView artistText = mMediaViewHolder.getArtistText(); + CachingIconView explicitIndicator = mMediaViewHolder.getExplicitIndicator(); AnimatorSet enter = loadAnimator(R.anim.media_metadata_enter, - Interpolators.EMPHASIZED_DECELERATE, titleText, artistText); + Interpolators.EMPHASIZED_DECELERATE, titleText, artistText, explicitIndicator); AnimatorSet exit = loadAnimator(R.anim.media_metadata_exit, - Interpolators.EMPHASIZED_ACCELERATE, titleText, artistText); + Interpolators.EMPHASIZED_ACCELERATE, titleText, artistText, explicitIndicator); MultiRippleView multiRippleView = vh.getMultiRippleView(); mMultiRippleController = new MultiRippleController(multiRippleView); @@ -668,11 +670,15 @@ public class MediaControlPanel { private boolean bindSongMetadata(MediaData data) { TextView titleText = mMediaViewHolder.getTitleText(); TextView artistText = mMediaViewHolder.getArtistText(); + ConstraintSet expandedSet = mMediaViewController.getExpandedLayout(); + ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout(); return mMetadataAnimationHandler.setNext( - Pair.create(data.getSong(), data.getArtist()), + new Triple(data.getSong(), data.getArtist(), data.isExplicit()), () -> { titleText.setText(data.getSong()); artistText.setText(data.getArtist()); + setVisibleAndAlpha(expandedSet, R.id.media_explicit_indicator, data.isExplicit()); + setVisibleAndAlpha(collapsedSet, R.id.media_explicit_indicator, data.isExplicit()); // refreshState is required here to resize the text views (and prevent ellipsis) mMediaViewController.refreshState(); diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt index f7a9bc760caf..66f12d6242b0 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaHierarchyManager.kt @@ -41,6 +41,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dreams.DreamOverlayStateController import com.android.systemui.keyguard.WakefulnessLifecycle +import com.android.systemui.media.controls.pipeline.MediaDataManager import com.android.systemui.media.dream.MediaDreamComplication import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.shade.ShadeStateEvents @@ -93,6 +94,7 @@ constructor( private val keyguardStateController: KeyguardStateController, private val bypassController: KeyguardBypassController, private val mediaCarouselController: MediaCarouselController, + private val mediaManager: MediaDataManager, private val keyguardViewController: KeyguardViewController, private val dreamOverlayStateController: DreamOverlayStateController, configurationController: ConfigurationController, @@ -224,9 +226,9 @@ constructor( private var inSplitShade = false - /** Is there any active media in the carousel? */ - private var hasActiveMedia: Boolean = false - get() = mediaHosts.get(LOCATION_QQS)?.visible == true + /** Is there any active media or recommendation in the carousel? */ + private var hasActiveMediaOrRecommendation: Boolean = false + get() = mediaManager.hasActiveMediaOrRecommendation() /** Are we currently waiting on an animation to start? */ private var animationPending: Boolean = false @@ -582,12 +584,8 @@ constructor( val viewHost = createUniqueObjectHost() mediaObject.hostView = viewHost mediaObject.addVisibilityChangeListener { - // If QQS changes visibility, we need to force an update to ensure the transition - // goes into the correct state - val stateUpdate = mediaObject.location == LOCATION_QQS - // Never animate because of a visibility change, only state changes should do that - updateDesiredLocation(forceNoAnimation = true, forceStateUpdate = stateUpdate) + updateDesiredLocation(forceNoAnimation = true) } mediaHosts[mediaObject.location] = mediaObject if (mediaObject.location == desiredLocation) { @@ -908,7 +906,7 @@ constructor( fun isCurrentlyInGuidedTransformation(): Boolean { return hasValidStartAndEndLocations() && getTransformationProgress() >= 0 && - areGuidedTransitionHostsVisible() + (areGuidedTransitionHostsVisible() || !hasActiveMediaOrRecommendation) } private fun hasValidStartAndEndLocations(): Boolean { @@ -965,7 +963,7 @@ constructor( private fun getQSTransformationProgress(): Float { val currentHost = getHost(desiredLocation) val previousHost = getHost(previousLocation) - if (hasActiveMedia && (currentHost?.location == LOCATION_QS && !inSplitShade)) { + if (currentHost?.location == LOCATION_QS && !inSplitShade) { if (previousHost?.location == LOCATION_QQS) { if (previousHost.visible || statusbarState != StatusBarState.KEYGUARD) { return qsExpansion @@ -1028,7 +1026,8 @@ constructor( private fun updateHostAttachment() = traceSection("MediaHierarchyManager#updateHostAttachment") { var newLocation = resolveLocationForFading() - var canUseOverlay = !isCurrentlyFading() + // Don't use the overlay when fading or when we don't have active media + var canUseOverlay = !isCurrentlyFading() && hasActiveMediaOrRecommendation if (isCrossFadeAnimatorRunning) { if ( getHost(newLocation)?.visible == true && @@ -1122,7 +1121,6 @@ constructor( dreamOverlayActive && dreamMediaComplicationActive -> LOCATION_DREAM_OVERLAY (qsExpansion > 0.0f || inSplitShade) && !onLockscreen -> LOCATION_QS qsExpansion > 0.4f && onLockscreen -> LOCATION_QS - !hasActiveMedia -> LOCATION_QS onLockscreen && isSplitShadeExpanding() -> LOCATION_QS onLockscreen && isTransformingToFullShadeAndInQQS() -> LOCATION_QQS onLockscreen && allowMediaPlayerOnLockScreen -> LOCATION_LOCKSCREEN diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt index 322421318cb8..d2c8e15868dc 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaViewController.kt @@ -80,6 +80,7 @@ constructor( setOf( R.id.header_title, R.id.header_artist, + R.id.media_explicit_indicator, R.id.actionPlayPause, ) diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt index 8d4931a5d08c..5bc35caed515 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaFlags.kt @@ -42,4 +42,7 @@ class MediaFlags @Inject constructor(private val featureFlags: FeatureFlags) { * [android.app.StatusBarManager.registerNearbyMediaDevicesProvider] for more information. */ fun areNearbyMediaDevicesEnabled() = featureFlags.isEnabled(Flags.MEDIA_NEARBY_DEVICES) + + /** Check whether we show explicit indicator on UMO */ + fun isExplicitIndicatorEnabled() = featureFlags.isEnabled(Flags.MEDIA_EXPLICIT_INDICATOR) } diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java index 316b64209f83..7bc0c0cc614b 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java @@ -637,44 +637,21 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, } // For the first time building list, to make sure the top device is the connected // device. + boolean needToHandleMutingExpectedDevice = + hasMutingExpectedDevice() && !isCurrentConnectedDeviceRemote(); + final MediaDevice connectedMediaDevice = + needToHandleMutingExpectedDevice ? null + : getCurrentConnectedMediaDevice(); if (mMediaItemList.isEmpty()) { - boolean needToHandleMutingExpectedDevice = - hasMutingExpectedDevice() && !isCurrentConnectedDeviceRemote(); - final MediaDevice connectedMediaDevice = - needToHandleMutingExpectedDevice ? null - : getCurrentConnectedMediaDevice(); if (connectedMediaDevice == null) { if (DEBUG) { Log.d(TAG, "No connected media device or muting expected device exist."); } - if (needToHandleMutingExpectedDevice) { - for (MediaDevice device : devices) { - if (device.isMutingExpectedDevice()) { - mMediaItemList.add(0, new MediaItem(device)); - mMediaItemList.add(1, new MediaItem(mContext.getString( - R.string.media_output_group_title_speakers_and_displays), - MediaItem.MediaItemType.TYPE_GROUP_DIVIDER)); - } else { - mMediaItemList.add(new MediaItem(device)); - } - } - mMediaItemList.add(new MediaItem()); - } else { - mMediaItemList.addAll( - devices.stream().map(MediaItem::new).collect(Collectors.toList())); - categorizeMediaItems(null); - } + categorizeMediaItems(null, devices, needToHandleMutingExpectedDevice); return; } // selected device exist - for (MediaDevice device : devices) { - if (TextUtils.equals(device.getId(), connectedMediaDevice.getId())) { - mMediaItemList.add(0, new MediaItem(device)); - } else { - mMediaItemList.add(new MediaItem(device)); - } - } - categorizeMediaItems(connectedMediaDevice); + categorizeMediaItems(connectedMediaDevice, devices, false); return; } // To keep the same list order @@ -708,31 +685,46 @@ public class MediaOutputController implements LocalMediaManager.DeviceCallback, } } - private void categorizeMediaItems(MediaDevice connectedMediaDevice) { + private void categorizeMediaItems(MediaDevice connectedMediaDevice, List<MediaDevice> devices, + boolean needToHandleMutingExpectedDevice) { synchronized (mMediaDevicesLock) { Set<String> selectedDevicesIds = getSelectedMediaDevice().stream().map( MediaDevice::getId).collect(Collectors.toSet()); if (connectedMediaDevice != null) { selectedDevicesIds.add(connectedMediaDevice.getId()); } - int latestSelected = 1; - for (MediaItem item : mMediaItemList) { - if (item.getMediaDevice().isPresent()) { - MediaDevice device = item.getMediaDevice().get(); - if (selectedDevicesIds.contains(device.getId())) { - latestSelected = mMediaItemList.indexOf(item) + 1; - } else { - mMediaItemList.add(latestSelected, new MediaItem(mContext.getString( - R.string.media_output_group_title_speakers_and_displays), - MediaItem.MediaItemType.TYPE_GROUP_DIVIDER)); - break; + boolean suggestedDeviceAdded = false; + boolean displayGroupAdded = false; + for (MediaDevice device : devices) { + if (needToHandleMutingExpectedDevice && device.isMutingExpectedDevice()) { + mMediaItemList.add(0, new MediaItem(device)); + } else if (!needToHandleMutingExpectedDevice && selectedDevicesIds.contains( + device.getId())) { + mMediaItemList.add(0, new MediaItem(device)); + } else { + if (device.isSuggestedDevice() && !suggestedDeviceAdded) { + attachGroupDivider(mContext.getString( + R.string.media_output_group_title_suggested_device)); + suggestedDeviceAdded = true; + } else if (!device.isSuggestedDevice() && !displayGroupAdded) { + attachGroupDivider(mContext.getString( + R.string.media_output_group_title_speakers_and_displays)); + displayGroupAdded = true; } + mMediaItemList.add(new MediaItem(device)); } } mMediaItemList.add(new MediaItem()); } } + private void attachGroupDivider(String title) { + synchronized (mMediaDevicesLock) { + mMediaItemList.add( + new MediaItem(title, MediaItem.MediaItemType.TYPE_GROUP_DIVIDER)); + } + } + private void attachRangeInfo(List<MediaDevice> devices) { for (MediaDevice mediaDevice : devices) { if (mNearbyDeviceInfoMap.containsKey(mediaDevice.getId())) { diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt index 9f44d984124f..935f38de2e4f 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt @@ -150,7 +150,12 @@ constructor( logger: MediaTttLogger<ChipbarInfo>, ): ChipbarInfo { val packageName = routeInfo.clientPackageName - val otherDeviceName = routeInfo.name.toString() + val otherDeviceName = + if (routeInfo.name.isBlank()) { + context.getString(R.string.media_ttt_default_device_type) + } else { + routeInfo.name.toString() + } return ChipbarInfo( // Display the app's icon as the start icon diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt index 6dd60d043a06..8356440714e6 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt @@ -57,7 +57,9 @@ constructor( * If the keyguard is locked, notes will open as a full screen experience. A locked device has * no contextual information which let us use the whole screen space available. * - * If no in multi-window or the keyguard is unlocked, notes will open as a floating experience. + * If no in multi-window or the keyguard is unlocked, notes will open as a bubble OR it will be + * collapsed if the notes bubble is already opened. + * * That will let users open other apps in full screen, and take contextual notes. */ fun showNoteTask(isInMultiWindowMode: Boolean = false) { @@ -75,7 +77,7 @@ constructor( context.startActivity(intent) } else { // TODO(b/254606432): Should include Intent.EXTRA_FLOATING_WINDOW_MODE parameter. - bubbles.showAppBubble(intent) + bubbles.showOrHideAppBubble(intent) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java index da18b5734e81..5ef71269e84b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java @@ -67,6 +67,7 @@ import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.policy.BrightnessMirrorController; import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler; import com.android.systemui.util.LifecycleFragment; +import com.android.systemui.util.Utils; import java.io.PrintWriter; import java.util.Arrays; @@ -682,7 +683,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca } else { mQsMediaHost.setSquishFraction(mSquishinessFraction); } - + updateMediaPositions(); } private void setAlphaAnimationProgress(float progress) { @@ -757,6 +758,22 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca - mQSPanelController.getPaddingBottom()); } + private void updateMediaPositions() { + if (Utils.useQsMediaPlayer(getContext())) { + View hostView = mQsMediaHost.getHostView(); + // Make sure the media appears a bit from the top to make it look nicer + if (mLastQSExpansion > 0 && !isKeyguardState() && !mQqsMediaHost.getVisible() + && !mQSPanelController.shouldUseHorizontalLayout() && !mInSplitShade) { + float interpolation = 1.0f - mLastQSExpansion; + interpolation = Interpolators.ACCELERATE.getInterpolation(interpolation); + float translationY = -hostView.getHeight() * 1.3f * interpolation; + hostView.setTranslationY(translationY); + } else { + hostView.setTranslationY(0); + } + } + } + private boolean headerWillBeAnimating() { return mStatusBarState == KEYGUARD && mShowCollapsedOnKeyguard && !isKeyguardState(); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java index 7bb672ce5880..e85d0a32cfa8 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java @@ -372,18 +372,18 @@ public abstract class QSPanelControllerBase<T extends QSPanel> extends ViewContr if (mUsingHorizontalLayout) { // Only height remaining parameters.getDisappearSize().set(0.0f, 0.4f); - // Disappearing on the right side on the bottom - parameters.getGonePivot().set(1.0f, 1.0f); + // Disappearing on the right side on the top + parameters.getGonePivot().set(1.0f, 0.0f); // translating a bit horizontal parameters.getContentTranslationFraction().set(0.25f, 1.0f); parameters.setDisappearEnd(0.6f); } else { // Only width remaining parameters.getDisappearSize().set(1.0f, 0.0f); - // Disappearing on the bottom - parameters.getGonePivot().set(0.0f, 1.0f); + // Disappearing on the top + parameters.getGonePivot().set(0.0f, 0.0f); // translating a bit vertical - parameters.getContentTranslationFraction().set(0.0f, 1.05f); + parameters.getContentTranslationFraction().set(0.0f, 1f); parameters.setDisappearEnd(0.95f); } parameters.setFadeStartPosition(0.95f); diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java index 79fcc7d81372..17124901e4de 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java +++ b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizer.java @@ -24,6 +24,7 @@ import android.util.AttributeSet; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.Menu; +import android.view.MenuItem; import android.view.View; import android.widget.LinearLayout; import android.widget.Toolbar; @@ -74,8 +75,8 @@ public class QSCustomizer extends LinearLayout { toolbar.setNavigationIcon( getResources().getDrawable(value.resourceId, mContext.getTheme())); - toolbar.getMenu().add(Menu.NONE, MENU_RESET, 0, - mContext.getString(com.android.internal.R.string.reset)); + toolbar.getMenu().add(Menu.NONE, MENU_RESET, 0, com.android.internal.R.string.reset) + .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); toolbar.setTitle(R.string.qs_edit); mRecyclerView = findViewById(android.R.id.list); mTransparentView = findViewById(R.id.customizer_transparent_view); diff --git a/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt b/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt index 7fc0a5f6d4bf..e406be1ea0a3 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt @@ -175,9 +175,10 @@ class LargeScreenShadeHeaderController @Inject constructor( */ var shadeExpandedFraction = -1f set(value) { - if (visible && field != value) { + if (field != value) { header.alpha = ShadeInterpolation.getContentAlpha(value) field = value + updateVisibility() } } @@ -331,6 +332,9 @@ class LargeScreenShadeHeaderController @Inject constructor( .setDuration(duration) .alpha(if (show) 0f else 1f) .setInterpolator(if (show) Interpolators.ALPHA_OUT else Interpolators.ALPHA_IN) + .setUpdateListener { + updateVisibility() + } .start() } @@ -414,7 +418,7 @@ class LargeScreenShadeHeaderController @Inject constructor( private fun updateVisibility() { val visibility = if (!largeScreenActive && !combinedHeaders || qsDisabled) { View.GONE - } else if (qsVisible) { + } else if (qsVisible && header.alpha > 0f) { View.VISIBLE } else { View.INVISIBLE diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java index 96a61695ded1..0f5213373cb9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java @@ -41,6 +41,7 @@ import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewCont import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_USER_LOCKED; import static com.android.systemui.keyguard.ScreenLifecycle.SCREEN_ON; import static com.android.systemui.plugins.FalsingManager.LOW_PENALTY; +import static com.android.systemui.plugins.log.LogLevel.ERROR; import android.app.admin.DevicePolicyManager; import android.content.BroadcastReceiver; @@ -1044,7 +1045,7 @@ public class KeyguardIndicationController { mChargingTimeRemaining = mPowerPluggedIn ? mBatteryInfo.computeChargeTimeRemaining() : -1; } catch (RemoteException e) { - mKeyguardLogger.logException(e, "Error calling IBatteryStats"); + mKeyguardLogger.log(TAG, ERROR, "Error calling IBatteryStats", e); mChargingTimeRemaining = -1; } updateDeviceEntryIndication(!wasPluggedIn && mPowerPluggedInWired); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java index 8f9365cd4dc4..99081e98c4a3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java @@ -65,8 +65,6 @@ import com.android.systemui.statusbar.policy.RemoteInputView; import com.android.systemui.util.DumpUtilsKt; import com.android.systemui.util.ListenerSet; -import dagger.Lazy; - import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; @@ -74,6 +72,8 @@ import java.util.Objects; import java.util.Optional; import java.util.function.Consumer; +import dagger.Lazy; + /** * Class for handling remote input state over a set of notifications. This class handles things * like keeping notifications temporarily that were cancelled as a response to a remote input @@ -465,8 +465,7 @@ public class NotificationRemoteInputManager implements Dumpable { riv.getController().setRemoteInput(input); riv.getController().setRemoteInputs(inputs); riv.getController().setEditedSuggestionInfo(editedSuggestionInfo); - ViewGroup parent = view.getParent() != null ? (ViewGroup) view.getParent() : null; - riv.focusAnimated(parent); + riv.focusAnimated(); if (userMessageContent != null) { riv.setEditTextContent(userMessageContent); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationViewController.java index c4961029dc33..b084a765956d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationViewController.java @@ -109,7 +109,7 @@ public class ActivatableNotificationViewController return true; } if (ev.getAction() == MotionEvent.ACTION_UP) { - mView.setLastActionUpTime(SystemClock.uptimeMillis()); + mView.setLastActionUpTime(ev.getEventTime()); } // With a11y, just do nothing. if (mAccessibilityManager.isTouchExplorationEnabled()) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java index 8d48d738f0f2..9b93d7b9e1d0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java @@ -1431,6 +1431,22 @@ public class NotificationChildrenContainer extends ViewGroup @Override public void applyRoundnessAndInvalidate() { boolean last = true; + if (mUseRoundnessSourceTypes) { + if (mNotificationHeaderWrapper != null) { + mNotificationHeaderWrapper.requestTopRoundness( + /* value = */ getTopRoundness(), + /* sourceType = */ FROM_PARENT, + /* animate = */ false + ); + } + if (mNotificationHeaderWrapperLowPriority != null) { + mNotificationHeaderWrapperLowPriority.requestTopRoundness( + /* value = */ getTopRoundness(), + /* sourceType = */ FROM_PARENT, + /* animate = */ false + ); + } + } for (int i = mAttachedChildren.size() - 1; i >= 0; i--) { ExpandableNotificationRow child = mAttachedChildren.get(i); if (child.getVisibility() == View.GONE) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index ca1e397f930a..356ddfa2d482 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -1811,9 +1811,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable @Override @ShadeViewRefactor(RefactorComponent.COORDINATOR) public WindowInsets onApplyWindowInsets(WindowInsets insets) { - mBottomInset = insets.getSystemWindowInsetBottom() - + insets.getInsets(WindowInsets.Type.ime()).bottom; - + mBottomInset = insets.getInsets(WindowInsets.Type.ime()).bottom; mWaterfallTopInset = 0; final DisplayCutout cutout = insets.getDisplayCutout(); if (cutout != null) { @@ -2262,7 +2260,11 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable @ShadeViewRefactor(RefactorComponent.COORDINATOR) private int getImeInset() { - return Math.max(0, mBottomInset - (getRootView().getHeight() - getHeight())); + // The NotificationStackScrollLayout does not extend all the way to the bottom of the + // display. Therefore, subtract that space from the mBottomInset, in order to only include + // the portion of the bottom inset that actually overlaps the NotificationStackScrollLayout. + return Math.max(0, mBottomInset + - (getRootView().getHeight() - getHeight() - getLocationOnScreen()[1])); } /** @@ -2970,12 +2972,19 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable childInGroup = (ExpandableNotificationRow) requestedView; requestedView = requestedRow = childInGroup.getNotificationParent(); } - int position = 0; + final float scrimTopPadding = mAmbientState.isOnKeyguard() ? 0 : mMinimumPaddings; + int position = (int) scrimTopPadding; + int visibleIndex = -1; + ExpandableView lastVisibleChild = null; for (int i = 0; i < getChildCount(); i++) { ExpandableView child = getChildAtIndex(i); boolean notGone = child.getVisibility() != View.GONE; + if (notGone) visibleIndex++; if (notGone && !child.hasNoContentHeight()) { - if (position != 0) { + if (position != scrimTopPadding) { + if (lastVisibleChild != null) { + position += calculateGapHeight(lastVisibleChild, child, visibleIndex); + } position += mPaddingBetweenElements; } } @@ -2987,6 +2996,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } if (notGone) { position += getIntrinsicHeight(child); + lastVisibleChild = child; } } return 0; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java index 9a8c5d709f3a..9f3836105a95 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java @@ -18,6 +18,8 @@ package com.android.systemui.statusbar.phone; import static android.app.StatusBarManager.SESSION_KEYGUARD; +import static com.android.systemui.keyguard.WakefulnessLifecycle.UNKNOWN_LAST_WAKE_TIME; + import android.annotation.IntDef; import android.content.res.Resources; import android.hardware.biometrics.BiometricFaceConstants; @@ -27,7 +29,6 @@ import android.hardware.fingerprint.FingerprintManager; import android.metrics.LogMaker; import android.os.Handler; import android.os.PowerManager; -import android.os.SystemClock; import android.os.Trace; import androidx.annotation.Nullable; @@ -59,6 +60,7 @@ import com.android.systemui.statusbar.NotificationMediaManager; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.policy.KeyguardStateController; +import com.android.systemui.util.time.SystemClock; import java.io.PrintWriter; import java.lang.annotation.Retention; @@ -75,6 +77,7 @@ import javax.inject.Inject; */ @SysUISingleton public class BiometricUnlockController extends KeyguardUpdateMonitorCallback implements Dumpable { + private static final long RECENT_POWER_BUTTON_PRESS_THRESHOLD_MS = 400L; private static final long BIOMETRIC_WAKELOCK_TIMEOUT_MS = 15 * 1000; private static final String BIOMETRIC_WAKE_LOCK_NAME = "wake-and-unlock:wakelock"; private static final UiEventLogger UI_EVENT_LOGGER = new UiEventLoggerImpl(); @@ -165,9 +168,11 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp private final MetricsLogger mMetricsLogger; private final AuthController mAuthController; private final StatusBarStateController mStatusBarStateController; + private final WakefulnessLifecycle mWakefulnessLifecycle; private final LatencyTracker mLatencyTracker; private final VibratorHelper mVibratorHelper; private final BiometricUnlockLogger mLogger; + private final SystemClock mSystemClock; private long mLastFpFailureUptimeMillis; private int mNumConsecutiveFpFailures; @@ -272,13 +277,16 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp SessionTracker sessionTracker, LatencyTracker latencyTracker, ScreenOffAnimationController screenOffAnimationController, - VibratorHelper vibrator) { + VibratorHelper vibrator, + SystemClock systemClock + ) { mPowerManager = powerManager; mUpdateMonitor = keyguardUpdateMonitor; mUpdateMonitor.registerCallback(this); mMediaManager = notificationMediaManager; mLatencyTracker = latencyTracker; - wakefulnessLifecycle.addObserver(mWakefulnessObserver); + mWakefulnessLifecycle = wakefulnessLifecycle; + mWakefulnessLifecycle.addObserver(mWakefulnessObserver); screenLifecycle.addObserver(mScreenObserver); mNotificationShadeWindowController = notificationShadeWindowController; @@ -297,6 +305,7 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp mScreenOffAnimationController = screenOffAnimationController; mVibratorHelper = vibrator; mLogger = biometricUnlockLogger; + mSystemClock = systemClock; dumpManager.registerDumpable(getClass().getName(), this); } @@ -420,8 +429,11 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp Runnable wakeUp = ()-> { if (!wasDeviceInteractive || mUpdateMonitor.isDreaming()) { mLogger.i("bio wakelock: Authenticated, waking up..."); - mPowerManager.wakeUp(SystemClock.uptimeMillis(), PowerManager.WAKE_REASON_BIOMETRIC, - "android.policy:BIOMETRIC"); + mPowerManager.wakeUp( + mSystemClock.uptimeMillis(), + PowerManager.WAKE_REASON_BIOMETRIC, + "android.policy:BIOMETRIC" + ); } Trace.beginSection("release wake-and-unlock"); releaseBiometricWakeLock(); @@ -652,7 +664,7 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp startWakeAndUnlock(MODE_ONLY_WAKE); } else if (biometricSourceType == BiometricSourceType.FINGERPRINT && mUpdateMonitor.isUdfpsSupported()) { - long currUptimeMillis = SystemClock.uptimeMillis(); + long currUptimeMillis = mSystemClock.uptimeMillis(); if (currUptimeMillis - mLastFpFailureUptimeMillis < mConsecutiveFpFailureThreshold) { mNumConsecutiveFpFailures += 1; } else { @@ -700,12 +712,26 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp cleanup(); } - //these haptics are for device-entry only + // these haptics are for device-entry only private void vibrateSuccess(BiometricSourceType type) { + if (mAuthController.isSfpsEnrolled(KeyguardUpdateMonitor.getCurrentUser()) + && lastWakeupFromPowerButtonWithinHapticThreshold()) { + mLogger.d("Skip auth success haptic. Power button was recently pressed."); + return; + } mVibratorHelper.vibrateAuthSuccess( getClass().getSimpleName() + ", type =" + type + "device-entry::success"); } + private boolean lastWakeupFromPowerButtonWithinHapticThreshold() { + final boolean lastWakeupFromPowerButton = mWakefulnessLifecycle.getLastWakeReason() + == PowerManager.WAKE_REASON_POWER_BUTTON; + return lastWakeupFromPowerButton + && mWakefulnessLifecycle.getLastWakeTime() != UNKNOWN_LAST_WAKE_TIME + && mSystemClock.uptimeMillis() - mWakefulnessLifecycle.getLastWakeTime() + < RECENT_POWER_BUTTON_PRESS_THRESHOLD_MS; + } + private void vibrateError(BiometricSourceType type) { mVibratorHelper.vibrateAuthError( getClass().getSimpleName() + ", type =" + type + "device-entry::error"); @@ -798,7 +824,7 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp if (mUpdateMonitor.isUdfpsSupported()) { pw.print(" mNumConsecutiveFpFailures="); pw.println(mNumConsecutiveFpFailures); pw.print(" time since last failure="); - pw.println(SystemClock.uptimeMillis() - mLastFpFailureUptimeMillis); + pw.println(mSystemClock.uptimeMillis() - mLastFpFailureUptimeMillis); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index 22ebcab777aa..4b56594e082d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -4247,8 +4247,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { @Override public void onDozeAmountChanged(float linear, float eased) { - if (mFeatureFlags.isEnabled(Flags.LOCKSCREEN_ANIMATIONS) - && !mFeatureFlags.isEnabled(Flags.LIGHT_REVEAL_MIGRATION) + if (!mFeatureFlags.isEnabled(Flags.LIGHT_REVEAL_MIGRATION) && !(mLightRevealScrim.getRevealEffect() instanceof CircleReveal)) { mLightRevealScrim.setRevealAmount(1f - linear); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java index de7b152adaab..0446cefb10dc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java @@ -44,10 +44,9 @@ import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.doze.AlwaysOnDisplayPolicy; import com.android.systemui.doze.DozeScreenState; import com.android.systemui.dump.DumpManager; -import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.flags.Flags; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.policy.BatteryController; +import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.DevicePostureController; import com.android.systemui.tuner.TunerService; @@ -82,7 +81,6 @@ public class DozeParameters implements private final AlwaysOnDisplayPolicy mAlwaysOnPolicy; private final Resources mResources; private final BatteryController mBatteryController; - private final FeatureFlags mFeatureFlags; private final ScreenOffAnimationController mScreenOffAnimationController; private final FoldAodAnimationController mFoldAodAnimationController; private final UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController; @@ -125,7 +123,6 @@ public class DozeParameters implements BatteryController batteryController, TunerService tunerService, DumpManager dumpManager, - FeatureFlags featureFlags, ScreenOffAnimationController screenOffAnimationController, Optional<SysUIUnfoldComponent> sysUiUnfoldComponent, UnlockedScreenOffAnimationController unlockedScreenOffAnimationController, @@ -141,7 +138,6 @@ public class DozeParameters implements mControlScreenOffAnimation = !getDisplayNeedsBlanking(); mPowerManager = powerManager; mPowerManager.setDozeAfterScreenOff(!mControlScreenOffAnimation); - mFeatureFlags = featureFlags; mScreenOffAnimationController = screenOffAnimationController; mUnlockedScreenOffAnimationController = unlockedScreenOffAnimationController; @@ -162,6 +158,13 @@ public class DozeParameters implements SettingsObserver quickPickupSettingsObserver = new SettingsObserver(context, handler); quickPickupSettingsObserver.observe(); + + batteryController.addCallback(new BatteryStateChangeCallback() { + @Override + public void onPowerSaveChanged(boolean isPowerSave) { + dispatchAlwaysOnEvent(); + } + }); } private void updateQuickPickupEnabled() { @@ -300,13 +303,10 @@ public class DozeParameters implements /** * Whether we're capable of controlling the screen off animation if we want to. This isn't - * possible if AOD isn't even enabled or if the flag is disabled, or if the display needs - * blanking. + * possible if AOD isn't even enabled or if the display needs blanking. */ public boolean canControlUnlockedScreenOff() { - return getAlwaysOn() - && mFeatureFlags.isEnabled(Flags.LOCKSCREEN_ANIMATIONS) - && !getDisplayNeedsBlanking(); + return getAlwaysOn() && !getDisplayNeedsBlanking(); } /** @@ -424,9 +424,7 @@ public class DozeParameters implements updateControlScreenOff(); } - for (Callback callback : mCallbacks) { - callback.onAlwaysOnChange(); - } + dispatchAlwaysOnEvent(); mScreenOffAnimationController.onAlwaysOnChanged(getAlwaysOn()); } @@ -463,6 +461,12 @@ public class DozeParameters implements pw.print("isQuickPickupEnabled(): "); pw.println(isQuickPickupEnabled()); } + private void dispatchAlwaysOnEvent() { + for (Callback callback : mCallbacks) { + callback.onAlwaysOnChange(); + } + } + private boolean getPostureSpecificBool( int[] postureMapping, boolean defaultSensorBool, @@ -477,7 +481,8 @@ public class DozeParameters implements return bool; } - interface Callback { + /** Callbacks for doze parameter related information */ + public interface Callback { /** * Invoked when the value of getAlwaysOn may have changed. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java index 348357445223..4ad319969eaf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java @@ -45,6 +45,7 @@ import com.android.systemui.R; import com.android.systemui.animation.Interpolators; import com.android.systemui.battery.BatteryMeterViewController; import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.plugins.log.LogLevel; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.shade.NotificationPanelViewController; import com.android.systemui.statusbar.CommandQueue; @@ -76,6 +77,7 @@ import javax.inject.Inject; /** View Controller for {@link com.android.systemui.statusbar.phone.KeyguardStatusBarView}. */ public class KeyguardStatusBarViewController extends ViewController<KeyguardStatusBarView> { + private static final String TAG = "KeyguardStatusBarViewController"; private static final AnimationProperties KEYGUARD_HUN_PROPERTIES = new AnimationProperties().setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); @@ -422,7 +424,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat /** Animate the keyguard status bar in. */ public void animateKeyguardStatusBarIn() { - mLogger.d("animating status bar in"); + mLogger.log(TAG, LogLevel.DEBUG, "animating status bar in"); if (mDisableStateTracker.isDisabled()) { // If our view is disabled, don't allow us to animate in. return; @@ -438,7 +440,7 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat /** Animate the keyguard status bar out. */ public void animateKeyguardStatusBarOut(long startDelay, long duration) { - mLogger.d("animating status bar out"); + mLogger.log(TAG, LogLevel.DEBUG, "animating status bar out"); ValueAnimator anim = ValueAnimator.ofFloat(mView.getAlpha(), 0f); anim.addUpdateListener(mAnimatorUpdateListener); anim.setStartDelay(startDelay); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemBarAttributesListener.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemBarAttributesListener.kt index 08599c27f4b8..fbe374c32952 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemBarAttributesListener.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemBarAttributesListener.kt @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.phone +import android.view.InsetsFlags +import android.view.ViewDebug import android.view.WindowInsets.Type.InsetsType import android.view.WindowInsetsController.Appearance import android.view.WindowInsetsController.Behavior @@ -148,4 +150,20 @@ private data class SystemBarAttributesParams( ) { val letterboxesArray = letterboxes.toTypedArray() val appearanceRegionsArray = appearanceRegions.toTypedArray() + override fun toString(): String { + val appearanceToString = + ViewDebug.flagsToString(InsetsFlags::class.java, "appearance", appearance) + return """SystemBarAttributesParams( + displayId=$displayId, + appearance=$appearanceToString, + appearanceRegions=$appearanceRegions, + navbarColorManagedByIme=$navbarColorManagedByIme, + behavior=$behavior, + requestedVisibleTypes=$requestedVisibleTypes, + packageName='$packageName', + letterboxes=$letterboxes, + letterboxesArray=${letterboxesArray.contentToString()}, + appearanceRegionsArray=${appearanceRegionsArray.contentToString()} + )""".trimMargin() + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt index 0e164e7ee859..8ac12379e59e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt @@ -149,7 +149,7 @@ constructor( } private fun createDemoMobileConnectionRepo(subId: Int): DemoMobileConnectionRepository { - val tableLogBuffer = logFactory.create("DemoMobileConnectionLog [$subId]", 100) + val tableLogBuffer = logFactory.getOrCreate("DemoMobileConnectionLog [$subId]", 100) return DemoMobileConnectionRepository( subId, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt index 0fa0fea0bebf..4e42f9b31e5c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt @@ -308,7 +308,7 @@ class MobileConnectionRepositoryImpl( networkNameSeparator: String, globalMobileDataSettingChangedEvent: Flow<Unit>, ): MobileConnectionRepository { - val mobileLogger = logFactory.create(tableBufferLogName(subId), 100) + val mobileLogger = logFactory.getOrCreate(tableBufferLogName(subId), 100) return MobileConnectionRepositoryImpl( context, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java index c9ed0cb4155d..f8c17e8c8379 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java @@ -109,6 +109,8 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene private static final long FOCUS_ANIMATION_FADE_IN_DELAY = 33; private static final long FOCUS_ANIMATION_FADE_IN_DURATION = 83; private static final float FOCUS_ANIMATION_MIN_SCALE = 0.5f; + private static final long DEFOCUS_ANIMATION_FADE_OUT_DELAY = 120; + private static final long DEFOCUS_ANIMATION_CROSSFADE_DELAY = 180; public final Object mToken = new Object(); @@ -421,7 +423,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene } @VisibleForTesting - void onDefocus(boolean animate, boolean logClose) { + void onDefocus(boolean animate, boolean logClose, @Nullable Runnable doAfterDefocus) { mController.removeRemoteInput(mEntry, mToken); mEntry.remoteInputText = mEditText.getText(); @@ -431,18 +433,20 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene ViewGroup parent = (ViewGroup) getParent(); if (animate && parent != null && mIsFocusAnimationFlagActive) { - ViewGroup grandParent = (ViewGroup) parent.getParent(); ViewGroupOverlay overlay = parent.getOverlay(); + View actionsContainer = getActionsContainerLayout(); + int actionsContainerHeight = + actionsContainer != null ? actionsContainer.getHeight() : 0; // After adding this RemoteInputView to the overlay of the parent (and thus removing // it from the parent itself), the parent will shrink in height. This causes the // overlay to be moved. To correct the position of the overlay we need to offset it. - int overlayOffsetY = getMaxSiblingHeight() - getHeight(); + int overlayOffsetY = actionsContainerHeight - getHeight(); overlay.add(this); if (grandParent != null) grandParent.setClipChildren(false); - Animator animator = getDefocusAnimator(overlayOffsetY); + Animator animator = getDefocusAnimator(actionsContainer, overlayOffsetY); View self = this; animator.addListener(new AnimatorListenerAdapter() { @Override @@ -454,8 +458,12 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene if (mWrapper != null) { mWrapper.setRemoteInputVisible(false); } + if (doAfterDefocus != null) { + doAfterDefocus.run(); + } } }); + if (actionsContainer != null) actionsContainer.setAlpha(0f); animator.start(); } else if (animate && mRevealParams != null && mRevealParams.radius > 0) { @@ -474,6 +482,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene reveal.start(); } else { setVisibility(GONE); + if (doAfterDefocus != null) doAfterDefocus.run(); if (mWrapper != null) { mWrapper.setRemoteInputVisible(false); } @@ -596,10 +605,8 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene /** * Focuses the RemoteInputView and animates its appearance - * - * @param crossFadeView view that will be crossfaded during the appearance animation */ - public void focusAnimated(View crossFadeView) { + public void focusAnimated() { if (!mIsFocusAnimationFlagActive && getVisibility() != VISIBLE && mRevealParams != null) { android.animation.Animator animator = mRevealParams.createCircularRevealAnimator(this); @@ -609,7 +616,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene } else if (mIsFocusAnimationFlagActive && getVisibility() != VISIBLE) { mIsAnimatingAppearance = true; setAlpha(0f); - Animator focusAnimator = getFocusAnimator(crossFadeView); + Animator focusAnimator = getFocusAnimator(getActionsContainerLayout()); focusAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation, boolean isReverse) { @@ -661,6 +668,23 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene } private void reset() { + if (mIsFocusAnimationFlagActive) { + mProgressBar.setVisibility(INVISIBLE); + mResetting = true; + mSending = false; + onDefocus(true /* animate */, false /* logClose */, () -> { + mEntry.remoteInputTextWhenReset = SpannedString.valueOf(mEditText.getText()); + mEditText.getText().clear(); + mEditText.setEnabled(isAggregatedVisible()); + mSendButton.setVisibility(VISIBLE); + mController.removeSpinning(mEntry.getKey(), mToken); + updateSendButton(); + setAttachment(null); + mResetting = false; + }); + return; + } + mResetting = true; mSending = false; mEntry.remoteInputTextWhenReset = SpannedString.valueOf(mEditText.getText()); @@ -671,7 +695,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene mProgressBar.setVisibility(INVISIBLE); mController.removeSpinning(mEntry.getKey(), mToken); updateSendButton(); - onDefocus(false /* animate */, false /* logClose */); + onDefocus(false /* animate */, false /* logClose */, null /* doAfterDefocus */); setAttachment(null); mResetting = false; @@ -825,23 +849,22 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene } /** - * @return max sibling height (0 in case of no siblings) + * @return action button container view (i.e. ViewGroup containing Reply button etc.) */ - public int getMaxSiblingHeight() { + public View getActionsContainerLayout() { ViewGroup parentView = (ViewGroup) getParent(); - int maxHeight = 0; - if (parentView == null) return 0; - for (int i = 0; i < parentView.getChildCount(); i++) { - View siblingView = parentView.getChildAt(i); - if (siblingView != this) maxHeight = Math.max(maxHeight, siblingView.getHeight()); - } - return maxHeight; + if (parentView == null) return null; + return parentView.findViewById(com.android.internal.R.id.actions_container_layout); } /** * Creates an animator for the focus animation. + * + * @param fadeOutView View that will be faded out during the focus animation. */ - private Animator getFocusAnimator(View crossFadeView) { + private Animator getFocusAnimator(@Nullable View fadeOutView) { + final AnimatorSet animatorSet = new AnimatorSet(); + final Animator alphaAnimator = ObjectAnimator.ofFloat(this, View.ALPHA, 0f, 1f); alphaAnimator.setStartDelay(FOCUS_ANIMATION_FADE_IN_DELAY); alphaAnimator.setDuration(FOCUS_ANIMATION_FADE_IN_DURATION); @@ -854,30 +877,36 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene scaleAnimator.setDuration(FOCUS_ANIMATION_TOTAL_DURATION); scaleAnimator.setInterpolator(InterpolatorsAndroidX.FAST_OUT_SLOW_IN); - final Animator crossFadeViewAlphaAnimator = - ObjectAnimator.ofFloat(crossFadeView, View.ALPHA, 1f, 0f); - crossFadeViewAlphaAnimator.setDuration(FOCUS_ANIMATION_CROSSFADE_DURATION); - crossFadeViewAlphaAnimator.setInterpolator(InterpolatorsAndroidX.LINEAR); - alphaAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation, boolean isReverse) { - crossFadeView.setAlpha(1f); - } - }); - - final AnimatorSet animatorSet = new AnimatorSet(); - animatorSet.playTogether(alphaAnimator, scaleAnimator, crossFadeViewAlphaAnimator); + if (fadeOutView == null) { + animatorSet.playTogether(alphaAnimator, scaleAnimator); + } else { + final Animator fadeOutViewAlphaAnimator = + ObjectAnimator.ofFloat(fadeOutView, View.ALPHA, 1f, 0f); + fadeOutViewAlphaAnimator.setDuration(FOCUS_ANIMATION_CROSSFADE_DURATION); + fadeOutViewAlphaAnimator.setInterpolator(InterpolatorsAndroidX.LINEAR); + animatorSet.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation, boolean isReverse) { + fadeOutView.setAlpha(1f); + } + }); + animatorSet.playTogether(alphaAnimator, scaleAnimator, fadeOutViewAlphaAnimator); + } return animatorSet; } /** * Creates an animator for the defocus animation. * - * @param offsetY The RemoteInputView will be offset by offsetY during the animation + * @param fadeInView View that will be faded in during the defocus animation. + * @param offsetY The RemoteInputView will be offset by offsetY during the animation */ - private Animator getDefocusAnimator(int offsetY) { + private Animator getDefocusAnimator(@Nullable View fadeInView, int offsetY) { + final AnimatorSet animatorSet = new AnimatorSet(); + final Animator alphaAnimator = ObjectAnimator.ofFloat(this, View.ALPHA, 1f, 0f); - alphaAnimator.setDuration(FOCUS_ANIMATION_CROSSFADE_DURATION); + alphaAnimator.setDuration(FOCUS_ANIMATION_FADE_IN_DURATION); + alphaAnimator.setStartDelay(DEFOCUS_ANIMATION_FADE_OUT_DELAY); alphaAnimator.setInterpolator(InterpolatorsAndroidX.LINEAR); ValueAnimator scaleAnimator = ValueAnimator.ofFloat(1f, FOCUS_ANIMATION_MIN_SCALE); @@ -893,8 +922,17 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene } }); - final AnimatorSet animatorSet = new AnimatorSet(); - animatorSet.playTogether(alphaAnimator, scaleAnimator); + if (fadeInView == null) { + animatorSet.playTogether(alphaAnimator, scaleAnimator); + } else { + fadeInView.forceHasOverlappingRendering(false); + Animator fadeInViewAlphaAnimator = + ObjectAnimator.ofFloat(fadeInView, View.ALPHA, 0f, 1f); + fadeInViewAlphaAnimator.setDuration(FOCUS_ANIMATION_FADE_IN_DURATION); + fadeInViewAlphaAnimator.setInterpolator(InterpolatorsAndroidX.LINEAR); + fadeInViewAlphaAnimator.setStartDelay(DEFOCUS_ANIMATION_CROSSFADE_DELAY); + animatorSet.playTogether(alphaAnimator, scaleAnimator, fadeInViewAlphaAnimator); + } return animatorSet; } @@ -1011,7 +1049,8 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene if (isFocusable() && isEnabled()) { setInnerFocusable(false); if (mRemoteInputView != null) { - mRemoteInputView.onDefocus(animate, true /* logClose */); + mRemoteInputView + .onDefocus(animate, true /* logClose */, null /* doAfterDefocus */); } mShowImeOnInputConnection = false; } diff --git a/packages/SystemUI/src/com/android/systemui/stylus/StylusFirstUsageListener.kt b/packages/SystemUI/src/com/android/systemui/stylus/StylusFirstUsageListener.kt deleted file mode 100644 index 154c6e2e3158..000000000000 --- a/packages/SystemUI/src/com/android/systemui/stylus/StylusFirstUsageListener.kt +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.systemui.stylus - -import android.content.Context -import android.hardware.BatteryState -import android.hardware.input.InputManager -import android.os.Handler -import android.util.Log -import android.view.InputDevice -import androidx.annotation.VisibleForTesting -import com.android.systemui.CoreStartable -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags -import java.util.concurrent.Executor -import javax.inject.Inject - -/** - * A listener that detects when a stylus has first been used, by detecting 1) the presence of an - * internal SOURCE_STYLUS with a battery, or 2) any added SOURCE_STYLUS device with a bluetooth - * address. - */ -@SysUISingleton -class StylusFirstUsageListener -@Inject -constructor( - private val context: Context, - private val inputManager: InputManager, - private val stylusManager: StylusManager, - private val featureFlags: FeatureFlags, - @Background private val executor: Executor, - @Background private val handler: Handler, -) : CoreStartable, StylusManager.StylusCallback, InputManager.InputDeviceBatteryListener { - - // Set must be only accessed from the background handler, which is the same handler that - // runs the StylusManager callbacks. - private val internalStylusDeviceIds: MutableSet<Int> = mutableSetOf() - @VisibleForTesting var hasStarted = false - - override fun start() { - if (true) return // TODO(b/261826950): remove on main - if (hasStarted) return - if (!featureFlags.isEnabled(Flags.TRACK_STYLUS_EVER_USED)) return - if (inputManager.isStylusEverUsed(context)) return - if (!hostDeviceSupportsStylusInput()) return - - hasStarted = true - inputManager.inputDeviceIds.forEach(this::onStylusAdded) - stylusManager.registerCallback(this) - stylusManager.startListener() - } - - override fun onStylusAdded(deviceId: Int) { - if (!hasStarted) return - - val device = inputManager.getInputDevice(deviceId) ?: return - if (device.isExternal || !device.supportsSource(InputDevice.SOURCE_STYLUS)) return - - try { - inputManager.addInputDeviceBatteryListener(deviceId, executor, this) - internalStylusDeviceIds += deviceId - } catch (e: SecurityException) { - Log.e(TAG, "$e: Failed to register battery listener for $deviceId ${device.name}.") - } - } - - override fun onStylusRemoved(deviceId: Int) { - if (!hasStarted) return - - if (!internalStylusDeviceIds.contains(deviceId)) return - try { - inputManager.removeInputDeviceBatteryListener(deviceId, this) - internalStylusDeviceIds.remove(deviceId) - } catch (e: SecurityException) { - Log.e(TAG, "$e: Failed to remove registered battery listener for $deviceId.") - } - } - - override fun onStylusBluetoothConnected(deviceId: Int, btAddress: String) { - if (!hasStarted) return - - onRemoteDeviceFound() - } - - override fun onBatteryStateChanged( - deviceId: Int, - eventTimeMillis: Long, - batteryState: BatteryState - ) { - if (!hasStarted) return - - if (batteryState.isPresent) { - onRemoteDeviceFound() - } - } - - private fun onRemoteDeviceFound() { - inputManager.setStylusEverUsed(context, true) - cleanupListeners() - } - - private fun cleanupListeners() { - stylusManager.unregisterCallback(this) - handler.post { - internalStylusDeviceIds.forEach { - inputManager.removeInputDeviceBatteryListener(it, this) - } - } - } - - private fun hostDeviceSupportsStylusInput(): Boolean { - return inputManager.inputDeviceIds - .asSequence() - .mapNotNull { inputManager.getInputDevice(it) } - .any { it.supportsSource(InputDevice.SOURCE_STYLUS) && !it.isExternal } - } - - companion object { - private val TAG = StylusFirstUsageListener::class.simpleName.orEmpty() - } -} diff --git a/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt b/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt index 302d6a9ca1b7..235495cfa50d 100644 --- a/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt +++ b/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt @@ -18,6 +18,8 @@ package com.android.systemui.stylus import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice +import android.content.Context +import android.hardware.BatteryState import android.hardware.input.InputManager import android.os.Handler import android.util.ArrayMap @@ -25,6 +27,8 @@ import android.util.Log import android.view.InputDevice import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.Executor import javax.inject.Inject @@ -37,25 +41,37 @@ import javax.inject.Inject class StylusManager @Inject constructor( + private val context: Context, private val inputManager: InputManager, private val bluetoothAdapter: BluetoothAdapter?, @Background private val handler: Handler, @Background private val executor: Executor, -) : InputManager.InputDeviceListener, BluetoothAdapter.OnMetadataChangedListener { + private val featureFlags: FeatureFlags, +) : + InputManager.InputDeviceListener, + InputManager.InputDeviceBatteryListener, + BluetoothAdapter.OnMetadataChangedListener { private val stylusCallbacks: CopyOnWriteArrayList<StylusCallback> = CopyOnWriteArrayList() private val stylusBatteryCallbacks: CopyOnWriteArrayList<StylusBatteryCallback> = CopyOnWriteArrayList() // This map should only be accessed on the handler private val inputDeviceAddressMap: MutableMap<Int, String?> = ArrayMap() + // This variable should only be accessed on the handler + private var hasStarted: Boolean = false /** * Starts listening to InputManager InputDevice events. Will also load the InputManager snapshot * at time of starting. */ fun startListener() { - addExistingStylusToMap() - inputManager.registerInputDeviceListener(this, handler) + handler.post { + if (hasStarted) return@post + hasStarted = true + addExistingStylusToMap() + + inputManager.registerInputDeviceListener(this, handler) + } } /** Registers a StylusCallback to listen to stylus events. */ @@ -77,21 +93,30 @@ constructor( } override fun onInputDeviceAdded(deviceId: Int) { + if (!hasStarted) return + val device: InputDevice = inputManager.getInputDevice(deviceId) ?: return if (!device.supportsSource(InputDevice.SOURCE_STYLUS)) return + if (!device.isExternal) { + registerBatteryListener(deviceId) + } + // TODO(b/257936830): get address once input api available val btAddress: String? = null inputDeviceAddressMap[deviceId] = btAddress executeStylusCallbacks { cb -> cb.onStylusAdded(deviceId) } if (btAddress != null) { + onStylusUsed() onStylusBluetoothConnected(btAddress) executeStylusCallbacks { cb -> cb.onStylusBluetoothConnected(deviceId, btAddress) } } } override fun onInputDeviceChanged(deviceId: Int) { + if (!hasStarted) return + val device: InputDevice = inputManager.getInputDevice(deviceId) ?: return if (!device.supportsSource(InputDevice.SOURCE_STYLUS)) return @@ -112,7 +137,10 @@ constructor( } override fun onInputDeviceRemoved(deviceId: Int) { + if (!hasStarted) return + if (!inputDeviceAddressMap.contains(deviceId)) return + unregisterBatteryListener(deviceId) val btAddress: String? = inputDeviceAddressMap[deviceId] inputDeviceAddressMap.remove(deviceId) @@ -124,13 +152,14 @@ constructor( } override fun onMetadataChanged(device: BluetoothDevice, key: Int, value: ByteArray?) { - handler.post executeMetadataChanged@{ - if (key != BluetoothDevice.METADATA_MAIN_CHARGING || value == null) - return@executeMetadataChanged + handler.post { + if (!hasStarted) return@post + + if (key != BluetoothDevice.METADATA_MAIN_CHARGING || value == null) return@post val inputDeviceId: Int = inputDeviceAddressMap.filterValues { it == device.address }.keys.firstOrNull() - ?: return@executeMetadataChanged + ?: return@post val isCharging = String(value) == "true" @@ -140,6 +169,24 @@ constructor( } } + override fun onBatteryStateChanged( + deviceId: Int, + eventTimeMillis: Long, + batteryState: BatteryState + ) { + handler.post { + if (!hasStarted) return@post + + if (batteryState.isPresent) { + onStylusUsed() + } + + executeStylusBatteryCallbacks { cb -> + cb.onStylusUsiBatteryStateChanged(deviceId, eventTimeMillis, batteryState) + } + } + } + private fun onStylusBluetoothConnected(btAddress: String) { val device: BluetoothDevice = bluetoothAdapter?.getRemoteDevice(btAddress) ?: return try { @@ -158,6 +205,21 @@ constructor( } } + /** + * An InputDevice that supports [InputDevice.SOURCE_STYLUS] may still be present even when a + * physical stylus device has never been used. This method is run when 1) a USI stylus battery + * event happens, or 2) a bluetooth stylus is connected, as they are both indicators that a + * physical stylus device has actually been used. + */ + private fun onStylusUsed() { + if (true) return // TODO(b/261826950): remove on main + if (!featureFlags.isEnabled(Flags.TRACK_STYLUS_EVER_USED)) return + if (inputManager.isStylusEverUsed(context)) return + + inputManager.setStylusEverUsed(context, true) + executeStylusCallbacks { cb -> cb.onStylusFirstUsed() } + } + private fun executeStylusCallbacks(run: (cb: StylusCallback) -> Unit) { stylusCallbacks.forEach(run) } @@ -166,31 +228,69 @@ constructor( stylusBatteryCallbacks.forEach(run) } + private fun registerBatteryListener(deviceId: Int) { + try { + inputManager.addInputDeviceBatteryListener(deviceId, executor, this) + } catch (e: SecurityException) { + Log.e(TAG, "$e: Failed to register battery listener for $deviceId.") + } + } + + private fun unregisterBatteryListener(deviceId: Int) { + // If deviceId wasn't registered, the result is a no-op, so an "is registered" + // check is not needed. + try { + inputManager.removeInputDeviceBatteryListener(deviceId, this) + } catch (e: SecurityException) { + Log.e(TAG, "$e: Failed to remove registered battery listener for $deviceId.") + } + } + private fun addExistingStylusToMap() { for (deviceId: Int in inputManager.inputDeviceIds) { val device: InputDevice = inputManager.getInputDevice(deviceId) ?: continue if (device.supportsSource(InputDevice.SOURCE_STYLUS)) { // TODO(b/257936830): get address once input api available inputDeviceAddressMap[deviceId] = null + + if (!device.isExternal) { // TODO(b/263556967): add supportsUsi check once available + // For most devices, an active (non-bluetooth) stylus is represented by an + // internal InputDevice. This InputDevice will be present in InputManager + // before CoreStartables run, and will not be removed. + // In many cases, it reports the battery level of the stylus. + registerBatteryListener(deviceId) + } } } } - /** Callback interface to receive events from the StylusManager. */ + /** + * Callback interface to receive events from the StylusManager. All callbacks are run on the + * same background handler. + */ interface StylusCallback { fun onStylusAdded(deviceId: Int) {} fun onStylusRemoved(deviceId: Int) {} fun onStylusBluetoothConnected(deviceId: Int, btAddress: String) {} fun onStylusBluetoothDisconnected(deviceId: Int, btAddress: String) {} + fun onStylusFirstUsed() {} } - /** Callback interface to receive stylus battery events from the StylusManager. */ + /** + * Callback interface to receive stylus battery events from the StylusManager. All callbacks are + * runs on the same background handler. + */ interface StylusBatteryCallback { fun onStylusBluetoothChargingStateChanged( inputDeviceId: Int, btDevice: BluetoothDevice, isCharging: Boolean ) {} + fun onStylusUsiBatteryStateChanged( + deviceId: Int, + eventTimeMillis: Long, + batteryState: BatteryState, + ) {} } companion object { diff --git a/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerStartable.kt b/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerStartable.kt index 11233dda165c..14a9161ac291 100644 --- a/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerStartable.kt @@ -18,14 +18,11 @@ package com.android.systemui.stylus import android.hardware.BatteryState import android.hardware.input.InputManager -import android.util.Log import android.view.InputDevice import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags -import java.util.concurrent.Executor import javax.inject.Inject /** @@ -40,16 +37,7 @@ constructor( private val inputManager: InputManager, private val stylusUsiPowerUi: StylusUsiPowerUI, private val featureFlags: FeatureFlags, - @Background private val executor: Executor, -) : CoreStartable, StylusManager.StylusCallback, InputManager.InputDeviceBatteryListener { - - override fun onStylusAdded(deviceId: Int) { - val device = inputManager.getInputDevice(deviceId) ?: return - - if (!device.isExternal) { - registerBatteryListener(deviceId) - } - } +) : CoreStartable, StylusManager.StylusCallback, StylusManager.StylusBatteryCallback { override fun onStylusBluetoothConnected(deviceId: Int, btAddress: String) { stylusUsiPowerUi.refresh() @@ -59,15 +47,7 @@ constructor( stylusUsiPowerUi.refresh() } - override fun onStylusRemoved(deviceId: Int) { - val device = inputManager.getInputDevice(deviceId) ?: return - - if (!device.isExternal) { - unregisterBatteryListener(deviceId) - } - } - - override fun onBatteryStateChanged( + override fun onStylusUsiBatteryStateChanged( deviceId: Int, eventTimeMillis: Long, batteryState: BatteryState @@ -77,39 +57,19 @@ constructor( } } - private fun registerBatteryListener(deviceId: Int) { - try { - inputManager.addInputDeviceBatteryListener(deviceId, executor, this) - } catch (e: SecurityException) { - Log.e(TAG, "$e: Failed to register battery listener for $deviceId.") - } - } - - private fun unregisterBatteryListener(deviceId: Int) { - try { - inputManager.removeInputDeviceBatteryListener(deviceId, this) - } catch (e: SecurityException) { - Log.e(TAG, "$e: Failed to unregister battery listener for $deviceId.") - } - } - override fun start() { if (!featureFlags.isEnabled(Flags.ENABLE_USI_BATTERY_NOTIFICATIONS)) return - addBatteryListenerForInternalStyluses() + if (!hostDeviceSupportsStylusInput()) return stylusManager.registerCallback(this) stylusManager.startListener() } - private fun addBatteryListenerForInternalStyluses() { - // For most devices, an active stylus is represented by an internal InputDevice. - // This InputDevice will be present in InputManager before CoreStartables run, - // and will not be removed. In many cases, it reports the battery level of the stylus. - inputManager.inputDeviceIds + private fun hostDeviceSupportsStylusInput(): Boolean { + return inputManager.inputDeviceIds .asSequence() .mapNotNull { inputManager.getInputDevice(it) } - .filter { it.supportsSource(InputDevice.SOURCE_STYLUS) } - .forEach { onStylusAdded(it.id) } + .any { it.supportsSource(InputDevice.SOURCE_STYLUS) && !it.isExternal } } companion object { diff --git a/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerUI.kt b/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerUI.kt index 70a5b366263e..e8216576811a 100644 --- a/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerUI.kt +++ b/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerUI.kt @@ -123,13 +123,13 @@ constructor( .setSmallIcon(R.drawable.ic_power_low) .setDeleteIntent(getPendingBroadcast(ACTION_DISMISSED_LOW_BATTERY)) .setContentIntent(getPendingBroadcast(ACTION_CLICKED_LOW_BATTERY)) - .setContentTitle(context.getString(R.string.stylus_battery_low)) - .setContentText( + .setContentTitle( context.getString( - R.string.battery_low_percent_format, + R.string.stylus_battery_low_percentage, NumberFormat.getPercentInstance().format(batteryCapacity) ) ) + .setContentText(context.getString(R.string.stylus_battery_low_subtitle)) .setPriority(NotificationCompat.PRIORITY_DEFAULT) .setLocalOnly(true) .setAutoCancel(true) @@ -177,7 +177,7 @@ constructor( // https://source.chromium.org/chromium/chromium/src/+/main:ash/system/power/peripheral_battery_notifier.cc;l=41 private const val LOW_BATTERY_THRESHOLD = 0.16f - private val USI_NOTIFICATION_ID = R.string.stylus_battery_low + private val USI_NOTIFICATION_ID = R.string.stylus_battery_low_percentage private const val ACTION_DISMISSED_LOW_BATTERY = "StylusUsiPowerUI.dismiss" private const val ACTION_CLICKED_LOW_BATTERY = "StylusUsiPowerUI.click" diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt index b4baa44985bb..c76b127a161c 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt @@ -84,7 +84,8 @@ class ClockEventControllerTest : SysuiTestCase() { @Mock private lateinit var transitionRepository: KeyguardTransitionRepository @Mock private lateinit var commandQueue: CommandQueue private lateinit var repository: FakeKeyguardRepository - @Mock private lateinit var logBuffer: LogBuffer + @Mock private lateinit var smallLogBuffer: LogBuffer + @Mock private lateinit var largeLogBuffer: LogBuffer private lateinit var underTest: ClockEventController @Before @@ -111,7 +112,8 @@ class ClockEventControllerTest : SysuiTestCase() { context, mainExecutor, bgExecutor, - logBuffer, + smallLogBuffer, + largeLogBuffer, featureFlags ) underTest.clock = clock diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java index c8e753844c64..9a9acf3dd986 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java @@ -48,6 +48,7 @@ import com.android.systemui.plugins.ClockAnimations; import com.android.systemui.plugins.ClockController; import com.android.systemui.plugins.ClockEvents; import com.android.systemui.plugins.ClockFaceController; +import com.android.systemui.plugins.log.LogBuffer; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.shared.clocks.AnimatableClockView; import com.android.systemui.shared.clocks.ClockRegistry; @@ -115,6 +116,8 @@ public class KeyguardClockSwitchControllerTest extends SysuiTestCase { private FrameLayout mLargeClockFrame; @Mock private SecureSettings mSecureSettings; + @Mock + private LogBuffer mLogBuffer; private final View mFakeSmartspaceView = new View(mContext); @@ -156,7 +159,8 @@ public class KeyguardClockSwitchControllerTest extends SysuiTestCase { mSecureSettings, mExecutor, mDumpManager, - mClockEventController + mClockEventController, + mLogBuffer ); when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE); diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java index 254f9531ef83..8dc1e8fba600 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java @@ -16,6 +16,7 @@ package com.android.keyguard; +import static android.view.View.INVISIBLE; import static android.view.View.VISIBLE; import static com.android.keyguard.KeyguardClockSwitch.LARGE; @@ -189,6 +190,7 @@ public class KeyguardClockSwitchTest extends SysuiTestCase { assertThat(mLargeClockFrame.getAlpha()).isEqualTo(1); assertThat(mLargeClockFrame.getVisibility()).isEqualTo(VISIBLE); assertThat(mSmallClockFrame.getAlpha()).isEqualTo(0); + assertThat(mSmallClockFrame.getVisibility()).isEqualTo(INVISIBLE); } @Test @@ -198,6 +200,7 @@ public class KeyguardClockSwitchTest extends SysuiTestCase { assertThat(mLargeClockFrame.getAlpha()).isEqualTo(1); assertThat(mLargeClockFrame.getVisibility()).isEqualTo(VISIBLE); assertThat(mSmallClockFrame.getAlpha()).isEqualTo(0); + assertThat(mSmallClockFrame.getVisibility()).isEqualTo(INVISIBLE); } @Test @@ -212,6 +215,7 @@ public class KeyguardClockSwitchTest extends SysuiTestCase { // only big clock is removed at switch assertThat(mLargeClockFrame.getParent()).isNull(); assertThat(mLargeClockFrame.getAlpha()).isEqualTo(0); + assertThat(mLargeClockFrame.getVisibility()).isEqualTo(INVISIBLE); } @Test @@ -223,6 +227,7 @@ public class KeyguardClockSwitchTest extends SysuiTestCase { // only big clock is removed at switch assertThat(mLargeClockFrame.getParent()).isNull(); assertThat(mLargeClockFrame.getAlpha()).isEqualTo(0); + assertThat(mLargeClockFrame.getVisibility()).isEqualTo(INVISIBLE); } @Test diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java index ac22de9f9c52..87dd6a4cfa5e 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java @@ -33,6 +33,9 @@ import static com.android.keyguard.KeyguardUpdateMonitor.BIOMETRIC_STATE_CANCELL import static com.android.keyguard.KeyguardUpdateMonitor.DEFAULT_CANCEL_SIGNAL_TIMEOUT; import static com.android.keyguard.KeyguardUpdateMonitor.HAL_POWER_PRESS_TIMEOUT; import static com.android.keyguard.KeyguardUpdateMonitor.getCurrentUser; +import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_CLOSED; +import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_OPENED; +import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_UNKNOWN; import static com.google.common.truth.Truth.assertThat; @@ -93,6 +96,7 @@ import android.os.PowerManager; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; +import android.provider.Settings; import android.service.dreams.IDreamManager; import android.service.trust.TrustAgentService; import android.telephony.ServiceState; @@ -125,6 +129,7 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.phone.KeyguardBypassController; +import com.android.systemui.statusbar.policy.DevicePostureController; import com.android.systemui.telephony.TelephonyListenerManager; import com.android.systemui.util.settings.GlobalSettings; import com.android.systemui.util.settings.SecureSettings; @@ -194,6 +199,8 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Mock private DevicePolicyManager mDevicePolicyManager; @Mock + private DevicePostureController mDevicePostureController; + @Mock private IDreamManager mDreamManager; @Mock private KeyguardBypassController mKeyguardBypassController; @@ -300,6 +307,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { .thenReturn(new ServiceState()); when(mLockPatternUtils.getLockSettings()).thenReturn(mLockSettings); when(mAuthController.isUdfpsEnrolled(anyInt())).thenReturn(false); + when(mDevicePostureController.getDevicePosture()).thenReturn(DEVICE_POSTURE_UNKNOWN); mMockitoSession = ExtendedMockito.mockitoSession() .spyStatic(SubscriptionManager.class) @@ -311,6 +319,9 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { when(mUserTracker.getUserId()).thenReturn(mCurrentUserId); ExtendedMockito.doReturn(mActivityService).when(ActivityManager::getService); + mContext.getOrCreateTestableResources().addOverride( + com.android.systemui.R.integer.config_face_auth_supported_posture, + DEVICE_POSTURE_UNKNOWN); mFaceWakeUpTriggersConfig = new FaceWakeUpTriggersConfig( mContext.getResources(), mGlobalSettings, @@ -1254,7 +1265,7 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { } @Test - public void testStartsListeningForSfps_whenKeyguardIsVisible_ifRequireScreenOnToAuthEnabled() + public void startsListeningForSfps_whenKeyguardIsVisible_ifRequireInteractiveToAuthEnabled() throws RemoteException { // SFPS supported and enrolled final ArrayList<FingerprintSensorPropertiesInternal> props = new ArrayList<>(); @@ -1262,12 +1273,9 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { when(mAuthController.getSfpsProps()).thenReturn(props); when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); - // WHEN require screen on to auth is disabled, and keyguard is not awake + // WHEN require interactive to auth is disabled, and keyguard is not awake when(mInteractiveToAuthProvider.isEnabled(anyInt())).thenReturn(false); - mContext.getOrCreateTestableResources().addOverride( - com.android.internal.R.bool.config_requireScreenOnToAuthEnabled, true); - // Preconditions for sfps auth to run keyguardNotGoingAway(); currentUserIsPrimary(); @@ -1282,8 +1290,8 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { // THEN we should listen for sfps when screen off, because require screen on is disabled assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isTrue(); - // WHEN require screen on to auth is enabled, and keyguard is not awake - when(mInteractiveToAuthProvider.isEnabled(anyInt())).thenReturn(false); + // WHEN require interactive to auth is enabled, and keyguard is not awake + when(mInteractiveToAuthProvider.isEnabled(anyInt())).thenReturn(true); // THEN we shouldn't listen for sfps when screen off, because require screen on is enabled assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isFalse(); @@ -1297,6 +1305,62 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isTrue(); } + @Test + public void notListeningForSfps_whenGoingToSleep_ifRequireInteractiveToAuthEnabled() + throws RemoteException { + // GIVEN SFPS supported and enrolled + final ArrayList<FingerprintSensorPropertiesInternal> props = new ArrayList<>(); + props.add(newFingerprintSensorPropertiesInternal(TYPE_POWER_BUTTON)); + when(mAuthController.getSfpsProps()).thenReturn(props); + when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); + + // GIVEN Preconditions for sfps auth to run + keyguardNotGoingAway(); + currentUserIsPrimary(); + currentUserDoesNotHaveTrust(); + biometricsNotDisabledThroughDevicePolicyManager(); + biometricsEnabledForCurrentUser(); + userNotCurrentlySwitching(); + statusBarShadeIsLocked(); + + // WHEN require interactive to auth is enabled & keyguard is going to sleep + when(mInteractiveToAuthProvider.isEnabled(anyInt())).thenReturn(true); + deviceGoingToSleep(); + + mTestableLooper.processAllMessages(); + + // THEN we should NOT listen for sfps because device is going to sleep + assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isFalse(); + } + + @Test + public void listeningForSfps_whenGoingToSleep_ifRequireInteractiveToAuthDisabled() + throws RemoteException { + // GIVEN SFPS supported and enrolled + final ArrayList<FingerprintSensorPropertiesInternal> props = new ArrayList<>(); + props.add(newFingerprintSensorPropertiesInternal(TYPE_POWER_BUTTON)); + when(mAuthController.getSfpsProps()).thenReturn(props); + when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); + + // GIVEN Preconditions for sfps auth to run + keyguardNotGoingAway(); + currentUserIsPrimary(); + currentUserDoesNotHaveTrust(); + biometricsNotDisabledThroughDevicePolicyManager(); + biometricsEnabledForCurrentUser(); + userNotCurrentlySwitching(); + statusBarShadeIsLocked(); + + // WHEN require interactive to auth is disabled & keyguard is going to sleep + when(mInteractiveToAuthProvider.isEnabled(anyInt())).thenReturn(false); + deviceGoingToSleep(); + + mTestableLooper.processAllMessages(); + + // THEN we should listen for sfps because screen on to auth is disabled + assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isTrue(); + } + private FingerprintSensorPropertiesInternal newFingerprintSensorPropertiesInternal( @FingerprintSensorProperties.SensorType int sensorType) { return new FingerprintSensorPropertiesInternal( @@ -2188,6 +2252,54 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { eq(true)); } + @Test + public void testShouldListenForFace_withAuthSupportPostureConfig_returnsTrue() + throws RemoteException { + mKeyguardUpdateMonitor.mConfigFaceAuthSupportedPosture = DEVICE_POSTURE_CLOSED; + keyguardNotGoingAway(); + bouncerFullyVisibleAndNotGoingToSleep(); + currentUserIsPrimary(); + currentUserDoesNotHaveTrust(); + biometricsNotDisabledThroughDevicePolicyManager(); + biometricsEnabledForCurrentUser(); + userNotCurrentlySwitching(); + supportsFaceDetection(); + + deviceInPostureStateOpened(); + mTestableLooper.processAllMessages(); + // Should not listen for face when posture state in DEVICE_POSTURE_OPENED + assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isFalse(); + + deviceInPostureStateClosed(); + mTestableLooper.processAllMessages(); + // Should listen for face when posture state in DEVICE_POSTURE_CLOSED + assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isTrue(); + } + + @Test + public void testShouldListenForFace_withoutAuthSupportPostureConfig_returnsTrue() + throws RemoteException { + mKeyguardUpdateMonitor.mConfigFaceAuthSupportedPosture = DEVICE_POSTURE_UNKNOWN; + keyguardNotGoingAway(); + bouncerFullyVisibleAndNotGoingToSleep(); + currentUserIsPrimary(); + currentUserDoesNotHaveTrust(); + biometricsNotDisabledThroughDevicePolicyManager(); + biometricsEnabledForCurrentUser(); + userNotCurrentlySwitching(); + supportsFaceDetection(); + + deviceInPostureStateClosed(); + mTestableLooper.processAllMessages(); + // Whether device in any posture state, always listen for face + assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isTrue(); + + deviceInPostureStateOpened(); + mTestableLooper.processAllMessages(); + // Whether device in any posture state, always listen for face + assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isTrue(); + } + private void userDeviceLockDown() { when(mStrongAuthTracker.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(false); when(mStrongAuthTracker.getStrongAuthForUser(mCurrentUserId)) @@ -2267,6 +2379,14 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { .onAuthenticationAcquired(FINGERPRINT_ACQUIRED_START); } + private void deviceInPostureStateOpened() { + mKeyguardUpdateMonitor.mPostureCallback.onPostureChanged(DEVICE_POSTURE_OPENED); + } + + private void deviceInPostureStateClosed() { + mKeyguardUpdateMonitor.mPostureCallback.onPostureChanged(DEVICE_POSTURE_CLOSED); + } + private void successfulFingerprintAuth() { mKeyguardUpdateMonitor.mFingerprintAuthenticationCallback .onAuthenticationSucceeded( @@ -2408,7 +2528,8 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { mPowerManager, mTrustManager, mSubscriptionManager, mUserManager, mDreamManager, mDevicePolicyManager, mSensorPrivacyManager, mTelephonyManager, mPackageManager, mFaceManager, mFingerprintManager, mBiometricManager, - mFaceWakeUpTriggersConfig, Optional.of(mInteractiveToAuthProvider)); + mFaceWakeUpTriggersConfig, mDevicePostureController, + Optional.of(mInteractiveToAuthProvider)); setStrongAuthTracker(KeyguardUpdateMonitorTest.this.mStrongAuthTracker); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessorTest.kt index 95c53b408056..56043e306c16 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/udfps/SinglePointerTouchProcessorTest.kt @@ -221,6 +221,14 @@ class SinglePointerTouchProcessorTest(val testCase: TestCase) : SysuiTestCase() private const val ROTATION_0_NATIVE_DISPLAY_WIDTH = 400 private const val ROTATION_0_NATIVE_DISPLAY_HEIGHT = 600 +/* Placeholder touch parameters. */ +private const val POINTER_ID = 42 +private const val NATIVE_MINOR = 2.71828f +private const val NATIVE_MAJOR = 3.14f +private const val ORIENTATION = 1.2345f +private const val TIME = 12345699L +private const val GESTURE_START = 12345600L + /* * ROTATION_0 map: * _ _ _ _ @@ -244,6 +252,7 @@ private val ROTATION_0_NATIVE_SENSOR_BOUNDS = private val ROTATION_0_INPUTS = OrientationBasedInputs( rotation = Surface.ROTATION_0, + nativeOrientation = ORIENTATION, nativeXWithinSensor = ROTATION_0_NATIVE_SENSOR_BOUNDS.exactCenterX(), nativeYWithinSensor = ROTATION_0_NATIVE_SENSOR_BOUNDS.exactCenterY(), nativeXOutsideSensor = 250f, @@ -271,6 +280,7 @@ private val ROTATION_90_NATIVE_SENSOR_BOUNDS = private val ROTATION_90_INPUTS = OrientationBasedInputs( rotation = Surface.ROTATION_90, + nativeOrientation = (ORIENTATION - Math.PI.toFloat() / 2), nativeXWithinSensor = ROTATION_90_NATIVE_SENSOR_BOUNDS.exactCenterX(), nativeYWithinSensor = ROTATION_90_NATIVE_SENSOR_BOUNDS.exactCenterY(), nativeXOutsideSensor = 150f, @@ -304,20 +314,13 @@ private val ROTATION_270_NATIVE_SENSOR_BOUNDS = private val ROTATION_270_INPUTS = OrientationBasedInputs( rotation = Surface.ROTATION_270, + nativeOrientation = (ORIENTATION + Math.PI.toFloat() / 2), nativeXWithinSensor = ROTATION_270_NATIVE_SENSOR_BOUNDS.exactCenterX(), nativeYWithinSensor = ROTATION_270_NATIVE_SENSOR_BOUNDS.exactCenterY(), nativeXOutsideSensor = 450f, nativeYOutsideSensor = 250f, ) -/* Placeholder touch parameters. */ -private const val POINTER_ID = 42 -private const val NATIVE_MINOR = 2.71828f -private const val NATIVE_MAJOR = 3.14f -private const val ORIENTATION = 1.23f -private const val TIME = 12345699L -private const val GESTURE_START = 12345600L - /* Template [MotionEvent]. */ private val MOTION_EVENT = obtainMotionEvent( @@ -352,6 +355,7 @@ private val NORMALIZED_TOUCH_DATA = */ private data class OrientationBasedInputs( @Rotation val rotation: Int, + val nativeOrientation: Float, val nativeXWithinSensor: Float, val nativeYWithinSensor: Float, val nativeXOutsideSensor: Float, @@ -404,6 +408,7 @@ private fun genPositiveTestCases( y = nativeY * scaleFactor, minor = NATIVE_MINOR * scaleFactor, major = NATIVE_MAJOR * scaleFactor, + orientation = orientation.nativeOrientation ) val expectedTouchData = NORMALIZED_TOUCH_DATA.copy( diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/WakefulnessLifecycleTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/WakefulnessLifecycleTest.java index f32d76bb601e..39a453da7f92 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/WakefulnessLifecycleTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/WakefulnessLifecycleTest.java @@ -30,6 +30,7 @@ import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; import com.android.systemui.dump.DumpManager; +import com.android.systemui.util.time.FakeSystemClock; import org.junit.Before; import org.junit.Test; @@ -51,7 +52,12 @@ public class WakefulnessLifecycleTest extends SysuiTestCase { public void setUp() throws Exception { mWallpaperManager = mock(IWallpaperManager.class); mWakefulness = - new WakefulnessLifecycle(mContext, mWallpaperManager, mock(DumpManager.class)); + new WakefulnessLifecycle( + mContext, + mWallpaperManager, + new FakeSystemClock(), + mock(DumpManager.class) + ); mWakefulnessObserver = mock(WakefulnessLifecycle.Observer.class); mWakefulness.addObserver(mWakefulnessObserver); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt index be712f699b7b..f997d18a57a5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt @@ -24,6 +24,7 @@ import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.AuthController import com.android.systemui.common.shared.model.Position +import com.android.systemui.coroutines.collectLastValue import com.android.systemui.doze.DozeHost import com.android.systemui.doze.DozeMachine import com.android.systemui.doze.DozeTransitionCallback @@ -38,14 +39,17 @@ import com.android.systemui.keyguard.shared.model.WakefulnessModel import com.android.systemui.keyguard.shared.model.WakefulnessState import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.phone.BiometricUnlockController +import com.android.systemui.statusbar.phone.DozeParameters import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.whenever import com.android.systemui.util.mockito.withArgCaptor import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test @@ -68,6 +72,7 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { @Mock private lateinit var authController: AuthController @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor @Mock private lateinit var dreamOverlayCallbackController: DreamOverlayCallbackController + @Mock private lateinit var dozeParameters: DozeParameters private lateinit var underTest: KeyguardRepositoryImpl @@ -84,6 +89,7 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { keyguardStateController, keyguardUpdateMonitor, dozeTransitionListener, + dozeParameters, authController, dreamOverlayCallbackController, ) @@ -170,6 +176,26 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { } @Test + fun isAodAvailable() = runTest { + val flow = underTest.isAodAvailable + var isAodAvailable = collectLastValue(flow) + runCurrent() + + val callback = + withArgCaptor<DozeParameters.Callback> { verify(dozeParameters).addCallback(capture()) } + + whenever(dozeParameters.getAlwaysOn()).thenReturn(false) + callback.onAlwaysOnChange() + assertThat(isAodAvailable()).isEqualTo(false) + + whenever(dozeParameters.getAlwaysOn()).thenReturn(true) + callback.onAlwaysOnChange() + assertThat(isAodAvailable()).isEqualTo(true) + + flow.onCompletion { verify(dozeParameters).removeCallback(callback) } + } + + @Test fun isKeyguardOccluded() = runTest(UnconfinedTestDispatcher()) { whenever(keyguardStateController.isOccluded).thenReturn(false) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt index 754adfdc48b3..b3cee2273012 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt @@ -71,6 +71,10 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { private lateinit var fromLockscreenTransitionInteractor: FromLockscreenTransitionInteractor private lateinit var fromDreamingTransitionInteractor: FromDreamingTransitionInteractor + private lateinit var fromDozingTransitionInteractor: FromDozingTransitionInteractor + private lateinit var fromOccludedTransitionInteractor: FromOccludedTransitionInteractor + private lateinit var fromGoneTransitionInteractor: FromGoneTransitionInteractor + private lateinit var fromAodTransitionInteractor: FromAodTransitionInteractor @Before fun setUp() { @@ -102,6 +106,42 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { keyguardTransitionInteractor = KeyguardTransitionInteractor(transitionRepository), ) fromDreamingTransitionInteractor.start() + + fromAodTransitionInteractor = + FromAodTransitionInteractor( + scope = testScope, + keyguardInteractor = KeyguardInteractor(keyguardRepository, commandQueue), + keyguardTransitionRepository = mockTransitionRepository, + keyguardTransitionInteractor = KeyguardTransitionInteractor(transitionRepository), + ) + fromAodTransitionInteractor.start() + + fromGoneTransitionInteractor = + FromGoneTransitionInteractor( + scope = testScope, + keyguardInteractor = KeyguardInteractor(keyguardRepository, commandQueue), + keyguardTransitionRepository = mockTransitionRepository, + keyguardTransitionInteractor = KeyguardTransitionInteractor(transitionRepository), + ) + fromGoneTransitionInteractor.start() + + fromDozingTransitionInteractor = + FromDozingTransitionInteractor( + scope = testScope, + keyguardInteractor = KeyguardInteractor(keyguardRepository, commandQueue), + keyguardTransitionRepository = mockTransitionRepository, + keyguardTransitionInteractor = KeyguardTransitionInteractor(transitionRepository), + ) + fromDozingTransitionInteractor.start() + + fromOccludedTransitionInteractor = + FromOccludedTransitionInteractor( + scope = testScope, + keyguardInteractor = KeyguardInteractor(keyguardRepository, commandQueue), + keyguardTransitionRepository = mockTransitionRepository, + keyguardTransitionInteractor = KeyguardTransitionInteractor(transitionRepository), + ) + fromOccludedTransitionInteractor.start() } @Test @@ -192,6 +232,289 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { coroutineContext.cancelChildren() } + @Test + fun `OCCLUDED to DOZING`() = + testScope.runTest { + // GIVEN a device with AOD not available + keyguardRepository.setAodAvailable(false) + runCurrent() + + // GIVEN a prior transition has run to OCCLUDED + runner.startTransition( + testScope, + TransitionInfo( + ownerName = "", + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.OCCLUDED, + animator = + ValueAnimator().apply { + duration = 10 + interpolator = Interpolators.LINEAR + }, + ) + ) + runCurrent() + reset(mockTransitionRepository) + + // WHEN the device begins to sleep + keyguardRepository.setWakefulnessModel(startingToSleep()) + runCurrent() + + val info = + withArgCaptor<TransitionInfo> { + verify(mockTransitionRepository).startTransition(capture()) + } + // THEN a transition to DOZING should occur + assertThat(info.ownerName).isEqualTo("FromOccludedTransitionInteractor") + assertThat(info.from).isEqualTo(KeyguardState.OCCLUDED) + assertThat(info.to).isEqualTo(KeyguardState.DOZING) + assertThat(info.animator).isNotNull() + + coroutineContext.cancelChildren() + } + + @Test + fun `OCCLUDED to AOD`() = + testScope.runTest { + // GIVEN a device with AOD available + keyguardRepository.setAodAvailable(true) + runCurrent() + + // GIVEN a prior transition has run to OCCLUDED + runner.startTransition( + testScope, + TransitionInfo( + ownerName = "", + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.OCCLUDED, + animator = + ValueAnimator().apply { + duration = 10 + interpolator = Interpolators.LINEAR + }, + ) + ) + runCurrent() + reset(mockTransitionRepository) + + // WHEN the device begins to sleep + keyguardRepository.setWakefulnessModel(startingToSleep()) + runCurrent() + + val info = + withArgCaptor<TransitionInfo> { + verify(mockTransitionRepository).startTransition(capture()) + } + // THEN a transition to DOZING should occur + assertThat(info.ownerName).isEqualTo("FromOccludedTransitionInteractor") + assertThat(info.from).isEqualTo(KeyguardState.OCCLUDED) + assertThat(info.to).isEqualTo(KeyguardState.AOD) + assertThat(info.animator).isNotNull() + + coroutineContext.cancelChildren() + } + + @Test + fun `LOCKSCREEN to DOZING`() = + testScope.runTest { + // GIVEN a device with AOD not available + keyguardRepository.setAodAvailable(false) + runCurrent() + + // GIVEN a prior transition has run to LOCKSCREEN + runner.startTransition( + testScope, + TransitionInfo( + ownerName = "", + from = KeyguardState.GONE, + to = KeyguardState.LOCKSCREEN, + animator = + ValueAnimator().apply { + duration = 10 + interpolator = Interpolators.LINEAR + }, + ) + ) + runCurrent() + reset(mockTransitionRepository) + + // WHEN the device begins to sleep + keyguardRepository.setWakefulnessModel(startingToSleep()) + runCurrent() + + val info = + withArgCaptor<TransitionInfo> { + verify(mockTransitionRepository).startTransition(capture()) + } + // THEN a transition to DOZING should occur + assertThat(info.ownerName).isEqualTo("FromLockscreenTransitionInteractor") + assertThat(info.from).isEqualTo(KeyguardState.LOCKSCREEN) + assertThat(info.to).isEqualTo(KeyguardState.DOZING) + assertThat(info.animator).isNotNull() + + coroutineContext.cancelChildren() + } + + @Test + fun `LOCKSCREEN to AOD`() = + testScope.runTest { + // GIVEN a device with AOD available + keyguardRepository.setAodAvailable(true) + runCurrent() + + // GIVEN a prior transition has run to LOCKSCREEN + runner.startTransition( + testScope, + TransitionInfo( + ownerName = "", + from = KeyguardState.GONE, + to = KeyguardState.LOCKSCREEN, + animator = + ValueAnimator().apply { + duration = 10 + interpolator = Interpolators.LINEAR + }, + ) + ) + runCurrent() + reset(mockTransitionRepository) + + // WHEN the device begins to sleep + keyguardRepository.setWakefulnessModel(startingToSleep()) + runCurrent() + + val info = + withArgCaptor<TransitionInfo> { + verify(mockTransitionRepository).startTransition(capture()) + } + // THEN a transition to DOZING should occur + assertThat(info.ownerName).isEqualTo("FromLockscreenTransitionInteractor") + assertThat(info.from).isEqualTo(KeyguardState.LOCKSCREEN) + assertThat(info.to).isEqualTo(KeyguardState.AOD) + assertThat(info.animator).isNotNull() + + coroutineContext.cancelChildren() + } + + @Test + fun `DOZING to LOCKSCREEN`() = + testScope.runTest { + // GIVEN a prior transition has run to DOZING + runner.startTransition( + testScope, + TransitionInfo( + ownerName = "", + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.DOZING, + animator = + ValueAnimator().apply { + duration = 10 + interpolator = Interpolators.LINEAR + }, + ) + ) + runCurrent() + reset(mockTransitionRepository) + + // WHEN the device begins to wake + keyguardRepository.setWakefulnessModel(startingToWake()) + runCurrent() + + val info = + withArgCaptor<TransitionInfo> { + verify(mockTransitionRepository).startTransition(capture()) + } + // THEN a transition to DOZING should occur + assertThat(info.ownerName).isEqualTo("FromDozingTransitionInteractor") + assertThat(info.from).isEqualTo(KeyguardState.DOZING) + assertThat(info.to).isEqualTo(KeyguardState.LOCKSCREEN) + assertThat(info.animator).isNotNull() + + coroutineContext.cancelChildren() + } + + @Test + fun `GONE to DOZING`() = + testScope.runTest { + // GIVEN a device with AOD not available + keyguardRepository.setAodAvailable(false) + runCurrent() + + // GIVEN a prior transition has run to GONE + runner.startTransition( + testScope, + TransitionInfo( + ownerName = "", + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + animator = + ValueAnimator().apply { + duration = 10 + interpolator = Interpolators.LINEAR + }, + ) + ) + runCurrent() + reset(mockTransitionRepository) + + // WHEN the device begins to sleep + keyguardRepository.setWakefulnessModel(startingToSleep()) + runCurrent() + + val info = + withArgCaptor<TransitionInfo> { + verify(mockTransitionRepository).startTransition(capture()) + } + // THEN a transition to DOZING should occur + assertThat(info.ownerName).isEqualTo("FromGoneTransitionInteractor") + assertThat(info.from).isEqualTo(KeyguardState.GONE) + assertThat(info.to).isEqualTo(KeyguardState.DOZING) + assertThat(info.animator).isNotNull() + + coroutineContext.cancelChildren() + } + + @Test + fun `GONE to AOD`() = + testScope.runTest { + // GIVEN a device with AOD available + keyguardRepository.setAodAvailable(true) + runCurrent() + + // GIVEN a prior transition has run to GONE + runner.startTransition( + testScope, + TransitionInfo( + ownerName = "", + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GONE, + animator = + ValueAnimator().apply { + duration = 10 + interpolator = Interpolators.LINEAR + }, + ) + ) + runCurrent() + reset(mockTransitionRepository) + + // WHEN the device begins to sleep + keyguardRepository.setWakefulnessModel(startingToSleep()) + runCurrent() + + val info = + withArgCaptor<TransitionInfo> { + verify(mockTransitionRepository).startTransition(capture()) + } + // THEN a transition to DOZING should occur + assertThat(info.ownerName).isEqualTo("FromGoneTransitionInteractor") + assertThat(info.from).isEqualTo(KeyguardState.GONE) + assertThat(info.to).isEqualTo(KeyguardState.AOD) + assertThat(info.animator).isNotNull() + + coroutineContext.cancelChildren() + } + private fun startingToWake() = WakefulnessModel( WakefulnessState.STARTING_TO_WAKE, @@ -199,4 +522,12 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { WakeSleepReason.OTHER, WakeSleepReason.OTHER ) + + private fun startingToSleep() = + WakefulnessModel( + WakefulnessState.STARTING_TO_SLEEP, + true, + WakeSleepReason.OTHER, + WakeSleepReason.OTHER + ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/log/table/TableLogBufferFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/log/table/TableLogBufferFactoryTest.kt new file mode 100644 index 000000000000..411b1bd04c52 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/log/table/TableLogBufferFactoryTest.kt @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.log.table + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.dump.DumpManager +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.time.FakeSystemClock +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +@SmallTest +class TableLogBufferFactoryTest : SysuiTestCase() { + private val dumpManager: DumpManager = mock() + private val systemClock = FakeSystemClock() + private val underTest = TableLogBufferFactory(dumpManager, systemClock) + + @Test + fun `create - always creates new instance`() { + val b1 = underTest.create(NAME_1, SIZE) + val b1_copy = underTest.create(NAME_1, SIZE) + val b2 = underTest.create(NAME_2, SIZE) + val b2_copy = underTest.create(NAME_2, SIZE) + + assertThat(b1).isNotSameInstanceAs(b1_copy) + assertThat(b1).isNotSameInstanceAs(b2) + assertThat(b2).isNotSameInstanceAs(b2_copy) + } + + @Test + fun `getOrCreate - reuses instance`() { + val b1 = underTest.getOrCreate(NAME_1, SIZE) + val b1_copy = underTest.getOrCreate(NAME_1, SIZE) + val b2 = underTest.getOrCreate(NAME_2, SIZE) + val b2_copy = underTest.getOrCreate(NAME_2, SIZE) + + assertThat(b1).isSameInstanceAs(b1_copy) + assertThat(b2).isSameInstanceAs(b2_copy) + assertThat(b1).isNotSameInstanceAs(b2) + assertThat(b1_copy).isNotSameInstanceAs(b2_copy) + } + + companion object { + const val NAME_1 = "name 1" + const val NAME_2 = "name 2" + + const val SIZE = 8 + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java index 4d2d0f05b76a..c0639f34484c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataCombineLatestTest.java @@ -79,7 +79,7 @@ public class MediaDataCombineLatestTest extends SysuiTestCase { USER_ID, true, APP, null, ARTIST, TITLE, null, new ArrayList<>(), new ArrayList<>(), null, PACKAGE, null, null, null, true, null, MediaData.PLAYBACK_LOCAL, false, KEY, false, false, false, 0L, - InstanceId.fakeInstanceId(-1), -1); + InstanceId.fakeInstanceId(-1), -1, false); mDeviceData = new MediaDeviceData(true, null, DEVICE_NAME, null, false); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt index 52b694fac07c..c24c8c7f7cf6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt @@ -228,6 +228,7 @@ class MediaDataManagerTest : SysuiTestCase() { whenever(mediaSmartspaceTarget.iconGrid).thenReturn(validRecommendationList) whenever(mediaSmartspaceTarget.creationTimeMillis).thenReturn(1234L) whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(false) + whenever(mediaFlags.isExplicitIndicatorEnabled()).thenReturn(true) whenever(logger.getNewInstanceId()).thenReturn(instanceIdSequence.newInstanceId()) } @@ -300,6 +301,60 @@ class MediaDataManagerTest : SysuiTestCase() { } @Test + fun testLoadMetadata_withExplicitIndicator() { + val metadata = + MediaMetadata.Builder().run { + putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST) + putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE) + putLong( + MediaConstants.METADATA_KEY_IS_EXPLICIT, + MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT + ) + build() + } + whenever(mediaControllerFactory.create(anyObject())).thenReturn(controller) + whenever(controller.metadata).thenReturn(metadata) + + mediaDataManager.addListener(listener) + mediaDataManager.onNotificationAdded(KEY, mediaNotification) + + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value!!.isExplicit).isTrue() + } + + @Test + fun testOnMetaDataLoaded_withoutExplicitIndicator() { + whenever(mediaControllerFactory.create(anyObject())).thenReturn(controller) + whenever(controller.metadata).thenReturn(metadataBuilder.build()) + + mediaDataManager.addListener(listener) + mediaDataManager.onNotificationAdded(KEY, mediaNotification) + + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value!!.isExplicit).isFalse() + } + + @Test fun testOnMetaDataLoaded_callsListener() { addNotificationAndLoad() verify(logger) @@ -603,6 +658,53 @@ class MediaDataManagerTest : SysuiTestCase() { } @Test + fun testAddResumptionControls_withExplicitIndicator() { + val bundle = Bundle() + // WHEN resumption controls are added with explicit indicator + bundle.putLong( + MediaConstants.METADATA_KEY_IS_EXPLICIT, + MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT + ) + val desc = + MediaDescription.Builder().run { + setTitle(SESSION_TITLE) + setExtras(bundle) + build() + } + val currentTime = clock.elapsedRealtime() + mediaDataManager.addResumptionControls( + USER_ID, + desc, + Runnable {}, + session.sessionToken, + APP_NAME, + pendingIntent, + PACKAGE_NAME + ) + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + // THEN the media data indicates that it is for resumption + verify(listener) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + val data = mediaDataCaptor.value + assertThat(data.resumption).isTrue() + assertThat(data.song).isEqualTo(SESSION_TITLE) + assertThat(data.app).isEqualTo(APP_NAME) + assertThat(data.actions).hasSize(1) + assertThat(data.semanticActions!!.playOrPause).isNotNull() + assertThat(data.lastActive).isAtLeast(currentTime) + assertThat(data.isExplicit).isTrue() + verify(logger).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId)) + } + + @Test fun testResumptionDisabled_dismissesResumeControls() { // WHEN there are resume controls and resumption is switched off val desc = diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt index b65f5cb51aaf..cfb19fc32bec 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaControlPanelTest.kt @@ -54,6 +54,7 @@ import androidx.constraintlayout.widget.ConstraintSet import androidx.lifecycle.LiveData import androidx.test.filters.SmallTest import com.android.internal.logging.InstanceId +import com.android.internal.widget.CachingIconView import com.android.systemui.ActivityIntentHelper import com.android.systemui.R import com.android.systemui.SysuiTestCase @@ -154,6 +155,7 @@ public class MediaControlPanelTest : SysuiTestCase() { @Mock private lateinit var albumView: ImageView private lateinit var titleText: TextView private lateinit var artistText: TextView + private lateinit var explicitIndicator: CachingIconView private lateinit var seamless: ViewGroup private lateinit var seamlessButton: View @Mock private lateinit var seamlessBackground: RippleDrawable @@ -216,6 +218,7 @@ public class MediaControlPanelTest : SysuiTestCase() { this.set(Flags.UMO_SURFACE_RIPPLE, false) this.set(Flags.UMO_TURBULENCE_NOISE, false) this.set(Flags.MEDIA_FALSING_PENALTY, true) + this.set(Flags.MEDIA_EXPLICIT_INDICATOR, true) } @JvmField @Rule val mockito = MockitoJUnit.rule() @@ -350,6 +353,7 @@ public class MediaControlPanelTest : SysuiTestCase() { appIcon = ImageView(context) titleText = TextView(context) artistText = TextView(context) + explicitIndicator = CachingIconView(context).also { it.id = R.id.media_explicit_indicator } seamless = FrameLayout(context) seamless.foreground = seamlessBackground seamlessButton = View(context) @@ -396,6 +400,7 @@ public class MediaControlPanelTest : SysuiTestCase() { whenever(albumView.foreground).thenReturn(mock(Drawable::class.java)) whenever(viewHolder.titleText).thenReturn(titleText) whenever(viewHolder.artistText).thenReturn(artistText) + whenever(viewHolder.explicitIndicator).thenReturn(explicitIndicator) whenever(seamlessBackground.getDrawable(0)).thenReturn(mock(GradientDrawable::class.java)) whenever(viewHolder.seamless).thenReturn(seamless) whenever(viewHolder.seamlessButton).thenReturn(seamlessButton) @@ -1019,6 +1024,7 @@ public class MediaControlPanelTest : SysuiTestCase() { @Test fun bindText() { + useRealConstraintSets() player.attachPlayer(viewHolder) player.bindPlayer(mediaData, PACKAGE) @@ -1036,6 +1042,8 @@ public class MediaControlPanelTest : SysuiTestCase() { handler.onAnimationEnd(mockAnimator) assertThat(titleText.getText()).isEqualTo(TITLE) assertThat(artistText.getText()).isEqualTo(ARTIST) + assertThat(expandedSet.getVisibility(explicitIndicator.id)).isEqualTo(ConstraintSet.GONE) + assertThat(collapsedSet.getVisibility(explicitIndicator.id)).isEqualTo(ConstraintSet.GONE) // Rebinding should not trigger animation player.bindPlayer(mediaData, PACKAGE) @@ -1043,6 +1051,36 @@ public class MediaControlPanelTest : SysuiTestCase() { } @Test + fun bindTextWithExplicitIndicator() { + useRealConstraintSets() + val mediaDataWitExp = mediaData.copy(isExplicit = true) + player.attachPlayer(viewHolder) + player.bindPlayer(mediaDataWitExp, PACKAGE) + + // Capture animation handler + val captor = argumentCaptor<Animator.AnimatorListener>() + verify(mockAnimator, times(2)).addListener(captor.capture()) + val handler = captor.value + + // Validate text views unchanged but animation started + assertThat(titleText.getText()).isEqualTo("") + assertThat(artistText.getText()).isEqualTo("") + verify(mockAnimator, times(1)).start() + + // Binding only after animator runs + handler.onAnimationEnd(mockAnimator) + assertThat(titleText.getText()).isEqualTo(TITLE) + assertThat(artistText.getText()).isEqualTo(ARTIST) + assertThat(expandedSet.getVisibility(explicitIndicator.id)).isEqualTo(ConstraintSet.VISIBLE) + assertThat(collapsedSet.getVisibility(explicitIndicator.id)) + .isEqualTo(ConstraintSet.VISIBLE) + + // Rebinding should not trigger animation + player.bindPlayer(mediaData, PACKAGE) + verify(mockAnimator, times(3)).start() + } + + @Test fun bindTextInterrupted() { val data0 = mediaData.copy(artist = "ARTIST_0") val data1 = mediaData.copy(artist = "ARTIST_1") diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt index 920801f95f5b..a5795184b493 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaHierarchyManagerTest.kt @@ -29,6 +29,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.controls.controller.ControlsControllerImplTest.Companion.eq import com.android.systemui.dreams.DreamOverlayStateController import com.android.systemui.keyguard.WakefulnessLifecycle +import com.android.systemui.media.controls.pipeline.MediaDataManager import com.android.systemui.media.dream.MediaDreamComplication import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.shade.ShadeExpansionStateManager @@ -76,6 +77,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() { @Mock private lateinit var mediaCarouselScrollHandler: MediaCarouselScrollHandler @Mock private lateinit var wakefulnessLifecycle: WakefulnessLifecycle @Mock private lateinit var keyguardViewController: KeyguardViewController + @Mock private lateinit var mediaDataManager: MediaDataManager @Mock private lateinit var uniqueObjectHostView: UniqueObjectHostView @Mock private lateinit var dreamOverlayStateController: DreamOverlayStateController @Captor @@ -110,6 +112,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() { keyguardStateController, bypassController, mediaCarouselController, + mediaDataManager, keyguardViewController, dreamOverlayStateController, configurationController, @@ -125,6 +128,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() { setupHost(qsHost, MediaHierarchyManager.LOCATION_QS, QS_TOP) setupHost(qqsHost, MediaHierarchyManager.LOCATION_QQS, QQS_TOP) whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE) + whenever(mediaDataManager.hasActiveMedia()).thenReturn(true) whenever(mediaCarouselController.mediaCarouselScrollHandler) .thenReturn(mediaCarouselScrollHandler) val observer = wakefullnessObserver.value @@ -357,17 +361,31 @@ class MediaHierarchyManagerTest : SysuiTestCase() { } @Test - fun isCurrentlyInGuidedTransformation_hostNotVisible_returnsTrue() { + fun isCurrentlyInGuidedTransformation_hostNotVisible_returnsFalse_with_active() { goToLockscreen() enterGuidedTransformation() whenever(lockHost.visible).thenReturn(false) whenever(qsHost.visible).thenReturn(true) whenever(qqsHost.visible).thenReturn(true) + whenever(mediaDataManager.hasActiveMediaOrRecommendation()).thenReturn(true) assertThat(mediaHierarchyManager.isCurrentlyInGuidedTransformation()).isFalse() } @Test + fun isCurrentlyInGuidedTransformation_hostNotVisible_returnsTrue_without_active() { + // To keep the appearing behavior, we need to be in a guided transition + goToLockscreen() + enterGuidedTransformation() + whenever(lockHost.visible).thenReturn(false) + whenever(qsHost.visible).thenReturn(true) + whenever(qqsHost.visible).thenReturn(true) + whenever(mediaDataManager.hasActiveMediaOrRecommendation()).thenReturn(false) + + assertThat(mediaHierarchyManager.isCurrentlyInGuidedTransformation()).isTrue() + } + + @Test fun testDream() { goToDream() setMediaDreamComplicationEnabled(true) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java index b16a39f37e39..f5432e22c57e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java @@ -102,8 +102,6 @@ public class MediaOutputControllerTest extends SysuiTestCase { private MediaOutputController.Callback mCb = mock(MediaOutputController.Callback.class); private MediaDevice mMediaDevice1 = mock(MediaDevice.class); private MediaDevice mMediaDevice2 = mock(MediaDevice.class); - private MediaItem mMediaItem1 = mock(MediaItem.class); - private MediaItem mMediaItem2 = mock(MediaItem.class); private NearbyDevice mNearbyDevice1 = mock(NearbyDevice.class); private NearbyDevice mNearbyDevice2 = mock(NearbyDevice.class); private MediaMetadata mMediaMetadata = mock(MediaMetadata.class); @@ -127,7 +125,6 @@ public class MediaOutputControllerTest extends SysuiTestCase { private LocalMediaManager mLocalMediaManager; private List<MediaController> mMediaControllers = new ArrayList<>(); private List<MediaDevice> mMediaDevices = new ArrayList<>(); - private List<MediaItem> mMediaItemList = new ArrayList<>(); private List<NearbyDevice> mNearbyDevices = new ArrayList<>(); private MediaDescription mMediaDescription; private List<RoutingSessionInfo> mRoutingSessionInfos = new ArrayList<>(); @@ -149,7 +146,9 @@ public class MediaOutputControllerTest extends SysuiTestCase { Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager, mKeyguardManager, mFlags); when(mFlags.isEnabled(Flags.OUTPUT_SWITCHER_ADVANCED_LAYOUT)).thenReturn(false); + when(mFlags.isEnabled(Flags.OUTPUT_SWITCHER_ROUTES_PROCESSING)).thenReturn(false); mLocalMediaManager = spy(mMediaOutputController.mLocalMediaManager); + when(mLocalMediaManager.isPreferenceRouteListingExist()).thenReturn(false); mMediaOutputController.mLocalMediaManager = mLocalMediaManager; MediaDescription.Builder builder = new MediaDescription.Builder(); builder.setTitle(TEST_SONG); @@ -160,16 +159,12 @@ public class MediaOutputControllerTest extends SysuiTestCase { when(mMediaDevice2.getId()).thenReturn(TEST_DEVICE_2_ID); mMediaDevices.add(mMediaDevice1); mMediaDevices.add(mMediaDevice2); - when(mMediaItem1.getMediaDevice()).thenReturn(Optional.of(mMediaDevice1)); - when(mMediaItem2.getMediaDevice()).thenReturn(Optional.of(mMediaDevice2)); - mMediaItemList.add(mMediaItem1); - mMediaItemList.add(mMediaItem2); when(mNearbyDevice1.getMediaRoute2Id()).thenReturn(TEST_DEVICE_1_ID); - when(mNearbyDevice1.getRangeZone()).thenReturn(NearbyDevice.RANGE_CLOSE); + when(mNearbyDevice1.getRangeZone()).thenReturn(NearbyDevice.RANGE_FAR); when(mNearbyDevice2.getMediaRoute2Id()).thenReturn(TEST_DEVICE_2_ID); - when(mNearbyDevice2.getRangeZone()).thenReturn(NearbyDevice.RANGE_FAR); + when(mNearbyDevice2.getRangeZone()).thenReturn(NearbyDevice.RANGE_CLOSE); mNearbyDevices.add(mNearbyDevice1); mNearbyDevices.add(mNearbyDevice2); } @@ -274,8 +269,20 @@ public class MediaOutputControllerTest extends SysuiTestCase { mMediaOutputController.onDevicesUpdated(mNearbyDevices); mMediaOutputController.onDeviceListUpdate(mMediaDevices); - verify(mMediaDevice1).setRangeZone(NearbyDevice.RANGE_CLOSE); - verify(mMediaDevice2).setRangeZone(NearbyDevice.RANGE_FAR); + verify(mMediaDevice1).setRangeZone(NearbyDevice.RANGE_FAR); + verify(mMediaDevice2).setRangeZone(NearbyDevice.RANGE_CLOSE); + } + + @Test + public void onDeviceListUpdate_withNearbyDevices_rankByRangeInformation() + throws RemoteException { + mMediaOutputController.start(mCb); + reset(mCb); + + mMediaOutputController.onDevicesUpdated(mNearbyDevices); + mMediaOutputController.onDeviceListUpdate(mMediaDevices); + + assertThat(mMediaDevices.get(0).getId()).isEqualTo(TEST_DEVICE_1_ID); } @Test @@ -292,6 +299,22 @@ public class MediaOutputControllerTest extends SysuiTestCase { } @Test + public void routeProcessSupport_onDeviceListUpdate_preferenceExist_NotUpdatesRangeInformation() + throws RemoteException { + when(mLocalMediaManager.isPreferenceRouteListingExist()).thenReturn(true); + when(mFlags.isEnabled(Flags.OUTPUT_SWITCHER_ROUTES_PROCESSING)).thenReturn(true); + when(mFlags.isEnabled(Flags.OUTPUT_SWITCHER_ADVANCED_LAYOUT)).thenReturn(true); + mMediaOutputController.start(mCb); + reset(mCb); + + mMediaOutputController.onDevicesUpdated(mNearbyDevices); + mMediaOutputController.onDeviceListUpdate(mMediaDevices); + + verify(mMediaDevice1, never()).setRangeZone(anyInt()); + verify(mMediaDevice2, never()).setRangeZone(anyInt()); + } + + @Test public void advanced_onDeviceListUpdate_verifyDeviceListCallback() { when(mFlags.isEnabled(Flags.OUTPUT_SWITCHER_ADVANCED_LAYOUT)).thenReturn(true); mMediaOutputController.start(mCb); @@ -307,6 +330,35 @@ public class MediaOutputControllerTest extends SysuiTestCase { assertThat(devices.containsAll(mMediaDevices)).isTrue(); assertThat(devices.size()).isEqualTo(mMediaDevices.size()); + assertThat(mMediaOutputController.getMediaItemList().size()).isEqualTo( + mMediaDevices.size() + 2); + verify(mCb).onDeviceListChanged(); + } + + @Test + public void advanced_categorizeMediaItems_withSuggestedDevice_verifyDeviceListSize() { + when(mFlags.isEnabled(Flags.OUTPUT_SWITCHER_ADVANCED_LAYOUT)).thenReturn(true); + when(mMediaDevice1.isSuggestedDevice()).thenReturn(true); + when(mMediaDevice2.isSuggestedDevice()).thenReturn(false); + + mMediaOutputController.start(mCb); + reset(mCb); + mMediaOutputController.getMediaItemList().clear(); + mMediaOutputController.onDeviceListUpdate(mMediaDevices); + final List<MediaDevice> devices = new ArrayList<>(); + int dividerSize = 0; + for (MediaItem item : mMediaOutputController.getMediaItemList()) { + if (item.getMediaDevice().isPresent()) { + devices.add(item.getMediaDevice().get()); + } + if (item.getMediaItemType() == MediaItem.MediaItemType.TYPE_GROUP_DIVIDER) { + dividerSize++; + } + } + + assertThat(devices.containsAll(mMediaDevices)).isTrue(); + assertThat(devices.size()).isEqualTo(mMediaDevices.size()); + assertThat(dividerSize).isEqualTo(2); verify(mCb).onDeviceListChanged(); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt index 4cc12c709fa7..f5b3959b322d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt @@ -206,6 +206,21 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { } @Test + fun commandQueueCallback_almostCloseToStartCast_deviceNameBlank_showsDefaultDeviceName() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_START_CAST, + routeInfoWithBlankDeviceName, + null, + ) + + val chipbarView = getChipbarView() + assertThat(chipbarView.getChipText()) + .contains(context.getString(R.string.media_ttt_default_device_type)) + assertThat(chipbarView.getChipText()) + .isNotEqualTo(ChipStateSender.ALMOST_CLOSE_TO_START_CAST.getExpectedStateText()) + } + + @Test fun commandQueueCallback_almostCloseToEndCast_triggersCorrectChip() { commandQueueCallback.updateMediaTapToTransferSenderDisplay( StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_ALMOST_CLOSE_TO_END_CAST, @@ -248,6 +263,21 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { } @Test + fun commandQueueCallback_transferToReceiverTriggered_deviceNameBlank_showsDefaultDeviceName() { + commandQueueCallback.updateMediaTapToTransferSenderDisplay( + StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_RECEIVER_TRIGGERED, + routeInfoWithBlankDeviceName, + null, + ) + + val chipbarView = getChipbarView() + assertThat(chipbarView.getChipText()) + .contains(context.getString(R.string.media_ttt_default_device_type)) + assertThat(chipbarView.getChipText()) + .isNotEqualTo(ChipStateSender.TRANSFER_TO_RECEIVER_TRIGGERED.getExpectedStateText()) + } + + @Test fun commandQueueCallback_transferToThisDeviceTriggered_triggersCorrectChip() { commandQueueCallback.updateMediaTapToTransferSenderDisplay( StatusBarManager.MEDIA_TRANSFER_SENDER_STATE_TRANSFER_TO_THIS_DEVICE_TRIGGERED, @@ -934,6 +964,7 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { private const val APP_NAME = "Fake app name" private const val OTHER_DEVICE_NAME = "My Tablet" +private const val BLANK_DEVICE_NAME = " " private const val PACKAGE_NAME = "com.android.systemui" private const val TIMEOUT = 10000 @@ -942,3 +973,9 @@ private val routeInfo = .addFeature("feature") .setClientPackageName(PACKAGE_NAME) .build() + +private val routeInfoWithBlankDeviceName = + MediaRoute2Info.Builder("id", BLANK_DEVICE_NAME) + .addFeature("feature") + .setClientPackageName(PACKAGE_NAME) + .build() diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt index 4a9c7508b1b3..fc90c1add5e9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt @@ -93,7 +93,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { createNoteTaskController().showNoteTask(isInMultiWindowMode = false) verify(context).startActivity(notesIntent) - verify(bubbles, never()).showAppBubble(notesIntent) + verify(bubbles, never()).showOrHideAppBubble(notesIntent) } @Test @@ -102,7 +102,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { createNoteTaskController().showNoteTask(isInMultiWindowMode = false) - verify(bubbles).showAppBubble(notesIntent) + verify(bubbles).showOrHideAppBubble(notesIntent) verify(context, never()).startActivity(notesIntent) } @@ -113,7 +113,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { createNoteTaskController().showNoteTask(isInMultiWindowMode = true) verify(context).startActivity(notesIntent) - verify(bubbles, never()).showAppBubble(notesIntent) + verify(bubbles, never()).showOrHideAppBubble(notesIntent) } @Test @@ -123,7 +123,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { createNoteTaskController().showNoteTask(isInMultiWindowMode = false) verify(context, never()).startActivity(notesIntent) - verify(bubbles, never()).showAppBubble(notesIntent) + verify(bubbles, never()).showOrHideAppBubble(notesIntent) } @Test @@ -133,7 +133,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { createNoteTaskController().showNoteTask(isInMultiWindowMode = false) verify(context, never()).startActivity(notesIntent) - verify(bubbles, never()).showAppBubble(notesIntent) + verify(bubbles, never()).showOrHideAppBubble(notesIntent) } @Test @@ -143,7 +143,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { createNoteTaskController().showNoteTask(isInMultiWindowMode = false) verify(context, never()).startActivity(notesIntent) - verify(bubbles, never()).showAppBubble(notesIntent) + verify(bubbles, never()).showOrHideAppBubble(notesIntent) } @Test @@ -153,7 +153,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { createNoteTaskController().showNoteTask(isInMultiWindowMode = false) verify(context, never()).startActivity(notesIntent) - verify(bubbles, never()).showAppBubble(notesIntent) + verify(bubbles, never()).showOrHideAppBubble(notesIntent) } @Test @@ -161,7 +161,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { createNoteTaskController(isEnabled = false).showNoteTask() verify(context, never()).startActivity(notesIntent) - verify(bubbles, never()).showAppBubble(notesIntent) + verify(bubbles, never()).showOrHideAppBubble(notesIntent) } @Test @@ -171,7 +171,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { createNoteTaskController().showNoteTask(isInMultiWindowMode = false) verify(context, never()).startActivity(notesIntent) - verify(bubbles, never()).showAppBubble(notesIntent) + verify(bubbles, never()).showOrHideAppBubble(notesIntent) } // endregion diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt index 1d30ad9293a0..f580f5e00f67 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt @@ -182,6 +182,7 @@ class LargeScreenShadeHeaderControllerCombinedTest : SysuiTestCase() { null } whenever(view.visibility).thenAnswer { _ -> viewVisibility } + whenever(view.alpha).thenReturn(1f) whenever(iconManagerFactory.create(any(), any())).thenReturn(iconManager) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerTest.kt index b4c8f981b760..b568122d3fed 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerTest.kt @@ -1,5 +1,6 @@ package com.android.systemui.shade +import android.animation.ValueAnimator import android.app.StatusBarManager import android.content.Context import android.testing.AndroidTestingRunner @@ -30,6 +31,7 @@ import com.android.systemui.statusbar.policy.VariableDateViewController import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.capture +import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat import org.junit.After import org.junit.Before @@ -37,6 +39,7 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Answers +import org.mockito.ArgumentMatchers.anyFloat import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock import org.mockito.Mockito.mock @@ -75,6 +78,7 @@ class LargeScreenShadeHeaderControllerTest : SysuiTestCase() { @JvmField @Rule val mockitoRule = MockitoJUnit.rule() var viewVisibility = View.GONE + var viewAlpha = 1f private lateinit var mLargeScreenShadeHeaderController: LargeScreenShadeHeaderController private lateinit var carrierIconSlots: List<String> @@ -101,6 +105,13 @@ class LargeScreenShadeHeaderControllerTest : SysuiTestCase() { null } whenever(view.visibility).thenAnswer { _ -> viewVisibility } + + whenever(view.setAlpha(anyFloat())).then { + viewAlpha = it.arguments[0] as Float + null + } + whenever(view.alpha).thenAnswer { _ -> viewAlpha } + whenever(variableDateViewControllerFactory.create(any())) .thenReturn(variableDateViewController) whenever(iconManagerFactory.create(any(), any())).thenReturn(iconManager) @@ -155,6 +166,16 @@ class LargeScreenShadeHeaderControllerTest : SysuiTestCase() { } @Test + fun alphaChangesUpdateVisibility() { + makeShadeVisible() + mLargeScreenShadeHeaderController.shadeExpandedFraction = 0f + assertThat(viewVisibility).isEqualTo(View.INVISIBLE) + + mLargeScreenShadeHeaderController.shadeExpandedFraction = 1f + assertThat(viewVisibility).isEqualTo(View.VISIBLE) + } + + @Test fun singleCarrier_enablesCarrierIconsInStatusIcons() { whenever(qsCarrierGroupController.isSingleCarrier).thenReturn(true) @@ -239,6 +260,39 @@ class LargeScreenShadeHeaderControllerTest : SysuiTestCase() { } @Test + fun testShadeExpanded_true_alpha_zero_invisible() { + view.alpha = 0f + mLargeScreenShadeHeaderController.largeScreenActive = true + mLargeScreenShadeHeaderController.qsVisible = true + + assertThat(viewVisibility).isEqualTo(View.INVISIBLE) + } + + @Test + fun animatorCallsUpdateVisibilityOnUpdate() { + val animator = mock(ViewPropertyAnimator::class.java, Answers.RETURNS_SELF) + whenever(view.animate()).thenReturn(animator) + + mLargeScreenShadeHeaderController.startCustomizingAnimation(show = false, 0L) + + val updateCaptor = argumentCaptor<ValueAnimator.AnimatorUpdateListener>() + verify(animator).setUpdateListener(capture(updateCaptor)) + + mLargeScreenShadeHeaderController.largeScreenActive = true + mLargeScreenShadeHeaderController.qsVisible = true + + view.alpha = 1f + updateCaptor.value.onAnimationUpdate(mock()) + + assertThat(viewVisibility).isEqualTo(View.VISIBLE) + + view.alpha = 0f + updateCaptor.value.onAnimationUpdate(mock()) + + assertThat(viewVisibility).isEqualTo(View.INVISIBLE) + } + + @Test fun demoMode_attachDemoMode() { val cb = argumentCaptor<DemoMode>() verify(demoModeController).addCallback(capture(cb)) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java index ca99e24fc105..e41929f7d578 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java @@ -29,6 +29,7 @@ import com.android.systemui.statusbar.notification.LegacySourceType; import com.android.systemui.statusbar.notification.SourceType; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.NotificationTestHelper; +import com.android.systemui.statusbar.notification.row.wrapper.NotificationHeaderViewWrapper; import org.junit.Assert; import org.junit.Before; @@ -216,4 +217,29 @@ public class NotificationChildrenContainerTest extends SysuiTestCase { Assert.assertEquals(1f, mChildrenContainer.getBottomRoundness(), 0.001f); Assert.assertEquals(1f, notificationRow.getBottomRoundness(), 0.001f); } + + @Test + public void applyRoundnessAndInvalidate_should_be_immediately_applied_on_header() { + mChildrenContainer.useRoundnessSourceTypes(true); + + NotificationHeaderViewWrapper header = mChildrenContainer.getNotificationHeaderWrapper(); + Assert.assertEquals(0f, header.getTopRoundness(), 0.001f); + + mChildrenContainer.requestTopRoundness(1f, SourceType.from(""), false); + + Assert.assertEquals(1f, header.getTopRoundness(), 0.001f); + } + + @Test + public void applyRoundnessAndInvalidate_should_be_immediately_applied_on_headerLowPriority() { + mChildrenContainer.useRoundnessSourceTypes(true); + mChildrenContainer.setIsLowPriority(true); + + NotificationHeaderViewWrapper header = mChildrenContainer.getNotificationHeaderWrapper(); + Assert.assertEquals(0f, header.getTopRoundness(), 0.001f); + + mChildrenContainer.requestTopRoundness(1f, SourceType.from(""), false); + + Assert.assertEquals(1f, header.getTopRoundness(), 0.001f); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java index 9695000fec57..ec294b12a11a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.phone; +import static com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK; + import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -54,6 +56,7 @@ import com.android.systemui.statusbar.NotificationMediaManager; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.policy.KeyguardStateController; +import com.android.systemui.util.time.FakeSystemClock; import org.junit.Before; import org.junit.Test; @@ -115,6 +118,7 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { private VibratorHelper mVibratorHelper; @Mock private BiometricUnlockLogger mLogger; + private final FakeSystemClock mSystemClock = new FakeSystemClock(); private BiometricUnlockController mBiometricUnlockController; @Before @@ -137,7 +141,9 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { mMetricsLogger, mDumpManager, mPowerManager, mLogger, mNotificationMediaManager, mWakefulnessLifecycle, mScreenLifecycle, mAuthController, mStatusBarStateController, - mSessionTracker, mLatencyTracker, mScreenOffAnimationController, mVibratorHelper); + mSessionTracker, mLatencyTracker, mScreenOffAnimationController, mVibratorHelper, + mSystemClock + ); mBiometricUnlockController.setKeyguardViewController(mStatusBarKeyguardViewManager); mBiometricUnlockController.addBiometricModeListener(mBiometricModeListener); when(mUpdateMonitor.getStrongAuthTracker()).thenReturn(mStrongAuthTracker); @@ -200,7 +206,7 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { verify(mKeyguardViewMediator).onWakeAndUnlocking(); assertThat(mBiometricUnlockController.getMode()) - .isEqualTo(BiometricUnlockController.MODE_WAKE_AND_UNLOCK); + .isEqualTo(MODE_WAKE_AND_UNLOCK); } @Test @@ -437,4 +443,83 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { // THEN wakeup the device verify(mPowerManager).wakeUp(anyLong(), anyInt(), anyString()); } + + @Test + public void onSideFingerprintSuccess_recentPowerButtonPress_noHaptic() { + // GIVEN side fingerprint enrolled, last wake reason was power button + when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); + when(mWakefulnessLifecycle.getLastWakeReason()) + .thenReturn(PowerManager.WAKE_REASON_POWER_BUTTON); + + // GIVEN last wake time just occurred + when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis()); + + // WHEN biometric fingerprint succeeds + givenFingerprintModeUnlockCollapsing(); + mBiometricUnlockController.startWakeAndUnlock(BiometricSourceType.FINGERPRINT, + true); + + // THEN DO NOT vibrate the device + verify(mVibratorHelper, never()).vibrateAuthSuccess(anyString()); + } + + @Test + public void onSideFingerprintSuccess_oldPowerButtonPress_playHaptic() { + // GIVEN side fingerprint enrolled, last wake reason was power button + when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); + when(mWakefulnessLifecycle.getLastWakeReason()) + .thenReturn(PowerManager.WAKE_REASON_POWER_BUTTON); + + // GIVEN last wake time was 500ms ago + when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis()); + mSystemClock.advanceTime(500); + + // WHEN biometric fingerprint succeeds + givenFingerprintModeUnlockCollapsing(); + mBiometricUnlockController.startWakeAndUnlock(BiometricSourceType.FINGERPRINT, + true); + + // THEN vibrate the device + verify(mVibratorHelper).vibrateAuthSuccess(anyString()); + } + + @Test + public void onSideFingerprintSuccess_recentGestureWakeUp_playHaptic() { + // GIVEN side fingerprint enrolled, wakeup just happened + when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); + when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis()); + + // GIVEN last wake reason was from a gesture + when(mWakefulnessLifecycle.getLastWakeReason()) + .thenReturn(PowerManager.WAKE_REASON_GESTURE); + + // WHEN biometric fingerprint succeeds + givenFingerprintModeUnlockCollapsing(); + mBiometricUnlockController.startWakeAndUnlock(BiometricSourceType.FINGERPRINT, + true); + + // THEN vibrate the device + verify(mVibratorHelper).vibrateAuthSuccess(anyString()); + } + + @Test + public void onSideFingerprintFail_alwaysPlaysHaptic() { + // GIVEN side fingerprint enrolled, last wake reason was recent power button + when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); + when(mWakefulnessLifecycle.getLastWakeReason()) + .thenReturn(PowerManager.WAKE_REASON_POWER_BUTTON); + when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis()); + + // WHEN biometric fingerprint fails + mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT); + + // THEN always vibrate the device + verify(mVibratorHelper).vibrateAuthError(anyString()); + } + + private void givenFingerprintModeUnlockCollapsing() { + when(mUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true); + when(mUpdateMonitor.isDeviceInteractive()).thenReturn(true); + when(mKeyguardStateController.isShowing()).thenReturn(true); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java index 3a1f9b748e49..c8157ccc8a9a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java @@ -178,8 +178,6 @@ import com.android.systemui.volume.VolumeComponent; import com.android.wm.shell.bubbles.Bubbles; import com.android.wm.shell.startingsurface.StartingSurface; -import dagger.Lazy; - import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -192,6 +190,8 @@ import java.io.ByteArrayOutputStream; import java.io.PrintWriter; import java.util.Optional; +import dagger.Lazy; + @SmallTest @RunWith(AndroidTestingRunner.class) @RunWithLooper(setAsMainLooper = true) @@ -380,7 +380,8 @@ public class CentralSurfacesImplTest extends SysuiTestCase { }).when(mStatusBarKeyguardViewManager).addAfterKeyguardGoneRunnable(any()); mWakefulnessLifecycle = - new WakefulnessLifecycle(mContext, mIWallpaperManager, mDumpManager); + new WakefulnessLifecycle(mContext, mIWallpaperManager, mFakeSystemClock, + mDumpManager); mWakefulnessLifecycle.dispatchStartedWakingUp(PowerManager.WAKE_REASON_UNKNOWN); mWakefulnessLifecycle.dispatchFinishedWakingUp(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java index 077b41a0aa90..c8438501b3e6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java @@ -23,6 +23,10 @@ import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.res.Resources; @@ -39,10 +43,9 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.doze.AlwaysOnDisplayPolicy; import com.android.systemui.doze.DozeScreenState; import com.android.systemui.dump.DumpManager; -import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.flags.Flags; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.statusbar.policy.BatteryController; +import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.tuner.TunerService; import com.android.systemui.unfold.FoldAodAnimationController; @@ -52,6 +55,8 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -69,7 +74,6 @@ public class DozeParametersTest extends SysuiTestCase { @Mock private PowerManager mPowerManager; @Mock private TunerService mTunerService; @Mock private BatteryController mBatteryController; - @Mock private FeatureFlags mFeatureFlags; @Mock private DumpManager mDumpManager; @Mock private ScreenOffAnimationController mScreenOffAnimationController; @Mock private FoldAodAnimationController mFoldAodAnimationController; @@ -78,6 +82,7 @@ public class DozeParametersTest extends SysuiTestCase { @Mock private KeyguardUpdateMonitor mKeyguardUpdateMonitor; @Mock private StatusBarStateController mStatusBarStateController; @Mock private ConfigurationController mConfigurationController; + @Captor private ArgumentCaptor<BatteryStateChangeCallback> mBatteryStateChangeCallback; /** * The current value of PowerManager's dozeAfterScreenOff property. @@ -113,7 +118,6 @@ public class DozeParametersTest extends SysuiTestCase { mBatteryController, mTunerService, mDumpManager, - mFeatureFlags, mScreenOffAnimationController, Optional.of(mSysUIUnfoldComponent), mUnlockedScreenOffAnimationController, @@ -122,7 +126,8 @@ public class DozeParametersTest extends SysuiTestCase { mStatusBarStateController ); - when(mFeatureFlags.isEnabled(Flags.LOCKSCREEN_ANIMATIONS)).thenReturn(true); + verify(mBatteryController).addCallback(mBatteryStateChangeCallback.capture()); + setAodEnabledForTest(true); setShouldControlUnlockedScreenOffForTest(true); setDisplayNeedsBlankingForTest(false); @@ -173,6 +178,29 @@ public class DozeParametersTest extends SysuiTestCase { assertThat(mDozeParameters.getAlwaysOn()).isFalse(); } + @Test + public void testGetAlwaysOn_whenBatterySaverCallback() { + DozeParameters.Callback callback = mock(DozeParameters.Callback.class); + mDozeParameters.addCallback(callback); + + when(mAmbientDisplayConfiguration.alwaysOnEnabled(anyInt())).thenReturn(true); + when(mBatteryController.isAodPowerSave()).thenReturn(true); + + // Both lines should trigger an event + mDozeParameters.onTuningChanged(Settings.Secure.DOZE_ALWAYS_ON, "1"); + mBatteryStateChangeCallback.getValue().onPowerSaveChanged(true); + + verify(callback, times(2)).onAlwaysOnChange(); + assertThat(mDozeParameters.getAlwaysOn()).isFalse(); + + reset(callback); + when(mBatteryController.isAodPowerSave()).thenReturn(false); + mBatteryStateChangeCallback.getValue().onPowerSaveChanged(true); + + verify(callback).onAlwaysOnChange(); + assertThat(mDozeParameters.getAlwaysOn()).isTrue(); + } + /** * PowerManager.setDozeAfterScreenOff(true) means we are not controlling screen off, and calling * it with false means we are. Confusing, but sure - make sure that we call PowerManager with @@ -196,17 +224,6 @@ public class DozeParametersTest extends SysuiTestCase { } @Test - public void testControlUnlockedScreenOffAnimationDisabled_dozeAfterScreenOff() { - when(mFeatureFlags.isEnabled(Flags.LOCKSCREEN_ANIMATIONS)).thenReturn(false); - - assertFalse(mDozeParameters.shouldControlUnlockedScreenOff()); - - // Trigger the setter for the current value. - mDozeParameters.setControlScreenOffAnimation(mDozeParameters.shouldControlScreenOff()); - assertFalse(mDozeParameters.shouldControlScreenOff()); - } - - @Test public void propagatesAnimateScreenOff_noAlwaysOn() { setAodEnabledForTest(false); setDisplayNeedsBlankingForTest(false); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt index 0da15e239932..09589707b331 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt @@ -37,6 +37,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.log.table.TableLogBufferFactory import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel +import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger @@ -46,6 +47,7 @@ import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.android.systemui.util.settings.FakeSettings +import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -94,7 +96,7 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { } } - whenever(logBufferFactory.create(anyString(), anyInt())).thenAnswer { _ -> + whenever(logBufferFactory.getOrCreate(anyString(), anyInt())).thenAnswer { _ -> mock<TableLogBuffer>() } @@ -292,13 +294,13 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { // Get repos to trigger creation underTest.getRepoForSubId(SUB_1_ID) verify(logBufferFactory) - .create( + .getOrCreate( eq(MobileConnectionRepositoryImpl.tableBufferLogName(SUB_1_ID)), anyInt(), ) underTest.getRepoForSubId(SUB_2_ID) verify(logBufferFactory) - .create( + .getOrCreate( eq(MobileConnectionRepositoryImpl.tableBufferLogName(SUB_2_ID)), anyInt(), ) @@ -307,6 +309,46 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { } @Test + fun `connection repository factory - reuses log buffers for same connection`() = + runBlocking(IMMEDIATE) { + val realLoggerFactory = TableLogBufferFactory(mock(), FakeSystemClock()) + + connectionFactory = + MobileConnectionRepositoryImpl.Factory( + fakeBroadcastDispatcher, + context = context, + telephonyManager = telephonyManager, + bgDispatcher = IMMEDIATE, + globalSettings = globalSettings, + logger = logger, + mobileMappingsProxy = mobileMappings, + scope = scope, + logFactory = realLoggerFactory, + ) + + // Create two connections for the same subId. Similar to if the connection appeared + // and disappeared from the connectionFactory's perspective + val connection1 = + connectionFactory.build( + 1, + NetworkNameModel.Default("default_name"), + "-", + underTest.globalMobileDataSettingChangedEvent, + ) + + val connection1_repeat = + connectionFactory.build( + 1, + NetworkNameModel.Default("default_name"), + "-", + underTest.globalMobileDataSettingChangedEvent, + ) + + assertThat(connection1.tableLogBuffer) + .isSameInstanceAs(connection1_repeat.tableLogBuffer) + } + + @Test fun mobileConnectivity_default() { assertThat(underTest.defaultMobileNetworkConnectivity.value) .isEqualTo(MobileConnectivityModel(isConnected = false, isValidated = false)) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java index 4b32ee262cdc..0cca7b2aa38c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java @@ -390,19 +390,27 @@ public class RemoteInputViewTest extends SysuiTestCase { bindController(view, row.getEntry()); view.setVisibility(View.GONE); - View crossFadeView = new View(mContext); + View fadeOutView = new View(mContext); + fadeOutView.setId(com.android.internal.R.id.actions_container_layout); - // Start focus animation - view.focusAnimated(crossFadeView); + FrameLayout parent = new FrameLayout(mContext); + parent.addView(view); + parent.addView(fadeOutView); + // Start focus animation + view.focusAnimated(); assertTrue(view.isAnimatingAppearance()); + // fast forward to 1 ms before end of animation and verify fadeOutView has alpha set to 0f + mAnimatorTestRule.advanceTimeBy(ANIMATION_DURATION_STANDARD - 1); + assertEquals(0f, fadeOutView.getAlpha()); + // fast forward to end of animation - mAnimatorTestRule.advanceTimeBy(ANIMATION_DURATION_STANDARD); + mAnimatorTestRule.advanceTimeBy(1); - // assert that crossFadeView's alpha is reset to 1f after the animation (hidden behind + // assert that fadeOutView's alpha is reset to 1f after the animation (hidden behind // RemoteInputView) - assertEquals(1f, crossFadeView.getAlpha()); + assertEquals(1f, fadeOutView.getAlpha()); assertFalse(view.isAnimatingAppearance()); assertEquals(View.VISIBLE, view.getVisibility()); assertEquals(1f, view.getAlpha()); @@ -415,20 +423,27 @@ public class RemoteInputViewTest extends SysuiTestCase { mDependency, TestableLooper.get(this)); ExpandableNotificationRow row = helper.createRow(); - FrameLayout remoteInputViewParent = new FrameLayout(mContext); RemoteInputView view = RemoteInputView.inflate(mContext, null, row.getEntry(), mController); - remoteInputViewParent.addView(view); bindController(view, row.getEntry()); + View fadeInView = new View(mContext); + fadeInView.setId(com.android.internal.R.id.actions_container_layout); + + FrameLayout parent = new FrameLayout(mContext); + parent.addView(view); + parent.addView(fadeInView); + // Start defocus animation - view.onDefocus(true, false); + view.onDefocus(true /* animate */, false /* logClose */, null /* doAfterDefocus */); assertEquals(View.VISIBLE, view.getVisibility()); + assertEquals(0f, fadeInView.getAlpha()); // fast forward to end of animation mAnimatorTestRule.advanceTimeBy(ANIMATION_DURATION_STANDARD); // assert that RemoteInputView is no longer visible assertEquals(View.GONE, view.getVisibility()); + assertEquals(1f, fadeInView.getAlpha()); } // NOTE: because we're refactoring the RemoteInputView and moving logic into the diff --git a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusFirstUsageListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusFirstUsageListenerTest.kt deleted file mode 100644 index 8dd088f5760c..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusFirstUsageListenerTest.kt +++ /dev/null @@ -1,289 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.systemui.stylus - -import android.content.Context -import android.hardware.BatteryState -import android.hardware.input.InputManager -import android.os.Handler -import android.testing.AndroidTestingRunner -import android.view.InputDevice -import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags -import com.android.systemui.util.concurrency.FakeExecutor -import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.whenever -import com.android.systemui.util.time.FakeSystemClock -import org.junit.Before -import org.junit.Ignore -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.Mockito.never -import org.mockito.Mockito.times -import org.mockito.Mockito.verify -import org.mockito.Mockito.verifyNoMoreInteractions -import org.mockito.Mockito.verifyZeroInteractions -import org.mockito.MockitoAnnotations - -@RunWith(AndroidTestingRunner::class) -@SmallTest -@Ignore("TODO(b/20579491): unignore on main") -class StylusFirstUsageListenerTest : SysuiTestCase() { - @Mock lateinit var context: Context - @Mock lateinit var inputManager: InputManager - @Mock lateinit var stylusManager: StylusManager - @Mock lateinit var featureFlags: FeatureFlags - @Mock lateinit var internalStylusDevice: InputDevice - @Mock lateinit var otherDevice: InputDevice - @Mock lateinit var externalStylusDevice: InputDevice - @Mock lateinit var batteryState: BatteryState - @Mock lateinit var handler: Handler - - private lateinit var stylusListener: StylusFirstUsageListener - - @Before - fun setUp() { - MockitoAnnotations.initMocks(this) - whenever(featureFlags.isEnabled(Flags.TRACK_STYLUS_EVER_USED)).thenReturn(true) - whenever(inputManager.isStylusEverUsed(context)).thenReturn(false) - - stylusListener = - StylusFirstUsageListener( - context, - inputManager, - stylusManager, - featureFlags, - EXECUTOR, - handler - ) - stylusListener.hasStarted = false - - whenever(handler.post(any())).thenAnswer { - (it.arguments[0] as Runnable).run() - true - } - - whenever(otherDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(false) - whenever(internalStylusDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(true) - whenever(internalStylusDevice.isExternal).thenReturn(false) - whenever(externalStylusDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(true) - whenever(externalStylusDevice.isExternal).thenReturn(true) - - whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf()) - whenever(inputManager.getInputDevice(OTHER_DEVICE_ID)).thenReturn(otherDevice) - whenever(inputManager.getInputDevice(INTERNAL_STYLUS_DEVICE_ID)) - .thenReturn(internalStylusDevice) - whenever(inputManager.getInputDevice(EXTERNAL_STYLUS_DEVICE_ID)) - .thenReturn(externalStylusDevice) - } - - @Test - fun start_flagDisabled_doesNotRegister() { - whenever(featureFlags.isEnabled(Flags.TRACK_STYLUS_EVER_USED)).thenReturn(false) - - stylusListener.start() - - verify(stylusManager, never()).registerCallback(any()) - verify(inputManager, never()).setStylusEverUsed(context, true) - } - - @Test - fun start_toggleHasStarted() { - stylusListener.start() - - assert(stylusListener.hasStarted) - } - - @Test - fun start_hasStarted_doesNotRegister() { - stylusListener.hasStarted = true - - stylusListener.start() - - verify(stylusManager, never()).registerCallback(any()) - } - - @Test - fun start_hostDeviceDoesNotSupportStylus_doesNotRegister() { - whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf(OTHER_DEVICE_ID)) - - stylusListener.start() - - verify(stylusManager, never()).registerCallback(any()) - verify(inputManager, never()).setStylusEverUsed(context, true) - } - - @Test - fun start_stylusEverUsed_doesNotRegister() { - whenever(inputManager.inputDeviceIds) - .thenReturn(intArrayOf(OTHER_DEVICE_ID, INTERNAL_STYLUS_DEVICE_ID)) - whenever(inputManager.isStylusEverUsed(context)).thenReturn(true) - - stylusListener.start() - - verify(stylusManager, never()).registerCallback(any()) - verify(inputManager, never()).setStylusEverUsed(context, true) - } - - @Test - fun start_hostDeviceSupportsStylus_registersListener() { - whenever(inputManager.inputDeviceIds) - .thenReturn(intArrayOf(OTHER_DEVICE_ID, INTERNAL_STYLUS_DEVICE_ID)) - - stylusListener.start() - - verify(stylusManager).registerCallback(any()) - verify(inputManager, never()).setStylusEverUsed(context, true) - } - - @Test - fun onStylusAdded_hasNotStarted_doesNotRegisterListener() { - stylusListener.hasStarted = false - - stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID) - - verifyZeroInteractions(inputManager) - } - - @Test - fun onStylusAdded_internalStylus_registersListener() { - stylusListener.hasStarted = true - - stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID) - - verify(inputManager, times(1)) - .addInputDeviceBatteryListener(INTERNAL_STYLUS_DEVICE_ID, EXECUTOR, stylusListener) - } - - @Test - fun onStylusAdded_externalStylus_doesNotRegisterListener() { - stylusListener.hasStarted = true - - stylusListener.onStylusAdded(EXTERNAL_STYLUS_DEVICE_ID) - - verify(inputManager, never()).addInputDeviceBatteryListener(any(), any(), any()) - } - - @Test - fun onStylusAdded_otherDevice_doesNotRegisterListener() { - stylusListener.onStylusAdded(OTHER_DEVICE_ID) - - verify(inputManager, never()).addInputDeviceBatteryListener(any(), any(), any()) - } - - @Test - fun onStylusRemoved_registeredDevice_unregistersListener() { - stylusListener.hasStarted = true - stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID) - - stylusListener.onStylusRemoved(INTERNAL_STYLUS_DEVICE_ID) - - verify(inputManager, times(1)) - .removeInputDeviceBatteryListener(INTERNAL_STYLUS_DEVICE_ID, stylusListener) - } - - @Test - fun onStylusRemoved_hasNotStarted_doesNotUnregisterListener() { - stylusListener.hasStarted = false - stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID) - - stylusListener.onStylusRemoved(INTERNAL_STYLUS_DEVICE_ID) - - verifyZeroInteractions(inputManager) - } - - @Test - fun onStylusRemoved_unregisteredDevice_doesNotUnregisterListener() { - stylusListener.hasStarted = true - - stylusListener.onStylusRemoved(INTERNAL_STYLUS_DEVICE_ID) - - verifyNoMoreInteractions(inputManager) - } - - @Test - fun onStylusBluetoothConnected_updateStylusFlagAndUnregisters() { - stylusListener.hasStarted = true - stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID) - - stylusListener.onStylusBluetoothConnected(EXTERNAL_STYLUS_DEVICE_ID, "ANY") - - verify(inputManager).setStylusEverUsed(context, true) - verify(inputManager, times(1)) - .removeInputDeviceBatteryListener(INTERNAL_STYLUS_DEVICE_ID, stylusListener) - verify(stylusManager).unregisterCallback(stylusListener) - } - - @Test - fun onStylusBluetoothConnected_hasNotStarted_doesNoting() { - stylusListener.hasStarted = false - stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID) - - stylusListener.onStylusBluetoothConnected(EXTERNAL_STYLUS_DEVICE_ID, "ANY") - - verifyZeroInteractions(inputManager) - verifyZeroInteractions(stylusManager) - } - - @Test - fun onBatteryStateChanged_batteryPresent_updateStylusFlagAndUnregisters() { - stylusListener.hasStarted = true - stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID) - whenever(batteryState.isPresent).thenReturn(true) - - stylusListener.onBatteryStateChanged(0, 1, batteryState) - - verify(inputManager).setStylusEverUsed(context, true) - verify(inputManager, times(1)) - .removeInputDeviceBatteryListener(INTERNAL_STYLUS_DEVICE_ID, stylusListener) - verify(stylusManager).unregisterCallback(stylusListener) - } - - @Test - fun onBatteryStateChanged_batteryNotPresent_doesNotUpdateFlagOrUnregister() { - stylusListener.hasStarted = true - stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID) - whenever(batteryState.isPresent).thenReturn(false) - - stylusListener.onBatteryStateChanged(0, 1, batteryState) - - verifyZeroInteractions(stylusManager) - verify(inputManager, never()) - .removeInputDeviceBatteryListener(INTERNAL_STYLUS_DEVICE_ID, stylusListener) - } - - @Test - fun onBatteryStateChanged_hasNotStarted_doesNothing() { - stylusListener.hasStarted = false - stylusListener.onStylusAdded(INTERNAL_STYLUS_DEVICE_ID) - whenever(batteryState.isPresent).thenReturn(false) - - stylusListener.onBatteryStateChanged(0, 1, batteryState) - - verifyZeroInteractions(inputManager) - verifyZeroInteractions(stylusManager) - } - - companion object { - private const val OTHER_DEVICE_ID = 0 - private const val INTERNAL_STYLUS_DEVICE_ID = 1 - private const val EXTERNAL_STYLUS_DEVICE_ID = 2 - private val EXECUTOR = FakeExecutor(FakeSystemClock()) - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusManagerTest.kt index 984de5b67bf5..6d6e40a90fa6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusManagerTest.kt @@ -17,12 +17,15 @@ package com.android.systemui.stylus import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice +import android.hardware.BatteryState import android.hardware.input.InputManager import android.os.Handler import android.testing.AndroidTestingRunner import android.view.InputDevice import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.whenever import java.util.concurrent.Executor @@ -31,30 +34,27 @@ import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock +import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.inOrder import org.mockito.Mockito.never import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.Mockito.verifyNoMoreInteractions +import org.mockito.Mockito.verifyZeroInteractions import org.mockito.MockitoAnnotations @RunWith(AndroidTestingRunner::class) @SmallTest -@Ignore("b/257936830 until bt APIs") class StylusManagerTest : SysuiTestCase() { @Mock lateinit var inputManager: InputManager - @Mock lateinit var stylusDevice: InputDevice - @Mock lateinit var btStylusDevice: InputDevice - @Mock lateinit var otherDevice: InputDevice - + @Mock lateinit var batteryState: BatteryState @Mock lateinit var bluetoothAdapter: BluetoothAdapter - @Mock lateinit var bluetoothDevice: BluetoothDevice - @Mock lateinit var handler: Handler + @Mock lateinit var featureFlags: FeatureFlags @Mock lateinit var stylusCallback: StylusManager.StylusCallback @@ -75,11 +75,8 @@ class StylusManagerTest : SysuiTestCase() { true } - stylusManager = StylusManager(inputManager, bluetoothAdapter, handler, EXECUTOR) - - stylusManager.registerCallback(stylusCallback) - - stylusManager.registerBatteryCallback(stylusBatteryCallback) + stylusManager = + StylusManager(mContext, inputManager, bluetoothAdapter, handler, EXECUTOR, featureFlags) whenever(otherDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(false) whenever(stylusDevice.supportsSource(InputDevice.SOURCE_STYLUS)).thenReturn(true) @@ -92,19 +89,47 @@ class StylusManagerTest : SysuiTestCase() { whenever(inputManager.getInputDevice(STYLUS_DEVICE_ID)).thenReturn(stylusDevice) whenever(inputManager.getInputDevice(BT_STYLUS_DEVICE_ID)).thenReturn(btStylusDevice) whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf(STYLUS_DEVICE_ID)) + whenever(inputManager.isStylusEverUsed(mContext)).thenReturn(false) whenever(bluetoothAdapter.getRemoteDevice(STYLUS_BT_ADDRESS)).thenReturn(bluetoothDevice) whenever(bluetoothDevice.address).thenReturn(STYLUS_BT_ADDRESS) + + whenever(featureFlags.isEnabled(Flags.TRACK_STYLUS_EVER_USED)).thenReturn(true) + + stylusManager.startListener() + stylusManager.registerCallback(stylusCallback) + stylusManager.registerBatteryCallback(stylusBatteryCallback) + clearInvocations(inputManager) } @Test - fun startListener_registersInputDeviceListener() { + fun startListener_hasNotStarted_registersInputDeviceListener() { + stylusManager = + StylusManager(mContext, inputManager, bluetoothAdapter, handler, EXECUTOR, featureFlags) + stylusManager.startListener() verify(inputManager, times(1)).registerInputDeviceListener(any(), any()) } @Test + fun startListener_hasStarted_doesNothing() { + stylusManager.startListener() + + verifyZeroInteractions(inputManager) + } + + @Test + fun onInputDeviceAdded_hasNotStarted_doesNothing() { + stylusManager = + StylusManager(mContext, inputManager, bluetoothAdapter, handler, EXECUTOR, featureFlags) + + stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID) + + verifyZeroInteractions(stylusCallback) + } + + @Test fun onInputDeviceAdded_multipleRegisteredCallbacks_callsAll() { stylusManager.registerCallback(otherStylusCallback) @@ -117,6 +142,26 @@ class StylusManagerTest : SysuiTestCase() { } @Test + fun onInputDeviceAdded_internalStylus_registersBatteryListener() { + whenever(stylusDevice.isExternal).thenReturn(false) + + stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID) + + verify(inputManager, times(1)) + .addInputDeviceBatteryListener(STYLUS_DEVICE_ID, EXECUTOR, stylusManager) + } + + @Test + fun onInputDeviceAdded_externalStylus_doesNotRegisterbatteryListener() { + whenever(stylusDevice.isExternal).thenReturn(true) + + stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID) + + verify(inputManager, never()) + .addInputDeviceBatteryListener(STYLUS_DEVICE_ID, EXECUTOR, stylusManager) + } + + @Test fun onInputDeviceAdded_stylus_callsCallbacksOnStylusAdded() { stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID) @@ -125,6 +170,23 @@ class StylusManagerTest : SysuiTestCase() { } @Test + @Ignore("b/257936830 until bt APIs") + fun onInputDeviceAdded_btStylus_firstUsed_callsCallbacksOnStylusFirstUsed() { + stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) + + verify(stylusCallback, times(1)).onStylusFirstUsed() + } + + @Test + @Ignore("b/257936830 until bt APIs") + fun onInputDeviceAdded_btStylus_firstUsed_setsFlag() { + stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) + + verify(inputManager, times(1)).setStylusEverUsed(mContext, true) + } + + @Test + @Ignore("b/257936830 until bt APIs") fun onInputDeviceAdded_btStylus_callsCallbacksWithAddress() { stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) @@ -143,6 +205,17 @@ class StylusManagerTest : SysuiTestCase() { } @Test + fun onInputDeviceChanged_hasNotStarted_doesNothing() { + stylusManager = + StylusManager(mContext, inputManager, bluetoothAdapter, handler, EXECUTOR, featureFlags) + + stylusManager.onInputDeviceChanged(STYLUS_DEVICE_ID) + + verifyZeroInteractions(stylusCallback) + } + + @Test + @Ignore("b/257936830 until bt APIs") fun onInputDeviceChanged_multipleRegisteredCallbacks_callsAll() { stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID) // whenever(stylusDevice.bluetoothAddress).thenReturn(STYLUS_BT_ADDRESS) @@ -157,6 +230,7 @@ class StylusManagerTest : SysuiTestCase() { } @Test + @Ignore("b/257936830 until bt APIs") fun onInputDeviceChanged_stylusNewBtConnection_callsCallbacks() { stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID) // whenever(stylusDevice.bluetoothAddress).thenReturn(STYLUS_BT_ADDRESS) @@ -168,6 +242,7 @@ class StylusManagerTest : SysuiTestCase() { } @Test + @Ignore("b/257936830 until bt APIs") fun onInputDeviceChanged_stylusLostBtConnection_callsCallbacks() { stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) // whenever(btStylusDevice.bluetoothAddress).thenReturn(null) @@ -179,6 +254,7 @@ class StylusManagerTest : SysuiTestCase() { } @Test + @Ignore("b/257936830 until bt APIs") fun onInputDeviceChanged_btConnection_stylusAlreadyBtConnected_onlyCallsListenersOnce() { stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) @@ -189,6 +265,7 @@ class StylusManagerTest : SysuiTestCase() { } @Test + @Ignore("b/257936830 until bt APIs") fun onInputDeviceChanged_noBtConnection_stylusNeverBtConnected_doesNotCallCallbacks() { stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID) @@ -198,6 +275,17 @@ class StylusManagerTest : SysuiTestCase() { } @Test + fun onInputDeviceRemoved_hasNotStarted_doesNothing() { + stylusManager = + StylusManager(mContext, inputManager, bluetoothAdapter, handler, EXECUTOR, featureFlags) + stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID) + + stylusManager.onInputDeviceRemoved(STYLUS_DEVICE_ID) + + verifyZeroInteractions(stylusCallback) + } + + @Test fun onInputDeviceRemoved_multipleRegisteredCallbacks_callsAll() { stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID) stylusManager.registerCallback(otherStylusCallback) @@ -219,6 +307,17 @@ class StylusManagerTest : SysuiTestCase() { } @Test + fun onInputDeviceRemoved_unregistersBatteryListener() { + stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID) + + stylusManager.onInputDeviceRemoved(STYLUS_DEVICE_ID) + + verify(inputManager, times(1)) + .removeInputDeviceBatteryListener(STYLUS_DEVICE_ID, stylusManager) + } + + @Test + @Ignore("b/257936830 until bt APIs") fun onInputDeviceRemoved_btStylus_callsCallbacks() { stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) @@ -232,6 +331,7 @@ class StylusManagerTest : SysuiTestCase() { } @Test + @Ignore("b/257936830 until bt APIs") fun onStylusBluetoothConnected_registersMetadataListener() { stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) @@ -239,6 +339,7 @@ class StylusManagerTest : SysuiTestCase() { } @Test + @Ignore("b/257936830 until bt APIs") fun onStylusBluetoothConnected_noBluetoothDevice_doesNotRegisterMetadataListener() { whenever(bluetoothAdapter.getRemoteDevice(STYLUS_BT_ADDRESS)).thenReturn(null) @@ -248,6 +349,7 @@ class StylusManagerTest : SysuiTestCase() { } @Test + @Ignore("b/257936830 until bt APIs") fun onStylusBluetoothDisconnected_unregistersMetadataListener() { stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) @@ -257,6 +359,7 @@ class StylusManagerTest : SysuiTestCase() { } @Test + @Ignore("b/257936830 until bt APIs") fun onMetadataChanged_multipleRegisteredBatteryCallbacks_executesAll() { stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) stylusManager.registerBatteryCallback(otherStylusBatteryCallback) @@ -274,6 +377,7 @@ class StylusManagerTest : SysuiTestCase() { } @Test + @Ignore("b/257936830 until bt APIs") fun onMetadataChanged_chargingStateTrue_executesBatteryCallbacks() { stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) @@ -288,6 +392,7 @@ class StylusManagerTest : SysuiTestCase() { } @Test + @Ignore("b/257936830 until bt APIs") fun onMetadataChanged_chargingStateFalse_executesBatteryCallbacks() { stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) @@ -302,6 +407,7 @@ class StylusManagerTest : SysuiTestCase() { } @Test + @Ignore("b/257936830 until bt APIs") fun onMetadataChanged_chargingStateNoDevice_doesNotExecuteBatteryCallbacks() { stylusManager.onMetadataChanged( bluetoothDevice, @@ -313,6 +419,7 @@ class StylusManagerTest : SysuiTestCase() { } @Test + @Ignore("b/257936830 until bt APIs") fun onMetadataChanged_notChargingState_doesNotExecuteBatteryCallbacks() { stylusManager.onInputDeviceAdded(BT_STYLUS_DEVICE_ID) @@ -326,6 +433,63 @@ class StylusManagerTest : SysuiTestCase() { .onStylusBluetoothChargingStateChanged(any(), any(), any()) } + @Test + @Ignore("TODO(b/261826950): remove on main") + fun onBatteryStateChanged_batteryPresent_stylusNeverUsed_updateEverUsedFlag() { + whenever(batteryState.isPresent).thenReturn(true) + + stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState) + + verify(inputManager).setStylusEverUsed(mContext, true) + } + + @Test + @Ignore("TODO(b/261826950): remove on main") + fun onBatteryStateChanged_batteryPresent_stylusNeverUsed_executesStylusFirstUsed() { + whenever(batteryState.isPresent).thenReturn(true) + + stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState) + + verify(stylusCallback, times(1)).onStylusFirstUsed() + } + + @Test + @Ignore("TODO(b/261826950): remove on main") + fun onBatteryStateChanged_batteryPresent_stylusUsed_doesNotUpdateEverUsedFlag() { + whenever(inputManager.isStylusEverUsed(mContext)).thenReturn(true) + whenever(batteryState.isPresent).thenReturn(true) + + stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState) + + verify(inputManager, never()).setStylusEverUsed(mContext, true) + } + + @Test + @Ignore("TODO(b/261826950): remove on main") + fun onBatteryStateChanged_batteryNotPresent_doesNotUpdateEverUsedFlag() { + whenever(batteryState.isPresent).thenReturn(false) + + stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState) + + verify(inputManager, never()) + .removeInputDeviceBatteryListener(STYLUS_DEVICE_ID, stylusManager) + } + + @Test + fun onBatteryStateChanged_hasNotStarted_doesNothing() { + stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState) + + verifyZeroInteractions(inputManager) + } + + @Test + fun onBatteryStateChanged_executesBatteryCallbacks() { + stylusManager.onBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState) + + verify(stylusBatteryCallback, times(1)) + .onStylusUsiBatteryStateChanged(STYLUS_DEVICE_ID, 1, batteryState) + } + companion object { private val EXECUTOR = Executor { r -> r.run() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerStartableTest.kt index ff382a3ec19f..117e00dd9b2a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerStartableTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerStartableTest.kt @@ -25,17 +25,15 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.util.mockito.whenever -import java.util.concurrent.Executor import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock -import org.mockito.Mockito.inOrder import org.mockito.Mockito.mock -import org.mockito.Mockito.never import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.Mockito.verifyNoMoreInteractions +import org.mockito.Mockito.verifyZeroInteractions import org.mockito.MockitoAnnotations @RunWith(AndroidTestingRunner::class) @@ -60,7 +58,6 @@ class StylusUsiPowerStartableTest : SysuiTestCase() { inputManager, stylusUsiPowerUi, featureFlags, - DIRECT_EXECUTOR, ) whenever(featureFlags.isEnabled(Flags.ENABLE_USI_BATTERY_NOTIFICATIONS)).thenReturn(true) @@ -79,40 +76,12 @@ class StylusUsiPowerStartableTest : SysuiTestCase() { } @Test - fun start_addsBatteryListenerForInternalStylus() { - startable.start() - - verify(inputManager, times(1)) - .addInputDeviceBatteryListener(STYLUS_DEVICE_ID, DIRECT_EXECUTOR, startable) - } - - @Test - fun onStylusAdded_internalStylus_addsBatteryListener() { - startable.onStylusAdded(STYLUS_DEVICE_ID) - - verify(inputManager, times(1)) - .addInputDeviceBatteryListener(STYLUS_DEVICE_ID, DIRECT_EXECUTOR, startable) - } + fun start_hostDeviceDoesNotSupportStylus_doesNotRegister() { + whenever(inputManager.inputDeviceIds).thenReturn(intArrayOf(EXTERNAL_DEVICE_ID)) - @Test - fun onStylusAdded_externalStylus_doesNotAddBatteryListener() { - startable.onStylusAdded(EXTERNAL_DEVICE_ID) - - verify(inputManager, never()) - .addInputDeviceBatteryListener(EXTERNAL_DEVICE_ID, DIRECT_EXECUTOR, startable) - } + startable.start() - @Test - fun onStylusRemoved_registeredStylus_removesBatteryListener() { - startable.onStylusAdded(STYLUS_DEVICE_ID) - startable.onStylusRemoved(STYLUS_DEVICE_ID) - - inOrder(inputManager).let { - it.verify(inputManager, times(1)) - .addInputDeviceBatteryListener(STYLUS_DEVICE_ID, DIRECT_EXECUTOR, startable) - it.verify(inputManager, times(1)) - .removeInputDeviceBatteryListener(STYLUS_DEVICE_ID, startable) - } + verifyZeroInteractions(stylusManager) } @Test @@ -130,28 +99,26 @@ class StylusUsiPowerStartableTest : SysuiTestCase() { } @Test - fun onBatteryStateChanged_batteryPresent_refreshesNotification() { + fun onStylusUsiBatteryStateChanged_batteryPresent_refreshesNotification() { val batteryState = mock(BatteryState::class.java) whenever(batteryState.isPresent).thenReturn(true) - startable.onBatteryStateChanged(STYLUS_DEVICE_ID, 123, batteryState) + startable.onStylusUsiBatteryStateChanged(STYLUS_DEVICE_ID, 123, batteryState) verify(stylusUsiPowerUi, times(1)).updateBatteryState(batteryState) } @Test - fun onBatteryStateChanged_batteryNotPresent_noop() { + fun onStylusUsiBatteryStateChanged_batteryNotPresent_noop() { val batteryState = mock(BatteryState::class.java) whenever(batteryState.isPresent).thenReturn(false) - startable.onBatteryStateChanged(STYLUS_DEVICE_ID, 123, batteryState) + startable.onStylusUsiBatteryStateChanged(STYLUS_DEVICE_ID, 123, batteryState) verifyNoMoreInteractions(stylusUsiPowerUi) } companion object { - private val DIRECT_EXECUTOR = Executor { r -> r.run() } - private const val EXTERNAL_DEVICE_ID = 0 private const val STYLUS_DEVICE_ID = 1 } diff --git a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerUiTest.kt b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerUiTest.kt index 59875507341d..a7951f4fa068 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerUiTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerUiTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.stylus +import android.app.Notification import android.hardware.BatteryState import android.hardware.input.InputManager import android.os.Handler @@ -28,10 +29,13 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.whenever +import junit.framework.Assert.assertEquals import org.junit.Before import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Captor import org.mockito.Mock import org.mockito.Mockito.inOrder import org.mockito.Mockito.times @@ -46,6 +50,7 @@ class StylusUsiPowerUiTest : SysuiTestCase() { @Mock lateinit var inputManager: InputManager @Mock lateinit var handler: Handler @Mock lateinit var btStylusDevice: InputDevice + @Captor lateinit var notificationCaptor: ArgumentCaptor<Notification> private lateinit var stylusUsiPowerUi: StylusUsiPowerUI @@ -70,7 +75,8 @@ class StylusUsiPowerUiTest : SysuiTestCase() { fun updateBatteryState_capacityBelowThreshold_notifies() { stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f)) - verify(notificationManager, times(1)).notify(eq(R.string.stylus_battery_low), any()) + verify(notificationManager, times(1)) + .notify(eq(R.string.stylus_battery_low_percentage), any()) verifyNoMoreInteractions(notificationManager) } @@ -78,7 +84,7 @@ class StylusUsiPowerUiTest : SysuiTestCase() { fun updateBatteryState_capacityAboveThreshold_cancelsNotificattion() { stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.8f)) - verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low) + verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low_percentage) verifyNoMoreInteractions(notificationManager) } @@ -88,8 +94,9 @@ class StylusUsiPowerUiTest : SysuiTestCase() { stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.8f)) inOrder(notificationManager).let { - it.verify(notificationManager, times(1)).notify(eq(R.string.stylus_battery_low), any()) - it.verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low) + it.verify(notificationManager, times(1)) + .notify(eq(R.string.stylus_battery_low_percentage), any()) + it.verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low_percentage) it.verifyNoMoreInteractions() } } @@ -99,7 +106,16 @@ class StylusUsiPowerUiTest : SysuiTestCase() { stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f)) stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.15f)) - verify(notificationManager, times(2)).notify(eq(R.string.stylus_battery_low), any()) + verify(notificationManager, times(2)) + .notify(eq(R.string.stylus_battery_low_percentage), notificationCaptor.capture()) + assertEquals( + notificationCaptor.value.extras.getString(Notification.EXTRA_TITLE), + context.getString(R.string.stylus_battery_low_percentage, "15%") + ) + assertEquals( + notificationCaptor.value.extras.getString(Notification.EXTRA_TEXT), + context.getString(R.string.stylus_battery_low_subtitle) + ) verifyNoMoreInteractions(notificationManager) } @@ -110,9 +126,11 @@ class StylusUsiPowerUiTest : SysuiTestCase() { stylusUsiPowerUi.updateBatteryState(FixedCapacityBatteryState(0.1f)) inOrder(notificationManager).let { - it.verify(notificationManager, times(1)).notify(eq(R.string.stylus_battery_low), any()) - it.verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low) - it.verify(notificationManager, times(1)).notify(eq(R.string.stylus_battery_low), any()) + it.verify(notificationManager, times(1)) + .notify(eq(R.string.stylus_battery_low_percentage), any()) + it.verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low_percentage) + it.verify(notificationManager, times(1)) + .notify(eq(R.string.stylus_battery_low_percentage), any()) it.verifyNoMoreInteractions() } } @@ -121,7 +139,7 @@ class StylusUsiPowerUiTest : SysuiTestCase() { fun updateSuppression_noExistingNotification_cancelsNotification() { stylusUsiPowerUi.updateSuppression(true) - verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low) + verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low_percentage) verifyNoMoreInteractions(notificationManager) } @@ -132,8 +150,9 @@ class StylusUsiPowerUiTest : SysuiTestCase() { stylusUsiPowerUi.updateSuppression(true) inOrder(notificationManager).let { - it.verify(notificationManager, times(1)).notify(eq(R.string.stylus_battery_low), any()) - it.verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low) + it.verify(notificationManager, times(1)) + .notify(eq(R.string.stylus_battery_low_percentage), any()) + it.verify(notificationManager, times(1)).cancel(R.string.stylus_battery_low_percentage) it.verifyNoMoreInteractions() } } @@ -156,7 +175,7 @@ class StylusUsiPowerUiTest : SysuiTestCase() { stylusUsiPowerUi.refresh() - verify(notificationManager).cancel(R.string.stylus_battery_low) + verify(notificationManager).cancel(R.string.stylus_battery_low_percentage) } class FixedCapacityBatteryState(private val capacity: Float) : BatteryState() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java index 5b424a39bb1e..a537848903bc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java @@ -24,6 +24,7 @@ import static android.service.notification.NotificationListenerService.REASON_AP import static android.service.notification.NotificationListenerService.REASON_GROUP_SUMMARY_CANCELED; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; +import static com.android.wm.shell.bubbles.Bubble.KEY_APP_BUBBLE; import static com.google.common.truth.Truth.assertThat; @@ -228,6 +229,8 @@ public class BubblesTest extends SysuiTestCase { private BubbleEntry mBubbleEntryUser11; private BubbleEntry mBubbleEntry2User11; + private Intent mAppBubbleIntent; + @Mock private ShellInit mShellInit; @Mock @@ -323,6 +326,9 @@ public class BubblesTest extends SysuiTestCase { mBubbleEntry2User11 = BubblesManager.notifToBubbleEntry( mNotificationTestHelper.createBubble(handle)); + mAppBubbleIntent = new Intent(mContext, BubblesTestActivity.class); + mAppBubbleIntent.setPackage(mContext.getPackageName()); + mZenModeConfig.suppressedVisualEffects = 0; when(mZenModeController.getConfig()).thenReturn(mZenModeConfig); @@ -1630,6 +1636,62 @@ public class BubblesTest extends SysuiTestCase { any(Bubble.class), anyBoolean(), anyBoolean()); } + @Test + public void testShowOrHideAppBubble_addsAndExpand() { + assertThat(mBubbleController.isStackExpanded()).isFalse(); + assertThat(mBubbleData.getBubbleInStackWithKey(KEY_APP_BUBBLE)).isNull(); + + mBubbleController.showOrHideAppBubble(mAppBubbleIntent); + + verify(mBubbleController).inflateAndAdd(any(Bubble.class), /* suppressFlyout= */ eq(true), + /* showInShade= */ eq(false)); + assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(KEY_APP_BUBBLE); + assertThat(mBubbleController.isStackExpanded()).isTrue(); + } + + @Test + public void testShowOrHideAppBubble_expandIfCollapsed() { + mBubbleController.showOrHideAppBubble(mAppBubbleIntent); + mBubbleController.updateBubble(mBubbleEntry); + mBubbleController.collapseStack(); + assertThat(mBubbleController.isStackExpanded()).isFalse(); + + // Calling this while collapsed will expand the app bubble + mBubbleController.showOrHideAppBubble(mAppBubbleIntent); + + assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(KEY_APP_BUBBLE); + assertThat(mBubbleController.isStackExpanded()).isTrue(); + assertThat(mBubbleData.getBubbles().size()).isEqualTo(2); + } + + @Test + public void testShowOrHideAppBubble_collapseIfSelected() { + mBubbleController.showOrHideAppBubble(mAppBubbleIntent); + assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(KEY_APP_BUBBLE); + assertThat(mBubbleController.isStackExpanded()).isTrue(); + + // Calling this while the app bubble is expanded should collapse the stack + mBubbleController.showOrHideAppBubble(mAppBubbleIntent); + + assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(KEY_APP_BUBBLE); + assertThat(mBubbleController.isStackExpanded()).isFalse(); + assertThat(mBubbleData.getBubbles().size()).isEqualTo(1); + } + + @Test + public void testShowOrHideAppBubble_selectIfNotSelected() { + mBubbleController.showOrHideAppBubble(mAppBubbleIntent); + mBubbleController.updateBubble(mBubbleEntry); + mBubbleController.expandStackAndSelectBubble(mBubbleEntry); + assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(mBubbleEntry.getKey()); + assertThat(mBubbleController.isStackExpanded()).isTrue(); + + mBubbleController.showOrHideAppBubble(mAppBubbleIntent); + assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(KEY_APP_BUBBLE); + assertThat(mBubbleController.isStackExpanded()).isTrue(); + assertThat(mBubbleData.getBubbles().size()).isEqualTo(2); + } + /** Creates a bubble using the userId and package. */ private Bubble createBubble(int userId, String pkg) { final UserHandle userHandle = new UserHandle(userId); diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/MemoryTrackingTestCase.java b/packages/SystemUI/tests/utils/src/com/android/systemui/MemoryTrackingTestCase.java index 3767fbe98dc1..342855357fd2 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/MemoryTrackingTestCase.java +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/MemoryTrackingTestCase.java @@ -40,24 +40,49 @@ import java.io.IOException; public class MemoryTrackingTestCase extends SysuiTestCase { private static File sFilesDir = null; private static String sLatestTestClassName = null; + private static int sHeapCount = 0; + private static File sLatestBaselineHeapFile = null; - @Before public void grabFilesDir() { + // Ideally, we would do this in @BeforeClass just once, but we need mContext to get the files + // dir, and that does not exist until @Before on each test method. + @Before + public void grabFilesDir() throws IOException { + // This should happen only once per suite if (sFilesDir == null) { sFilesDir = mContext.getFilesDir(); } - sLatestTestClassName = getClass().getName(); + + // This will happen before the first test method in each class + if (sLatestTestClassName == null) { + sLatestTestClassName = getClass().getName(); + sLatestBaselineHeapFile = dump("baseline" + (++sHeapCount), "before-test"); + } } @AfterClass public static void dumpHeap() throws IOException { + File afterTestHeap = dump(sLatestTestClassName, "after-test"); + if (sLatestBaselineHeapFile != null && afterTestHeap != null) { + Log.w("MEMORY", "To compare heap to baseline (use go/ahat):"); + Log.w("MEMORY", " adb pull " + sLatestBaselineHeapFile); + Log.w("MEMORY", " adb pull " + afterTestHeap); + Log.w("MEMORY", + " java -jar ahat.jar --baseline " + sLatestBaselineHeapFile.getName() + " " + + afterTestHeap.getName()); + } + sLatestTestClassName = null; + } + + private static File dump(String basename, String heapKind) throws IOException { if (sFilesDir == null) { Log.e("MEMORY", "Somehow no test cases??"); - return; + return null; } mockitoTearDown(); - Log.w("MEMORY", "about to dump heap"); - File path = new File(sFilesDir, sLatestTestClassName + ".ahprof"); + Log.w("MEMORY", "about to dump " + heapKind + " heap"); + File path = new File(sFilesDir, basename + ".ahprof"); Debug.dumpHprofData(path.getPath()); - Log.w("MEMORY", "did it! Location: " + path); + Log.w("MEMORY", "Success! Location: " + path); + return path; } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt index 39d2ecaef51a..15b473640de7 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt @@ -52,6 +52,9 @@ class FakeKeyguardRepository : KeyguardRepository { private val _isDozing = MutableStateFlow(false) override val isDozing: Flow<Boolean> = _isDozing + private val _isAodAvailable = MutableStateFlow(false) + override val isAodAvailable: Flow<Boolean> = _isAodAvailable + private val _isDreaming = MutableStateFlow(false) override val isDreaming: Flow<Boolean> = _isDreaming @@ -126,6 +129,10 @@ class FakeKeyguardRepository : KeyguardRepository { _isDozing.value = isDozing } + fun setAodAvailable(isAodAvailable: Boolean) { + _isAodAvailable.value = isAodAvailable + } + fun setDreamingWithOverlay(isDreaming: Boolean) { _isDreamingWithOverlay.value = isDreaming } diff --git a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java index 05e305c437ec..f4c6cc3de0b1 100644 --- a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java +++ b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java @@ -132,7 +132,7 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ private static final String TRACE_WM = "WindowManagerInternal"; private static final int WAIT_WINDOWS_TIMEOUT_MILLIS = 5000; - /** Display type for displays associated with the default user of th device. */ + /** Display type for displays associated with the default user of the device. */ public static final int DISPLAY_TYPE_DEFAULT = 1 << 0; /** Display type for displays associated with an AccessibilityDisplayProxy user. */ public static final int DISPLAY_TYPE_PROXY = 1 << 1; @@ -1993,17 +1993,29 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ private int resolveAccessibilityWindowIdLocked(int accessibilityWindowId) { if (accessibilityWindowId == AccessibilityWindowInfo.ACTIVE_WINDOW_ID) { - return mA11yWindowManager.getActiveWindowId(mSystemSupport.getCurrentUserIdLocked()); + final int focusedWindowId = + mA11yWindowManager.getActiveWindowId(mSystemSupport.getCurrentUserIdLocked()); + if (!mA11yWindowManager.windowIdBelongsToDisplayType(focusedWindowId, mDisplayTypes)) { + return AccessibilityWindowInfo.UNDEFINED_WINDOW_ID; + } + return focusedWindowId; } return accessibilityWindowId; } private int resolveAccessibilityWindowIdForFindFocusLocked(int windowId, int focusType) { - if (windowId == AccessibilityWindowInfo.ACTIVE_WINDOW_ID) { - return mA11yWindowManager.getActiveWindowId(mSystemSupport.getCurrentUserIdLocked()); - } if (windowId == AccessibilityWindowInfo.ANY_WINDOW_ID) { - return mA11yWindowManager.getFocusedWindowId(focusType); + final int focusedWindowId = mA11yWindowManager.getFocusedWindowId(focusType); + // If the caller is a proxy and the found window doesn't belong to a proxy display + // (or vice versa), then return null. This doesn't work if there are multiple active + // proxys, but in the future this code shouldn't be needed if input and a11y focus are + // properly split. (so we will deal with the issues if we see them). + //TODO(254545943): Remove this when there is user and proxy separation of input and a11y + // focus + if (!mA11yWindowManager.windowIdBelongsToDisplayType(focusedWindowId, mDisplayTypes)) { + return AccessibilityWindowInfo.UNDEFINED_WINDOW_ID; + } + return focusedWindowId; } return windowId; } diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java index c050449e01d9..c61e86489f04 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java @@ -156,6 +156,30 @@ public class AccessibilityWindowManager { } /** + * Returns {@code true} if the window belongs to a display of {@code displayTypes}. + */ + public boolean windowIdBelongsToDisplayType(int focusedWindowId, int displayTypes) { + // UIAutomation wants focus from any display type. + final int displayTypeMask = DISPLAY_TYPE_PROXY | DISPLAY_TYPE_DEFAULT; + if ((displayTypes & displayTypeMask) == displayTypeMask) { + return true; + } + synchronized (mLock) { + final int count = mDisplayWindowsObservers.size(); + for (int i = 0; i < count; i++) { + final DisplayWindowsObserver observer = mDisplayWindowsObservers.valueAt(i); + if (observer != null + && observer.findA11yWindowInfoByIdLocked(focusedWindowId) != null) { + return observer.mIsProxy + ? ((displayTypes & DISPLAY_TYPE_PROXY) != 0) + : (displayTypes & DISPLAY_TYPE_DEFAULT) != 0; + } + } + } + return false; + } + + /** * This class implements {@link WindowManagerInternal.WindowsForAccessibilityCallback} to * receive {@link WindowInfo}s from window manager when there's an accessibility change in * window and holds window lists information per display. diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java index bb286e61815d..02e810f0e671 100644 --- a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java +++ b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationController.java @@ -410,9 +410,6 @@ public class MagnificationController implements WindowMagnificationManager.Callb public void onRequestMagnificationSpec(int displayId, int serviceId) { final WindowMagnificationManager windowMagnificationManager; synchronized (mLock) { - if (serviceId == MAGNIFICATION_GESTURE_HANDLER_ID) { - return; - } updateMagnificationButton(displayId, ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN); windowMagnificationManager = mWindowMagnificationMgr; } diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java index bce8812d2e9f..7df48994655d 100644 --- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java +++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java @@ -826,7 +826,8 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku if (host != null) { host.callbacks = null; pruneHostLocked(host); - mAppOpsManagerInternal.updateAppWidgetVisibility(host.getWidgetUids(), false); + mAppOpsManagerInternal.updateAppWidgetVisibility(host.getWidgetUidsIfBound(), + false); } } } @@ -897,12 +898,8 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku Host host = lookupHostLocked(id); if (host != null) { - try { - mAppOpsManagerInternal.updateAppWidgetVisibility(host.getWidgetUids(), false); - } catch (NullPointerException e) { - Slog.e(TAG, "setAppWidgetHidden(): Getting host uids: " + host.toString(), e); - throw e; - } + mAppOpsManagerInternal.updateAppWidgetVisibility(host.getWidgetUidsIfBound(), + false); } } } @@ -4370,14 +4367,15 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku PendingHostUpdate.appWidgetRemoved(appWidgetId)); } - public SparseArray<String> getWidgetUids() { + public SparseArray<String> getWidgetUidsIfBound() { final SparseArray<String> uids = new SparseArray<>(); for (int i = widgets.size() - 1; i >= 0; i--) { final Widget widget = widgets.get(i); if (widget.provider == null) { if (DEBUG) { - Slog.e(TAG, "Widget with no provider " + widget.toString()); + Slog.d(TAG, "Widget with no provider " + widget.toString()); } + continue; } final ProviderId providerId = widget.provider.id; uids.put(providerId.uid, providerId.componentName.getPackageName()); diff --git a/services/autofill/java/com/android/server/autofill/ui/SaveUi.java b/services/autofill/java/com/android/server/autofill/ui/SaveUi.java index 677871f6c85f..8c2c964e2d2c 100644 --- a/services/autofill/java/com/android/server/autofill/ui/SaveUi.java +++ b/services/autofill/java/com/android/server/autofill/ui/SaveUi.java @@ -357,6 +357,7 @@ final class SaveUi { params.width = WindowManager.LayoutParams.MATCH_PARENT; params.accessibilityTitle = context.getString(R.string.autofill_save_accessibility_title); params.windowAnimations = R.style.AutofillSaveAnimation; + params.setTrustedOverlay(); show(); } diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java index 758345f716c3..b0f2464ff52a 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java @@ -139,15 +139,6 @@ public class VirtualDeviceManagerService extends SystemService { mActivityInterceptorCallback); } - @GuardedBy("mVirtualDeviceManagerLock") - private boolean isValidVirtualDeviceLocked(IVirtualDevice virtualDevice) { - try { - return mVirtualDevices.contains(virtualDevice.getDeviceId()); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } - } - void onCameraAccessBlocked(int appUid) { synchronized (mVirtualDeviceManagerLock) { for (int i = 0; i < mVirtualDevices.size(); i++) { @@ -347,6 +338,14 @@ public class VirtualDeviceManagerService extends SystemService { return VirtualDeviceManager.DEVICE_ID_DEFAULT; } + // Binder call + @Override + public boolean isValidVirtualDeviceId(int deviceId) { + synchronized (mVirtualDeviceManagerLock) { + return mVirtualDevices.contains(deviceId); + } + } + @Override // Binder call public int getAudioPlaybackSessionId(int deviceId) { synchronized (mVirtualDeviceManagerLock) { @@ -445,13 +444,6 @@ public class VirtualDeviceManagerService extends SystemService { private final ArraySet<Integer> mAllUidsOnVirtualDevice = new ArraySet<>(); @Override - public boolean isValidVirtualDevice(IVirtualDevice virtualDevice) { - synchronized (mVirtualDeviceManagerLock) { - return isValidVirtualDeviceLocked(virtualDevice); - } - } - - @Override public int getDeviceOwnerUid(int deviceId) { synchronized (mVirtualDeviceManagerLock) { VirtualDeviceImpl virtualDevice = mVirtualDevices.get(deviceId); diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index fc6d30bf58c9..d478dca3bbc4 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -399,7 +399,6 @@ import com.android.server.IoThread; import com.android.server.LocalManagerRegistry; import com.android.server.LocalServices; import com.android.server.LockGuard; -import com.android.server.NetworkManagementInternal; import com.android.server.PackageWatchdog; import com.android.server.ServiceThread; import com.android.server.SystemConfig; @@ -417,6 +416,7 @@ import com.android.server.criticalevents.CriticalEventLog; import com.android.server.firewall.IntentFirewall; import com.android.server.graphics.fonts.FontManagerInternal; import com.android.server.job.JobSchedulerInternal; +import com.android.server.net.NetworkManagementInternal; import com.android.server.os.NativeTombstoneManager; import com.android.server.pm.Computer; import com.android.server.pm.Installer; diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java index 80684bf9fed3..788c81c3864c 100644 --- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java +++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java @@ -2153,19 +2153,24 @@ final class ActivityManagerShellCommand extends ShellCommand { boolean success; String displaySuffix; - if (displayId == Display.INVALID_DISPLAY) { - success = mInterface.startUserInBackgroundWithListener(userId, waiter); - displaySuffix = ""; - } else { - if (!UserManager.isVisibleBackgroundUsersEnabled()) { - pw.println("Not supported"); - return -1; + Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "shell_runStartUser" + userId); + try { + if (displayId == Display.INVALID_DISPLAY) { + success = mInterface.startUserInBackgroundWithListener(userId, waiter); + displaySuffix = ""; + } else { + if (!UserManager.isVisibleBackgroundUsersEnabled()) { + pw.println("Not supported"); + return -1; + } + success = mInterface.startUserInBackgroundVisibleOnDisplay(userId, displayId); + displaySuffix = " on display " + displayId; } - success = mInterface.startUserInBackgroundVisibleOnDisplay(userId, displayId); - displaySuffix = " on display " + displayId; - } - if (wait && success) { - success = waiter.waitForFinish(USER_OPERATION_TIMEOUT_MS); + if (wait && success) { + success = waiter.waitForFinish(USER_OPERATION_TIMEOUT_MS); + } + } finally { + Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); } if (success) { diff --git a/services/core/java/com/android/server/ambientcontext/AmbientContextManagerService.java b/services/core/java/com/android/server/ambientcontext/AmbientContextManagerService.java index 5c18827ebd61..7d9b272906c6 100644 --- a/services/core/java/com/android/server/ambientcontext/AmbientContextManagerService.java +++ b/services/core/java/com/android/server/ambientcontext/AmbientContextManagerService.java @@ -50,6 +50,8 @@ import com.android.server.infra.AbstractMasterSystemService; import com.android.server.infra.FrameworkResourcesServiceNameResolver; import com.android.server.pm.KnownPackages; +import com.google.android.collect.Sets; + import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; @@ -67,12 +69,10 @@ public class AmbientContextManagerService extends AmbientContextManagerPerUserService> { private static final String TAG = AmbientContextManagerService.class.getSimpleName(); private static final String KEY_SERVICE_ENABLED = "service_enabled"; - private static final Set<Integer> DEFAULT_EVENT_SET = new HashSet<>(){{ - add(AmbientContextEvent.EVENT_COUGH); - add(AmbientContextEvent.EVENT_SNORE); - add(AmbientContextEvent.EVENT_BACK_DOUBLE_TAP); - } - }; + private static final Set<Integer> DEFAULT_EVENT_SET = Sets.newHashSet( + AmbientContextEvent.EVENT_COUGH, + AmbientContextEvent.EVENT_SNORE, + AmbientContextEvent.EVENT_BACK_DOUBLE_TAP); /** Default value in absence of {@link DeviceConfig} override. */ private static final boolean DEFAULT_SERVICE_ENABLED = true; @@ -409,14 +409,6 @@ public class AmbientContextManagerService extends } } - private Set<Integer> intArrayToIntegerSet(int[] eventTypes) { - Set<Integer> types = new HashSet<>(); - for (Integer i : eventTypes) { - types.add(i); - } - return types; - } - private AmbientContextManagerPerUserService.ServiceType getServiceType(String serviceName) { final String wearableService = mContext.getResources() .getString(R.string.config_defaultWearableSensingService); @@ -513,6 +505,14 @@ public class AmbientContextManagerService extends return intArray; } + private Set<Integer> intArrayToIntegerSet(int[] eventTypes) { + Set<Integer> types = new HashSet<>(); + for (Integer i : eventTypes) { + types.add(i); + } + return types; + } + @NonNull private static Integer[] intArrayToIntegerArray(@NonNull int[] integerSet) { Integer[] intArray = new Integer[integerSet.length]; @@ -567,37 +567,24 @@ public class AmbientContextManagerService extends mContext.enforceCallingOrSelfPermission( Manifest.permission.ACCESS_AMBIENT_CONTEXT_EVENT, TAG); assertCalledByPackageOwner(packageName); + AmbientContextManagerPerUserService service = getAmbientContextManagerPerUserServiceForEventTypes( UserHandle.getCallingUserId(), request.getEventTypes()); - if (service == null) { Slog.w(TAG, "onRegisterObserver unavailable user_id: " + UserHandle.getCallingUserId()); - } - - if (service.getServiceType() == ServiceType.DEFAULT && !mIsServiceEnabled) { - Slog.d(TAG, "Service not available."); - service.completeRegistration(observer, - AmbientContextManager.STATUS_SERVICE_UNAVAILABLE); - return; - } - if (service.getServiceType() == ServiceType.WEARABLE && !mIsWearableServiceEnabled) { - Slog.d(TAG, "Wearable Service not available."); - service.completeRegistration(observer, - AmbientContextManager.STATUS_SERVICE_UNAVAILABLE); - return; - } - if (containsMixedEvents(integerSetToIntArray(request.getEventTypes()))) { - Slog.d(TAG, "AmbientContextEventRequest contains mixed events," - + " this is not supported."); - service.completeRegistration(observer, - AmbientContextManager.STATUS_NOT_SUPPORTED); return; } - service.onRegisterObserver(request, packageName, observer); + int statusCode = checkStatusCode( + service, integerSetToIntArray(request.getEventTypes())); + if (statusCode == AmbientContextManager.STATUS_SUCCESS) { + service.onRegisterObserver(request, packageName, observer); + } else { + service.completeRegistration(observer, statusCode); + } } @Override @@ -606,10 +593,10 @@ public class AmbientContextManagerService extends Manifest.permission.ACCESS_AMBIENT_CONTEXT_EVENT, TAG); assertCalledByPackageOwner(callingPackage); - AmbientContextManagerPerUserService service = null; for (ClientRequest cr : mExistingClientRequests) { if (cr.getPackageName().equals(callingPackage)) { - service = getAmbientContextManagerPerUserServiceForEventTypes( + AmbientContextManagerPerUserService service = + getAmbientContextManagerPerUserServiceForEventTypes( UserHandle.getCallingUserId(), cr.getRequest().getEventTypes()); if (service != null) { service.onUnregisterObserver(callingPackage); @@ -635,34 +622,18 @@ public class AmbientContextManagerService extends getAmbientContextManagerPerUserServiceForEventTypes( UserHandle.getCallingUserId(), intArrayToIntegerSet(eventTypes)); if (service == null) { - Slog.w(TAG, "onQueryServiceStatus unavailable user_id: " + Slog.w(TAG, "queryServiceStatus unavailable user_id: " + UserHandle.getCallingUserId()); - } - - if (service.getServiceType() == ServiceType.DEFAULT && !mIsServiceEnabled) { - Slog.d(TAG, "Service not available."); - service.sendStatusCallback(statusCallback, - AmbientContextManager.STATUS_SERVICE_UNAVAILABLE); - return; - } - if (service.getServiceType() == ServiceType.WEARABLE - && !mIsWearableServiceEnabled) { - Slog.d(TAG, "Wearable Service not available."); - service.sendStatusCallback(statusCallback, - AmbientContextManager.STATUS_SERVICE_UNAVAILABLE); return; } - if (containsMixedEvents(eventTypes)) { - Slog.d(TAG, "AmbientContextEventRequest contains mixed events," - + " this is not supported."); - service.sendStatusCallback(statusCallback, - AmbientContextManager.STATUS_NOT_SUPPORTED); - return; + int statusCode = checkStatusCode(service, eventTypes); + if (statusCode == AmbientContextManager.STATUS_SUCCESS) { + service.onQueryServiceStatus(eventTypes, callingPackage, + statusCallback); + } else { + service.sendStatusCallback(statusCallback, statusCode); } - - service.onQueryServiceStatus(eventTypes, callingPackage, - statusCallback); } } @@ -708,5 +679,22 @@ public class AmbientContextManagerService extends new AmbientContextShellCommand(AmbientContextManagerService.this).exec( this, in, out, err, args, callback, resultReceiver); } + + private int checkStatusCode(AmbientContextManagerPerUserService service, int[] eventTypes) { + if (service.getServiceType() == ServiceType.DEFAULT && !mIsServiceEnabled) { + Slog.d(TAG, "Service not enabled."); + return AmbientContextManager.STATUS_SERVICE_UNAVAILABLE; + } + if (service.getServiceType() == ServiceType.WEARABLE && !mIsWearableServiceEnabled) { + Slog.d(TAG, "Wearable Service not available."); + return AmbientContextManager.STATUS_SERVICE_UNAVAILABLE; + } + if (containsMixedEvents(eventTypes)) { + Slog.d(TAG, "AmbientContextEventRequest contains mixed events," + + " this is not supported."); + return AmbientContextManager.STATUS_NOT_SUPPORTED; + } + return AmbientContextManager.STATUS_SUCCESS; + } } } diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 78bff9524b10..58ddd9c6a8b8 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -3717,9 +3717,11 @@ public class AudioService extends IAudioService.Stub setRingerMode(getNewRingerMode(stream, index, flags), TAG + ".onSetStreamVolume", false /*external*/); } - // setting non-zero volume for a muted stream unmutes the stream and vice versa, + // setting non-zero volume for a muted stream unmutes the stream and vice versa + // (only when changing volume for the current device), // except for BT SCO stream where only explicit mute is allowed to comply to BT requirements - if (streamType != AudioSystem.STREAM_BLUETOOTH_SCO) { + if ((streamType != AudioSystem.STREAM_BLUETOOTH_SCO) + && (getDeviceForStream(stream) == device)) { mStreamStates[stream].mute(index == 0); } } diff --git a/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java b/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java index e3ea1a6a3de7..974c04bc45f5 100644 --- a/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java +++ b/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java @@ -68,11 +68,6 @@ public abstract class VirtualDeviceManagerInternal { public abstract void onAppsOnVirtualDeviceChanged(); /** - * Validate the virtual device. - */ - public abstract boolean isValidVirtualDevice(IVirtualDevice virtualDevice); - - /** * Gets the owner uid for a deviceId. * * @param deviceId which device we're asking about diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index d44e1dc121c9..06b99f82362a 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -47,6 +47,7 @@ import android.annotation.UserIdInt; import android.app.AppOpsManager; import android.app.compat.CompatChanges; import android.companion.virtual.IVirtualDevice; +import android.companion.virtual.VirtualDeviceManager; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledSince; import android.content.BroadcastReceiver; @@ -1281,12 +1282,17 @@ public final class DisplayManagerService extends SystemService { final Surface surface = virtualDisplayConfig.getSurface(); int flags = virtualDisplayConfig.getFlags(); if (virtualDevice != null) { - final VirtualDeviceManagerInternal vdm = - getLocalService(VirtualDeviceManagerInternal.class); - if (!vdm.isValidVirtualDevice(virtualDevice)) { - throw new SecurityException("Invalid virtual device"); + final VirtualDeviceManager vdm = mContext.getSystemService(VirtualDeviceManager.class); + try { + if (!vdm.isValidVirtualDeviceId(virtualDevice.getDeviceId())) { + throw new SecurityException("Invalid virtual device"); + } + } catch (RemoteException ex) { + throw new SecurityException("Unable to validate virtual device"); } - flags |= vdm.getBaseVirtualDisplayFlags(virtualDevice); + final VirtualDeviceManagerInternal localVdm = + getLocalService(VirtualDeviceManagerInternal.class); + flags |= localVdm.getBaseVirtualDisplayFlags(virtualDevice); } if (surface != null && surface.isSingleBuffered()) { diff --git a/services/core/java/com/android/server/input/BatteryController.java b/services/core/java/com/android/server/input/BatteryController.java index c99a7a0de79c..993b4fdb2498 100644 --- a/services/core/java/com/android/server/input/BatteryController.java +++ b/services/core/java/com/android/server/input/BatteryController.java @@ -31,6 +31,7 @@ import android.hardware.input.IInputDeviceBatteryListener; import android.hardware.input.IInputDeviceBatteryState; import android.hardware.input.InputManager; import android.os.Handler; +import android.os.HandlerExecutor; import android.os.IBinder; import android.os.Looper; import android.os.RemoteException; @@ -51,6 +52,7 @@ import java.io.PrintWriter; import java.util.Arrays; import java.util.Objects; import java.util.Set; +import java.util.concurrent.Executor; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; @@ -102,7 +104,7 @@ final class BatteryController { BatteryController(Context context, NativeInputManagerService nativeService, Looper looper) { this(context, nativeService, looper, new UEventManager() {}, - new LocalBluetoothBatteryManager(context)); + new LocalBluetoothBatteryManager(context, looper)); } @VisibleForTesting @@ -163,7 +165,7 @@ final class BatteryController { // This is the first listener that is monitoring this device. monitor = new DeviceMonitor(deviceId); mDeviceMonitors.put(deviceId, monitor); - updateBluetoothMonitoring(); + updateBluetoothBatteryMonitoring(); } if (DEBUG) { @@ -378,13 +380,26 @@ final class BatteryController { } } - private void handleBluetoothBatteryLevelChange(long eventTime, String address) { + private void handleBluetoothBatteryLevelChange(long eventTime, String address, + int batteryLevel) { synchronized (mLock) { final DeviceMonitor monitor = findIf(mDeviceMonitors, (m) -> (m.mBluetoothDevice != null && address.equals(m.mBluetoothDevice.getAddress()))); if (monitor != null) { - monitor.onBluetoothBatteryChanged(eventTime); + monitor.onBluetoothBatteryChanged(eventTime, batteryLevel); + } + } + } + + private void handleBluetoothMetadataChange(@NonNull BluetoothDevice device, int key, + @Nullable byte[] value) { + synchronized (mLock) { + final DeviceMonitor monitor = + findIf(mDeviceMonitors, (m) -> device.equals(m.mBluetoothDevice)); + if (monitor != null) { + final long eventTime = SystemClock.uptimeMillis(); + monitor.onBluetoothMetadataChanged(eventTime, key, value); } } } @@ -514,31 +529,19 @@ final class BatteryController { isPresent ? mNative.getBatteryCapacity(deviceId) / 100.f : Float.NaN); } - // Queries the battery state of an input device from Bluetooth. - private State queryBatteryStateFromBluetooth(int deviceId, long updateTime, - @NonNull BluetoothDevice bluetoothDevice) { - final int level = mBluetoothBatteryManager.getBatteryLevel(bluetoothDevice.getAddress()); - if (level == BluetoothDevice.BATTERY_LEVEL_BLUETOOTH_OFF - || level == BluetoothDevice.BATTERY_LEVEL_UNKNOWN) { - return new State(deviceId); - } - return new State(deviceId, updateTime, true /*isPresent*/, BatteryState.STATUS_UNKNOWN, - level / 100.f); - } - - private void updateBluetoothMonitoring() { + private void updateBluetoothBatteryMonitoring() { synchronized (mLock) { if (anyOf(mDeviceMonitors, (m) -> m.mBluetoothDevice != null)) { // At least one input device being monitored is connected over Bluetooth. if (mBluetoothBatteryListener == null) { if (DEBUG) Slog.d(TAG, "Registering bluetooth battery listener"); mBluetoothBatteryListener = this::handleBluetoothBatteryLevelChange; - mBluetoothBatteryManager.addListener(mBluetoothBatteryListener); + mBluetoothBatteryManager.addBatteryListener(mBluetoothBatteryListener); } } else if (mBluetoothBatteryListener != null) { // No Bluetooth input devices are monitored, so remove the registered listener. if (DEBUG) Slog.d(TAG, "Unregistering bluetooth battery listener"); - mBluetoothBatteryManager.removeListener(mBluetoothBatteryListener); + mBluetoothBatteryManager.removeBatteryListener(mBluetoothBatteryListener); mBluetoothBatteryListener = null; } } @@ -550,16 +553,23 @@ final class BatteryController { // Represents whether the input device has a sysfs battery node. protected boolean mHasBattery = false; - protected final State mBluetoothState; @Nullable private BluetoothDevice mBluetoothDevice; + long mBluetoothEventTime = 0; + // The battery level reported by the Bluetooth Hands-Free Profile (HPF) obtained through + // BluetoothDevice#getBatteryLevel(). + int mBluetoothBatteryLevel = BluetoothDevice.BATTERY_LEVEL_UNKNOWN; + // The battery level and status reported through the Bluetooth device's metadata. + int mBluetoothMetadataBatteryLevel = BluetoothDevice.BATTERY_LEVEL_UNKNOWN; + int mBluetoothMetadataBatteryStatus = BatteryState.STATUS_UNKNOWN; + @Nullable + private BluetoothAdapter.OnMetadataChangedListener mBluetoothMetadataListener; @Nullable private UEventBatteryListener mUEventBatteryListener; DeviceMonitor(int deviceId) { mState = new State(deviceId); - mBluetoothState = new State(deviceId); // Load the initial battery state and start monitoring. final long eventTime = SystemClock.uptimeMillis(); @@ -570,7 +580,7 @@ final class BatteryController { final State oldState = getBatteryStateForReporting(); changes.accept(eventTime); final State newState = getBatteryStateForReporting(); - if (!oldState.equals(newState)) { + if (!oldState.equalsIgnoringUpdateTime(newState)) { notifyAllListenersForDevice(newState); } } @@ -594,13 +604,22 @@ final class BatteryController { final BluetoothDevice bluetoothDevice = getBluetoothDevice(deviceId); if (!Objects.equals(mBluetoothDevice, bluetoothDevice)) { if (DEBUG) { - Slog.d(TAG, "Bluetooth device " - + ((bluetoothDevice != null) ? "is" : "is not") - + " now present for deviceId " + deviceId); + Slog.d(TAG, "Bluetooth device is now " + + ((bluetoothDevice != null) ? "" : "not") + + " present for deviceId " + deviceId); } + + mBluetoothBatteryLevel = BluetoothDevice.BATTERY_LEVEL_UNKNOWN; + stopBluetoothMetadataMonitoring(); + mBluetoothDevice = bluetoothDevice; - updateBluetoothMonitoring(); - updateBatteryStateFromBluetooth(eventTime); + updateBluetoothBatteryMonitoring(); + + if (mBluetoothDevice != null) { + mBluetoothBatteryLevel = mBluetoothBatteryManager.getBatteryLevel( + mBluetoothDevice.getAddress()); + startBluetoothMetadataMonitoring(eventTime); + } } } @@ -632,11 +651,39 @@ final class BatteryController { } } + private void startBluetoothMetadataMonitoring(long eventTime) { + Objects.requireNonNull(mBluetoothDevice); + + mBluetoothMetadataListener = BatteryController.this::handleBluetoothMetadataChange; + mBluetoothBatteryManager.addMetadataListener(mBluetoothDevice.getAddress(), + mBluetoothMetadataListener); + updateBluetoothMetadataState(eventTime, BluetoothDevice.METADATA_MAIN_BATTERY, + mBluetoothBatteryManager.getMetadata(mBluetoothDevice.getAddress(), + BluetoothDevice.METADATA_MAIN_BATTERY)); + updateBluetoothMetadataState(eventTime, BluetoothDevice.METADATA_MAIN_CHARGING, + mBluetoothBatteryManager.getMetadata(mBluetoothDevice.getAddress(), + BluetoothDevice.METADATA_MAIN_CHARGING)); + } + + private void stopBluetoothMetadataMonitoring() { + if (mBluetoothMetadataListener == null) { + return; + } + Objects.requireNonNull(mBluetoothDevice); + + mBluetoothBatteryManager.removeMetadataListener( + mBluetoothDevice.getAddress(), mBluetoothMetadataListener); + mBluetoothMetadataListener = null; + mBluetoothMetadataBatteryLevel = BluetoothDevice.BATTERY_LEVEL_UNKNOWN; + mBluetoothMetadataBatteryStatus = BatteryState.STATUS_UNKNOWN; + } + // This must be called when the device is no longer being monitored. public void onMonitorDestroy() { stopNativeMonitoring(); + stopBluetoothMetadataMonitoring(); mBluetoothDevice = null; - updateBluetoothMonitoring(); + updateBluetoothBatteryMonitoring(); } protected void updateBatteryStateFromNative(long eventTime) { @@ -644,13 +691,6 @@ final class BatteryController { queryBatteryStateFromNative(mState.deviceId, eventTime, mHasBattery)); } - protected void updateBatteryStateFromBluetooth(long eventTime) { - final State bluetoothState = mBluetoothDevice == null ? new State(mState.deviceId) - : queryBatteryStateFromBluetooth(mState.deviceId, eventTime, - mBluetoothDevice); - mBluetoothState.updateIfChanged(bluetoothState); - } - public void onPoll(long eventTime) { processChangesAndNotify(eventTime, this::updateBatteryStateFromNative); } @@ -659,8 +699,51 @@ final class BatteryController { processChangesAndNotify(eventTime, this::updateBatteryStateFromNative); } - public void onBluetoothBatteryChanged(long eventTime) { - processChangesAndNotify(eventTime, this::updateBatteryStateFromBluetooth); + public void onBluetoothBatteryChanged(long eventTime, int bluetoothBatteryLevel) { + processChangesAndNotify(eventTime, (time) -> { + mBluetoothBatteryLevel = bluetoothBatteryLevel; + mBluetoothEventTime = time; + }); + } + + public void onBluetoothMetadataChanged(long eventTime, int key, @Nullable byte[] value) { + processChangesAndNotify(eventTime, + (time) -> updateBluetoothMetadataState(time, key, value)); + } + + private void updateBluetoothMetadataState(long eventTime, int key, + @Nullable byte[] value) { + switch (key) { + case BluetoothDevice.METADATA_MAIN_BATTERY: + mBluetoothEventTime = eventTime; + mBluetoothMetadataBatteryLevel = BluetoothDevice.BATTERY_LEVEL_UNKNOWN; + if (value != null) { + try { + mBluetoothMetadataBatteryLevel = Integer.parseInt( + new String(value)); + } catch (NumberFormatException e) { + Slog.wtf(TAG, + "Failed to parse bluetooth METADATA_MAIN_BATTERY with " + + "value '" + + new String(value) + "' for device " + + mBluetoothDevice); + } + } + break; + case BluetoothDevice.METADATA_MAIN_CHARGING: + mBluetoothEventTime = eventTime; + if (value != null) { + mBluetoothMetadataBatteryStatus = Boolean.parseBoolean( + new String(value)) + ? BatteryState.STATUS_CHARGING + : BatteryState.STATUS_DISCHARGING; + } else { + mBluetoothMetadataBatteryStatus = BatteryState.STATUS_UNKNOWN; + } + break; + default: + break; + } } public boolean requiresPolling() { @@ -677,11 +760,25 @@ final class BatteryController { // Returns the current battery state that can be used to notify listeners BatteryController. public State getBatteryStateForReporting() { - // Give precedence to the Bluetooth battery state if it's present. - if (mBluetoothState.isPresent) { - return new State(mBluetoothState); + // Give precedence to the Bluetooth battery state, and fall back to the native state. + return Objects.requireNonNullElseGet(resolveBluetoothBatteryState(), + () -> new State(mState)); + } + + @Nullable + protected State resolveBluetoothBatteryState() { + final int level; + // Prefer battery level obtained from the metadata over the Bluetooth Hands-Free + // Profile (HFP). + if (mBluetoothMetadataBatteryLevel >= 0 && mBluetoothMetadataBatteryLevel <= 100) { + level = mBluetoothMetadataBatteryLevel; + } else if (mBluetoothBatteryLevel >= 0 && mBluetoothBatteryLevel <= 100) { + level = mBluetoothBatteryLevel; + } else { + return null; } - return new State(mState); + return new State(mState.deviceId, mBluetoothEventTime, true, + mBluetoothMetadataBatteryStatus, level / 100.f); } @Override @@ -690,7 +787,7 @@ final class BatteryController { + ", Name='" + getInputDeviceName(mState.deviceId) + "'" + ", NativeBattery=" + mState + ", UEventListener=" + (mUEventBatteryListener != null ? "added" : "none") - + ", BluetoothBattery=" + mBluetoothState; + + ", BluetoothState=" + resolveBluetoothBatteryState(); } } @@ -775,12 +872,10 @@ final class BatteryController { @Override public State getBatteryStateForReporting() { - // Give precedence to the Bluetooth battery state if it's present. - if (mBluetoothState.isPresent) { - return new State(mBluetoothState); - } - return mValidityTimeoutCallback != null - ? new State(mState) : new State(mState.deviceId); + // Give precedence to the Bluetooth battery state, and fall back to the native state. + return Objects.requireNonNullElseGet(resolveBluetoothBatteryState(), + () -> mValidityTimeoutCallback != null + ? new State(mState) : new State(mState.deviceId)); } @Override @@ -844,15 +939,24 @@ final class BatteryController { interface BluetoothBatteryManager { @VisibleForTesting interface BluetoothBatteryListener { - void onBluetoothBatteryChanged(long eventTime, String address); + void onBluetoothBatteryChanged(long eventTime, String address, int batteryLevel); } - void addListener(BluetoothBatteryListener listener); - void removeListener(BluetoothBatteryListener listener); + // Methods used for obtaining the Bluetooth battery level through Bluetooth HFP. + void addBatteryListener(BluetoothBatteryListener listener); + void removeBatteryListener(BluetoothBatteryListener listener); int getBatteryLevel(String address); + + // Methods used for obtaining the battery level through Bluetooth metadata. + void addMetadataListener(String address, + BluetoothAdapter.OnMetadataChangedListener listener); + void removeMetadataListener(String address, + BluetoothAdapter.OnMetadataChangedListener listener); + byte[] getMetadata(String address, int key); } private static class LocalBluetoothBatteryManager implements BluetoothBatteryManager { private final Context mContext; + private final Executor mExecutor; @Nullable @GuardedBy("mBroadcastReceiver") private BluetoothBatteryListener mRegisteredListener; @@ -868,24 +972,25 @@ final class BatteryController { if (bluetoothDevice == null) { return; } - // We do not use the EXTRA_LEVEL value. Instead, the battery level will be queried - // from BluetoothDevice later so that we use a single source for the battery level. + final int batteryLevel = intent.getIntExtra(BluetoothDevice.EXTRA_BATTERY_LEVEL, + BluetoothDevice.BATTERY_LEVEL_UNKNOWN); synchronized (mBroadcastReceiver) { if (mRegisteredListener != null) { final long eventTime = SystemClock.uptimeMillis(); mRegisteredListener.onBluetoothBatteryChanged( - eventTime, bluetoothDevice.getAddress()); + eventTime, bluetoothDevice.getAddress(), batteryLevel); } } } }; - LocalBluetoothBatteryManager(Context context) { + LocalBluetoothBatteryManager(Context context, Looper looper) { mContext = context; + mExecutor = new HandlerExecutor(new Handler(looper)); } @Override - public void addListener(BluetoothBatteryListener listener) { + public void addBatteryListener(BluetoothBatteryListener listener) { synchronized (mBroadcastReceiver) { if (mRegisteredListener != null) { throw new IllegalStateException( @@ -898,7 +1003,7 @@ final class BatteryController { } @Override - public void removeListener(BluetoothBatteryListener listener) { + public void removeBatteryListener(BluetoothBatteryListener listener) { synchronized (mBroadcastReceiver) { if (!listener.equals(mRegisteredListener)) { throw new IllegalStateException("Listener is not registered."); @@ -912,6 +1017,28 @@ final class BatteryController { public int getBatteryLevel(String address) { return getBluetoothDevice(mContext, address).getBatteryLevel(); } + + @Override + public void addMetadataListener(String address, + BluetoothAdapter.OnMetadataChangedListener listener) { + Objects.requireNonNull(mContext.getSystemService(BluetoothManager.class)) + .getAdapter().addOnMetadataChangedListener( + getBluetoothDevice(mContext, address), mExecutor, + listener); + } + + @Override + public void removeMetadataListener(String address, + BluetoothAdapter.OnMetadataChangedListener listener) { + Objects.requireNonNull(mContext.getSystemService(BluetoothManager.class)) + .getAdapter().removeOnMetadataChangedListener( + getBluetoothDevice(mContext, address), listener); + } + + @Override + public byte[] getMetadata(String address, int key) { + return getBluetoothDevice(mContext, address).getMetadata(key); + } } // Helper class that adds copying and printing functionality to IInputDeviceBatteryState. @@ -954,7 +1081,7 @@ final class BatteryController { this.capacity = capacity; } - private boolean equalsIgnoringUpdateTime(IInputDeviceBatteryState other) { + public boolean equalsIgnoringUpdateTime(IInputDeviceBatteryState other) { long updateTime = this.updateTime; this.updateTime = other.updateTime; boolean eq = this.equals(other); diff --git a/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java b/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java new file mode 100644 index 000000000000..86a08579e38b --- /dev/null +++ b/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.inputmethod; + +import static android.view.inputmethod.ImeTracker.DEBUG_IME_VISIBILITY; + +import static com.android.server.EventLogTags.IMF_HIDE_IME; +import static com.android.server.EventLogTags.IMF_SHOW_IME; +import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_HIDE_IME; +import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_SHOW_IME; + +import android.annotation.Nullable; +import android.os.Binder; +import android.os.IBinder; +import android.os.ResultReceiver; +import android.util.EventLog; +import android.util.Slog; +import android.view.inputmethod.ImeTracker; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.inputmethod.InputMethodDebug; +import com.android.internal.inputmethod.SoftInputShowHideReason; +import com.android.server.LocalServices; +import com.android.server.wm.WindowManagerInternal; + +import java.util.Objects; + +/** + * The default implementation of {@link ImeVisibilityApplier} used in + * {@link InputMethodManagerService}. + */ +final class DefaultImeVisibilityApplier implements ImeVisibilityApplier { + + private static final String TAG = "DefaultImeVisibilityApplier"; + + private static final boolean DEBUG = InputMethodManagerService.DEBUG; + + private InputMethodManagerService mService; + + private final WindowManagerInternal mWindowManagerInternal; + + + DefaultImeVisibilityApplier(InputMethodManagerService service) { + mService = service; + mWindowManagerInternal = LocalServices.getService(WindowManagerInternal.class); + } + + @GuardedBy("ImfLock.class") + @Override + public void performShowIme(IBinder windowToken, @Nullable ImeTracker.Token statsToken, + int showFlags, ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) { + final IInputMethodInvoker curMethod = mService.getCurMethodLocked(); + if (curMethod != null) { + // create a placeholder token for IMS so that IMS cannot inject windows into client app. + final IBinder showInputToken = new Binder(); + mService.setRequestImeTokenToWindow(windowToken, showInputToken); + if (DEBUG) { + Slog.v(TAG, "Calling " + curMethod + ".showSoftInput(" + showInputToken + + ", " + showFlags + ", " + resultReceiver + ") for reason: " + + InputMethodDebug.softInputDisplayReasonToString(reason)); + } + // TODO(b/192412909): Check if we can always call onShowHideSoftInputRequested() or not. + if (curMethod.showSoftInput(showInputToken, statsToken, showFlags, resultReceiver)) { + if (DEBUG_IME_VISIBILITY) { + EventLog.writeEvent(IMF_SHOW_IME, statsToken.getTag(), + Objects.toString(mService.mCurFocusedWindow), + InputMethodDebug.softInputDisplayReasonToString(reason), + InputMethodDebug.softInputModeToString( + mService.mCurFocusedWindowSoftInputMode)); + } + mService.onShowHideSoftInputRequested(true /* show */, windowToken, reason, + statsToken); + } + } + } + + @GuardedBy("ImfLock.class") + @Override + public void performHideIme(IBinder windowToken, @Nullable ImeTracker.Token statsToken, + ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) { + final IInputMethodInvoker curMethod = mService.getCurMethodLocked(); + if (curMethod != null) { + final Binder hideInputToken = new Binder(); + mService.setRequestImeTokenToWindow(windowToken, hideInputToken); + // The IME will report its visible state again after the following message finally + // delivered to the IME process as an IPC. Hence the inconsistency between + // IMMS#mInputShown and IMMS#mImeWindowVis should be resolved spontaneously in + // the final state. + if (DEBUG) { + Slog.v(TAG, "Calling " + curMethod + ".hideSoftInput(0, " + hideInputToken + + ", " + resultReceiver + ") for reason: " + + InputMethodDebug.softInputDisplayReasonToString(reason)); + } + // TODO(b/192412909): Check if we can always call onShowHideSoftInputRequested() or not. + if (curMethod.hideSoftInput(hideInputToken, statsToken, 0, resultReceiver)) { + if (DEBUG_IME_VISIBILITY) { + EventLog.writeEvent(IMF_HIDE_IME, statsToken.getTag(), + Objects.toString(mService.mCurFocusedWindow), + InputMethodDebug.softInputDisplayReasonToString(reason), + InputMethodDebug.softInputModeToString( + mService.mCurFocusedWindowSoftInputMode)); + } + mService.onShowHideSoftInputRequested(false /* show */, windowToken, reason, + statsToken); + } + } + } + + @GuardedBy("ImfLock.class") + @Override + public void applyImeVisibility(IBinder windowToken, @Nullable ImeTracker.Token statsToken, + @ImeVisibilityStateComputer.VisibilityState int state) { + switch (state) { + case STATE_SHOW_IME: + ImeTracker.get().onProgress(statsToken, + ImeTracker.PHASE_SERVER_APPLY_IME_VISIBILITY); + // Send to window manager to show IME after IME layout finishes. + mWindowManagerInternal.showImePostLayout(windowToken, statsToken); + break; + case STATE_HIDE_IME: + if (mService.mCurFocusedWindowClient != null) { + ImeTracker.get().onProgress(statsToken, + ImeTracker.PHASE_SERVER_APPLY_IME_VISIBILITY); + // IMMS only knows of focused window, not the actual IME target. + // e.g. it isn't aware of any window that has both + // NOT_FOCUSABLE, ALT_FOCUSABLE_IM flags set and can the IME target. + // Send it to window manager to hide IME from IME target window. + // TODO(b/139861270): send to mCurClient.client once IMMS is aware of + // actual IME target. + mWindowManagerInternal.hideIme(windowToken, + mService.mCurFocusedWindowClient.mSelfReportedDisplayId, statsToken); + } else { + ImeTracker.get().onFailed(statsToken, + ImeTracker.PHASE_SERVER_APPLY_IME_VISIBILITY); + } + break; + default: + throw new IllegalArgumentException("Invalid IME visibility state: " + state); + } + } +} diff --git a/services/core/java/com/android/server/inputmethod/ImeVisibilityApplier.java b/services/core/java/com/android/server/inputmethod/ImeVisibilityApplier.java new file mode 100644 index 000000000000..e97ec93b23c8 --- /dev/null +++ b/services/core/java/com/android/server/inputmethod/ImeVisibilityApplier.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.inputmethod; + +import android.annotation.Nullable; +import android.os.IBinder; +import android.os.ResultReceiver; +import android.view.inputmethod.ImeTracker; + +import com.android.internal.inputmethod.SoftInputShowHideReason; + +/** + * Interface for IME visibility operations like show/hide and update Z-ordering relative to the IME + * targeted window. + */ +interface ImeVisibilityApplier { + /** + * Performs showing IME on top of the given window. + * + * @param windowToken The token of a window that currently has focus. + * @param statsToken A token that tracks the progress of an IME request. + * @param showFlags Provides additional operating flags to show IME. + * @param resultReceiver If non-null, this will be called back to the caller when + * it has processed request to tell what it has done. + * @param reason The reason for requesting to show IME. + */ + default void performShowIme(IBinder windowToken, @Nullable ImeTracker.Token statsToken, + int showFlags, ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {} + + /** + * Performs hiding IME to the given window + * + * @param windowToken The token of a window that currently has focus. + * @param statsToken A token that tracks the progress of an IME request. + * @param resultReceiver If non-null, this will be called back to the caller when + * it has processed request to tell what it has done. + * @param reason The reason for requesting to hide IME. + */ + default void performHideIme(IBinder windowToken, @Nullable ImeTracker.Token statsToken, + ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {} + + /** + * Applies the IME visibility from {@link android.inputmethodservice.InputMethodService} with + * according to the given visibility state. + * + * @param windowToken The token of a window for applying the IME visibility + * @param statsToken A token that tracks the progress of an IME request. + * @param state The new IME visibility state for the applier to handle + */ + default void applyImeVisibility(IBinder windowToken, @Nullable ImeTracker.Token statsToken, + @ImeVisibilityStateComputer.VisibilityState int state) {} + + /** + * Updates the IME Z-ordering relative to the given window. + * + * This used to adjust the IME relative layer of the window during + * {@link InputMethodManagerService} is in switching IME clients. + * + * @param windowToken The token of a window to update the Z-ordering relative to the IME. + */ + default void updateImeLayeringByTarget(IBinder windowToken) { + // TODO: add a method in WindowManagerInternal to call DC#updateImeInputAndControlTarget + // here to end up updating IME layering after IMMS#attachNewInputLocked called. + } +} diff --git a/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java b/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java new file mode 100644 index 000000000000..a2655f4c0f4d --- /dev/null +++ b/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java @@ -0,0 +1,424 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.inputmethod; + +import static android.accessibilityservice.AccessibilityService.SHOW_MODE_HIDDEN; +import static android.server.inputmethod.InputMethodManagerServiceProto.ACCESSIBILITY_REQUESTING_NO_SOFT_KEYBOARD; +import static android.server.inputmethod.InputMethodManagerServiceProto.SHOW_EXPLICITLY_REQUESTED; +import static android.server.inputmethod.InputMethodManagerServiceProto.SHOW_FORCED; +import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.Display.INVALID_DISPLAY; +import static android.view.WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION; +import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED; +import static android.view.WindowManager.LayoutParams.SoftInputModeFlags; + +import static com.android.internal.inputmethod.InputMethodDebug.softInputModeToString; +import static com.android.server.inputmethod.InputMethodManagerService.computeImeDisplayIdForTarget; + +import android.accessibilityservice.AccessibilityService; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.os.IBinder; +import android.util.PrintWriterPrinter; +import android.util.Printer; +import android.util.Slog; +import android.util.proto.ProtoOutputStream; +import android.view.WindowManager; +import android.view.inputmethod.ImeTracker; +import android.view.inputmethod.InputMethod; +import android.view.inputmethod.InputMethodManager; + +import com.android.server.LocalServices; +import com.android.server.wm.WindowManagerInternal; + +import java.io.PrintWriter; +import java.util.WeakHashMap; + +/** + * A computer used by {@link InputMethodManagerService} that computes the IME visibility state + * according the given {@link ImeTargetWindowState} from the focused window or the app requested IME + * visibility from {@link InputMethodManager}. + */ +public final class ImeVisibilityStateComputer { + + private static final String TAG = "ImeVisibilityStateComputer"; + + private static final boolean DEBUG = InputMethodManagerService.DEBUG; + + private final InputMethodManagerService mService; + private final WindowManagerInternal mWindowManagerInternal; + + final InputMethodManagerService.ImeDisplayValidator mImeDisplayValidator; + + /** + * A map used to track the requested IME target window and its state. The key represents the + * token of the window and the value is the corresponding IME window state. + */ + private final WeakHashMap<IBinder, ImeTargetWindowState> mRequestWindowStateMap = + new WeakHashMap<>(); + + /** + * Set if IME was explicitly told to show the input method. + * + * @see InputMethodManager#SHOW_IMPLICIT that we set the value is {@code false}. + * @see InputMethodManager#HIDE_IMPLICIT_ONLY that system will not hide IME when the value is + * {@code true}. + */ + boolean mRequestedShowExplicitly; + + /** + * Set if we were forced to be shown. + * + * @see InputMethodManager#SHOW_FORCED + * @see InputMethodManager#HIDE_NOT_ALWAYS + */ + boolean mShowForced; + + /** Represent the invalid IME visibility state */ + public static final int STATE_INVALID = -1; + + /** State to handle hiding the IME window requested by the app. */ + public static final int STATE_HIDE_IME = 0; + + /** State to handle showing the IME window requested by the app. */ + public static final int STATE_SHOW_IME = 1; + + /** State to handle showing the IME window with making the overlay window above it. */ + public static final int STATE_SHOW_IME_ABOVE_OVERLAY = 2; + + /** State to handle showing the IME window with making the overlay window behind it. */ + public static final int STATE_SHOW_IME_BEHIND_OVERLAY = 3; + + /** State to handle showing an IME preview surface during the app was loosing the IME focus */ + public static final int STATE_SHOW_IME_SNAPSHOT = 4; + @IntDef({ + STATE_INVALID, + STATE_HIDE_IME, + STATE_SHOW_IME, + STATE_SHOW_IME_ABOVE_OVERLAY, + STATE_SHOW_IME_BEHIND_OVERLAY, + STATE_SHOW_IME_SNAPSHOT, + }) + @interface VisibilityState {} + + /** + * The policy to configure the IME visibility. + */ + private final ImeVisibilityPolicy mPolicy; + + public ImeVisibilityStateComputer(InputMethodManagerService service) { + mService = service; + mWindowManagerInternal = LocalServices.getService(WindowManagerInternal.class); + mImeDisplayValidator = mWindowManagerInternal::getDisplayImePolicy; + mPolicy = new ImeVisibilityPolicy(); + } + + /** + * Called when {@link InputMethodManagerService} is processing the show IME request. + * @param statsToken The token for tracking this show request + * @param showFlags The additional operation flags to indicate whether this show request mode is + * implicit or explicit. + * @return {@code true} when the computer has proceed this show request operation. + */ + boolean onImeShowFlags(@NonNull ImeTracker.Token statsToken, int showFlags) { + if (mPolicy.mA11yRequestingNoSoftKeyboard || mPolicy.mImeHiddenByDisplayPolicy) { + ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_SERVER_ACCESSIBILITY); + return false; + } + ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_ACCESSIBILITY); + if ((showFlags & InputMethodManager.SHOW_FORCED) != 0) { + mRequestedShowExplicitly = true; + mShowForced = true; + } else if ((showFlags & InputMethodManager.SHOW_IMPLICIT) == 0) { + mRequestedShowExplicitly = true; + } + return true; + } + + /** + * Called when {@link InputMethodManagerService} is processing the hide IME request. + * @param statsToken The token for tracking this hide request + * @param hideFlags The additional operation flags to indicate whether this hide request mode is + * implicit or explicit. + * @return {@code true} when the computer has proceed this hide request operations. + */ + boolean canHideIme(@NonNull ImeTracker.Token statsToken, int hideFlags) { + if ((hideFlags & InputMethodManager.HIDE_IMPLICIT_ONLY) != 0 + && (mRequestedShowExplicitly || mShowForced)) { + if (DEBUG) Slog.v(TAG, "Not hiding: explicit show not cancelled by non-explicit hide"); + ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_SERVER_HIDE_IMPLICIT); + return false; + } + if (mShowForced && (hideFlags & InputMethodManager.HIDE_NOT_ALWAYS) != 0) { + if (DEBUG) Slog.v(TAG, "Not hiding: forced show not cancelled by not-always hide"); + ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_SERVER_HIDE_NOT_ALWAYS); + return false; + } + ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_HIDE_NOT_ALWAYS); + return true; + } + + int getImeShowFlags() { + int flags = 0; + if (mShowForced) { + flags |= InputMethod.SHOW_FORCED | InputMethod.SHOW_EXPLICIT; + } else if (mRequestedShowExplicitly) { + flags |= InputMethod.SHOW_EXPLICIT; + } else { + flags |= InputMethodManager.SHOW_IMPLICIT; + } + return flags; + } + + void clearImeShowFlags() { + mRequestedShowExplicitly = false; + mShowForced = false; + } + + int computeImeDisplayId(@NonNull ImeTargetWindowState state, int displayId) { + final int displayToShowIme = computeImeDisplayIdForTarget(displayId, mImeDisplayValidator); + state.setImeDisplayId(displayToShowIme); + final boolean imeHiddenByPolicy = displayToShowIme == INVALID_DISPLAY; + mPolicy.setImeHiddenByDisplayPolicy(imeHiddenByPolicy); + return displayToShowIme; + } + + /** + * Request to show/hide IME from the given window. + * + * @param windowToken The window which requests to show/hide IME. + * @param showIme {@code true} means to show IME, {@code false} otherwise. + * Note that in the computer will take this option to compute the + * visibility state, it could be {@link #STATE_SHOW_IME} or + * {@link #STATE_HIDE_IME}. + */ + void requestImeVisibility(IBinder windowToken, boolean showIme) { + final ImeTargetWindowState state = getOrCreateWindowState(windowToken); + state.setRequestedImeVisible(showIme); + setWindowState(windowToken, state); + } + + ImeTargetWindowState getOrCreateWindowState(IBinder windowToken) { + ImeTargetWindowState state = mRequestWindowStateMap.get(windowToken); + if (state == null) { + state = new ImeTargetWindowState(SOFT_INPUT_STATE_UNSPECIFIED, false, false); + } + return state; + } + + ImeTargetWindowState getWindowStateOrNull(IBinder windowToken) { + ImeTargetWindowState state = mRequestWindowStateMap.get(windowToken); + return state; + } + + void setRequestImeTokenToWindow(IBinder windowToken, IBinder token) { + ImeTargetWindowState state = getWindowStateOrNull(windowToken); + if (state != null) { + state.setRequestImeToken(token); + setWindowState(windowToken, state); + } + } + + void setWindowState(IBinder windowToken, ImeTargetWindowState newState) { + if (DEBUG) Slog.d(TAG, "setWindowState, windowToken=" + windowToken + + ", state=" + newState); + mRequestWindowStateMap.put(windowToken, newState); + } + + IBinder getWindowTokenFrom(IBinder requestImeToken) { + for (IBinder windowToken : mRequestWindowStateMap.keySet()) { + final ImeTargetWindowState state = mRequestWindowStateMap.get(windowToken); + if (state.getRequestImeToken() == requestImeToken) { + return windowToken; + } + } + // Fallback to the focused window for some edge cases (e.g. relaunching the activity) + return mService.mCurFocusedWindow; + } + + IBinder getWindowTokenFrom(ImeTargetWindowState windowState) { + for (IBinder windowToken : mRequestWindowStateMap.keySet()) { + final ImeTargetWindowState state = mRequestWindowStateMap.get(windowToken); + if (state == windowState) { + return windowToken; + } + } + return null; + } + + boolean shouldRestoreImeVisibility(@NonNull ImeTargetWindowState state) { + final int softInputMode = state.getSoftInputModeState(); + switch (softInputMode & WindowManager.LayoutParams.SOFT_INPUT_MASK_STATE) { + case WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN: + return false; + case WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN: + if ((softInputMode & SOFT_INPUT_IS_FORWARD_NAVIGATION) != 0) { + return false; + } + } + return mWindowManagerInternal.shouldRestoreImeVisibility(getWindowTokenFrom(state)); + } + + void dumpDebug(ProtoOutputStream proto, long fieldId) { + proto.write(SHOW_EXPLICITLY_REQUESTED, mRequestedShowExplicitly); + proto.write(SHOW_FORCED, mShowForced); + proto.write(ACCESSIBILITY_REQUESTING_NO_SOFT_KEYBOARD, + mPolicy.isA11yRequestNoSoftKeyboard()); + } + + void dump(PrintWriter pw) { + final Printer p = new PrintWriterPrinter(pw); + p.println(" mRequestedShowExplicitly=" + mRequestedShowExplicitly + + " mShowForced=" + mShowForced); + p.println(" mImeHiddenByDisplayPolicy=" + mPolicy.isImeHiddenByDisplayPolicy()); + } + + /** + * A settings class to manage all IME related visibility policies or settings. + * + * This is used for the visibility computer to manage and tell + * {@link InputMethodManagerService} if the requested IME visibility is valid from + * application call or the focus window. + */ + static class ImeVisibilityPolicy { + /** + * {@code true} if the Ime policy has been set to + * {@link WindowManager#DISPLAY_IME_POLICY_HIDE}. + * + * This prevents the IME from showing when it otherwise may have shown. + */ + private boolean mImeHiddenByDisplayPolicy; + + /** + * Set when the accessibility service requests to hide IME by + * {@link AccessibilityService.SoftKeyboardController#setShowMode} + */ + private boolean mA11yRequestingNoSoftKeyboard; + + void setImeHiddenByDisplayPolicy(boolean hideIme) { + mImeHiddenByDisplayPolicy = hideIme; + } + + boolean isImeHiddenByDisplayPolicy() { + return mImeHiddenByDisplayPolicy; + } + + void setA11yRequestNoSoftKeyboard(int keyboardShowMode) { + mA11yRequestingNoSoftKeyboard = + (keyboardShowMode & AccessibilityService.SHOW_MODE_MASK) == SHOW_MODE_HIDDEN; + } + + boolean isA11yRequestNoSoftKeyboard() { + return mA11yRequestingNoSoftKeyboard; + } + } + + ImeVisibilityPolicy getImePolicy() { + return mPolicy; + } + + /** + * A class that represents the current state of the IME target window. + */ + static class ImeTargetWindowState { + ImeTargetWindowState(@SoftInputModeFlags int softInputModeState, boolean imeFocusChanged, + boolean hasFocusedEditor) { + mSoftInputModeState = softInputModeState; + mImeFocusChanged = imeFocusChanged; + mHasFocusedEditor = hasFocusedEditor; + } + + /** + * Visibility state for this window. By default no state has been specified. + */ + private final @SoftInputModeFlags int mSoftInputModeState; + + /** + * {@code true} means the IME focus changed from the previous window, {@code false} + * otherwise. + */ + private final boolean mImeFocusChanged; + + /** + * {@code true} when the window has focused an editor, {@code false} otherwise. + */ + private final boolean mHasFocusedEditor; + + /** + * Set if the client has asked for the input method to be shown. + */ + private boolean mRequestedImeVisible; + + /** + * A identifier for knowing the requester of {@link InputMethodManager#showSoftInput} or + * {@link InputMethodManager#hideSoftInputFromWindow}. + */ + private IBinder mRequestImeToken; + + /** + * The IME target display id for which the latest startInput was called. + */ + private int mImeDisplayId = DEFAULT_DISPLAY; + + boolean hasImeFocusChanged() { + return mImeFocusChanged; + } + + boolean hasEdiorFocused() { + return mHasFocusedEditor; + } + + int getSoftInputModeState() { + return mSoftInputModeState; + } + + private void setImeDisplayId(int imeDisplayId) { + mImeDisplayId = imeDisplayId; + } + + int getImeDisplayId() { + return mImeDisplayId; + } + + private void setRequestedImeVisible(boolean requestedImeVisible) { + mRequestedImeVisible = requestedImeVisible; + } + + boolean isRequestedImeVisible() { + return mRequestedImeVisible; + } + + void setRequestImeToken(IBinder token) { + mRequestImeToken = token; + } + + IBinder getRequestImeToken() { + return mRequestImeToken; + } + + @Override + public String toString() { + return "ImeTargetWindowState{ imeToken " + mRequestImeToken + + " imeFocusChanged " + mImeFocusChanged + + " hasEditorFocused " + mHasFocusedEditor + + " requestedImeVisible " + mRequestedImeVisible + + " imeDisplayId " + mImeDisplayId + + " softInputModeState " + softInputModeToString(mSoftInputModeState) + + "}"; + } + } +} diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 5b9a6639bff6..2dbbb1085bb1 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -20,7 +20,6 @@ import static android.os.IServiceManager.DUMP_FLAG_PRIORITY_CRITICAL; import static android.os.IServiceManager.DUMP_FLAG_PRIORITY_NORMAL; import static android.os.IServiceManager.DUMP_FLAG_PROTO; import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; -import static android.server.inputmethod.InputMethodManagerServiceProto.ACCESSIBILITY_REQUESTING_NO_SOFT_KEYBOARD; import static android.server.inputmethod.InputMethodManagerServiceProto.BACK_DISPOSITION; import static android.server.inputmethod.InputMethodManagerServiceProto.BOUND_TO_METHOD; import static android.server.inputmethod.InputMethodManagerServiceProto.CUR_ATTRIBUTE; @@ -39,8 +38,6 @@ import static android.server.inputmethod.InputMethodManagerServiceProto.IN_FULLS import static android.server.inputmethod.InputMethodManagerServiceProto.IS_INTERACTIVE; import static android.server.inputmethod.InputMethodManagerServiceProto.LAST_IME_TARGET_WINDOW_NAME; import static android.server.inputmethod.InputMethodManagerServiceProto.LAST_SWITCH_USER_ID; -import static android.server.inputmethod.InputMethodManagerServiceProto.SHOW_EXPLICITLY_REQUESTED; -import static android.server.inputmethod.InputMethodManagerServiceProto.SHOW_FORCED; import static android.server.inputmethod.InputMethodManagerServiceProto.SHOW_IME_WITH_HARD_KEYBOARD; import static android.server.inputmethod.InputMethodManagerServiceProto.SHOW_REQUESTED; import static android.server.inputmethod.InputMethodManagerServiceProto.SYSTEM_READY; @@ -48,17 +45,14 @@ import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.INVALID_DISPLAY; import static android.view.WindowManager.DISPLAY_IME_POLICY_HIDE; import static android.view.WindowManager.DISPLAY_IME_POLICY_LOCAL; -import static android.view.inputmethod.ImeTracker.DEBUG_IME_VISIBILITY; -import static com.android.server.EventLogTags.IMF_HIDE_IME; -import static com.android.server.EventLogTags.IMF_SHOW_IME; +import static com.android.server.inputmethod.ImeVisibilityStateComputer.ImeTargetWindowState; import static com.android.server.inputmethod.InputMethodBindingController.TIME_TO_RECONNECT; import static com.android.server.inputmethod.InputMethodUtils.isSoftInputModeStateVisibleAllowed; import static java.lang.annotation.RetentionPolicy.SOURCE; import android.Manifest; -import android.accessibilityservice.AccessibilityService; import android.annotation.AnyThread; import android.annotation.BinderThread; import android.annotation.DrawableRes; @@ -126,7 +120,6 @@ import android.view.DisplayInfo; import android.view.InputChannel; import android.view.InputDevice; import android.view.MotionEvent; -import android.view.View; import android.view.WindowManager; import android.view.WindowManager.DisplayImePolicy; import android.view.WindowManager.LayoutParams; @@ -299,6 +292,12 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @NonNull private final InputMethodBindingController mBindingController; @NonNull private final AutofillSuggestionsController mAutofillController; + @GuardedBy("ImfLock.class") + @NonNull private final ImeVisibilityStateComputer mVisibilityStateComputer; + + @GuardedBy("ImfLock.class") + @NonNull private final DefaultImeVisibilityApplier mVisibilityApplier; + /** * Cache the result of {@code LocalServices.getService(AudioManagerInternal.class)}. * @@ -530,13 +529,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } /** - * {@code true} if the Ime policy has been set to {@link WindowManager#DISPLAY_IME_POLICY_HIDE}. - * - * This prevents the IME from showing when it otherwise may have shown. - */ - boolean mImeHiddenByDisplayPolicy; - - /** * The client that is currently bound to an input method. */ private ClientState mCurClient; @@ -638,16 +630,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub private boolean mShowRequested; /** - * Set if we were explicitly told to show the input method. - */ - boolean mShowExplicitlyRequested; - - /** - * Set if we were forced to be shown. - */ - boolean mShowForced; - - /** * Set if we last told the input method to show itself. */ private boolean mInputShown; @@ -709,8 +691,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub */ private static final int FALLBACK_DISPLAY_ID = DEFAULT_DISPLAY; - final ImeDisplayValidator mImeDisplayValidator; - /** * If non-null, this is the input method service we are currently connected * to. @@ -786,7 +766,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub int mImeWindowVis; private LocaleList mLastSystemLocales; - private boolean mAccessibilityRequestingNoSoftKeyboard; private final MyPackageMonitor mMyPackageMonitor = new MyPackageMonitor(); private final String mSlotIme; @@ -974,22 +953,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } /** - * Map of generated token to windowToken that is requesting - * {@link InputMethodManager#showSoftInput(View, int)}. - * This map tracks origin of showSoftInput requests. - */ - @GuardedBy("ImfLock.class") - private final WeakHashMap<IBinder, IBinder> mShowRequestWindowMap = new WeakHashMap<>(); - - /** - * Map of generated token to windowToken that is requesting - * {@link InputMethodManager#hideSoftInputFromWindow(IBinder, int)}. - * This map tracks origin of hideSoftInput requests. - */ - @GuardedBy("ImfLock.class") - private final WeakHashMap<IBinder, IBinder> mHideRequestWindowMap = new WeakHashMap<>(); - - /** * A ring buffer to store the history of {@link StartInputInfo}. */ private static final class StartInputHistory { @@ -1207,11 +1170,10 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } else if (accessibilityRequestingNoImeUri.equals(uri)) { final int accessibilitySoftKeyboardSetting = Settings.Secure.getIntForUser( mContext.getContentResolver(), - Settings.Secure.ACCESSIBILITY_SOFT_KEYBOARD_MODE, 0 /* def */, mUserId); - mAccessibilityRequestingNoSoftKeyboard = - (accessibilitySoftKeyboardSetting & AccessibilityService.SHOW_MODE_MASK) - == AccessibilityService.SHOW_MODE_HIDDEN; - if (mAccessibilityRequestingNoSoftKeyboard) { + Settings.Secure.ACCESSIBILITY_SOFT_KEYBOARD_MODE, 0, mUserId); + mVisibilityStateComputer.getImePolicy().setA11yRequestNoSoftKeyboard( + accessibilitySoftKeyboardSetting); + if (mVisibilityStateComputer.getImePolicy().isA11yRequestNoSoftKeyboard()) { final boolean showRequested = mShowRequested; hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */, null /* resultReceiver */, @@ -1722,7 +1684,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mInputManagerInternal = LocalServices.getService(InputManagerInternal.class); mImePlatformCompatUtils = new ImePlatformCompatUtils(); mInputMethodDeviceConfigs = new InputMethodDeviceConfigs(); - mImeDisplayValidator = mWindowManagerInternal::getDisplayImePolicy; mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class); mUserManagerInternal = LocalServices.getService(UserManagerInternal.class); @@ -1747,6 +1708,10 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub ? bindingControllerForTesting : new InputMethodBindingController(this); mAutofillController = new AutofillSuggestionsController(this); + + mVisibilityStateComputer = new ImeVisibilityStateComputer(this); + mVisibilityApplier = new DefaultImeVisibilityApplier(this); + mPreventImeStartupUnlessTextEditor = mRes.getBoolean( com.android.internal.R.bool.config_preventImeStartupUnlessTextEditor); mNonPreemptibleInputMethods = mRes.getStringArray( @@ -2340,29 +2305,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } @GuardedBy("ImfLock.class") - private int getImeShowFlagsLocked() { - int flags = 0; - if (mShowForced) { - flags |= InputMethod.SHOW_FORCED - | InputMethod.SHOW_EXPLICIT; - } else if (mShowExplicitlyRequested) { - flags |= InputMethod.SHOW_EXPLICIT; - } - return flags; - } - - @GuardedBy("ImfLock.class") - private int getAppShowFlagsLocked() { - int flags = 0; - if (mShowForced) { - flags |= InputMethodManager.SHOW_FORCED; - } else if (!mShowExplicitlyRequested) { - flags |= InputMethodManager.SHOW_IMPLICIT; - } - return flags; - } - - @GuardedBy("ImfLock.class") @NonNull InputBindResult attachNewInputLocked(@StartInputReason int startInputReason, boolean initial) { if (!mBoundToMethod) { @@ -2403,7 +2345,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // Re-use current statsToken, if it exists. final ImeTracker.Token statsToken = mCurStatsToken; mCurStatsToken = null; - showCurrentInputLocked(mCurFocusedWindow, statsToken, getAppShowFlagsLocked(), + showCurrentInputLocked(mCurFocusedWindow, statsToken, + mVisibilityStateComputer.getImeShowFlags(), null /* resultReceiver */, SoftInputShowHideReason.ATTACH_NEW_INPUT); } @@ -2518,17 +2461,20 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // Compute the final shown display ID with validated cs.selfReportedDisplayId for this // session & other conditions. - mDisplayIdToShowIme = computeImeDisplayIdForTarget(cs.mSelfReportedDisplayId, - mImeDisplayValidator); + ImeTargetWindowState winState = mVisibilityStateComputer.getWindowStateOrNull( + mCurFocusedWindow); + if (winState == null) { + return InputBindResult.NOT_IME_TARGET_WINDOW; + } + final int csDisplayId = cs.mSelfReportedDisplayId; + mDisplayIdToShowIme = mVisibilityStateComputer.computeImeDisplayId(winState, csDisplayId); - if (mDisplayIdToShowIme == INVALID_DISPLAY) { - mImeHiddenByDisplayPolicy = true; + if (mVisibilityStateComputer.getImePolicy().isImeHiddenByDisplayPolicy()) { hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */, null /* resultReceiver */, SoftInputShowHideReason.HIDE_DISPLAY_IME_POLICY_HIDE); return InputBindResult.NO_IME; } - mImeHiddenByDisplayPolicy = false; if (mCurClient != cs) { prepareClientSwitchLocked(cs); @@ -3385,6 +3331,11 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } } + @GuardedBy("ImfLock.class") + void setRequestImeTokenToWindow(IBinder windowToken, IBinder token) { + mVisibilityStateComputer.setRequestImeTokenToWindow(windowToken, token); + } + @BinderThread @Override public void reportPerceptibleAsync(IBinder windowToken, boolean perceptible) { @@ -3419,18 +3370,11 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub ImeTracker.ORIGIN_SERVER_START_INPUT, reason); } + // TODO(b/246309664): make mShowRequested as per-window state. mShowRequested = true; - if (mAccessibilityRequestingNoSoftKeyboard || mImeHiddenByDisplayPolicy) { - ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_SERVER_ACCESSIBILITY); - return false; - } - ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_ACCESSIBILITY); - if ((flags & InputMethodManager.SHOW_FORCED) != 0) { - mShowExplicitlyRequested = true; - mShowForced = true; - } else if ((flags & InputMethodManager.SHOW_IMPLICIT) == 0) { - mShowExplicitlyRequested = true; + if (!mVisibilityStateComputer.onImeShowFlags(statsToken, flags)) { + return false; } if (!mSystemReady) { @@ -3439,39 +3383,25 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_SYSTEM_READY); + mVisibilityStateComputer.requestImeVisibility(windowToken, true); + + // Ensure binding the connection when IME is going to show. mBindingController.setCurrentMethodVisible(); final IInputMethodInvoker curMethod = getCurMethodLocked(); + ImeTracker.get().onCancelled(mCurStatsToken, ImeTracker.PHASE_SERVER_WAIT_IME); if (curMethod != null) { - // create a placeholder token for IMS so that IMS cannot inject windows into client app. - Binder showInputToken = new Binder(); - mShowRequestWindowMap.put(showInputToken, windowToken); - ImeTracker.get().onCancelled(mCurStatsToken, ImeTracker.PHASE_SERVER_WAIT_IME); ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_HAS_IME); mCurStatsToken = null; - final int showFlags = getImeShowFlagsLocked(); - if (DEBUG) { - Slog.v(TAG, "Calling " + curMethod + ".showSoftInput(" + showInputToken - + ", " + showFlags + ", " + resultReceiver + ") for reason: " - + InputMethodDebug.softInputDisplayReasonToString(reason)); - } if (lastClickToolType != MotionEvent.TOOL_TYPE_UNKNOWN) { curMethod.updateEditorToolType(lastClickToolType); } - // TODO(b/192412909): Check if we can always call onShowHideSoftInputRequested() or not. - if (curMethod.showSoftInput(showInputToken, statsToken, showFlags, resultReceiver)) { - if (DEBUG_IME_VISIBILITY) { - EventLog.writeEvent(IMF_SHOW_IME, statsToken.getTag(), - Objects.toString(mCurFocusedWindow), - InputMethodDebug.softInputDisplayReasonToString(reason), - InputMethodDebug.softInputModeToString(mCurFocusedWindowSoftInputMode)); - } - onShowHideSoftInputRequested(true /* show */, windowToken, reason, statsToken); - } + mVisibilityApplier.performShowIme(windowToken, statsToken, + mVisibilityStateComputer.getImeShowFlags(), resultReceiver, reason); + // TODO(b/246309664): make mInputShown tracked by the Ime visibility computer. mInputShown = true; return true; } else { - ImeTracker.get().onCancelled(mCurStatsToken, ImeTracker.PHASE_SERVER_WAIT_IME); ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_WAIT_IME); mCurStatsToken = statsToken; } @@ -3527,20 +3457,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub ImeTracker.ORIGIN_SERVER_HIDE_INPUT, reason); } - if ((flags & InputMethodManager.HIDE_IMPLICIT_ONLY) != 0 - && (mShowExplicitlyRequested || mShowForced)) { - if (DEBUG) Slog.v(TAG, "Not hiding: explicit show not cancelled by non-explicit hide"); - ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_SERVER_HIDE_IMPLICIT); + if (!mVisibilityStateComputer.canHideIme(statsToken, flags)) { return false; } - ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_HIDE_IMPLICIT); - - if (mShowForced && (flags & InputMethodManager.HIDE_NOT_ALWAYS) != 0) { - if (DEBUG) Slog.v(TAG, "Not hiding: forced show not cancelled by not-always hide"); - ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_SERVER_HIDE_NOT_ALWAYS); - return false; - } - ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_HIDE_NOT_ALWAYS); // There is a chance that IMM#hideSoftInput() is called in a transient state where // IMMS#InputShown is already updated to be true whereas IMMS#mImeWindowVis is still waiting @@ -3549,49 +3468,30 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // application process as a valid request, and have even promised such a behavior with CTS // since Android Eclair. That's why we need to accept IMM#hideSoftInput() even when only // IMMS#InputShown indicates that the software keyboard is shown. - // TODO: Clean up, IMMS#mInputShown, IMMS#mImeWindowVis and mShowRequested. + // TODO(b/246309664): Clean up, IMMS#mInputShown, IMMS#mImeWindowVis and mShowRequested. IInputMethodInvoker curMethod = getCurMethodLocked(); final boolean shouldHideSoftInput = (curMethod != null) && (mInputShown || (mImeWindowVis & InputMethodService.IME_ACTIVE) != 0); - boolean res; + + mVisibilityStateComputer.requestImeVisibility(windowToken, false); if (shouldHideSoftInput) { - final Binder hideInputToken = new Binder(); - mHideRequestWindowMap.put(hideInputToken, windowToken); // The IME will report its visible state again after the following message finally // delivered to the IME process as an IPC. Hence the inconsistency between // IMMS#mInputShown and IMMS#mImeWindowVis should be resolved spontaneously in // the final state. ImeTracker.get().onProgress(statsToken, ImeTracker.PHASE_SERVER_SHOULD_HIDE); - if (DEBUG) { - Slog.v(TAG, "Calling " + curMethod + ".hideSoftInput(0, " + hideInputToken - + ", " + resultReceiver + ") for reason: " - + InputMethodDebug.softInputDisplayReasonToString(reason)); - } - // TODO(b/192412909): Check if we can always call onShowHideSoftInputRequested() or not. - if (curMethod.hideSoftInput(hideInputToken, statsToken, 0 /* flags */, - resultReceiver)) { - if (DEBUG_IME_VISIBILITY) { - EventLog.writeEvent(IMF_HIDE_IME, statsToken.getTag(), - Objects.toString(mCurFocusedWindow), - InputMethodDebug.softInputDisplayReasonToString(reason), - InputMethodDebug.softInputModeToString(mCurFocusedWindowSoftInputMode)); - } - onShowHideSoftInputRequested(false /* show */, windowToken, reason, statsToken); - } - res = true; + mVisibilityApplier.performHideIme(windowToken, statsToken, resultReceiver, reason); } else { ImeTracker.get().onCancelled(statsToken, ImeTracker.PHASE_SERVER_SHOULD_HIDE); - res = false; } mBindingController.setCurrentMethodNotVisible(); + mVisibilityStateComputer.clearImeShowFlags(); mInputShown = false; mShowRequested = false; - mShowExplicitlyRequested = false; - mShowForced = false; // Cancel existing statsToken for show IME as we got a hide request. ImeTracker.get().onCancelled(mCurStatsToken, ImeTracker.PHASE_SERVER_WAIT_IME); mCurStatsToken = null; - return res; + return shouldHideSoftInput; } private boolean isImeClientFocused(IBinder windowToken, ClientState cs) { @@ -3738,8 +3638,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // In case mShowForced flag affects the next client to keep IME visible, when the current // client is leaving due to the next focused client, we clear mShowForced flag when the // next client's targetSdkVersion is T or higher. - if (mCurFocusedWindow != windowToken && mShowForced && shouldClearFlag) { - mShowForced = false; + final boolean showForced = mVisibilityStateComputer.mShowForced; + if (mCurFocusedWindow != windowToken && showForced && shouldClearFlag) { + mVisibilityStateComputer.mShowForced = false; } // cross-profile access is always allowed here to allow profile-switching. @@ -3763,6 +3664,12 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub final boolean startInputByWinGainedFocus = (startInputFlags & StartInputFlags.WINDOW_GAINED_FOCUS) != 0; + // Init the focused window state (e.g. whether the editor has focused or IME focus has + // changed from another window). + final ImeTargetWindowState windowState = new ImeTargetWindowState( + softInputMode, !sameWindowFocused, isTextEditor); + mVisibilityStateComputer.setWindowState(windowToken, windowState); + if (sameWindowFocused && isTextEditor) { if (DEBUG) { Slog.w(TAG, "Window already focused, ignoring focus gain of: " + client @@ -3812,7 +3719,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // Because the app might leverage these flags to hide soft-keyboard with showing their own // UI for input. if (isTextEditor && editorInfo != null - && shouldRestoreImeVisibility(windowToken, softInputMode)) { + && mVisibilityStateComputer.shouldRestoreImeVisibility(windowState)) { if (DEBUG) Slog.v(TAG, "Will show input to restore visibility"); res = startInputUncheckedLocked(cs, inputContext, remoteAccessibilityInputConnection, editorInfo, startInputFlags, startInputReason, unverifiedTargetSdkVersion, @@ -4001,19 +3908,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return true; } - private boolean shouldRestoreImeVisibility(IBinder windowToken, - @SoftInputModeFlags int softInputMode) { - switch (softInputMode & LayoutParams.SOFT_INPUT_MASK_STATE) { - case LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN: - return false; - case LayoutParams.SOFT_INPUT_STATE_HIDDEN: - if ((softInputMode & LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) != 0) { - return false; - } - } - return mWindowManagerInternal.shouldRestoreImeVisibility(windowToken); - } - @GuardedBy("ImfLock.class") private boolean canShowInputMethodPickerLocked(IInputMethodClient client) { final int uid = Binder.getCallingUid(); @@ -4746,8 +4640,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } proto.write(CUR_ID, getCurIdLocked()); proto.write(SHOW_REQUESTED, mShowRequested); - proto.write(SHOW_EXPLICITLY_REQUESTED, mShowExplicitlyRequested); - proto.write(SHOW_FORCED, mShowForced); + mVisibilityStateComputer.dumpDebug(proto, fieldId); proto.write(INPUT_SHOWN, mInputShown); proto.write(IN_FULLSCREEN_MODE, mInFullscreenMode); proto.write(CUR_TOKEN, Objects.toString(getCurTokenLocked())); @@ -4760,8 +4653,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub proto.write(BACK_DISPOSITION, mBackDisposition); proto.write(IME_WINDOW_VISIBILITY, mImeWindowVis); proto.write(SHOW_IME_WITH_HARD_KEYBOARD, mMenuController.getShowImeWithHardKeyboard()); - proto.write(ACCESSIBILITY_REQUESTING_NO_SOFT_KEYBOARD, - mAccessibilityRequestingNoSoftKeyboard); proto.end(token); } } @@ -4795,25 +4686,10 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub ImeTracker.get().onFailed(statsToken, ImeTracker.PHASE_SERVER_APPLY_IME_VISIBILITY); return; } - if (!setVisible) { - if (mCurClient != null) { - ImeTracker.get().onProgress(statsToken, - ImeTracker.PHASE_SERVER_APPLY_IME_VISIBILITY); - - mWindowManagerInternal.hideIme( - mHideRequestWindowMap.get(windowToken), - mCurClient.mSelfReportedDisplayId, statsToken); - } else { - ImeTracker.get().onFailed(statsToken, - ImeTracker.PHASE_SERVER_APPLY_IME_VISIBILITY); - } - } else { - ImeTracker.get().onProgress(statsToken, - ImeTracker.PHASE_SERVER_APPLY_IME_VISIBILITY); - // Send to window manager to show IME after IME layout finishes. - mWindowManagerInternal.showImePostLayout(mShowRequestWindowMap.get(windowToken), - statsToken); - } + final IBinder requestToken = mVisibilityStateComputer.getWindowTokenFrom(windowToken); + mVisibilityApplier.applyImeVisibility(requestToken, statsToken, + setVisible ? ImeVisibilityStateComputer.STATE_SHOW_IME + : ImeVisibilityStateComputer.STATE_HIDE_IME); } Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); } @@ -4857,7 +4733,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub /** Called right after {@link com.android.internal.inputmethod.IInputMethod#showSoftInput}. */ @GuardedBy("ImfLock.class") - private void onShowHideSoftInputRequested(boolean show, IBinder requestToken, + void onShowHideSoftInputRequested(boolean show, IBinder requestToken, @SoftInputShowHideReason int reason, @Nullable ImeTracker.Token statsToken) { final WindowManagerInternal.ImeTargetInfo info = mWindowManagerInternal.onToggleImeRequested( @@ -5988,14 +5864,11 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub method = getCurMethodLocked(); p.println(" mCurMethod=" + getCurMethodLocked()); p.println(" mEnabledSession=" + mEnabledSession); - p.println(" mShowRequested=" + mShowRequested - + " mShowExplicitlyRequested=" + mShowExplicitlyRequested - + " mShowForced=" + mShowForced - + " mInputShown=" + mInputShown); + p.println(" mShowRequested=" + mShowRequested + " mInputShown=" + mInputShown); + mVisibilityStateComputer.dump(pw); p.println(" mInFullscreenMode=" + mInFullscreenMode); p.println(" mSystemReady=" + mSystemReady + " mInteractive=" + mIsInteractive); p.println(" mSettingsObserver=" + mSettingsObserver); - p.println(" mImeHiddenByDisplayPolicy=" + mImeHiddenByDisplayPolicy); p.println(" mStylusIds=" + (mStylusIds != null ? Arrays.toString(mStylusIds.toArray()) : "")); p.println(" mSwitchingController:"); diff --git a/services/core/java/com/android/server/media/BluetoothRouteProvider.java b/services/core/java/com/android/server/media/BluetoothRouteProvider.java index e1a990d0f6bd..c90554d9cdd8 100644 --- a/services/core/java/com/android/server/media/BluetoothRouteProvider.java +++ b/services/core/java/com/android/server/media/BluetoothRouteProvider.java @@ -59,17 +59,9 @@ class BluetoothRouteProvider { private static final String HEARING_AID_ROUTE_ID_PREFIX = "HEARING_AID_"; private static final String LE_AUDIO_ROUTE_ID_PREFIX = "LE_AUDIO_"; - @SuppressWarnings("WeakerAccess") /* synthetic access */ // Maps hardware address to BluetoothRouteInfo - final Map<String, BluetoothRouteInfo> mBluetoothRoutes = new HashMap<>(); - @SuppressWarnings("WeakerAccess") /* synthetic access */ - final List<BluetoothRouteInfo> mActiveRoutes = new ArrayList<>(); - @SuppressWarnings("WeakerAccess") /* synthetic access */ - BluetoothA2dp mA2dpProfile; - @SuppressWarnings("WeakerAccess") /* synthetic access */ - BluetoothHearingAid mHearingAidProfile; - @SuppressWarnings("WeakerAccess") /* synthetic access */ - BluetoothLeAudio mLeAudioProfile; + private final Map<String, BluetoothRouteInfo> mBluetoothRoutes = new HashMap<>(); + private final List<BluetoothRouteInfo> mActiveRoutes = new ArrayList<>(); // Route type -> volume map private final SparseIntArray mVolumeMap = new SparseIntArray(); @@ -83,6 +75,10 @@ class BluetoothRouteProvider { private final BroadcastReceiver mBroadcastReceiver = new BluetoothBroadcastReceiver(); private final BluetoothProfileListener mProfileListener = new BluetoothProfileListener(); + private BluetoothA2dp mA2dpProfile; + private BluetoothHearingAid mHearingAidProfile; + private BluetoothLeAudio mLeAudioProfile; + /** * Create an instance of {@link BluetoothRouteProvider}. * It may return {@code null} if Bluetooth is not supported on this hardware platform. @@ -109,7 +105,7 @@ class BluetoothRouteProvider { buildBluetoothRoutes(); } - public void start(UserHandle user) { + void start(UserHandle user) { mBluetoothAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.A2DP); mBluetoothAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.HEARING_AID); mBluetoothAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.LE_AUDIO); @@ -133,7 +129,7 @@ class BluetoothRouteProvider { mIntentFilter, null, null); } - public void stop() { + void stop() { mContext.unregisterReceiver(mBroadcastReceiver); } @@ -144,7 +140,7 @@ class BluetoothRouteProvider { * @param routeId the id of the Bluetooth device. {@code null} denotes to clear the use of * BT routes. */ - public void transferTo(@Nullable String routeId) { + void transferTo(@Nullable String routeId) { if (routeId == null) { clearActiveDevices(); return; @@ -158,7 +154,7 @@ class BluetoothRouteProvider { } if (mBluetoothAdapter != null) { - mBluetoothAdapter.setActiveDevice(btRouteInfo.btDevice, ACTIVE_DEVICE_AUDIO); + mBluetoothAdapter.setActiveDevice(btRouteInfo.mBtDevice, ACTIVE_DEVICE_AUDIO); } } @@ -183,7 +179,7 @@ class BluetoothRouteProvider { for (BluetoothDevice device : bondedDevices) { if (device.isConnected()) { BluetoothRouteInfo newBtRoute = createBluetoothRoute(device); - if (newBtRoute.connectedProfiles.size() > 0) { + if (newBtRoute.mConnectedProfiles.size() > 0) { mBluetoothRoutes.put(device.getAddress(), newBtRoute); } } @@ -195,14 +191,14 @@ class BluetoothRouteProvider { MediaRoute2Info getSelectedRoute() { // For now, active routes can be multiple only when a pair of hearing aid devices is active. // Let the first active device represent them. - return (mActiveRoutes.isEmpty() ? null : mActiveRoutes.get(0).route); + return (mActiveRoutes.isEmpty() ? null : mActiveRoutes.get(0).mRoute); } @NonNull List<MediaRoute2Info> getTransferableRoutes() { List<MediaRoute2Info> routes = getAllBluetoothRoutes(); for (BluetoothRouteInfo btRoute : mActiveRoutes) { - routes.remove(btRoute.route); + routes.remove(btRoute.mRoute); } return routes; } @@ -220,11 +216,11 @@ class BluetoothRouteProvider { for (BluetoothRouteInfo btRoute : mBluetoothRoutes.values()) { // A pair of hearing aid devices or having the same hardware address - if (routeIds.contains(btRoute.route.getId())) { + if (routeIds.contains(btRoute.mRoute.getId())) { continue; } - routes.add(btRoute.route); - routeIds.add(btRoute.route.getId()); + routes.add(btRoute.mRoute); + routeIds.add(btRoute.mRoute.getId()); } return routes; } @@ -234,7 +230,7 @@ class BluetoothRouteProvider { return null; } for (BluetoothRouteInfo btRouteInfo : mBluetoothRoutes.values()) { - if (TextUtils.equals(btRouteInfo.route.getId(), routeId)) { + if (TextUtils.equals(btRouteInfo.mRoute.getId(), routeId)) { return btRouteInfo; } } @@ -246,7 +242,7 @@ class BluetoothRouteProvider { * * @return true if devices can be handled by the provider. */ - public boolean updateVolumeForDevices(int devices, int volume) { + boolean updateVolumeForDevices(int devices, int volume) { int routeType; if ((devices & (AudioSystem.DEVICE_OUT_HEARING_AID)) != 0) { routeType = MediaRoute2Info.TYPE_HEARING_AID; @@ -263,10 +259,10 @@ class BluetoothRouteProvider { boolean shouldNotify = false; for (BluetoothRouteInfo btRoute : mActiveRoutes) { - if (btRoute.route.getType() != routeType) { + if (btRoute.mRoute.getType() != routeType) { continue; } - btRoute.route = new MediaRoute2Info.Builder(btRoute.route) + btRoute.mRoute = new MediaRoute2Info.Builder(btRoute.mRoute) .setVolume(volume) .build(); shouldNotify = true; @@ -285,7 +281,7 @@ class BluetoothRouteProvider { private BluetoothRouteInfo createBluetoothRoute(BluetoothDevice device) { BluetoothRouteInfo newBtRoute = new BluetoothRouteInfo(); - newBtRoute.btDevice = device; + newBtRoute.mBtDevice = device; String routeId = device.getAddress(); String deviceName = device.getName(); @@ -293,26 +289,26 @@ class BluetoothRouteProvider { deviceName = mContext.getResources().getText(R.string.unknownName).toString(); } int type = MediaRoute2Info.TYPE_BLUETOOTH_A2DP; - newBtRoute.connectedProfiles = new SparseBooleanArray(); + newBtRoute.mConnectedProfiles = new SparseBooleanArray(); if (mA2dpProfile != null && mA2dpProfile.getConnectedDevices().contains(device)) { - newBtRoute.connectedProfiles.put(BluetoothProfile.A2DP, true); + newBtRoute.mConnectedProfiles.put(BluetoothProfile.A2DP, true); } if (mHearingAidProfile != null && mHearingAidProfile.getConnectedDevices().contains(device)) { - newBtRoute.connectedProfiles.put(BluetoothProfile.HEARING_AID, true); + newBtRoute.mConnectedProfiles.put(BluetoothProfile.HEARING_AID, true); // Intentionally assign the same ID for a pair of devices to publish only one of them. routeId = HEARING_AID_ROUTE_ID_PREFIX + mHearingAidProfile.getHiSyncId(device); type = MediaRoute2Info.TYPE_HEARING_AID; } if (mLeAudioProfile != null && mLeAudioProfile.getConnectedDevices().contains(device)) { - newBtRoute.connectedProfiles.put(BluetoothProfile.LE_AUDIO, true); + newBtRoute.mConnectedProfiles.put(BluetoothProfile.LE_AUDIO, true); routeId = LE_AUDIO_ROUTE_ID_PREFIX + mLeAudioProfile.getGroupId(device); type = MediaRoute2Info.TYPE_BLE_HEADSET; } // Current volume will be set when connected. - newBtRoute.route = new MediaRoute2Info.Builder(routeId, deviceName) + newBtRoute.mRoute = new MediaRoute2Info.Builder(routeId, deviceName) .addFeature(MediaRoute2Info.FEATURE_LIVE_AUDIO) .addFeature(MediaRoute2Info.FEATURE_LOCAL_PLAYBACK) .setConnectionState(MediaRoute2Info.CONNECTION_STATE_DISCONNECTED) @@ -332,18 +328,18 @@ class BluetoothRouteProvider { Slog.w(TAG, "setRouteConnectionState: route shouldn't be null"); return; } - if (btRoute.route.getConnectionState() == state) { + if (btRoute.mRoute.getConnectionState() == state) { return; } - MediaRoute2Info.Builder builder = new MediaRoute2Info.Builder(btRoute.route) + MediaRoute2Info.Builder builder = new MediaRoute2Info.Builder(btRoute.mRoute) .setConnectionState(state); builder.setType(btRoute.getRouteType()); if (state == MediaRoute2Info.CONNECTION_STATE_CONNECTED) { builder.setVolume(mVolumeMap.get(btRoute.getRouteType(), 0)); } - btRoute.route = builder.build(); + btRoute.mRoute = builder.build(); } private void addActiveRoute(BluetoothRouteInfo btRoute) { @@ -352,7 +348,7 @@ class BluetoothRouteProvider { return; } if (DEBUG) { - Log.d(TAG, "Adding active route: " + btRoute.route); + Log.d(TAG, "Adding active route: " + btRoute.mRoute); } if (mActiveRoutes.contains(btRoute)) { Slog.w(TAG, "addActiveRoute: btRoute is already added."); @@ -364,7 +360,7 @@ class BluetoothRouteProvider { private void removeActiveRoute(BluetoothRouteInfo btRoute) { if (DEBUG) { - Log.d(TAG, "Removing active route: " + btRoute.route); + Log.d(TAG, "Removing active route: " + btRoute.mRoute); } if (mActiveRoutes.remove(btRoute)) { setRouteConnectionState(btRoute, STATE_DISCONNECTED); @@ -378,7 +374,7 @@ class BluetoothRouteProvider { Iterator<BluetoothRouteInfo> iter = mActiveRoutes.iterator(); while (iter.hasNext()) { BluetoothRouteInfo btRoute = iter.next(); - if (btRoute.route.getType() == type) { + if (btRoute.mRoute.getType() == type) { iter.remove(); setRouteConnectionState(btRoute, STATE_DISCONNECTED); } @@ -398,9 +394,9 @@ class BluetoothRouteProvider { // A bluetooth route with the same route ID should be added. for (BluetoothRouteInfo btRoute : mBluetoothRoutes.values()) { - if (TextUtils.equals(btRoute.route.getId(), activeBtRoute.route.getId()) - && !TextUtils.equals(btRoute.btDevice.getAddress(), - activeBtRoute.btDevice.getAddress())) { + if (TextUtils.equals(btRoute.mRoute.getId(), activeBtRoute.mRoute.getId()) + && !TextUtils.equals(btRoute.mBtDevice.getAddress(), + activeBtRoute.mBtDevice.getAddress())) { addActiveRoute(btRoute); } } @@ -425,19 +421,19 @@ class BluetoothRouteProvider { void onBluetoothRoutesUpdated(@NonNull List<MediaRoute2Info> routes); } - private class BluetoothRouteInfo { - public BluetoothDevice btDevice; - public MediaRoute2Info route; - public SparseBooleanArray connectedProfiles; + private static class BluetoothRouteInfo { + private BluetoothDevice mBtDevice; + private MediaRoute2Info mRoute; + private SparseBooleanArray mConnectedProfiles; @MediaRoute2Info.Type int getRouteType() { // Let hearing aid profile have a priority. - if (connectedProfiles.get(BluetoothProfile.HEARING_AID, false)) { + if (mConnectedProfiles.get(BluetoothProfile.HEARING_AID, false)) { return MediaRoute2Info.TYPE_HEARING_AID; } - if (connectedProfiles.get(BluetoothProfile.LE_AUDIO, false)) { + if (mConnectedProfiles.get(BluetoothProfile.LE_AUDIO, false)) { return MediaRoute2Info.TYPE_BLE_HEADSET; } @@ -447,6 +443,7 @@ class BluetoothRouteProvider { // These callbacks run on the main thread. private final class BluetoothProfileListener implements BluetoothProfile.ServiceListener { + @Override public void onServiceConnected(int profile, BluetoothProfile proxy) { List<BluetoothDevice> activeDevices; switch (profile) { @@ -480,6 +477,7 @@ class BluetoothRouteProvider { notifyBluetoothRoutesUpdated(); } + @Override public void onServiceDisconnected(int profile) { switch (profile) { case BluetoothProfile.A2DP: @@ -496,6 +494,7 @@ class BluetoothRouteProvider { } } } + private class BluetoothBroadcastReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { @@ -514,6 +513,7 @@ class BluetoothRouteProvider { } private class AdapterStateChangedReceiver implements BluetoothEventReceiver { + @Override public void onReceive(Context context, Intent intent, BluetoothDevice device) { int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1); if (state == BluetoothAdapter.STATE_OFF @@ -573,18 +573,18 @@ class BluetoothRouteProvider { if (state == BluetoothProfile.STATE_CONNECTED) { if (btRoute == null) { btRoute = createBluetoothRoute(device); - if (btRoute.connectedProfiles.size() > 0) { + if (btRoute.mConnectedProfiles.size() > 0) { mBluetoothRoutes.put(device.getAddress(), btRoute); notifyBluetoothRoutesUpdated(); } } else { - btRoute.connectedProfiles.put(profile, true); + btRoute.mConnectedProfiles.put(profile, true); } } else if (state == BluetoothProfile.STATE_DISCONNECTING || state == BluetoothProfile.STATE_DISCONNECTED) { if (btRoute != null) { - btRoute.connectedProfiles.delete(profile); - if (btRoute.connectedProfiles.size() == 0) { + btRoute.mConnectedProfiles.delete(profile); + if (btRoute.mConnectedProfiles.size() == 0) { removeActiveRoute(mBluetoothRoutes.remove(device.getAddress())); notifyBluetoothRoutesUpdated(); } diff --git a/services/core/java/com/android/server/NetworkManagementInternal.java b/services/core/java/com/android/server/net/NetworkManagementInternal.java index f53c454cb917..492696078e55 100644 --- a/services/core/java/com/android/server/NetworkManagementInternal.java +++ b/services/core/java/com/android/server/net/NetworkManagementInternal.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.server; +package com.android.server.net; /** * NetworkManagement local system service interface. diff --git a/services/core/java/com/android/server/NetworkManagementService.java b/services/core/java/com/android/server/net/NetworkManagementService.java index fc26f0989f45..acfa66595afa 100644 --- a/services/core/java/com/android/server/NetworkManagementService.java +++ b/services/core/java/com/android/server/net/NetworkManagementService.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.server; +package com.android.server.net; import static android.Manifest.permission.CONNECTIVITY_INTERNAL; import static android.net.ConnectivityManager.FIREWALL_CHAIN_DOZABLE; @@ -82,6 +82,8 @@ import com.android.internal.util.HexDump; import com.android.internal.util.Preconditions; import com.android.net.module.util.NetdUtils; import com.android.net.module.util.NetdUtils.ModifyOperation; +import com.android.server.FgThread; +import com.android.server.LocalServices; import com.google.android.collect.Maps; diff --git a/services/core/java/com/android/server/net/TEST_MAPPING b/services/core/java/com/android/server/net/TEST_MAPPING index 4ccf09e5e020..e0376ed6461b 100644 --- a/services/core/java/com/android/server/net/TEST_MAPPING +++ b/services/core/java/com/android/server/net/TEST_MAPPING @@ -15,7 +15,7 @@ "presubmit": [ { "name": "FrameworksServicesTests", - "file_patterns": ["(/|^)NetworkPolicy[^/]*\\.java"], + "file_patterns": ["(/|^)Network(Policy|Management)[^/]*\\.java"], "options": [ { "include-filter": "com.android.server.net." diff --git a/services/core/java/com/android/server/pm/AppDataHelper.java b/services/core/java/com/android/server/pm/AppDataHelper.java index abaff4afeeea..d252d409732a 100644 --- a/services/core/java/com/android/server/pm/AppDataHelper.java +++ b/services/core/java/com/android/server/pm/AppDataHelper.java @@ -616,6 +616,7 @@ public class AppDataHelper { Slog.w(TAG, String.valueOf(e)); } mPm.getDexManager().notifyPackageDataDestroyed(pkg.getPackageName(), userId); + mPm.getDynamicCodeLogger().notifyPackageDataDestroyed(pkg.getPackageName(), userId); } } diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java index 0eac9ef5e1a2..c6700e62afa0 100644 --- a/services/core/java/com/android/server/pm/ComputerEngine.java +++ b/services/core/java/com/android/server/pm/ComputerEngine.java @@ -4981,6 +4981,7 @@ public class ComputerEngine implements Computer { String installerPackageName; String initiatingPackageName; String originatingPackageName; + String updateOwnerPackageName; final InstallSource installSource = getInstallSource(packageName, callingUid, userId); if (installSource == null) { @@ -4996,6 +4997,15 @@ public class ComputerEngine implements Computer { } } + updateOwnerPackageName = installSource.mUpdateOwnerPackageName; + if (updateOwnerPackageName != null) { + final PackageStateInternal ps = mSettings.getPackage(updateOwnerPackageName); + if (ps == null + || shouldFilterApplicationIncludingUninstalled(ps, callingUid, userId)) { + updateOwnerPackageName = null; + } + } + if (installSource.mIsInitiatingPackageUninstalled) { // We can't check visibility in the usual way, since the initiating package is no // longer present. So we apply simpler rules to whether to expose the info: @@ -5052,7 +5062,8 @@ public class ComputerEngine implements Computer { } return new InstallSourceInfo(initiatingPackageName, initiatingPackageSigningInfo, - originatingPackageName, installerPackageName, installSource.mPackageSource); + originatingPackageName, installerPackageName, updateOwnerPackageName, + installSource.mPackageSource); } @PackageManager.EnabledState diff --git a/services/core/java/com/android/server/pm/DexOptHelper.java b/services/core/java/com/android/server/pm/DexOptHelper.java index 8757310cce74..cf447a75ea86 100644 --- a/services/core/java/com/android/server/pm/DexOptHelper.java +++ b/services/core/java/com/android/server/pm/DexOptHelper.java @@ -41,6 +41,7 @@ import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.app.ActivityManager; import android.app.AppGlobals; +import android.content.Context; import android.content.Intent; import android.content.pm.ResolveInfo; import android.content.pm.SharedLibraryInfo; @@ -84,8 +85,10 @@ import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import java.util.function.Predicate; @@ -416,17 +419,22 @@ public final class DexOptHelper { return true; } + @DexOptResult int dexoptStatus; if (options.isDexoptOnlySecondaryDex()) { - // TODO(b/251903639): Call into ART Service. - try { - return mPm.getDexManager().dexoptSecondaryDex(options); - } catch (LegacyDexoptDisabledException e) { - throw new RuntimeException(e); + Optional<Integer> artSrvRes = performDexOptWithArtService(options, 0 /* extraFlags */); + if (artSrvRes.isPresent()) { + dexoptStatus = artSrvRes.get(); + } else { + try { + return mPm.getDexManager().dexoptSecondaryDex(options); + } catch (LegacyDexoptDisabledException e) { + throw new RuntimeException(e); + } } } else { - int dexoptStatus = performDexOptWithStatus(options); - return dexoptStatus != PackageDexOptimizer.DEX_OPT_FAILED; + dexoptStatus = performDexOptWithStatus(options); } + return dexoptStatus != PackageDexOptimizer.DEX_OPT_FAILED; } /** @@ -455,7 +463,8 @@ public final class DexOptHelper { // if the package can now be considered up to date for the given filter. @DexOptResult private int performDexOptInternal(DexoptOptions options) { - Optional<Integer> artSrvRes = performDexOptWithArtService(options); + Optional<Integer> artSrvRes = + performDexOptWithArtService(options, ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES); if (artSrvRes.isPresent()) { return artSrvRes.get(); } @@ -492,7 +501,8 @@ public final class DexOptHelper { * @return a {@link DexOptResult}, or empty if the request isn't supported so that it is * necessary to fall back to the legacy code paths. */ - private Optional<Integer> performDexOptWithArtService(DexoptOptions options) { + private Optional<Integer> performDexOptWithArtService(DexoptOptions options, + /*@OptimizeFlags*/ int extraFlags) { ArtManagerLocal artManager = getArtManagerLocal(); if (artManager == null) { return Optional.empty(); @@ -512,18 +522,11 @@ public final class DexOptHelper { return Optional.of(PackageDexOptimizer.DEX_OPT_SKIPPED); } - // TODO(b/245301593): Delete the conditional when ART Service supports - // FLAG_SHOULD_INCLUDE_DEPENDENCIES and we can just set it unconditionally. - /*@OptimizeFlags*/ int extraFlags = ops.getUsesLibraries().isEmpty() - ? 0 - : ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES; - OptimizeParams params = options.convertToOptimizeParams(extraFlags); if (params == null) { return Optional.empty(); } - // TODO(b/251903639): Either remove controlDexOptBlocking, or don't ignore it here. OptimizeResult result; try { result = artManager.optimizePackage(snapshot, options.getPackageName(), params); @@ -532,21 +535,6 @@ public final class DexOptHelper { return Optional.empty(); } - // TODO(b/251903639): Move this to ArtManagerLocal.addOptimizePackageDoneCallback when - // it is implemented. - for (OptimizeResult.PackageOptimizeResult pkgRes : result.getPackageOptimizeResults()) { - PackageState ps = snapshot.getPackageState(pkgRes.getPackageName()); - AndroidPackage ap = ps != null ? ps.getAndroidPackage() : null; - if (ap != null) { - CompilerStats.PackageStats stats = mPm.getOrCreateCompilerPackageStats(ap); - for (OptimizeResult.DexContainerFileOptimizeResult dexRes : - pkgRes.getDexContainerFileOptimizeResults()) { - stats.setCompileTime( - dexRes.getDexContainerFile(), dexRes.getDex2oatWallTimeMillis()); - } - } - } - return Optional.of(convertToDexOptResult(result)); } } @@ -628,7 +616,8 @@ public final class DexOptHelper { // performDexOptWithArtService ignores the snapshot and takes its own, so it can race with // the package checks above, but at worst the effect is only a bit less friendly error // below. - Optional<Integer> artSrvRes = performDexOptWithArtService(options); + Optional<Integer> artSrvRes = + performDexOptWithArtService(options, ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES); int res; if (artSrvRes.isPresent()) { res = artSrvRes.get(); @@ -965,6 +954,51 @@ public final class DexOptHelper { } } + private static class OptimizePackageDoneHandler + implements ArtManagerLocal.OptimizePackageDoneCallback { + @NonNull private final PackageManagerService mPm; + + OptimizePackageDoneHandler(@NonNull PackageManagerService pm) { mPm = pm; } + + /** + * Called after every package optimization operation done by {@link ArtManagerLocal}. + */ + @Override + public void onOptimizePackageDone(@NonNull OptimizeResult result) { + for (OptimizeResult.PackageOptimizeResult pkgRes : result.getPackageOptimizeResults()) { + CompilerStats.PackageStats stats = + mPm.getOrCreateCompilerPackageStats(pkgRes.getPackageName()); + for (OptimizeResult.DexContainerFileOptimizeResult dexRes : + pkgRes.getDexContainerFileOptimizeResults()) { + stats.setCompileTime( + dexRes.getDexContainerFile(), dexRes.getDex2oatWallTimeMillis()); + } + } + + synchronized (mPm.mLock) { + mPm.getPackageUsage().maybeWriteAsync(mPm.mSettings.getPackagesLocked()); + mPm.mCompilerStats.maybeWriteAsync(); + } + } + } + + /** + * Initializes {@link ArtManagerLocal} before {@link getArtManagerLocal} is called. + */ + public static void initializeArtManagerLocal( + @NonNull Context systemContext, @NonNull PackageManagerService pm) { + if (!useArtService()) { + return; + } + + ArtManagerLocal artManager = new ArtManagerLocal(systemContext); + // There doesn't appear to be any checks that @NonNull is heeded, so use requireNonNull + // below to ensure we don't store away a null that we'll fail on later. + artManager.addOptimizePackageDoneCallback(false /* onlyIncludeUpdates */, + Runnable::run, new OptimizePackageDoneHandler(Objects.requireNonNull(pm))); + LocalManagerRegistry.addManager(ArtManagerLocal.class, artManager); + } + /** * Returns {@link ArtManagerLocal} if ART Service should be used for package optimization. */ diff --git a/services/core/java/com/android/server/pm/DynamicCodeLoggingService.java b/services/core/java/com/android/server/pm/DynamicCodeLoggingService.java index b4bcd5b3308c..cae7079c75e0 100644 --- a/services/core/java/com/android/server/pm/DynamicCodeLoggingService.java +++ b/services/core/java/com/android/server/pm/DynamicCodeLoggingService.java @@ -27,13 +27,17 @@ import android.os.Process; import android.util.EventLog; import android.util.Log; +import com.android.server.LocalManagerRegistry; import com.android.server.LocalServices; +import com.android.server.art.DexUseManagerLocal; +import com.android.server.art.model.DexContainerFileUseInfo; import com.android.server.pm.dex.DynamicCodeLogger; import libcore.util.HexEncoding; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -137,6 +141,28 @@ public class DynamicCodeLoggingService extends JobService { return LocalServices.getService(PackageManagerInternal.class).getDynamicCodeLogger(); } + private static void syncDataFromArtService(DynamicCodeLogger dynamicCodeLogger) { + DexUseManagerLocal dexUseManagerLocal = DexOptHelper.getDexUseManagerLocal(); + if (dexUseManagerLocal == null) { + // ART Service is not enabled. + return; + } + PackageManagerLocal packageManagerLocal = + Objects.requireNonNull(LocalManagerRegistry.getManager(PackageManagerLocal.class)); + try (PackageManagerLocal.UnfilteredSnapshot snapshot = + packageManagerLocal.withUnfilteredSnapshot()) { + for (String owningPackageName : snapshot.getPackageStates().keySet()) { + for (DexContainerFileUseInfo info : + dexUseManagerLocal.getSecondaryDexContainerFileUseInfo(owningPackageName)) { + for (String loadingPackageName : info.getLoadingPackages()) { + dynamicCodeLogger.recordDex(info.getUserHandle().getIdentifier(), + info.getDexContainerFile(), owningPackageName, loadingPackageName); + } + } + } + } + } + private class IdleLoggingThread extends Thread { private final JobParameters mParams; @@ -152,6 +178,7 @@ public class DynamicCodeLoggingService extends JobService { } DynamicCodeLogger dynamicCodeLogger = getDynamicCodeLogger(); + syncDataFromArtService(dynamicCodeLogger); for (String packageName : dynamicCodeLogger.getAllPackagesWithDynamicCodeLoading()) { if (mIdleLoggingStopRequested) { Log.w(TAG, "Stopping IdleLoggingJob run at scheduler request"); diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index 27d312ec4d4d..ac4da2ed7d73 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -311,10 +311,15 @@ final class InstallPackageHelper { pkgSetting.setForceQueryableOverride(true); } - // If this is part of a standard install, set the initiating package name, else rely on - // previous device state. InstallSource installSource = request.getInstallSource(); + final boolean isApex = (scanFlags & SCAN_AS_APEX) != 0; + final String updateOwnerFromSysconfig = isApex || !pkgSetting.isSystem() ? null + : mPm.mInjector.getSystemConfig().getSystemAppUpdateOwnerPackageName( + parsedPackage.getPackageName()); + // For new install (standard install), the installSource isn't null. if (installSource != null) { + // If this is part of a standard install, set the initiating package name, else rely on + // previous device state. if (installSource.mInitiatingPackageName != null) { final PackageSetting ips = mPm.mSettings.getPackageLPr( installSource.mInitiatingPackageName); @@ -323,7 +328,41 @@ final class InstallPackageHelper { ips.getSignatures()); } } + + // Handle the update ownership enforcement for APK + if (updateOwnerFromSysconfig != null) { + // For system app, we always use the update owner from sysconfig if presented. + installSource = installSource.setUpdateOwnerPackageName(updateOwnerFromSysconfig); + } else if (!parsedPackage.isAllowUpdateOwnership()) { + // If the app wants to opt-out of the update ownership enforcement via manifest, + // it overrides the installer's use of #setRequestUpdateOwnership. + installSource = installSource.setUpdateOwnerPackageName(null); + } else if (!isApex) { + final boolean isUpdate = oldPkgSetting != null; + final String oldUpdateOwner = + isUpdate ? oldPkgSetting.getInstallSource().mUpdateOwnerPackageName : null; + final boolean isUpdateOwnershipEnabled = oldUpdateOwner != null; + final boolean isRequestUpdateOwnership = (request.getInstallFlags() + & PackageManager.INSTALL_REQUEST_UPDATE_OWNERSHIP) != 0; + + // Here we assign the update owner for the package, and the rules are: + // -. If the installer doesn't request update ownership on initial installation, + // keep the update owner as null. + // -. If the installer doesn't want to be the owner to provide the subsequent + // update (doesn't request to be the update owner), e.g., non-store installer + // (file manager), ADB, or DO/PO, we should not update the owner. + // -. Else, the installer requests update ownership on initial installation or + // update, we use installSource.mUpdateOwnerPackageName as the update owner. + if (!isRequestUpdateOwnership || (isUpdate && !isUpdateOwnershipEnabled)) { + installSource = installSource.setUpdateOwnerPackageName(oldUpdateOwner); + } + } + pkgSetting.setInstallSource(installSource); + // non-standard install (addForInit and install existing packages), installSource is null. + } else if (updateOwnerFromSysconfig != null) { + // For system app, we always use the update owner from sysconfig if presented. + pkgSetting.setUpdateOwnerPackage(updateOwnerFromSysconfig); } if ((scanFlags & SCAN_AS_APK_IN_APEX) != 0) { @@ -1039,15 +1078,48 @@ final class InstallPackageHelper { DeviceConfig.getInt(DeviceConfig.NAMESPACE_PACKAGE_MANAGER_SERVICE, "MinInstallableTargetSdk__min_installable_target_sdk", 0); - if (parsedPackage.getTargetSdkVersion() < minInstallableTargetSdk) { + + // Skip enforcement when the bypass flag is set + boolean bypassLowTargetSdkBlock = + ((installFlags & PackageManager.INSTALL_BYPASS_LOW_TARGET_SDK_BLOCK) != 0); + + // Skip enforcement for tests that were installed from adb + if (!bypassLowTargetSdkBlock + && ((installFlags & PackageManager.INSTALL_FROM_ADB) != 0)) { + bypassLowTargetSdkBlock = true; + } + + // Skip enforcement if the installer package name is not set + // (e.g. "pm install" from shell) + if (!bypassLowTargetSdkBlock) { + if (request.getInstallerPackageName() == null) { + bypassLowTargetSdkBlock = true; + } else { + // Also skip if the install is occurring from an app that was installed from adb + if (mContext + .getPackageManager() + .getInstallerPackageName(request.getInstallerPackageName()) == null) { + bypassLowTargetSdkBlock = true; + } + } + } + + // Skip enforcement when the testOnly flag is set + if (!bypassLowTargetSdkBlock && parsedPackage.isTestOnly()) { + bypassLowTargetSdkBlock = true; + } + + // Enforce the low target sdk install block except when + // the --bypass-low-target-sdk-block is set for the install + if (!bypassLowTargetSdkBlock + && parsedPackage.getTargetSdkVersion() < minInstallableTargetSdk) { Slog.w(TAG, "App " + parsedPackage.getPackageName() + " targets deprecated sdk version"); throw new PrepareFailure(INSTALL_FAILED_DEPRECATED_SDK_VERSION, - "App package must target at least version " - + minInstallableTargetSdk); + "App package must target at least SDK version " + + minInstallableTargetSdk + ", but found " + + parsedPackage.getTargetSdkVersion()); } - } else { - Slog.i(TAG, "Minimum installable target sdk enforcement not enabled"); } // Instant apps have several additional install-time checks. diff --git a/services/core/java/com/android/server/pm/InstallSource.java b/services/core/java/com/android/server/pm/InstallSource.java index dde9905ccc86..65bde518d0a1 100644 --- a/services/core/java/com/android/server/pm/InstallSource.java +++ b/services/core/java/com/android/server/pm/InstallSource.java @@ -34,12 +34,18 @@ public final class InstallSource { * An instance of InstallSource representing an absence of knowledge of the source of * a package. Used in preference to null. */ - static final InstallSource EMPTY = new InstallSource(null, null, null, INVALID_UID, null, - false, false, null, PackageInstaller.PACKAGE_SOURCE_UNSPECIFIED); + static final InstallSource EMPTY = new InstallSource(null /* initiatingPackageName */, + null /* originatingPackageName */, null /* installerPackageName */, INVALID_UID, + null /* updateOwnerPackageName */, null /* installerAttributionTag */, + false /* isOrphaned */, false /* isInitiatingPackageUninstalled */, + null /* initiatingPackageSignatures */, PackageInstaller.PACKAGE_SOURCE_UNSPECIFIED); /** We also memoize this case because it is common - all un-updated system apps. */ private static final InstallSource EMPTY_ORPHANED = new InstallSource( - null, null, null, INVALID_UID, null, true, false, null, + null /* initiatingPackageName */, null /* originatingPackageName */, + null /* installerPackageName */, INVALID_UID, null /* updateOwnerPackageName */, + null /* installerAttributionTag */, true /* isOrphaned */, + false /* isInitiatingPackageUninstalled */, null /* initiatingPackageSignatures */, PackageInstaller.PACKAGE_SOURCE_UNSPECIFIED); /** @@ -73,6 +79,13 @@ public final class InstallSource { final String mInstallerPackageName; /** + * Package name of the app that requested the installer ownership. Note that this may be + * modified. + */ + @Nullable + final String mUpdateOwnerPackageName; + + /** * UID of the installer package, corresponding to the {@link #mInstallerPackageName}. */ final int mInstallerPackageUid; @@ -96,55 +109,64 @@ public final class InstallSource { static InstallSource create(@Nullable String initiatingPackageName, @Nullable String originatingPackageName, @Nullable String installerPackageName, - int installerPackageUid, @Nullable String installerAttributionTag, boolean isOrphaned, + int installerPackageUid, @Nullable String updateOwnerPackageName, + @Nullable String installerAttributionTag, boolean isOrphaned, boolean isInitiatingPackageUninstalled) { return create(initiatingPackageName, originatingPackageName, installerPackageName, - installerPackageUid, installerAttributionTag, + installerPackageUid, updateOwnerPackageName, installerAttributionTag, PackageInstaller.PACKAGE_SOURCE_UNSPECIFIED, isOrphaned, isInitiatingPackageUninstalled); } static InstallSource create(@Nullable String initiatingPackageName, @Nullable String originatingPackageName, @Nullable String installerPackageName, - int installerPackageUid, @Nullable String installerAttributionTag, int packageSource) { + int installerPackageUid, @Nullable String updateOwnerPackageName, + @Nullable String installerAttributionTag, int packageSource) { return create(initiatingPackageName, originatingPackageName, installerPackageName, - installerPackageUid, installerAttributionTag, packageSource, false, false); + installerPackageUid, updateOwnerPackageName, installerAttributionTag, + packageSource, false /* isOrphaned */, false /* isInitiatingPackageUninstalled */); } static InstallSource create(@Nullable String initiatingPackageName, @Nullable String originatingPackageName, @Nullable String installerPackageName, - int installerPackageUid, @Nullable String installerAttributionTag, int packageSource, - boolean isOrphaned, boolean isInitiatingPackageUninstalled) { + int installerPackageUid, @Nullable String updateOwnerPackageName, + @Nullable String installerAttributionTag, int packageSource, boolean isOrphaned, + boolean isInitiatingPackageUninstalled) { return createInternal( intern(initiatingPackageName), intern(originatingPackageName), intern(installerPackageName), installerPackageUid, + intern(updateOwnerPackageName), installerAttributionTag, packageSource, - isOrphaned, isInitiatingPackageUninstalled, null); + isOrphaned, isInitiatingPackageUninstalled, + null /* initiatingPackageSignatures */); } private static InstallSource createInternal(@Nullable String initiatingPackageName, @Nullable String originatingPackageName, @Nullable String installerPackageName, - int installerPackageUid, @Nullable String installerAttributionTag, int packageSource, - boolean isOrphaned, boolean isInitiatingPackageUninstalled, + int installerPackageUid, @Nullable String updateOwnerPackageName, + @Nullable String installerAttributionTag, int packageSource, boolean isOrphaned, + boolean isInitiatingPackageUninstalled, @Nullable PackageSignatures initiatingPackageSignatures) { if (initiatingPackageName == null && originatingPackageName == null - && installerPackageName == null && initiatingPackageSignatures == null + && installerPackageName == null && updateOwnerPackageName == null + && initiatingPackageSignatures == null && !isInitiatingPackageUninstalled && packageSource == PackageInstaller.PACKAGE_SOURCE_UNSPECIFIED) { return isOrphaned ? EMPTY_ORPHANED : EMPTY; } return new InstallSource(initiatingPackageName, originatingPackageName, - installerPackageName, installerPackageUid, installerAttributionTag, isOrphaned, - isInitiatingPackageUninstalled, initiatingPackageSignatures, packageSource + installerPackageName, installerPackageUid, updateOwnerPackageName, + installerAttributionTag, isOrphaned, isInitiatingPackageUninstalled, + initiatingPackageSignatures, packageSource ); } private InstallSource(@Nullable String initiatingPackageName, @Nullable String originatingPackageName, @Nullable String installerPackageName, - int installerPackageUid, + int installerPackageUid, @Nullable String updateOwnerPackageName, @Nullable String installerAttributionTag, boolean isOrphaned, boolean isInitiatingPackageUninstalled, @Nullable PackageSignatures initiatingPackageSignatures, @@ -157,6 +179,7 @@ public final class InstallSource { mOriginatingPackageName = originatingPackageName; mInstallerPackageName = installerPackageName; mInstallerPackageUid = installerPackageUid; + mUpdateOwnerPackageName = updateOwnerPackageName; mInstallerAttributionTag = installerAttributionTag; mIsOrphaned = isOrphaned; mIsInitiatingPackageUninstalled = isInitiatingPackageUninstalled; @@ -174,9 +197,23 @@ public final class InstallSource { return this; } return createInternal(mInitiatingPackageName, mOriginatingPackageName, - intern(installerPackageName), installerPackageUid, mInstallerAttributionTag, - mPackageSource, mIsOrphaned, mIsInitiatingPackageUninstalled, - mInitiatingPackageSignatures); + intern(installerPackageName), installerPackageUid, mUpdateOwnerPackageName, + mInstallerAttributionTag, mPackageSource, mIsOrphaned, + mIsInitiatingPackageUninstalled, mInitiatingPackageSignatures); + } + + /** + * Return an InstallSource the same as this one except with the specified + * {@link #mUpdateOwnerPackageName}. + */ + InstallSource setUpdateOwnerPackageName(@Nullable String updateOwnerPackageName) { + if (Objects.equals(updateOwnerPackageName, mUpdateOwnerPackageName)) { + return this; + } + return createInternal(mInitiatingPackageName, mOriginatingPackageName, + mInstallerPackageName, mInstallerPackageUid, intern(updateOwnerPackageName), + mInstallerAttributionTag, mPackageSource, mIsOrphaned, + mIsInitiatingPackageUninstalled, mInitiatingPackageSignatures); } /** @@ -188,8 +225,8 @@ public final class InstallSource { return this; } return createInternal(mInitiatingPackageName, mOriginatingPackageName, - mInstallerPackageName, - mInstallerPackageUid, mInstallerAttributionTag, mPackageSource, isOrphaned, + mInstallerPackageName, mInstallerPackageUid, mUpdateOwnerPackageName, + mInstallerAttributionTag, mPackageSource, isOrphaned, mIsInitiatingPackageUninstalled, mInitiatingPackageSignatures); } @@ -202,8 +239,8 @@ public final class InstallSource { return this; } return createInternal(mInitiatingPackageName, mOriginatingPackageName, - mInstallerPackageName, - mInstallerPackageUid, mInstallerAttributionTag, mPackageSource, mIsOrphaned, + mInstallerPackageName, mInstallerPackageUid, mUpdateOwnerPackageName, + mInstallerAttributionTag, mPackageSource, mIsOrphaned, mIsInitiatingPackageUninstalled, signatures); } @@ -220,6 +257,7 @@ public final class InstallSource { boolean isInitiatingPackageUninstalled = mIsInitiatingPackageUninstalled; String originatingPackageName = mOriginatingPackageName; String installerPackageName = mInstallerPackageName; + String updateOwnerPackageName = mUpdateOwnerPackageName; int installerPackageUid = mInstallerPackageUid; boolean isOrphaned = mIsOrphaned; @@ -242,13 +280,18 @@ public final class InstallSource { isOrphaned = true; modified = true; } + if (packageName.equals(updateOwnerPackageName)) { + updateOwnerPackageName = null; + modified = true; + } if (!modified) { return this; } return createInternal(mInitiatingPackageName, originatingPackageName, installerPackageName, - installerPackageUid, null, mPackageSource, isOrphaned, + installerPackageUid, updateOwnerPackageName, + null /* installerAttributionTag */, mPackageSource, isOrphaned, isInitiatingPackageUninstalled, mInitiatingPackageSignatures); } diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java index 8c5bab6a55dd..239853c857cc 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerService.java +++ b/services/core/java/com/android/server/pm/PackageInstallerService.java @@ -878,9 +878,14 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements requestedInstallerPackageName = null; } + if (isApex || mContext.checkCallingOrSelfPermission( + Manifest.permission.ENFORCE_UPDATE_OWNERSHIP) == PackageManager.PERMISSION_DENIED) { + params.installFlags &= ~PackageManager.INSTALL_REQUEST_UPDATE_OWNERSHIP; + } + InstallSource installSource = InstallSource.create(installerPackageName, originatingPackageName, requestedInstallerPackageName, requestedInstallerPackageUid, - installerAttributionTag, params.packageSource); + requestedInstallerPackageName, installerAttributionTag, params.packageSource); session = new PackageInstallerSession(mInternalCallback, mContext, mPm, this, mSilentUpdatePolicy, mInstallThread.getLooper(), mStagingManager, sessionId, userId, callingUid, installSource, params, createdMillis, 0L, stageDir, stageCid, diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java index 0556c3ba8557..8ea77889acc7 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerSession.java +++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java @@ -89,6 +89,7 @@ import android.content.pm.PackageInstaller; import android.content.pm.PackageInstaller.PreapprovalDetails; import android.content.pm.PackageInstaller.SessionInfo; import android.content.pm.PackageInstaller.SessionParams; +import android.content.pm.PackageInstaller.UserActionReason; import android.content.pm.PackageManager; import android.content.pm.PackageManager.PackageInfoFlags; import android.content.pm.PackageManagerInternal; @@ -226,6 +227,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { private static final String ATTR_USER_ID = "userId"; private static final String ATTR_INSTALLER_PACKAGE_NAME = "installerPackageName"; private static final String ATTR_INSTALLER_PACKAGE_UID = "installerPackageUid"; + private static final String ATTR_UPDATE_OWNER_PACKAGE_NAME = "updateOwnererPackageName"; private static final String ATTR_INSTALLER_ATTRIBUTION_TAG = "installerAttributionTag"; private static final String ATTR_INSTALLER_UID = "installerUid"; private static final String ATTR_INITIATING_PACKAGE_NAME = @@ -446,6 +448,9 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { @GuardedBy("mLock") private boolean mHasDeviceAdminReceiver; + @GuardedBy("mLock") + private int mUserActionRequirement; + static class FileEntry { private final int mIndex; private final InstallationFile mFile; @@ -842,10 +847,17 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { private static final int USER_ACTION_NOT_NEEDED = 0; private static final int USER_ACTION_REQUIRED = 1; private static final int USER_ACTION_PENDING_APK_PARSING = 2; - - @IntDef({USER_ACTION_NOT_NEEDED, USER_ACTION_REQUIRED, USER_ACTION_PENDING_APK_PARSING}) - @interface - UserActionRequirement {} + private static final int USER_ACTION_REQUIRED_UPDATE_OWNER_CHANGED = 3; + private static final int USER_ACTION_REQUIRED_UPDATE_OWNER_RETAINED = 4; + + @IntDef({ + USER_ACTION_NOT_NEEDED, + USER_ACTION_REQUIRED, + USER_ACTION_PENDING_APK_PARSING, + USER_ACTION_REQUIRED_UPDATE_OWNER_CHANGED, + USER_ACTION_REQUIRED_UPDATE_OWNER_RETAINED + }) + @interface UserActionRequirement {} /** * Checks if the permissions still need to be confirmed. @@ -899,8 +911,13 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { final String existingInstallerPackageName = existingInstallSourceInfo != null ? existingInstallSourceInfo.getInstallingPackageName() : null; + final String existingUpdateOwnerPackageName = existingInstallSourceInfo != null + ? existingInstallSourceInfo.getUpdateOwnerPackageName() + : null; final boolean isInstallerOfRecord = isUpdate && Objects.equals(existingInstallerPackageName, getInstallerPackageName()); + final boolean isUpdateOwner = Objects.equals(existingUpdateOwnerPackageName, + getInstallerPackageName()); final boolean isSelfUpdate = targetPackageUid == mInstallerUid; final boolean isPermissionGranted = isInstallPermissionGranted || (isUpdatePermissionGranted && isUpdate) @@ -908,16 +925,35 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { || (isInstallDpcPackagesPermissionGranted && hasDeviceAdminReceiver); final boolean isInstallerRoot = (mInstallerUid == Process.ROOT_UID); final boolean isInstallerSystem = (mInstallerUid == Process.SYSTEM_UID); + final boolean isInstallerShell = (mInstallerUid == Process.SHELL_UID); + final boolean isUpdateOwnershipEnforcementEnabled = + mPm.isUpdateOwnershipEnforcementAvailable() + && existingUpdateOwnerPackageName != null; - // Device owners and affiliated profile owners are allowed to silently install packages, so + // Device owners and affiliated profile owners are allowed to silently install packages, so // the permission check is waived if the installer is the device owner. - final boolean noUserActionNecessary = isPermissionGranted || isInstallerRoot - || isInstallerSystem || isInstallerDeviceOwnerOrAffiliatedProfileOwner(); + final boolean noUserActionNecessary = isInstallerRoot || isInstallerSystem + || isInstallerDeviceOwnerOrAffiliatedProfileOwner(); if (noUserActionNecessary) { return USER_ACTION_NOT_NEEDED; } + if (isUpdateOwnershipEnforcementEnabled + && !isApexSession() + && !isUpdateOwner + && !isInstallerShell) { + final boolean isRequestUpdateOwner = + (params.installFlags & PackageManager.INSTALL_REQUEST_UPDATE_OWNERSHIP) != 0; + + return isRequestUpdateOwner ? USER_ACTION_REQUIRED_UPDATE_OWNER_CHANGED + : USER_ACTION_REQUIRED_UPDATE_OWNER_RETAINED; + } + + if (isPermissionGranted) { + return USER_ACTION_NOT_NEEDED; + } + if (snapshot.isInstallDisabledForPackage(getInstallerPackageName(), mInstallerUid, userId)) { // show the installer to account for device policy or unknown sources use cases @@ -926,13 +962,20 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { if (params.requireUserAction == SessionParams.USER_ACTION_NOT_REQUIRED && isUpdateWithoutUserActionPermissionGranted - && (isInstallerOfRecord || isSelfUpdate)) { + && ((isUpdateOwnershipEnforcementEnabled ? isUpdateOwner + : isInstallerOfRecord) || isSelfUpdate)) { return USER_ACTION_PENDING_APK_PARSING; } return USER_ACTION_REQUIRED; } + private void updateUserActionRequirement(int requirement) { + synchronized (mLock) { + mUserActionRequirement = requirement; + } + } + @SuppressWarnings("GuardedBy" /*mPm.mInstaller is {@code final} field*/) public PackageInstallerSession(PackageInstallerService.InternalCallback callback, Context context, PackageManagerService pm, @@ -1121,6 +1164,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { info.installerUid = mInstallerUid; info.packageSource = params.packageSource; info.keepApplicationEnabledSetting = params.keepApplicationEnabledSetting; + info.pendingUserActionReason = userActionRequirementToReason(mUserActionRequirement); } return info; } @@ -2205,8 +2249,9 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } mInstallerUid = newOwnerAppInfo.uid; - mInstallSource = InstallSource.create(packageName, null, packageName, - mInstallerUid, null, params.packageSource); + mInstallSource = InstallSource.create(packageName, null /* originatingPackageName */, + packageName, mInstallerUid, packageName, null /* installerAttributionTag */, + params.packageSource); } } @@ -2220,7 +2265,10 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { @UserActionRequirement int userActionRequirement = USER_ACTION_NOT_NEEDED; // TODO(b/159331446): Move this to makeSessionActiveForInstall and update javadoc userActionRequirement = session.computeUserActionRequirement(); - if (userActionRequirement == USER_ACTION_REQUIRED) { + session.updateUserActionRequirement(userActionRequirement); + if (userActionRequirement == USER_ACTION_REQUIRED + || userActionRequirement == USER_ACTION_REQUIRED_UPDATE_OWNER_CHANGED + || userActionRequirement == USER_ACTION_REQUIRED_UPDATE_OWNER_RETAINED) { session.sendPendingUserActionIntent(target); return true; } @@ -2253,6 +2301,18 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { return false; } + private static @UserActionReason int userActionRequirementToReason( + @UserActionRequirement int requirement) { + switch (requirement) { + case USER_ACTION_REQUIRED_UPDATE_OWNER_CHANGED: + return PackageInstaller.REASON_OWNERSHIP_CHANGED; + case USER_ACTION_REQUIRED_UPDATE_OWNER_RETAINED: + return PackageInstaller.REASON_REMIND_OWNERSHIP; + default: + return PackageInstaller.REASON_CONFIRM_PACKAGE_CHANGE; + } + } + /** * Find out any session needs user action. * @@ -4438,6 +4498,11 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { return params.keepApplicationEnabledSetting; } + @Override + public boolean isRequestUpdateOwnership() { + return (params.installFlags & PackageManager.INSTALL_REQUEST_UPDATE_OWNERSHIP) != 0; + } + void setSessionReady() { synchronized (mLock) { // Do not allow destroyed/failed session to change state @@ -4774,6 +4839,8 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { writeStringAttribute(out, ATTR_INSTALLER_PACKAGE_NAME, mInstallSource.mInstallerPackageName); out.attributeInt(null, ATTR_INSTALLER_PACKAGE_UID, mInstallSource.mInstallerPackageUid); + writeStringAttribute(out, ATTR_UPDATE_OWNER_PACKAGE_NAME, + mInstallSource.mUpdateOwnerPackageName); writeStringAttribute(out, ATTR_INSTALLER_ATTRIBUTION_TAG, mInstallSource.mInstallerAttributionTag); out.attributeInt(null, ATTR_INSTALLER_UID, mInstallerUid); @@ -4941,6 +5008,8 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { final String installerPackageName = readStringAttribute(in, ATTR_INSTALLER_PACKAGE_NAME); final int installPackageUid = in.getAttributeInt(null, ATTR_INSTALLER_PACKAGE_UID, INVALID_UID); + final String updateOwnerPackageName = readStringAttribute(in, + ATTR_UPDATE_OWNER_PACKAGE_NAME); final String installerAttributionTag = readStringAttribute(in, ATTR_INSTALLER_ATTRIBUTION_TAG); final int installerUid = in.getAttributeInt(null, ATTR_INSTALLER_UID, pm.snapshotComputer() @@ -5113,7 +5182,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { InstallSource installSource = InstallSource.create(installInitiatingPackageName, installOriginatingPackageName, installerPackageName, installPackageUid, - installerAttributionTag, params.packageSource); + updateOwnerPackageName, installerAttributionTag, params.packageSource); return new PackageInstallerSession(callback, context, pm, sessionProvider, silentUpdatePolicy, installerThread, stagingManager, sessionId, userId, installerUid, installSource, params, createdMillis, committedMillis, stageDir, diff --git a/services/core/java/com/android/server/pm/PackageManagerInternalBase.java b/services/core/java/com/android/server/pm/PackageManagerInternalBase.java index 04f5e56c4eb7..99fff720221e 100644 --- a/services/core/java/com/android/server/pm/PackageManagerInternalBase.java +++ b/services/core/java/com/android/server/pm/PackageManagerInternalBase.java @@ -49,7 +49,6 @@ import android.util.SparseArray; import com.android.server.pm.Installer.LegacyDexoptDisabledException; import com.android.server.pm.dex.DexManager; -import com.android.server.pm.dex.DynamicCodeLogger; import com.android.server.pm.permission.PermissionManagerServiceInternal; import com.android.server.pm.pkg.AndroidPackage; import com.android.server.pm.pkg.PackageStateInternal; @@ -773,10 +772,4 @@ abstract class PackageManagerInternalBase extends PackageManagerInternal { public final void shutdown() { mService.shutdown(); } - - @Override - @Deprecated - public final DynamicCodeLogger getDynamicCodeLogger() { - return getDexManager().getDynamicCodeLogger(); - } } diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 9a98e1e7d0e6..92bbb7e86327 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -204,6 +204,7 @@ import com.android.server.pm.Settings.VersionInfo; import com.android.server.pm.dex.ArtManagerService; import com.android.server.pm.dex.ArtUtils; import com.android.server.pm.dex.DexManager; +import com.android.server.pm.dex.DynamicCodeLogger; import com.android.server.pm.dex.ViewCompiler; import com.android.server.pm.local.PackageManagerLocalImpl; import com.android.server.pm.parsing.PackageInfoUtils; @@ -793,6 +794,7 @@ public class PackageManagerService implements PackageSender, TestUtilityService // DexManager handles the usage of dex files (e.g. secondary files, whether or not a package // is used by other apps). private final DexManager mDexManager; + private final DynamicCodeLogger mDynamicCodeLogger; final ViewCompiler mViewCompiler; @@ -1529,7 +1531,8 @@ public class PackageManagerService implements PackageSender, TestUtilityService (i, pm) -> new PackageDexOptimizer(i.getInstaller(), i.getInstallLock(), i.getContext(), "*dexopt*"), (i, pm) -> new DexManager(i.getContext(), i.getPackageDexOptimizer(), - i.getInstaller(), i.getInstallLock()), + i.getInstaller(), i.getInstallLock(), i.getDynamicCodeLogger()), + (i, pm) -> new DynamicCodeLogger(i.getInstaller()), (i, pm) -> new ArtManagerService(i.getContext(), i.getInstaller(), i.getInstallLock()), (i, pm) -> ApexManager.getInstance(), @@ -1711,6 +1714,7 @@ public class PackageManagerService implements PackageSender, TestUtilityService mDefaultAppProvider = testParams.defaultAppProvider; mLegacyPermissionManager = testParams.legacyPermissionManagerInternal; mDexManager = testParams.dexManager; + mDynamicCodeLogger = testParams.dynamicCodeLogger; mFactoryTest = testParams.factoryTest; mIncrementalManager = testParams.incrementalManager; mInstallerService = testParams.installerService; @@ -1889,6 +1893,7 @@ public class PackageManagerService implements PackageSender, TestUtilityService mPackageDexOptimizer = injector.getPackageDexOptimizer(); mDexManager = injector.getDexManager(); + mDynamicCodeLogger = injector.getDynamicCodeLogger(); mBackgroundDexOptService = injector.getBackgroundDexOptService(); mArtManagerService = injector.getArtManagerService(); mMoveCallbacks = new MovePackageHelper.MoveCallbacks(FgThread.get().getLooper()); @@ -2316,6 +2321,7 @@ public class PackageManagerService implements PackageSender, TestUtilityService .getList()); } mDexManager.load(userPackages); + mDynamicCodeLogger.load(userPackages); if (mIsUpgrade) { FrameworkStatsLog.write( FrameworkStatsLog.BOOT_TIME_EVENT_DURATION_REPORTED, @@ -2980,9 +2986,14 @@ public class PackageManagerService implements PackageSender, TestUtilityService return mDexManager; } + /*package*/ DynamicCodeLogger getDynamicCodeLogger() { + return mDynamicCodeLogger; + } + public void shutdown() { mCompilerStats.writeNow(); mDexManager.writePackageDexUsageNow(); + mDynamicCodeLogger.writeNow(); PackageWatchdog.getInstance(mContext).writeNow(); synchronized (mLock) { @@ -6013,6 +6024,42 @@ public class PackageManagerService implements PackageSender, TestUtilityService } @Override + public void relinquishUpdateOwnership(String targetPackage) { + final int callingUid = Binder.getCallingUid(); + final int callingUserId = UserHandle.getUserId(callingUid); + final Computer snapshot = snapshotComputer(); + + final PackageStateInternal targetPackageState = + snapshot.getPackageStateForInstalledAndFiltered(targetPackage, callingUid, + callingUserId); + if (targetPackageState == null) { + throw new IllegalArgumentException("Unknown target package: " + targetPackage); + } + + final String targetUpdateOwnerPackageName = + targetPackageState.getInstallSource().mUpdateOwnerPackageName; + final PackageStateInternal targetUpdateOwnerPkgSetting = + targetUpdateOwnerPackageName == null ? null + : snapshot.getPackageStateInternal(targetUpdateOwnerPackageName); + + if (targetUpdateOwnerPkgSetting == null) { + return; + } + + final int callingAppId = UserHandle.getAppId(callingUid); + final int targetUpdateOwnerAppId = targetUpdateOwnerPkgSetting.getAppId(); + if (callingAppId != Process.SYSTEM_UID + && callingAppId != Process.SHELL_UID + && callingAppId != targetUpdateOwnerAppId) { + throw new SecurityException("Caller is not the current update owner."); + } + + commitPackageStateMutation(null /* initialState */, targetPackage, + state -> state.setUpdateOwner(null /* updateOwnerPackageName */)); + scheduleWriteSettings(); + } + + @Override public boolean setInstantAppCookie(String packageName, byte[] cookie, int userId) { if (HIDE_EPHEMERAL_APIS) { return true; @@ -6346,6 +6393,12 @@ public class PackageManagerService implements PackageSender, TestUtilityService return mDexManager; } + @NonNull + @Override + public DynamicCodeLogger getDynamicCodeLogger() { + return mDynamicCodeLogger; + } + @Override public boolean isPlatformSigned(String packageName) { PackageStateInternal packageState = snapshot().getPackageStateInternal(packageName); diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceInjector.java b/services/core/java/com/android/server/pm/PackageManagerServiceInjector.java index 76e6e45fc873..eb033cb343d4 100644 --- a/services/core/java/com/android/server/pm/PackageManagerServiceInjector.java +++ b/services/core/java/com/android/server/pm/PackageManagerServiceInjector.java @@ -30,6 +30,7 @@ import com.android.server.SystemConfig; import com.android.server.compat.PlatformCompat; import com.android.server.pm.dex.ArtManagerService; import com.android.server.pm.dex.DexManager; +import com.android.server.pm.dex.DynamicCodeLogger; import com.android.server.pm.dex.ViewCompiler; import com.android.server.pm.parsing.PackageParser2; import com.android.server.pm.permission.LegacyPermissionManagerInternal; @@ -106,6 +107,7 @@ public class PackageManagerServiceInjector { private final Singleton<PackageDexOptimizer> mPackageDexOptimizerProducer; private final Singleton<DexManager> mDexManagerProducer; + private final Singleton<DynamicCodeLogger> mDynamicCodeLoggerProducer; private final Singleton<ArtManagerService> mArtManagerServiceProducer; private final Singleton<ApexManager> mApexManagerProducer; @@ -154,6 +156,7 @@ public class PackageManagerServiceInjector { Producer<SystemConfig> systemConfigProducer, Producer<PackageDexOptimizer> packageDexOptimizerProducer, Producer<DexManager> dexManagerProducer, + Producer<DynamicCodeLogger> dynamicCodeLoggerProducer, Producer<ArtManagerService> artManagerServiceProducer, Producer<ApexManager> apexManagerProducer, Producer<ViewCompiler> viewCompilerProducer, @@ -200,6 +203,7 @@ public class PackageManagerServiceInjector { mPackageDexOptimizerProducer = new Singleton<>( packageDexOptimizerProducer); mDexManagerProducer = new Singleton<>(dexManagerProducer); + mDynamicCodeLoggerProducer = new Singleton<>(dynamicCodeLoggerProducer); mArtManagerServiceProducer = new Singleton<>( artManagerServiceProducer); mApexManagerProducer = new Singleton<>(apexManagerProducer); @@ -314,6 +318,10 @@ public class PackageManagerServiceInjector { return mDexManagerProducer.get(this, mPackageManager); } + public DynamicCodeLogger getDynamicCodeLogger() { + return mDynamicCodeLoggerProducer.get(this, mPackageManager); + } + public ArtManagerService getArtManagerService() { return mArtManagerServiceProducer.get(this, mPackageManager); } diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceTestParams.java b/services/core/java/com/android/server/pm/PackageManagerServiceTestParams.java index bffbb84bcfae..0c617aef1ed6 100644 --- a/services/core/java/com/android/server/pm/PackageManagerServiceTestParams.java +++ b/services/core/java/com/android/server/pm/PackageManagerServiceTestParams.java @@ -32,6 +32,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.content.om.OverlayConfig; import com.android.server.pm.dex.ArtManagerService; import com.android.server.pm.dex.DexManager; +import com.android.server.pm.dex.DynamicCodeLogger; import com.android.server.pm.dex.ViewCompiler; import com.android.server.pm.parsing.PackageParser2; import com.android.server.pm.permission.LegacyPermissionManagerInternal; @@ -49,6 +50,7 @@ public final class PackageManagerServiceTestParams { public int defParseFlags; public DefaultAppProvider defaultAppProvider; public DexManager dexManager; + public DynamicCodeLogger dynamicCodeLogger; public List<ScanPartition> dirsToScanAsSystem; public boolean factoryTest; public ArrayMap<String, FeatureInfo> availableFeatures; diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java index 9fc6c6387223..849cbeba0300 100644 --- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java +++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java @@ -172,6 +172,11 @@ class PackageManagerShellCommand extends ShellCommand { SUPPORTED_PERMISSION_FLAGS.put("revoke-when-requested", FLAG_PERMISSION_REVOKE_WHEN_REQUESTED); } + // For backward compatibility. DO NOT add new commands here. New ART Service commands should be + // added under the "art" namespace. + private static final Set<String> ART_SERVICE_COMMANDS = Set.of("compile", + "reconcile-secondary-dex-files", "force-dex-opt", "bg-dexopt-job", + "cancel-bg-dexopt-job", "delete-dexopt", "dump-profiles", "snapshot-profile", "art"); final IPackageManager mInterface; final LegacyPermissionManagerInternal mLegacyPermissionManager; @@ -250,22 +255,6 @@ class PackageManagerShellCommand extends ShellCommand { return runMovePackage(); case "move-primary-storage": return runMovePrimaryStorage(); - case "compile": - return runCompile(); - case "reconcile-secondary-dex-files": - return runreconcileSecondaryDexFiles(); - case "force-dex-opt": - return runForceDexOpt(); - case "bg-dexopt-job": - return runBgDexOpt(); - case "cancel-bg-dexopt-job": - return cancelBgDexOptJob(); - case "delete-dexopt": - return runDeleteDexOpt(); - case "dump-profiles": - return runDumpProfiles(); - case "snapshot-profile": - return runSnapshotProfile(); case "uninstall": return runUninstall(); case "clear": @@ -355,9 +344,19 @@ class PackageManagerShellCommand extends ShellCommand { return runBypassAllowedApexUpdateCheck(); case "set-silent-updates-policy": return runSetSilentUpdatesPolicy(); - case "art": - return runArtSubCommand(); default: { + if (ART_SERVICE_COMMANDS.contains(cmd)) { + if (DexOptHelper.useArtService()) { + return runArtServiceCommand(); + } else { + try { + return runLegacyDexoptCommand(cmd); + } catch (LegacyDexoptDisabledException e) { + throw new RuntimeException(e); + } + } + } + Boolean domainVerificationResult = mDomainVerificationShell.runCommand(this, cmd); if (domainVerificationResult != null) { @@ -381,12 +380,39 @@ class PackageManagerShellCommand extends ShellCommand { } } catch (RemoteException e) { pw.println("Remote exception: " + e); - } catch (ManagerNotFoundException e) { - pw.println(e); } return -1; } + private int runLegacyDexoptCommand(@NonNull String cmd) + throws RemoteException, LegacyDexoptDisabledException { + Installer.checkLegacyDexoptDisabled(); + switch (cmd) { + case "compile": + return runCompile(); + case "reconcile-secondary-dex-files": + return runreconcileSecondaryDexFiles(); + case "force-dex-opt": + return runForceDexOpt(); + case "bg-dexopt-job": + return runBgDexOpt(); + case "cancel-bg-dexopt-job": + return cancelBgDexOptJob(); + case "delete-dexopt": + return runDeleteDexOpt(); + case "dump-profiles": + return runDumpProfiles(); + case "snapshot-profile": + return runSnapshotProfile(); + case "art": + getOutPrintWriter().println("ART Service not enabled"); + return -1; + default: + // Can't happen. + throw new IllegalArgumentException(); + } + } + /** * Shows module info * @@ -3211,6 +3237,15 @@ class PackageManagerShellCommand extends ShellCommand { case "--install-reason": sessionParams.installReason = Integer.parseInt(getNextArg()); break; + case "--update-ownership": + if (params.installerPackageName == null) { + // Enabling update ownership enforcement needs an installer. Since the + // default installer is null when using adb install, that effectively + // disable this enforcement. + params.installerPackageName = "com.android.shell"; + } + sessionParams.installFlags |= PackageManager.INSTALL_REQUEST_UPDATE_OWNERSHIP; + break; case "--force-uuid": sessionParams.installFlags |= PackageManager.INSTALL_FORCE_VOLUME_UUID; sessionParams.volumeUuid = getNextArg(); @@ -3258,6 +3293,10 @@ class PackageManagerShellCommand extends ShellCommand { case "--skip-enable": sessionParams.setKeepApplicationEnabledSetting(); break; + case "--bypass-low-target-sdk-block": + sessionParams.installFlags |= + PackageManager.INSTALL_BYPASS_LOW_TARGET_SDK_BLOCK; + break; default: throw new IllegalArgumentException("Unknown option " + opt); } @@ -3479,17 +3518,18 @@ class PackageManagerShellCommand extends ShellCommand { return 1; } - private int runArtSubCommand() throws ManagerNotFoundException { - // Remove the first arg "art" and forward to ART module. - String[] args = getAllArgs(); - args = Arrays.copyOfRange(args, 1, args.length); + private int runArtServiceCommand() { try (var in = ParcelFileDescriptor.dup(getInFileDescriptor()); var out = ParcelFileDescriptor.dup(getOutFileDescriptor()); var err = ParcelFileDescriptor.dup(getErrFileDescriptor())) { return LocalManagerRegistry.getManagerOrThrow(ArtManagerLocal.class) - .handleShellCommand(getTarget(), in, out, err, args); + .handleShellCommand(getTarget(), in, out, err, getAllArgs()); } catch (IOException e) { throw new IllegalStateException(e); + } catch (ManagerNotFoundException e) { + PrintWriter epw = getErrPrintWriter(); + epw.println("ART Service is not ready. Please try again later"); + return -1; } } @@ -4095,6 +4135,7 @@ class PackageManagerShellCommand extends ShellCommand { pw.println(" --install-reason: indicates why the app is being installed:"); pw.println(" 0=unknown, 1=admin policy, 2=device restore,"); pw.println(" 3=device setup, 4=user request"); + pw.println(" --update-ownership: request the update ownership enforcement"); pw.println(" --force-uuid: force install on to disk volume with given UUID"); pw.println(" --apex: install an .apex file, not an .apk"); pw.println(" --staged-ready-timeout: By default, staged sessions wait " @@ -4118,7 +4159,7 @@ class PackageManagerShellCommand extends ShellCommand { pw.println(" [--referrer URI] [--abi ABI_NAME] [--force-sdk]"); pw.println(" [--preload] [--instant] [--full] [--dont-kill]"); pw.println(" [--force-uuid internal|UUID] [--pkg PACKAGE] [--apex] [-S BYTES]"); - pw.println(" [--multi-package] [--staged]"); + pw.println(" [--multi-package] [--staged] [--update-ownership]"); pw.println(" Like \"install\", but starts an install session. Use \"install-write\""); pw.println(" to push data into the session, and \"install-commit\" to finish."); pw.println(""); @@ -4257,6 +4298,76 @@ class PackageManagerShellCommand extends ShellCommand { pw.println(""); pw.println(" get-max-running-users"); pw.println(""); + pw.println(" set-home-activity [--user USER_ID] TARGET-COMPONENT"); + pw.println(" Set the default home activity (aka launcher)."); + pw.println(" TARGET-COMPONENT can be a package name (com.package.my) or a full"); + pw.println(" component (com.package.my/component.name). However, only the package name"); + pw.println(" matters: the actual component used will be determined automatically from"); + pw.println(" the package."); + pw.println(""); + pw.println(" set-installer PACKAGE INSTALLER"); + pw.println(" Set installer package name"); + pw.println(""); + pw.println(" get-instantapp-resolver"); + pw.println( + " Return the name of the component that is the current instant app installer."); + pw.println(""); + pw.println(" set-harmful-app-warning [--user <USER_ID>] <PACKAGE> [<WARNING>]"); + pw.println(" Mark the app as harmful with the given warning message."); + pw.println(""); + pw.println(" get-harmful-app-warning [--user <USER_ID>] <PACKAGE>"); + pw.println(" Return the harmful app warning message for the given app, if present"); + pw.println(); + pw.println(" uninstall-system-updates [<PACKAGE>]"); + pw.println(" Removes updates to the given system application and falls back to its"); + pw.println(" /system version. Does nothing if the given package is not a system app."); + pw.println(" If no package is specified, removes updates to all system applications."); + pw.println(""); + pw.println(" get-moduleinfo [--all | --installed] [module-name]"); + pw.println(" Displays module info. If module-name is specified only that info is shown"); + pw.println(" By default, without any argument only installed modules are shown."); + pw.println(" --all: show all module info"); + pw.println(" --installed: show only installed modules"); + pw.println(""); + pw.println(" log-visibility [--enable|--disable] <PACKAGE>"); + pw.println(" Turns on debug logging when visibility is blocked for the given package."); + pw.println(" --enable: turn on debug logging (default)"); + pw.println(" --disable: turn off debug logging"); + pw.println(""); + pw.println(" set-silent-updates-policy [--allow-unlimited-silent-updates <INSTALLER>]"); + pw.println(" [--throttle-time <SECONDS>] [--reset]"); + pw.println(" Sets the policies of the silent updates."); + pw.println(" --allow-unlimited-silent-updates: allows unlimited silent updated"); + pw.println(" installation requests from the installer without the throttle time."); + pw.println(" --throttle-time: update the silent updates throttle time in seconds."); + pw.println(" --reset: restore the installer and throttle time to the default, and"); + pw.println(" clear tracks of silent updates in the system."); + pw.println(""); + if (DexOptHelper.useArtService()) { + printArtServiceHelp(); + } else { + printLegacyDexoptHelp(); + } + pw.println(""); + mDomainVerificationShell.printHelp(pw); + pw.println(""); + Intent.printIntentArgsHelp(pw, ""); + } + + private void printArtServiceHelp() { + final var ipw = new IndentingPrintWriter(getOutPrintWriter(), " " /* singleIndent */); + ipw.increaseIndent(); + try { + LocalManagerRegistry.getManagerOrThrow(ArtManagerLocal.class) + .printShellCommandHelp(ipw); + } catch (ManagerNotFoundException e) { + ipw.println("ART Service is not ready. Please try again later"); + } + ipw.decreaseIndent(); + } + + private void printLegacyDexoptHelp() { + final PrintWriter pw = getOutPrintWriter(); pw.println(" compile [-m MODE | -r REASON] [-f] [-c] [--split SPLIT_NAME]"); pw.println(" [--reset] [--check-prof (true | false)] (-a | TARGET-PACKAGE)"); pw.println(" Trigger compilation of TARGET-PACKAGE or all packages if \"-a\". Options are:"); @@ -4329,57 +4440,6 @@ class PackageManagerShellCommand extends ShellCommand { pw.println(" " + ART_PROFILE_SNAPSHOT_DEBUG_LOCATION + "TARGET-PACKAGE[-code-path].prof"); pw.println(" If TARGET-PACKAGE=android it will take a snapshot of the boot image"); - pw.println(""); - pw.println(" set-home-activity [--user USER_ID] TARGET-COMPONENT"); - pw.println(" Set the default home activity (aka launcher)."); - pw.println(" TARGET-COMPONENT can be a package name (com.package.my) or a full"); - pw.println(" component (com.package.my/component.name). However, only the package name"); - pw.println(" matters: the actual component used will be determined automatically from"); - pw.println(" the package."); - pw.println(""); - pw.println(" set-installer PACKAGE INSTALLER"); - pw.println(" Set installer package name"); - pw.println(""); - pw.println(" get-instantapp-resolver"); - pw.println(" Return the name of the component that is the current instant app installer."); - pw.println(""); - pw.println(" set-harmful-app-warning [--user <USER_ID>] <PACKAGE> [<WARNING>]"); - pw.println(" Mark the app as harmful with the given warning message."); - pw.println(""); - pw.println(" get-harmful-app-warning [--user <USER_ID>] <PACKAGE>"); - pw.println(" Return the harmful app warning message for the given app, if present"); - pw.println(); - pw.println(" uninstall-system-updates [<PACKAGE>]"); - pw.println(" Removes updates to the given system application and falls back to its"); - pw.println(" /system version. Does nothing if the given package is not a system app."); - pw.println(" If no package is specified, removes updates to all system applications."); - pw.println(""); - pw.println(" get-moduleinfo [--all | --installed] [module-name]"); - pw.println(" Displays module info. If module-name is specified only that info is shown"); - pw.println(" By default, without any argument only installed modules are shown."); - pw.println(" --all: show all module info"); - pw.println(" --installed: show only installed modules"); - pw.println(""); - pw.println(" log-visibility [--enable|--disable] <PACKAGE>"); - pw.println(" Turns on debug logging when visibility is blocked for the given package."); - pw.println(" --enable: turn on debug logging (default)"); - pw.println(" --disable: turn off debug logging"); - pw.println(""); - pw.println(" set-silent-updates-policy [--allow-unlimited-silent-updates <INSTALLER>]"); - pw.println(" [--throttle-time <SECONDS>] [--reset]"); - pw.println(" Sets the policies of the silent updates."); - pw.println(" --allow-unlimited-silent-updates: allows unlimited silent updated"); - pw.println(" installation requests from the installer without the throttle time."); - pw.println(" --throttle-time: update the silent updates throttle time in seconds."); - pw.println(" --reset: restore the installer and throttle time to the default, and"); - pw.println(" clear tracks of silent updates in the system."); - pw.println(""); - pw.println(" art [<SUB-COMMANDS>]"); - pw.println(" Invokes ART services commands. (Run `pm art help` for details.)"); - pw.println(""); - mDomainVerificationShell.printHelp(pw); - pw.println(""); - Intent.printIntentArgsHelp(pw , ""); } private static class LocalIntentReceiver { diff --git a/services/core/java/com/android/server/pm/PackageSetting.java b/services/core/java/com/android/server/pm/PackageSetting.java index 877b1127cfb4..6562de96388f 100644 --- a/services/core/java/com/android/server/pm/PackageSetting.java +++ b/services/core/java/com/android/server/pm/PackageSetting.java @@ -300,6 +300,8 @@ public class PackageSetting extends SettingBase implements PackageStateInternal installSource.mInitiatingPackageName); proto.write(PackageProto.InstallSourceProto.ORIGINATING_PACKAGE_NAME, installSource.mOriginatingPackageName); + proto.write(PackageProto.InstallSourceProto.UPDATE_OWNER_PACKAGE_NAME, + installSource.mUpdateOwnerPackageName); proto.end(sourceToken); } proto.write(PackageProto.StatesProto.IS_LOADING, isLoading()); @@ -368,6 +370,12 @@ public class PackageSetting extends SettingBase implements PackageStateInternal return this; } + public PackageSetting setUpdateOwnerPackage(@Nullable String updateOwnerPackageName) { + installSource = installSource.setUpdateOwnerPackageName(updateOwnerPackageName); + onChanged(); + return this; + } + public PackageSetting setInstallSource(InstallSource installSource) { this.installSource = Objects.requireNonNull(installSource); onChanged(); diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java index 6ebef2057e78..97fb0c2e3fe9 100644 --- a/services/core/java/com/android/server/pm/Settings.java +++ b/services/core/java/com/android/server/pm/Settings.java @@ -3050,6 +3050,9 @@ public final class Settings implements Watchable, Snappable { if (installSource.mInstallerPackageUid != INVALID_UID) { serializer.attributeInt(null, "installerUid", installSource.mInstallerPackageUid); } + if (installSource.mUpdateOwnerPackageName != null) { + serializer.attribute(null, "updateOwner", installSource.mUpdateOwnerPackageName); + } if (installSource.mInstallerAttributionTag != null) { serializer.attribute(null, "installerAttributionTag", installSource.mInstallerAttributionTag); @@ -3880,6 +3883,7 @@ public final class Settings implements Watchable, Snappable { String systemStr = null; String installerPackageName = null; int installerPackageUid = INVALID_UID; + String updateOwnerPackageName = null; String installerAttributionTag = null; int packageSource = PackageInstaller.PACKAGE_SOURCE_UNSPECIFIED; boolean isOrphaned = false; @@ -3923,6 +3927,7 @@ public final class Settings implements Watchable, Snappable { versionCode = parser.getAttributeLong(null, "version", 0); installerPackageName = parser.getAttributeValue(null, "installer"); installerPackageUid = parser.getAttributeInt(null, "installerUid", INVALID_UID); + updateOwnerPackageName = parser.getAttributeValue(null, "updateOwner"); installerAttributionTag = parser.getAttributeValue(null, "installerAttributionTag"); packageSource = parser.getAttributeInt(null, "packageSource", PackageInstaller.PACKAGE_SOURCE_UNSPECIFIED); @@ -4065,8 +4070,9 @@ public final class Settings implements Watchable, Snappable { if (packageSetting != null) { InstallSource installSource = InstallSource.create( installInitiatingPackageName, installOriginatingPackageName, - installerPackageName, installerPackageUid, installerAttributionTag, - packageSource, isOrphaned, installInitiatorUninstalled); + installerPackageName, installerPackageUid, updateOwnerPackageName, + installerAttributionTag, packageSource, isOrphaned, + installInitiatorUninstalled); packageSetting.setInstallSource(installSource) .setVolumeUuid(volumeUuid) .setCategoryOverride(categoryHint) @@ -4734,6 +4740,8 @@ public final class Settings implements Watchable, Snappable { pw.print(ps.getInstallSource().mInstallerPackageName != null ? ps.getInstallSource().mInstallerPackageName : "?"); pw.print(ps.getInstallSource().mInstallerPackageUid); + pw.print(ps.getInstallSource().mUpdateOwnerPackageName != null + ? ps.getInstallSource().mUpdateOwnerPackageName : "?"); pw.print(ps.getInstallSource().mInstallerAttributionTag != null ? "(" + ps.getInstallSource().mInstallerAttributionTag + ")" : ""); pw.print(","); @@ -5017,6 +5025,10 @@ public final class Settings implements Watchable, Snappable { pw.print(prefix); pw.print(" installerPackageUid="); pw.println(ps.getInstallSource().mInstallerPackageUid); } + if (ps.getInstallSource().mUpdateOwnerPackageName != null) { + pw.print(prefix); pw.print(" updateOwnerPackageName="); + pw.println(ps.getInstallSource().mUpdateOwnerPackageName); + } if (ps.getInstallSource().mInstallerAttributionTag != null) { pw.print(prefix); pw.print(" installerAttributionTag="); pw.println(ps.getInstallSource().mInstallerAttributionTag); diff --git a/services/core/java/com/android/server/pm/SharedLibrariesImpl.java b/services/core/java/com/android/server/pm/SharedLibrariesImpl.java index d2ce23efd47c..99878679431c 100644 --- a/services/core/java/com/android/server/pm/SharedLibrariesImpl.java +++ b/services/core/java/com/android/server/pm/SharedLibrariesImpl.java @@ -17,6 +17,7 @@ package com.android.server.pm; import static android.content.pm.PackageManager.INSTALL_FAILED_MISSING_SHARED_LIBRARY; +import static android.content.pm.PackageManager.INSTALL_FAILED_SHARED_LIBRARY_BAD_CERTIFICATE_DIGEST; import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME; import static com.android.server.pm.PackageManagerService.SCAN_BOOTING; @@ -1035,8 +1036,17 @@ public final class SharedLibrariesImpl implements SharedLibrariesRead, Watchable } else { // lib signing cert could have rotated beyond the one expected, check to see // if the new one has been blessed by the old - byte[] digestBytes = HexEncoding.decode( - expectedCertDigests[0], false /* allowSingleChar */); + final byte[] digestBytes; + try { + digestBytes = HexEncoding.decode( + expectedCertDigests[0], false /* allowSingleChar */); + } catch (IllegalArgumentException e) { + throw new PackageManagerException( + INSTALL_FAILED_SHARED_LIBRARY_BAD_CERTIFICATE_DIGEST, + "Package " + packageName + " declares bad certificate digest " + + "for " + libraryType + " library " + libName + + "; failing!"); + } if (!libPkg.hasSha256Certificate(digestBytes)) { throw new PackageManagerException(INSTALL_FAILED_MISSING_SHARED_LIBRARY, "Package " + packageName + " requires differently signed " diff --git a/services/core/java/com/android/server/pm/UserManagerInternal.java b/services/core/java/com/android/server/pm/UserManagerInternal.java index 2ae8b52da172..7b15e760107b 100644 --- a/services/core/java/com/android/server/pm/UserManagerInternal.java +++ b/services/core/java/com/android/server/pm/UserManagerInternal.java @@ -388,8 +388,8 @@ public abstract class UserManagerInternal { * and the user is {@link UserManager#isUserVisible() visible}. * * <p><b>NOTE: </b>this method is meant to be used only by {@code UserController} (when a user - * is started). If other clients (like {@code CarService} need to explicitly change the user / - * display assignment, we'll need to provide other APIs. + * is started); for extra unassignments, callers should call {@link + * #assignUserToExtraDisplay(int, int)} instead. * * <p><b>NOTE: </b>this method doesn't validate if the display exists, it's up to the caller to * pass a valid display id. @@ -398,15 +398,43 @@ public abstract class UserManagerInternal { @UserIdInt int profileGroupId, @UserStartMode int userStartMode, int displayId); /** + * Assigns an extra display to the given user, so the user is visible on that display. + * + * <p>This method is meant to be used on automotive builds where a passenger zone has more than + * one display (for example, the "main" display and a smaller display used for input). + * + * <p><b>NOTE: </b>this call will be ignored on devices that do not + * {@link UserManager#isVisibleBackgroundUsersSupported() support visible background users}. + * + * @return whether the operation succeeded, in which case the user would be visible on the + * display. + */ + public abstract boolean assignUserToExtraDisplay(@UserIdInt int userId, int displayId); + + /** * Unassigns a user from its current display when it's stopping. * * <p><b>NOTE: </b>this method is meant to be used only by {@code UserController} (when a user - * is stopped). If other clients (like {@code CarService} need to explicitly change the user / - * display assignment, we'll need to provide other APIs. + * is stopped); for extra unassignments, callers should call + * {@link #unassignUserFromExtraDisplay(int, int)} instead. */ public abstract void unassignUserFromDisplayOnStop(@UserIdInt int userId); /** + * Unassigns the extra display from the given user. + * + * <p>This method is meant to be used on automotive builds where a passenger zone has more than + * one display (for example, the "main" display and a smaller display used for input). + * + * <p><b>NOTE: </b>this call will be ignored on devices that do not + * {@link UserManager#isVisibleBackgroundUsersSupported() support visible background users}. + * + * @return whether the operation succeeded, i.e., the user was previously + * {@link #assignUserToExtraDisplay(int, int) assigned to an extra display}. + */ + public abstract boolean unassignUserFromExtraDisplay(@UserIdInt int userId, int displayId); + + /** * Returns {@code true} if the user is visible (as defined by * {@link UserManager#isUserVisible()}. */ diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index 81f83b0591c4..a966d986c566 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -1476,20 +1476,15 @@ public class UserManagerService extends IUserManager.Stub { @Override public void revokeUserAdmin(@UserIdInt int userId) { checkManageUserAndAcrossUsersFullPermission("revoke admin privileges"); - synchronized (mPackagesLock) { - UserInfo info; - synchronized (mUsersLock) { - info = getUserInfoLU(userId); - } - if (info == null || !info.isAdmin()) { - // Exit if no user found with that id, or the user is not an Admin. - return; - } - - info.flags ^= UserInfo.FLAG_ADMIN; synchronized (mUsersLock) { - writeUserLP(getUserDataLU(info.id)); + UserData user = getUserDataLU(userId); + if (user == null || !user.info.isAdmin()) { + // Exit if no user found with that id, or the user is not an Admin. + return; + } + user.info.flags ^= UserInfo.FLAG_ADMIN; + writeUserLP(user); } } } @@ -7043,6 +7038,16 @@ public class UserManagerService extends IUserManager.Stub { } @Override + public boolean assignUserToExtraDisplay(int userId, int displayId) { + return mUserVisibilityMediator.assignUserToExtraDisplay(userId, displayId); + } + + @Override + public boolean unassignUserFromExtraDisplay(int userId, int displayId) { + return mUserVisibilityMediator.unassignUserFromExtraDisplay(userId, displayId); + } + + @Override public void unassignUserFromDisplayOnStop(@UserIdInt int userId) { mUserVisibilityMediator.unassignUserFromDisplayOnStop(userId); } diff --git a/services/core/java/com/android/server/pm/UserVisibilityMediator.java b/services/core/java/com/android/server/pm/UserVisibilityMediator.java index d8e4dac48ebe..66d390f4f3e9 100644 --- a/services/core/java/com/android/server/pm/UserVisibilityMediator.java +++ b/services/core/java/com/android/server/pm/UserVisibilityMediator.java @@ -19,6 +19,7 @@ import static android.content.pm.UserInfo.NO_PROFILE_GROUP_ID; import static android.os.UserHandle.USER_NULL; import static android.os.UserHandle.USER_SYSTEM; import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.Display.INVALID_DISPLAY; import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_FAILURE; import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE; @@ -113,7 +114,18 @@ public final class UserVisibilityMediator implements Dumpable { */ @Nullable @GuardedBy("mLock") - private final SparseIntArray mUsersOnDisplaysMap; + private final SparseIntArray mUsersAssignedToDisplayOnStart; + + /** + * Map of extra (i.e., not assigned on start, but by explicit calls to + * {@link #assignUserToExtraDisplay(int, int)}) displays assigned to user (key is display id, + * value is user id). + * + * <p>Only set when {@code mUsersOnSecondaryDisplaysEnabled} is {@code true}. + */ + @Nullable + @GuardedBy("mLock") + private final SparseIntArray mExtraDisplaysAssignedToUsers; /** * Mapping from each started user to its profile group. @@ -137,7 +149,13 @@ public final class UserVisibilityMediator implements Dumpable { @VisibleForTesting UserVisibilityMediator(boolean backgroundUsersOnDisplaysEnabled, Handler handler) { mVisibleBackgroundUsersEnabled = backgroundUsersOnDisplaysEnabled; - mUsersOnDisplaysMap = mVisibleBackgroundUsersEnabled ? new SparseIntArray() : null; + if (mVisibleBackgroundUsersEnabled) { + mUsersAssignedToDisplayOnStart = new SparseIntArray(); + mExtraDisplaysAssignedToUsers = new SparseIntArray(); + } else { + mUsersAssignedToDisplayOnStart = null; + mExtraDisplaysAssignedToUsers = null; + } mHandler = handler; // TODO(b/242195409): might need to change this if boot logic is refactored for HSUM devices mStartedProfileGroupIds.put(INITIAL_CURRENT_USER_ID, INITIAL_CURRENT_USER_ID); @@ -207,7 +225,7 @@ public final class UserVisibilityMediator implements Dumpable { if (DBG) { Slogf.d(TAG, "adding user / display mapping (%d -> %d)", userId, displayId); } - mUsersOnDisplaysMap.put(userId, displayId); + mUsersAssignedToDisplayOnStart.put(userId, displayId); break; case SECONDARY_DISPLAY_MAPPING_NOT_NEEDED: if (DBG) { @@ -341,9 +359,9 @@ public final class UserVisibilityMediator implements Dumpable { } // Check if display is available - for (int i = 0; i < mUsersOnDisplaysMap.size(); i++) { - int assignedUserId = mUsersOnDisplaysMap.keyAt(i); - int assignedDisplayId = mUsersOnDisplaysMap.valueAt(i); + for (int i = 0; i < mUsersAssignedToDisplayOnStart.size(); i++) { + int assignedUserId = mUsersAssignedToDisplayOnStart.keyAt(i); + int assignedDisplayId = mUsersAssignedToDisplayOnStart.valueAt(i); if (DBG) { Slogf.d(TAG, "%d: assignedUserId=%d, assignedDisplayId=%d", i, assignedUserId, assignedDisplayId); @@ -363,6 +381,100 @@ public final class UserVisibilityMediator implements Dumpable { } /** + * See {@link UserManagerInternal#assignUserToExtraDisplay(int, int)}. + */ + public boolean assignUserToExtraDisplay(@UserIdInt int userId, int displayId) { + if (DBG) { + Slogf.d(TAG, "assignUserToExtraDisplay(%d, %d)", userId, displayId); + } + if (!mVisibleBackgroundUsersEnabled) { + Slogf.w(TAG, "assignUserToExtraDisplay(%d, %d): called when not supported", userId, + displayId); + return false; + } + if (displayId == INVALID_DISPLAY) { + Slogf.w(TAG, "assignUserToExtraDisplay(%d, %d): called with INVALID_DISPLAY", userId, + displayId); + return false; + } + if (displayId == DEFAULT_DISPLAY) { + Slogf.w(TAG, "assignUserToExtraDisplay(%d, %d): DEFAULT_DISPLAY is automatically " + + "assigned to current user", userId, displayId); + return false; + } + + synchronized (mLock) { + if (!isUserVisible(userId)) { + Slogf.w(TAG, "assignUserToExtraDisplay(%d, %d): failed because user is not visible", + userId, displayId); + return false; + } + if (isStartedProfile(userId)) { + Slogf.w(TAG, "assignUserToExtraDisplay(%d, %d): failed because user is a profile", + userId, displayId); + return false; + } + + if (mExtraDisplaysAssignedToUsers.get(displayId, USER_NULL) == userId) { + Slogf.w(TAG, "assignUserToExtraDisplay(%d, %d): failed because user is already " + + "assigned to that display", userId, displayId); + return false; + } + + int userAssignedToDisplay = getUserAssignedToDisplay(displayId, + /* returnCurrentUserByDefault= */ false); + if (userAssignedToDisplay != USER_NULL) { + Slogf.w(TAG, "assignUserToExtraDisplay(%d, %d): failed because display was assigned" + + " to user %d on start", userId, displayId, userAssignedToDisplay); + return false; + } + userAssignedToDisplay = mExtraDisplaysAssignedToUsers.get(userId, USER_NULL); + if (userAssignedToDisplay != USER_NULL) { + Slogf.w(TAG, "assignUserToExtraDisplay(%d, %d): failed because user %d was already " + + "assigned that extra display", userId, displayId, userAssignedToDisplay); + return false; + } + if (DBG) { + Slogf.d(TAG, "addding %d -> %d to map", displayId, userId); + } + mExtraDisplaysAssignedToUsers.put(displayId, userId); + } + return true; + } + + /** + * See {@link UserManagerInternal#unassignUserFromExtraDisplay(int, int)}. + */ + public boolean unassignUserFromExtraDisplay(@UserIdInt int userId, int displayId) { + if (DBG) { + Slogf.d(TAG, "unassignUserFromExtraDisplay(%d, %d)", userId, displayId); + } + if (!mVisibleBackgroundUsersEnabled) { + Slogf.w(TAG, "unassignUserFromExtraDisplay(%d, %d): called when not supported", + userId, displayId); + return false; + } + synchronized (mLock) { + int assignedUserId = mExtraDisplaysAssignedToUsers.get(displayId, USER_NULL); + if (assignedUserId == USER_NULL) { + Slogf.w(TAG, "unassignUserFromExtraDisplay(%d, %d): not assigned to any user", + userId, displayId); + return false; + } + if (assignedUserId != userId) { + Slogf.w(TAG, "unassignUserFromExtraDisplay(%d, %d): was assigned to user %d", + userId, displayId, assignedUserId); + return false; + } + if (DBG) { + Slogf.d(TAG, "removing %d from map", displayId); + } + mExtraDisplaysAssignedToUsers.delete(displayId); + } + return true; + } + + /** * See {@link UserManagerInternal#unassignUserFromDisplayOnStop(int)}. */ public void unassignUserFromDisplayOnStop(@UserIdInt int userId) { @@ -373,7 +485,7 @@ public final class UserVisibilityMediator implements Dumpable { synchronized (mLock) { visibleUsersBefore = getVisibleUsers(); - unassignUserFromDisplayOnStopLocked(userId); + unassignUserFromAllDisplaysOnStopLocked(userId); visibleUsersAfter = getVisibleUsers(); } @@ -381,7 +493,7 @@ public final class UserVisibilityMediator implements Dumpable { } @GuardedBy("mLock") - private void unassignUserFromDisplayOnStopLocked(@UserIdInt int userId) { + private void unassignUserFromAllDisplaysOnStopLocked(@UserIdInt int userId) { if (DBG) { Slogf.d(TAG, "Removing %d from mStartedProfileGroupIds (%s)", userId, mStartedProfileGroupIds); @@ -395,10 +507,21 @@ public final class UserVisibilityMediator implements Dumpable { return; } if (DBG) { - Slogf.d(TAG, "Removing %d from mUsersOnSecondaryDisplays (%s)", userId, - mUsersOnDisplaysMap); + Slogf.d(TAG, "Removing user %d from mUsersOnDisplaysMap (%s)", userId, + mUsersAssignedToDisplayOnStart); + } + mUsersAssignedToDisplayOnStart.delete(userId); + + // Remove extra displays as well + for (int i = mExtraDisplaysAssignedToUsers.size() - 1; i >= 0; i--) { + if (mExtraDisplaysAssignedToUsers.valueAt(i) == userId) { + if (DBG) { + Slogf.d(TAG, "Removing display %d from mExtraDisplaysAssignedToUsers (%s)", + mExtraDisplaysAssignedToUsers.keyAt(i), mExtraDisplaysAssignedToUsers); + } + mExtraDisplaysAssignedToUsers.removeAt(i); + } } - mUsersOnDisplaysMap.delete(userId); } /** @@ -424,7 +547,7 @@ public final class UserVisibilityMediator implements Dumpable { boolean visible; synchronized (mLock) { - visible = mUsersOnDisplaysMap.indexOfKey(userId) >= 0; + visible = mUsersAssignedToDisplayOnStart.indexOfKey(userId) >= 0; } if (DBG) { Slogf.d(TAG, "isUserVisible(%d): %b from mapping", userId, visible); @@ -448,7 +571,12 @@ public final class UserVisibilityMediator implements Dumpable { } synchronized (mLock) { - return mUsersOnDisplaysMap.get(userId, Display.INVALID_DISPLAY) == displayId; + if (mUsersAssignedToDisplayOnStart.get(userId, Display.INVALID_DISPLAY) == displayId) { + // User assigned to display on start + return true; + } + // Check for extra assignment + return mExtraDisplaysAssignedToUsers.get(displayId, USER_NULL) == userId; } } @@ -465,24 +593,34 @@ public final class UserVisibilityMediator implements Dumpable { } synchronized (mLock) { - return mUsersOnDisplaysMap.get(userId, Display.INVALID_DISPLAY); + return mUsersAssignedToDisplayOnStart.get(userId, Display.INVALID_DISPLAY); } } /** * See {@link UserManagerInternal#getUserAssignedToDisplay(int)}. */ - public int getUserAssignedToDisplay(@UserIdInt int displayId) { - if (displayId == Display.DEFAULT_DISPLAY || !mVisibleBackgroundUsersEnabled) { + public @UserIdInt int getUserAssignedToDisplay(@UserIdInt int displayId) { + return getUserAssignedToDisplay(displayId, /* returnCurrentUserByDefault= */ true); + } + + /** + * Gets the user explicitly assigned to a display, or the current user when no user is assigned + * to it (and {@code returnCurrentUserByDefault} is {@code true}). + */ + private @UserIdInt int getUserAssignedToDisplay(@UserIdInt int displayId, + boolean returnCurrentUserByDefault) { + if (returnCurrentUserByDefault + && (displayId == Display.DEFAULT_DISPLAY || !mVisibleBackgroundUsersEnabled)) { return getCurrentUserId(); } synchronized (mLock) { - for (int i = 0; i < mUsersOnDisplaysMap.size(); i++) { - if (mUsersOnDisplaysMap.valueAt(i) != displayId) { + for (int i = 0; i < mUsersAssignedToDisplayOnStart.size(); i++) { + if (mUsersAssignedToDisplayOnStart.valueAt(i) != displayId) { continue; } - int userId = mUsersOnDisplaysMap.keyAt(i); + int userId = mUsersAssignedToDisplayOnStart.keyAt(i); if (!isStartedProfile(userId)) { return userId; } else if (DBG) { @@ -491,6 +629,13 @@ public final class UserVisibilityMediator implements Dumpable { } } } + if (!returnCurrentUserByDefault) { + if (DBG) { + Slogf.d(TAG, "getUserAssignedToDisplay(%d): no user assigned to display, returning " + + "USER_NULL instead", displayId); + } + return USER_NULL; + } int currentUserId = getCurrentUserId(); if (DBG) { @@ -618,9 +763,11 @@ public final class UserVisibilityMediator implements Dumpable { ipw.print("Supports visible background users on displays: "); ipw.println(mVisibleBackgroundUsersEnabled); - if (mUsersOnDisplaysMap != null) { - dumpSparseIntArray(ipw, mUsersOnDisplaysMap, "user / display", "u", "d"); - } + dumpSparseIntArray(ipw, mUsersAssignedToDisplayOnStart, "user / display", "u", "d"); + + dumpSparseIntArray(ipw, mExtraDisplaysAssignedToUsers, "extra display / user", + "d", "u"); + int numberListeners = mListeners.size(); ipw.print("Number of listeners: "); ipw.println(numberListeners); @@ -638,8 +785,14 @@ public final class UserVisibilityMediator implements Dumpable { ipw.decreaseIndent(); } - private static void dumpSparseIntArray(IndentingPrintWriter ipw, SparseIntArray array, + private static void dumpSparseIntArray(IndentingPrintWriter ipw, @Nullable SparseIntArray array, String arrayDescription, String keyName, String valueName) { + if (array == null) { + ipw.print("No "); + ipw.print(arrayDescription); + ipw.println(" mappings"); + return; + } ipw.print("Number of "); ipw.print(arrayDescription); ipw.print(" mappings: "); diff --git a/services/core/java/com/android/server/pm/dex/DexManager.java b/services/core/java/com/android/server/pm/dex/DexManager.java index 3d4f7b0e4ecc..7f0c3f9f4f06 100644 --- a/services/core/java/com/android/server/pm/dex/DexManager.java +++ b/services/core/java/com/android/server/pm/dex/DexManager.java @@ -126,20 +126,21 @@ public class DexManager { private static int DEX_SEARCH_FOUND_SECONDARY = 3; // dex file is a secondary dex public DexManager(Context context, PackageDexOptimizer pdo, Installer installer, - Object installLock) { - this(context, pdo, installer, installLock, null); + Object installLock, DynamicCodeLogger dynamicCodeLogger) { + this(context, pdo, installer, installLock, dynamicCodeLogger, null); } @VisibleForTesting public DexManager(Context context, PackageDexOptimizer pdo, Installer installer, - Object installLock, @Nullable IPackageManager packageManager) { + Object installLock, DynamicCodeLogger dynamicCodeLogger, + @Nullable IPackageManager packageManager) { mContext = context; mPackageCodeLocationsCache = new HashMap<>(); mPackageDexUsage = new PackageDexUsage(); mPackageDexOptimizer = pdo; mInstaller = installer; mInstallLock = installLock; - mDynamicCodeLogger = new DynamicCodeLogger(installer); + mDynamicCodeLogger = dynamicCodeLogger; mPackageManager = packageManager; // This is currently checked to handle tests that pass in a null context. @@ -169,10 +170,6 @@ public class DexManager { return mPackageManager; } - public DynamicCodeLogger getDynamicCodeLogger() { - return mDynamicCodeLogger; - } - /** * Notify about dex files loads. * Note that this method is invoked when apps load dex files and it should @@ -328,7 +325,6 @@ public class DexManager { loadInternal(existingPackages); } catch (RuntimeException e) { mPackageDexUsage.clear(); - mDynamicCodeLogger.clear(); Slog.w(TAG, "Exception while loading. Starting with a fresh state.", e); } } @@ -379,12 +375,10 @@ public class DexManager { if (mPackageDexUsage.removePackage(packageName)) { mPackageDexUsage.maybeWriteAsync(); } - mDynamicCodeLogger.removePackage(packageName); } else { if (mPackageDexUsage.removeUserPackage(packageName, userId)) { mPackageDexUsage.maybeWriteAsync(); } - mDynamicCodeLogger.removeUserPackage(packageName, userId); } } @@ -463,14 +457,6 @@ public class DexManager { Slog.w(TAG, "Exception while loading package dex usage. " + "Starting with a fresh state.", e); } - - try { - mDynamicCodeLogger.readAndSync(packageToUsersMap); - } catch (RuntimeException e) { - mDynamicCodeLogger.clear(); - Slog.w(TAG, "Exception while loading package dynamic code usage. " - + "Starting with a fresh state.", e); - } } /** @@ -819,7 +805,6 @@ public class DexManager { */ public void writePackageDexUsageNow() { mPackageDexUsage.writeNow(); - mDynamicCodeLogger.writeNow(); } /** diff --git a/services/core/java/com/android/server/pm/dex/DynamicCodeLogger.java b/services/core/java/com/android/server/pm/dex/DynamicCodeLogger.java index 9b94e993f967..da8fafaca5d8 100644 --- a/services/core/java/com/android/server/pm/dex/DynamicCodeLogger.java +++ b/services/core/java/com/android/server/pm/dex/DynamicCodeLogger.java @@ -43,6 +43,9 @@ import libcore.util.HexEncoding; import java.io.File; import java.io.IOException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; @@ -64,7 +67,7 @@ public class DynamicCodeLogger { private final PackageDynamicCodeLoading mPackageDynamicCodeLoading; private final Installer mInstaller; - DynamicCodeLogger(Installer installer) { + public DynamicCodeLogger(Installer installer) { mInstaller = installer; mPackageDynamicCodeLoading = new PackageDynamicCodeLoading(); } @@ -220,8 +223,12 @@ public class DynamicCodeLogger { EventLog.writeEvent(SNET_TAG, subtag, uid, message); } - void recordDex(int loaderUserId, String dexPath, String owningPackageName, - String loadingPackageName) { + /** + * Records that an app running in the specified uid has executed dex code from the file at + * {@code path}. + */ + public void recordDex( + int loaderUserId, String dexPath, String owningPackageName, String loadingPackageName) { if (mPackageDynamicCodeLoading.record(owningPackageName, dexPath, FILE_TYPE_DEX, loaderUserId, loadingPackageName)) { mPackageDynamicCodeLoading.maybeWriteAsync(); @@ -229,8 +236,8 @@ public class DynamicCodeLogger { } /** - * Record that an app running in the specified uid has executed native code from the file at - * {@param path}. + * Records that an app running in the specified uid has executed native code from the file at + * {@code path}. */ public void recordNative(int loadingUid, String path) { String[] packages; @@ -274,7 +281,39 @@ public class DynamicCodeLogger { mPackageDynamicCodeLoading.syncData(packageToUsersMap); } - void writeNow() { + /** Writes the in-memory dynamic code information to disk right away. */ + public void writeNow() { mPackageDynamicCodeLoading.writeNow(); } + + /** Reads the dynamic code information from disk. */ + public void load(Map<Integer, List<PackageInfo>> userToPackagesMap) { + // Compute a reverse map. + Map<String, Set<Integer>> packageToUsersMap = new HashMap<>(); + for (Map.Entry<Integer, List<PackageInfo>> entry : userToPackagesMap.entrySet()) { + List<PackageInfo> packageInfoList = entry.getValue(); + int userId = entry.getKey(); + for (PackageInfo pi : packageInfoList) { + Set<Integer> users = + packageToUsersMap.computeIfAbsent(pi.packageName, k -> new HashSet<>()); + users.add(userId); + } + } + + readAndSync(packageToUsersMap); + } + + /** + * Notifies that the user {@code userId} data for package {@code packageName} was destroyed. + * This will remove all dynamic code information associated with the package for the given user. + * {@code userId} is allowed to be {@code UserHandle.USER_ALL} in which case + * all dynamic code information for the package will be removed. + */ + public void notifyPackageDataDestroyed(String packageName, int userId) { + if (userId == UserHandle.USER_ALL) { + removePackage(packageName); + } else { + removeUserPackage(packageName, userId); + } + } } diff --git a/services/core/java/com/android/server/pm/parsing/pkg/PackageImpl.java b/services/core/java/com/android/server/pm/parsing/pkg/PackageImpl.java index 1778e57ce4ae..d7c4a09d045c 100644 --- a/services/core/java/com/android/server/pm/parsing/pkg/PackageImpl.java +++ b/services/core/java/com/android/server/pm/parsing/pkg/PackageImpl.java @@ -1810,6 +1810,11 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, } @Override + public boolean isAllowUpdateOwnership() { + return getBoolean2(Booleans2.ALLOW_UPDATE_OWNERSHIP); + } + + @Override public boolean isVmSafeMode() { return getBoolean(Booleans.VM_SAFE_MODE); } @@ -2513,6 +2518,11 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, } @Override + public PackageImpl setAllowUpdateOwnership(boolean value) { + return setBoolean2(Booleans2.ALLOW_UPDATE_OWNERSHIP, value); + } + + @Override public PackageImpl sortActivities() { Collections.sort(this.activities, ORDER_COMPARATOR); return this; @@ -3726,5 +3736,6 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, private static final long STUB = 1L; private static final long APEX = 1L << 1; + private static final long ALLOW_UPDATE_OWNERSHIP = 1L << 2; } } diff --git a/services/core/java/com/android/server/pm/permission/Permission.java b/services/core/java/com/android/server/pm/permission/Permission.java index f3b9246de371..c81d6d7d0918 100644 --- a/services/core/java/com/android/server/pm/permission/Permission.java +++ b/services/core/java/com/android/server/pm/permission/Permission.java @@ -105,6 +105,15 @@ public final class Permission { mType = type; } + public Permission(@NonNull PermissionInfo permissionInfo, @PermissionType int type, + boolean reconciled, int uid, int[] gids, boolean gidsPerUser) { + this(permissionInfo, type); + mReconciled = reconciled; + mUid = uid; + mGids = gids; + mGidsPerUser = gidsPerUser; + } + @NonNull public PermissionInfo getPermissionInfo() { return mPermissionInfo; diff --git a/services/core/java/com/android/server/pm/pkg/AndroidPackage.java b/services/core/java/com/android/server/pm/pkg/AndroidPackage.java index 78091bc49449..ad738730598f 100644 --- a/services/core/java/com/android/server/pm/pkg/AndroidPackage.java +++ b/services/core/java/com/android/server/pm/pkg/AndroidPackage.java @@ -1483,4 +1483,10 @@ public interface AndroidPackage { * @hide */ boolean isVisibleToInstantApps(); + + /** + * @see R.styleable#AndroidManifest_allowUpdateOwnership + * @hide + */ + boolean isAllowUpdateOwnership(); } 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 4a8ef963959b..5947d4735faa 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 @@ -291,6 +291,15 @@ public class PackageStateMutator { return this; } + @NonNull + @Override + public PackageStateWrite setUpdateOwner(@NonNull String updateOwnerPackageName) { + if (mState != null) { + mState.setUpdateOwnerPackage(updateOwnerPackageName); + } + return this; + } + private static class UserStateWriteWrapper implements PackageUserStateWrite { @Nullable 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 dc9cd3b6ceb7..c610c02a6e9c 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 @@ -57,4 +57,7 @@ public interface PackageStateWrite { @NonNull PackageStateWrite setInstaller(@Nullable String installerPackageName, int installerPackageUid); + + @NonNull + PackageStateWrite setUpdateOwner(@Nullable String updateOwnerPackageName); } diff --git a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackage.java b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackage.java index 69f2716a6c6e..bb36758f1e77 100644 --- a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackage.java +++ b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackage.java @@ -387,6 +387,8 @@ public interface ParsingPackage { ParsingPackage setLocaleConfigRes(int localeConfigRes); + ParsingPackage setAllowUpdateOwnership(boolean value); + /** * Sets the trusted host certificates of apps that are allowed to embed activities of this * application. diff --git a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java index 995b9e58b3e3..e7159dbf3130 100644 --- a/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java +++ b/services/core/java/com/android/server/pm/pkg/parsing/ParsingPackageUtils.java @@ -219,6 +219,7 @@ public class ParsingPackageUtils { public static final int PARSE_DEFAULT_INSTALL_LOCATION = PackageInfo.INSTALL_LOCATION_UNSPECIFIED; public static final int PARSE_DEFAULT_TARGET_SANDBOX = 1; + public static final boolean PARSE_DEFAULT_ALLOW_UPDATE_OWNERSHIP = true; /** * If set to true, we will only allow package files that exactly match the DTD. Otherwise, we @@ -883,7 +884,9 @@ public class ParsingPackageUtils { .setTargetSandboxVersion(anInteger(PARSE_DEFAULT_TARGET_SANDBOX, R.styleable.AndroidManifest_targetSandboxVersion, sa)) /* Set the global "on SD card" flag */ - .setExternalStorage((flags & PARSE_EXTERNAL_STORAGE) != 0); + .setExternalStorage((flags & PARSE_EXTERNAL_STORAGE) != 0) + .setAllowUpdateOwnership(bool(PARSE_DEFAULT_ALLOW_UPDATE_OWNERSHIP, + R.styleable.AndroidManifest_allowUpdateOwnership, sa)); boolean foundApp = false; final int depth = parser.getDepth(); diff --git a/services/core/java/com/android/server/timedetector/NetworkTimeUpdateService.java b/services/core/java/com/android/server/timedetector/NetworkTimeUpdateService.java index 41824de5953c..b0d301e42634 100644 --- a/services/core/java/com/android/server/timedetector/NetworkTimeUpdateService.java +++ b/services/core/java/com/android/server/timedetector/NetworkTimeUpdateService.java @@ -16,6 +16,7 @@ package com.android.server.timedetector; +import android.annotation.ElapsedRealtimeLong; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; @@ -39,12 +40,14 @@ import android.os.ResultReceiver; import android.os.ShellCallback; import android.os.SystemClock; import android.provider.Settings; +import android.util.IndentingPrintWriter; import android.util.LocalLog; import android.util.Log; import android.util.NtpTrustedTime; import android.util.NtpTrustedTime.TimeResult; import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.DumpUtils; import com.android.server.LocalServices; @@ -52,12 +55,17 @@ import java.io.FileDescriptor; import java.io.PrintWriter; import java.time.Duration; import java.util.Objects; +import java.util.function.Supplier; /** - * Monitors the network time. If looking up the network time fails for some reason, it tries a few - * times with a short interval and then resets to checking on longer intervals. + * Refreshes network time periodically, when network connectivity becomes available and when the + * user enables automatic time detection. * - * <p>When available, the time is always suggested to the {@link + * <p>For periodic requests, this service attempts to leave an interval between successful requests. + * If a request fails, it retries a number of times with a "short" interval and then resets to the + * normal interval. The process then repeats. + * + * <p>When a valid network time is available, the time is always suggested to the {@link * com.android.server.timedetector.TimeDetectorService} where it may be used to set the device * system clock, depending on user settings and what other signals are available. */ @@ -72,25 +80,11 @@ public class NetworkTimeUpdateService extends Binder { private final Object mLock = new Object(); private final Context mContext; - private final NtpTrustedTime mNtpTrustedTime; - private final AlarmManager mAlarmManager; - private final TimeDetectorInternal mTimeDetectorInternal; private final ConnectivityManager mCM; - private final PendingIntent mPendingPollIntent; private final PowerManager.WakeLock mWakeLock; - - // Normal polling frequency - private final int mNormalPollingIntervalMillis; - // Try-again polling interval, in case the network request failed - private final int mShortPollingIntervalMillis; - // Number of times to try again - private final int mTryAgainTimesMax; - - /** - * A log that records the decisions to fetch a network time update. - * This is logged in bug reports to assist with debugging issues with network time suggestions. - */ - private final LocalLog mLocalLog = new LocalLog(30, false /* useLocalTimestamps */); + private final NtpTrustedTime mNtpTrustedTime; + private final Engine.RefreshCallbacks mRefreshCallbacks; + private final Engine mEngine; // Blocking NTP lookup is done using this handler private final Handler mHandler; @@ -100,33 +94,43 @@ public class NetworkTimeUpdateService extends Binder { @Nullable private Network mDefaultNetwork = null; - // Keeps track of how many quick attempts were made to fetch NTP time. - // During bootup, the network may not have been up yet, or it's taking time for the - // connection to happen. - // This field is only updated and accessed by the mHandler thread (except dump()). - @GuardedBy("mLock") - private int mTryAgainCounter; - public NetworkTimeUpdateService(@NonNull Context context) { mContext = Objects.requireNonNull(context); - mAlarmManager = mContext.getSystemService(AlarmManager.class); - mTimeDetectorInternal = LocalServices.getService(TimeDetectorInternal.class); mCM = mContext.getSystemService(ConnectivityManager.class); mWakeLock = context.getSystemService(PowerManager.class).newWakeLock( PowerManager.PARTIAL_WAKE_LOCK, TAG); mNtpTrustedTime = NtpTrustedTime.getInstance(context); - mTryAgainTimesMax = mContext.getResources().getInteger( + Supplier<Long> elapsedRealtimeMillisSupplier = SystemClock::elapsedRealtime; + int tryAgainTimesMax = mContext.getResources().getInteger( com.android.internal.R.integer.config_ntpRetry); - mNormalPollingIntervalMillis = mContext.getResources().getInteger( + int normalPollingIntervalMillis = mContext.getResources().getInteger( com.android.internal.R.integer.config_ntpPollingInterval); - mShortPollingIntervalMillis = mContext.getResources().getInteger( + int shortPollingIntervalMillis = mContext.getResources().getInteger( com.android.internal.R.integer.config_ntpPollingIntervalShorter); + mEngine = new EngineImpl(elapsedRealtimeMillisSupplier, normalPollingIntervalMillis, + shortPollingIntervalMillis, tryAgainTimesMax, mNtpTrustedTime); + AlarmManager alarmManager = mContext.getSystemService(AlarmManager.class); + TimeDetectorInternal timeDetectorInternal = + LocalServices.getService(TimeDetectorInternal.class); // Broadcast alarms sent by system are immutable Intent pollIntent = new Intent(ACTION_POLL, null); - mPendingPollIntent = PendingIntent.getBroadcast(mContext, POLL_REQUEST, + PendingIntent pendingPollIntent = PendingIntent.getBroadcast(mContext, POLL_REQUEST, pollIntent, PendingIntent.FLAG_IMMUTABLE); + mRefreshCallbacks = new Engine.RefreshCallbacks() { + @Override + public void scheduleNextRefresh(@ElapsedRealtimeLong long elapsedRealtimeMillis) { + alarmManager.cancel(pendingPollIntent); + alarmManager.set( + AlarmManager.ELAPSED_REALTIME, elapsedRealtimeMillis, pendingPollIntent); + } + + @Override + public void submitSuggestion(NetworkTimeSuggestion suggestion) { + timeDetectorInternal.suggestNetworkTime(suggestion); + } + }; HandlerThread thread = new HandlerThread(TAG); thread.start(); @@ -217,12 +221,7 @@ public class NetworkTimeUpdateService extends Binder { } if (network == null) return false; - boolean success = mNtpTrustedTime.forceRefresh(network); - if (success) { - makeNetworkTimeSuggestion(mNtpTrustedTime.getCachedTimeResult(), - "Origin: NetworkTimeUpdateService: forceRefreshForTests"); - } - return success; + return mEngine.forceRefreshForTests(network, mRefreshCallbacks); } finally { Binder.restoreCallingIdentity(token); } @@ -238,96 +237,12 @@ public class NetworkTimeUpdateService extends Binder { mWakeLock.acquire(); try { - onPollNetworkTimeUnderWakeLock(network, reason); + mEngine.refreshIfRequiredAndReschedule(network, reason, mRefreshCallbacks); } finally { mWakeLock.release(); } } - private void onPollNetworkTimeUnderWakeLock( - @NonNull Network network, @NonNull String reason) { - long currentElapsedRealtimeMillis = SystemClock.elapsedRealtime(); - - final int maxNetworkTimeAgeMillis = mNormalPollingIntervalMillis; - // Force an NTP fix when outdated - NtpTrustedTime.TimeResult cachedNtpResult = mNtpTrustedTime.getCachedTimeResult(); - if (cachedNtpResult == null - || cachedNtpResult.getAgeMillis(currentElapsedRealtimeMillis) - >= maxNetworkTimeAgeMillis) { - if (DBG) Log.d(TAG, "Stale NTP fix; forcing refresh using network=" + network); - boolean success = mNtpTrustedTime.forceRefresh(network); - if (success) { - synchronized (mLock) { - mTryAgainCounter = 0; - } - } else { - String logMsg = "forceRefresh() returned false:" - + " cachedNtpResult=" + cachedNtpResult - + ", currentElapsedRealtimeMillis=" + currentElapsedRealtimeMillis; - - if (DBG) { - Log.d(TAG, logMsg); - } - mLocalLog.log(logMsg); - } - - cachedNtpResult = mNtpTrustedTime.getCachedTimeResult(); - } - - if (cachedNtpResult != null - && cachedNtpResult.getAgeMillis(currentElapsedRealtimeMillis) - < maxNetworkTimeAgeMillis) { - // Obtained fresh fix; schedule next normal update - scheduleNextRefresh(mNormalPollingIntervalMillis - - cachedNtpResult.getAgeMillis(currentElapsedRealtimeMillis)); - - makeNetworkTimeSuggestion(cachedNtpResult, reason); - } else { - synchronized (mLock) { - // No fresh fix; schedule retry - mTryAgainCounter++; - if (mTryAgainTimesMax < 0 || mTryAgainCounter <= mTryAgainTimesMax) { - scheduleNextRefresh(mShortPollingIntervalMillis); - } else { - // Try much later - String logMsg = "mTryAgainTimesMax exceeded," - + " cachedNtpResult=" + cachedNtpResult; - if (DBG) { - Log.d(TAG, logMsg); - } - mLocalLog.log(logMsg); - mTryAgainCounter = 0; - - scheduleNextRefresh(mNormalPollingIntervalMillis); - } - } - } - } - - /** Suggests the time to the time detector. It may choose use it to set the system clock. */ - private void makeNetworkTimeSuggestion( - @NonNull TimeResult ntpResult, @NonNull String debugInfo) { - UnixEpochTime timeSignal = new UnixEpochTime( - ntpResult.getElapsedRealtimeMillis(), ntpResult.getTimeMillis()); - NetworkTimeSuggestion timeSuggestion = - new NetworkTimeSuggestion(timeSignal, ntpResult.getUncertaintyMillis()); - timeSuggestion.addDebugInfo(debugInfo); - timeSuggestion.addDebugInfo(ntpResult.toString()); - mTimeDetectorInternal.suggestNetworkTime(timeSuggestion); - } - - /** - * Cancel old alarm and starts a new one for the specified interval. - * - * @param delayMillis when to trigger the alarm, starting from now. - */ - private void scheduleNextRefresh(long delayMillis) { - mAlarmManager.cancel(mPendingPollIntent); - long now = SystemClock.elapsedRealtime(); - long next = now + delayMillis; - mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, next, mPendingPollIntent); - } - // All callbacks will be invoked using mHandler because of how the callback is registered. private class NetworkTimeUpdateCallback extends NetworkCallback { @Override @@ -385,21 +300,10 @@ public class NetworkTimeUpdateService extends Binder { protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return; - pw.println("mNormalPollingIntervalMillis=" - + Duration.ofMillis(mNormalPollingIntervalMillis)); - pw.println("mShortPollingIntervalMillis=" - + Duration.ofMillis(mShortPollingIntervalMillis)); - pw.println("mTryAgainTimesMax=" + mTryAgainTimesMax); synchronized (mLock) { pw.println("mDefaultNetwork=" + mDefaultNetwork); - pw.println("mTryAgainCounter=" + mTryAgainCounter); } - pw.println(); - pw.println("NtpTrustedTime:"); - mNtpTrustedTime.dump(pw); - pw.println(); - pw.println("Local logs:"); - mLocalLog.dump(fd, pw, args); + mEngine.dump(pw); pw.println(); } @@ -409,4 +313,204 @@ public class NetworkTimeUpdateService extends Binder { new NetworkTimeUpdateServiceShellCommand(this).exec( this, in, out, err, args, callback, resultReceiver); } + + /** + * The interface the service uses to interact with the time refresh logic. + * Extracted for testing. + */ + @VisibleForTesting + interface Engine { + interface RefreshCallbacks { + void scheduleNextRefresh(@ElapsedRealtimeLong long elapsedRealtimeMillis); + + void submitSuggestion(@NonNull NetworkTimeSuggestion suggestion); + } + + /** + * Forces the engine to refresh the network time (for tests). See {@link + * NetworkTimeUpdateService#forceRefreshForTests()}. This is a blocking call. This method + * must not schedule any calls. + */ + boolean forceRefreshForTests( + @NonNull Network network, @NonNull RefreshCallbacks refreshCallbacks); + + /** + * Attempts to refresh the network time if required, i.e. if there isn't a recent-enough + * network time available. It must also schedule the next call. This is a blocking call. + * + * @param network the network to use + * @param reason the reason for the refresh (for logging) + */ + void refreshIfRequiredAndReschedule(@NonNull Network network, @NonNull String reason, + @NonNull RefreshCallbacks refreshCallbacks); + + void dump(@NonNull PrintWriter pw); + } + + @VisibleForTesting + static class EngineImpl implements Engine { + + /** + * A log that records the decisions to fetch a network time update. + * This is logged in bug reports to assist with debugging issues with network time + * suggestions. + */ + @NonNull + private final LocalLog mLocalDebugLog = new LocalLog(30, false /* useLocalTimestamps */); + + private final int mNormalPollingIntervalMillis; + private final int mShortPollingIntervalMillis; + private final int mTryAgainTimesMax; + private final NtpTrustedTime mNtpTrustedTime; + + /** + * Keeps track of successive time refresh failures have occurred. This is reset to zero when + * time refresh is successful or if the number exceeds (a non-negative) {@link + * #mTryAgainTimesMax}. + */ + @GuardedBy("this") + private int mTryAgainCounter; + + private final Supplier<Long> mElapsedRealtimeMillisSupplier; + + @VisibleForTesting + EngineImpl(@NonNull Supplier<Long> elapsedRealtimeMillisSupplier, + int normalPollingIntervalMillis, int shortPollingIntervalMillis, + int tryAgainTimesMax, @NonNull NtpTrustedTime ntpTrustedTime) { + mElapsedRealtimeMillisSupplier = Objects.requireNonNull(elapsedRealtimeMillisSupplier); + mNormalPollingIntervalMillis = normalPollingIntervalMillis; + mShortPollingIntervalMillis = shortPollingIntervalMillis; + mTryAgainTimesMax = tryAgainTimesMax; + mNtpTrustedTime = Objects.requireNonNull(ntpTrustedTime); + } + + @Override + public boolean forceRefreshForTests( + @NonNull Network network, @NonNull RefreshCallbacks refreshCallbacks) { + boolean success = mNtpTrustedTime.forceRefresh(network); + logToDebugAndDumpsys("forceRefreshForTests: success=" + success); + + if (success) { + makeNetworkTimeSuggestion(mNtpTrustedTime.getCachedTimeResult(), + "EngineImpl.forceRefreshForTests()", refreshCallbacks); + } + return success; + } + + @Override + public void refreshIfRequiredAndReschedule( + @NonNull Network network, @NonNull String reason, + @NonNull RefreshCallbacks refreshCallbacks) { + long currentElapsedRealtimeMillis = mElapsedRealtimeMillisSupplier.get(); + + final int maxNetworkTimeAgeMillis = mNormalPollingIntervalMillis; + // Force an NTP fix when outdated + NtpTrustedTime.TimeResult initialTimeResult = mNtpTrustedTime.getCachedTimeResult(); + if (calculateTimeResultAgeMillis(initialTimeResult, currentElapsedRealtimeMillis) + >= maxNetworkTimeAgeMillis) { + if (DBG) Log.d(TAG, "Stale NTP fix; forcing refresh using network=" + network); + boolean successful = mNtpTrustedTime.forceRefresh(network); + if (successful) { + synchronized (this) { + mTryAgainCounter = 0; + } + } else { + String logMsg = "forceRefresh() returned false:" + + " initialTimeResult=" + initialTimeResult + + ", currentElapsedRealtimeMillis=" + currentElapsedRealtimeMillis; + logToDebugAndDumpsys(logMsg); + } + } + + synchronized (this) { + long nextPollDelayMillis; + NtpTrustedTime.TimeResult latestTimeResult = mNtpTrustedTime.getCachedTimeResult(); + if (calculateTimeResultAgeMillis(latestTimeResult, currentElapsedRealtimeMillis) + < maxNetworkTimeAgeMillis) { + // Obtained fresh fix; schedule next normal update + nextPollDelayMillis = mNormalPollingIntervalMillis + - latestTimeResult.getAgeMillis(currentElapsedRealtimeMillis); + + makeNetworkTimeSuggestion(latestTimeResult, reason, refreshCallbacks); + } else { + // No fresh fix; schedule retry + mTryAgainCounter++; + if (mTryAgainTimesMax < 0 || mTryAgainCounter <= mTryAgainTimesMax) { + nextPollDelayMillis = mShortPollingIntervalMillis; + } else { + // Try much later + mTryAgainCounter = 0; + + nextPollDelayMillis = mNormalPollingIntervalMillis; + } + } + long nextRefreshElapsedRealtimeMillis = + currentElapsedRealtimeMillis + nextPollDelayMillis; + refreshCallbacks.scheduleNextRefresh(nextRefreshElapsedRealtimeMillis); + + logToDebugAndDumpsys("refreshIfRequiredAndReschedule:" + + " network=" + network + + ", reason=" + reason + + ", currentElapsedRealtimeMillis=" + currentElapsedRealtimeMillis + + ", initialTimeResult=" + initialTimeResult + + ", latestTimeResult=" + latestTimeResult + + ", mTryAgainCounter=" + mTryAgainCounter + + ", nextPollDelayMillis=" + nextPollDelayMillis + + ", nextRefreshElapsedRealtimeMillis=" + + Duration.ofMillis(nextRefreshElapsedRealtimeMillis) + + " (" + nextRefreshElapsedRealtimeMillis + ")"); + } + } + + private static long calculateTimeResultAgeMillis( + @Nullable TimeResult timeResult, + @ElapsedRealtimeLong long currentElapsedRealtimeMillis) { + return timeResult == null ? Long.MAX_VALUE + : timeResult.getAgeMillis(currentElapsedRealtimeMillis); + } + + /** Suggests the time to the time detector. It may choose use it to set the system clock. */ + private void makeNetworkTimeSuggestion(@NonNull TimeResult ntpResult, + @NonNull String debugInfo, @NonNull RefreshCallbacks refreshCallbacks) { + UnixEpochTime timeSignal = new UnixEpochTime( + ntpResult.getElapsedRealtimeMillis(), ntpResult.getTimeMillis()); + NetworkTimeSuggestion timeSuggestion = + new NetworkTimeSuggestion(timeSignal, ntpResult.getUncertaintyMillis()); + timeSuggestion.addDebugInfo(debugInfo); + timeSuggestion.addDebugInfo(ntpResult.toString()); + refreshCallbacks.submitSuggestion(timeSuggestion); + } + + @Override + public void dump(PrintWriter pw) { + IndentingPrintWriter ipw = new IndentingPrintWriter(pw); + ipw.println("mNormalPollingIntervalMillis=" + mNormalPollingIntervalMillis); + ipw.println("mShortPollingIntervalMillis=" + mShortPollingIntervalMillis); + ipw.println("mTryAgainTimesMax=" + mTryAgainTimesMax); + + synchronized (this) { + ipw.println("mTryAgainCounter=" + mTryAgainCounter); + } + ipw.println(); + + ipw.println("NtpTrustedTime:"); + ipw.increaseIndent(); + mNtpTrustedTime.dump(ipw); + ipw.decreaseIndent(); + ipw.println(); + + ipw.println("Debug log:"); + ipw.increaseIndent(); + mLocalDebugLog.dump(ipw); + ipw.decreaseIndent(); + ipw.println(); + } + + private void logToDebugAndDumpsys(String logMsg) { + if (DBG) { + Log.d(TAG, logMsg); + } + mLocalDebugLog.log(logMsg); + } + } } diff --git a/services/core/java/com/android/server/timedetector/TimeDetectorService.java b/services/core/java/com/android/server/timedetector/TimeDetectorService.java index 1be9074e079a..3e2395303354 100644 --- a/services/core/java/com/android/server/timedetector/TimeDetectorService.java +++ b/services/core/java/com/android/server/timedetector/TimeDetectorService.java @@ -143,7 +143,7 @@ public final class TimeDetectorService extends ITimeDetectorService.Stub return getTimeCapabilitiesAndConfig(userId); } - TimeCapabilitiesAndConfig getTimeCapabilitiesAndConfig(@UserIdInt int userId) { + private TimeCapabilitiesAndConfig getTimeCapabilitiesAndConfig(@UserIdInt int userId) { enforceManageTimeDetectorPermission(); final long token = mCallerIdentityInjector.clearCallingIdentity(); @@ -163,6 +163,9 @@ public final class TimeDetectorService extends ITimeDetectorService.Stub return updateConfiguration(callingUserId, configuration); } + /** + * Updates the user's configuration. Exposed for use by {@link TimeDetectorShellCommand}. + */ boolean updateConfiguration(@UserIdInt int userId, @NonNull TimeConfiguration configuration) { // Resolve constants like USER_CURRENT to the true user ID as needed. int resolvedUserId = ActivityManager.handleIncomingUser(Binder.getCallingPid(), @@ -256,7 +259,7 @@ public final class TimeDetectorService extends ITimeDetectorService.Stub } } - void handleConfigurationInternalChangedOnHandlerThread() { + private void handleConfigurationInternalChangedOnHandlerThread() { // Configuration has changed, but each user may have a different view of the configuration. // It's possible that this will cause unnecessary notifications but that shouldn't be a // problem. @@ -287,6 +290,10 @@ public final class TimeDetectorService extends ITimeDetectorService.Stub } } + /** + * Sets the system time state. See {@link TimeState} for details. For use by {@link + * TimeDetectorShellCommand}. + */ void setTimeState(@NonNull TimeState timeState) { enforceManageTimeDetectorPermission(); @@ -353,6 +360,9 @@ public final class TimeDetectorService extends ITimeDetectorService.Stub } } + /** + * Suggests network time with permission checks. For use by {@link TimeDetectorShellCommand}. + */ void suggestNetworkTime(@NonNull NetworkTimeSuggestion timeSignal) { enforceSuggestNetworkTimePermission(); Objects.requireNonNull(timeSignal); @@ -360,6 +370,23 @@ public final class TimeDetectorService extends ITimeDetectorService.Stub mHandler.post(() -> mTimeDetectorStrategy.suggestNetworkTime(timeSignal)); } + /** + * Clears the cached network time information. For use during tests to simulate when no network + * time has been made available. For use by {@link TimeDetectorShellCommand}. + * + * <p>This operation takes place in the calling thread. + */ + void clearNetworkTime() { + enforceSuggestNetworkTimePermission(); + + final long token = Binder.clearCallingIdentity(); + try { + mTimeDetectorStrategy.clearLatestNetworkSuggestion(); + } finally { + Binder.restoreCallingIdentity(token); + } + } + @Override public UnixEpochTime latestNetworkTime() { NetworkTimeSuggestion suggestion = getLatestNetworkSuggestion(); @@ -388,6 +415,9 @@ public final class TimeDetectorService extends ITimeDetectorService.Stub } } + /** + * Suggests GNSS time with permission checks. For use by {@link TimeDetectorShellCommand}. + */ void suggestGnssTime(@NonNull GnssTimeSuggestion timeSignal) { enforceSuggestGnssTimePermission(); Objects.requireNonNull(timeSignal); diff --git a/services/core/java/com/android/server/timedetector/TimeDetectorShellCommand.java b/services/core/java/com/android/server/timedetector/TimeDetectorShellCommand.java index 990c00feae16..cce570986168 100644 --- a/services/core/java/com/android/server/timedetector/TimeDetectorShellCommand.java +++ b/services/core/java/com/android/server/timedetector/TimeDetectorShellCommand.java @@ -15,7 +15,9 @@ */ package com.android.server.timedetector; +import static android.app.timedetector.TimeDetector.SHELL_COMMAND_CLEAR_NETWORK_TIME; import static android.app.timedetector.TimeDetector.SHELL_COMMAND_CONFIRM_TIME; +import static android.app.timedetector.TimeDetector.SHELL_COMMAND_GET_NETWORK_TIME; import static android.app.timedetector.TimeDetector.SHELL_COMMAND_GET_TIME_STATE; import static android.app.timedetector.TimeDetector.SHELL_COMMAND_IS_AUTO_DETECTION_ENABLED; import static android.app.timedetector.TimeDetector.SHELL_COMMAND_SERVICE_NAME; @@ -70,6 +72,10 @@ class TimeDetectorShellCommand extends ShellCommand { return runSuggestTelephonyTime(); case SHELL_COMMAND_SUGGEST_NETWORK_TIME: return runSuggestNetworkTime(); + case SHELL_COMMAND_GET_NETWORK_TIME: + return runGetNetworkTime(); + case SHELL_COMMAND_CLEAR_NETWORK_TIME: + return runClearNetworkTime(); case SHELL_COMMAND_SUGGEST_GNSS_TIME: return runSuggestGnssTime(); case SHELL_COMMAND_SUGGEST_EXTERNAL_TIME: @@ -122,6 +128,18 @@ class TimeDetectorShellCommand extends ShellCommand { mInterface::suggestNetworkTime); } + private int runGetNetworkTime() { + NetworkTimeSuggestion networkTimeSuggestion = mInterface.getLatestNetworkSuggestion(); + final PrintWriter pw = getOutPrintWriter(); + pw.println(networkTimeSuggestion); + return 0; + } + + private int runClearNetworkTime() { + mInterface.clearNetworkTime(); + return 0; + } + private int runSuggestGnssTime() { return runSuggestTime( () -> GnssTimeSuggestion.parseCommandLineArg(this), @@ -196,6 +214,10 @@ class TimeDetectorShellCommand extends ShellCommand { pw.printf(" Sets the current time state for tests.\n"); pw.printf(" %s <unix epoch time options>\n", SHELL_COMMAND_CONFIRM_TIME); pw.printf(" Tries to confirms the time, raising the confidence.\n"); + pw.printf(" %s\n", SHELL_COMMAND_GET_NETWORK_TIME); + pw.printf(" Prints the network time information held by the detector.\n"); + pw.printf(" %s\n", SHELL_COMMAND_CLEAR_NETWORK_TIME); + pw.printf(" Clears the network time information held by the detector.\n"); pw.println(); ManualTimeSuggestion.printCommandLineOpts(pw); pw.println(); diff --git a/services/core/java/com/android/server/timedetector/TimeDetectorStrategy.java b/services/core/java/com/android/server/timedetector/TimeDetectorStrategy.java index 03f236d9b30d..9dca6ec26d29 100644 --- a/services/core/java/com/android/server/timedetector/TimeDetectorStrategy.java +++ b/services/core/java/com/android/server/timedetector/TimeDetectorStrategy.java @@ -18,6 +18,7 @@ package com.android.server.timedetector; import android.annotation.IntDef; import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.UserIdInt; import android.app.time.ExternalTimeSuggestion; import android.app.time.TimeState; @@ -103,6 +104,20 @@ public interface TimeDetectorStrategy extends Dumpable { /** Processes the suggested time from network sources. */ void suggestNetworkTime(@NonNull NetworkTimeSuggestion timeSuggestion); + /** + * Returns the latest (accepted) network time suggestion. Returns {@code null} if there isn't + * one. + */ + @Nullable + NetworkTimeSuggestion getLatestNetworkSuggestion(); + + /** + * Clears the latest network time suggestion, leaving none. The remaining time signals from + * other sources will be reassessed causing the device's time to be updated if config and + * settings allow. + */ + void clearLatestNetworkSuggestion(); + /** Processes the suggested time from gnss sources. */ void suggestGnssTime(@NonNull GnssTimeSuggestion timeSuggestion); diff --git a/services/core/java/com/android/server/timedetector/TimeDetectorStrategyImpl.java b/services/core/java/com/android/server/timedetector/TimeDetectorStrategyImpl.java index 13ec75329e39..09bb8036406d 100644 --- a/services/core/java/com/android/server/timedetector/TimeDetectorStrategyImpl.java +++ b/services/core/java/com/android/server/timedetector/TimeDetectorStrategyImpl.java @@ -316,6 +316,21 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy { } @Override + @Nullable + public synchronized NetworkTimeSuggestion getLatestNetworkSuggestion() { + return mLastNetworkSuggestion.get(); + } + + @Override + public synchronized void clearLatestNetworkSuggestion() { + mLastNetworkSuggestion.set(null); + + // The loss of network time may change the time signal to use to set the system clock. + String reason = "Network time cleared"; + doAutoTimeDetection(reason); + } + + @Override @NonNull public synchronized TimeState getTimeState() { boolean userShouldConfirmTime = mEnvironment.systemClockConfidence() < TIME_CONFIDENCE_HIGH; @@ -1068,15 +1083,6 @@ public final class TimeDetectorStrategyImpl implements TimeDetectorStrategy { */ @VisibleForTesting @Nullable - public synchronized NetworkTimeSuggestion getLatestNetworkSuggestion() { - return mLastNetworkSuggestion.get(); - } - - /** - * A method used to inspect state during tests. Not intended for general use. - */ - @VisibleForTesting - @Nullable public synchronized GnssTimeSuggestion getLatestGnssSuggestion() { return mLastGnssSuggestion.get(); } diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 9a7b16516bb4..45ae3d8210c8 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -727,6 +727,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A /** * When set to true, the IME insets will be frozen until the next app becomes IME input target. * @see InsetsPolicy#adjustVisibilityForIme + * @see ImeInsetsSourceProvider#updateClientVisibility */ boolean mImeInsetsFrozenUntilStartInput; @@ -1576,7 +1577,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A if (newParent != null) { if (isState(RESUMED)) { newParent.setResumedActivity(this, "onParentChanged"); - mImeInsetsFrozenUntilStartInput = false; } mLetterboxUiController.onActivityParentChanged(newParent); } @@ -8874,13 +8874,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } } - @Override - void onResize() { - // Reset freezing IME insets flag when the activity resized. - mImeInsetsFrozenUntilStartInput = false; - super.onResize(); - } - private boolean applyAspectRatio(Rect outBounds, Rect containingAppBounds, Rect containingBounds) { return applyAspectRatio(outBounds, containingAppBounds, containingBounds, diff --git a/services/core/java/com/android/server/wm/ActivitySecurityModelFeatureFlags.java b/services/core/java/com/android/server/wm/ActivitySecurityModelFeatureFlags.java new file mode 100644 index 000000000000..64af9dd755ed --- /dev/null +++ b/services/core/java/com/android/server/wm/ActivitySecurityModelFeatureFlags.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm; + +import static android.provider.DeviceConfig.NAMESPACE_WINDOW_MANAGER; + +import static com.android.server.wm.ActivityStarter.ASM_RESTRICTIONS; + +import android.annotation.NonNull; +import android.app.compat.CompatChanges; +import android.content.pm.PackageManager; +import android.provider.DeviceConfig; + +import com.android.internal.annotations.GuardedBy; + +import java.util.HashSet; +import java.util.concurrent.Executor; + +/** + * Contains utility methods to query whether or not go/activity-security should be enabled + * asm_start_rules_enabled - Enable rule enforcement in ActivityStarter.java + * asm_start_rules_toasts_enabled - Show toasts when rules would block from ActivityStarter.java + * asm_start_rules_exception_list - Comma separated list of packages to exclude from the above + * 2 rules. + * TODO(b/258792202) Cleanup once ASM is ready to launch + */ +class ActivitySecurityModelFeatureFlags { + // TODO(b/230590090): Replace with public documentation once ready + static final String DOC_LINK = "go/android-asm"; + + private static final String NAMESPACE = NAMESPACE_WINDOW_MANAGER; + private static final String KEY_ASM_RESTRICTIONS_ENABLED = "asm_restrictions_enabled"; + private static final String KEY_ASM_TOASTS_ENABLED = "asm_toasts_enabled"; + private static final String KEY_ASM_EXEMPTED_PACKAGES = "asm_exempted_packages"; + private static final int VALUE_DISABLE = 0; + private static final int VALUE_ENABLE_FOR_U = 1; + private static final int VALUE_ENABLE_FOR_ALL = 2; + + private static final int DEFAULT_VALUE = VALUE_DISABLE; + private static final String DEFAULT_EXCEPTION_LIST = ""; + + private static int sAsmToastsEnabled; + private static int sAsmRestrictionsEnabled; + private static final HashSet<String> sExcludedPackageNames = new HashSet<>(); + private static PackageManager sPm; + + @GuardedBy("ActivityTaskManagerService.mGlobalLock") + static void initialize(@NonNull Executor executor, @NonNull PackageManager pm) { + updateFromDeviceConfig(); + DeviceConfig.addOnPropertiesChangedListener(NAMESPACE, executor, + properties -> updateFromDeviceConfig()); + sPm = pm; + } + + @GuardedBy("ActivityTaskManagerService.mGlobalLock") + static boolean shouldShowToast(int uid) { + return flagEnabledForUid(sAsmToastsEnabled, uid); + } + + @GuardedBy("ActivityTaskManagerService.mGlobalLock") + static boolean shouldBlockActivityStart(int uid) { + return flagEnabledForUid(sAsmRestrictionsEnabled, uid); + } + + private static boolean flagEnabledForUid(int flag, int uid) { + boolean flagEnabled = flag == VALUE_ENABLE_FOR_ALL + || (flag == VALUE_ENABLE_FOR_U + && CompatChanges.isChangeEnabled(ASM_RESTRICTIONS, uid)); + + if (flagEnabled) { + String[] packageNames = sPm.getPackagesForUid(uid); + for (int i = 0; i < packageNames.length; i++) { + if (sExcludedPackageNames.contains(packageNames[i])) { + return false; + } + } + return true; + } + + return false; + } + + private static void updateFromDeviceConfig() { + sAsmToastsEnabled = DeviceConfig.getInt(NAMESPACE, KEY_ASM_TOASTS_ENABLED, + DEFAULT_VALUE); + sAsmRestrictionsEnabled = DeviceConfig.getInt(NAMESPACE, KEY_ASM_RESTRICTIONS_ENABLED, + DEFAULT_VALUE); + + String rawExceptionList = DeviceConfig.getString(NAMESPACE, + KEY_ASM_EXEMPTED_PACKAGES, DEFAULT_EXCEPTION_LIST); + sExcludedPackageNames.clear(); + String[] packages = rawExceptionList.split(","); + for (String packageName : packages) { + String packageNameTrimmed = packageName.trim(); + if (!packageNameTrimmed.isEmpty()) { + sExcludedPackageNames.add(packageNameTrimmed); + } + } + } +} diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index 86493ebde535..40432dc49790 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -56,7 +56,7 @@ import static android.view.Display.DEFAULT_DISPLAY; import static android.view.WindowManager.TRANSIT_NONE; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_FRONT; -import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT; +import static android.window.TaskFragmentOperation.OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_CONFIGURATION; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_TASKS; @@ -125,6 +125,7 @@ import android.service.voice.IVoiceInteractionSession; import android.text.TextUtils; import android.util.Pools.SynchronizedPool; import android.util.Slog; +import android.widget.Toast; import android.window.RemoteTransition; import com.android.internal.annotations.VisibleForTesting; @@ -132,6 +133,7 @@ import com.android.internal.app.HeavyWeightSwitcherActivity; import com.android.internal.app.IVoiceInteractor; import com.android.internal.protolog.common.ProtoLog; import com.android.internal.util.FrameworkStatsLog; +import com.android.server.UiThread; import com.android.server.am.PendingIntentRecord; import com.android.server.pm.InstantAppResolver; import com.android.server.power.ShutdownCheckPoints; @@ -168,6 +170,13 @@ class ActivityStarter { @EnabledSince(targetSdkVersion = Build.VERSION_CODES.TIRAMISU) static final long ENABLE_PENDING_INTENT_BAL_OPTION = 192341120L; + /** + * Feature flag for go/activity-security rules + */ + @ChangeId + @EnabledSince(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + static final long ASM_RESTRICTIONS = 230590090L; + private final ActivityTaskManagerService mService; private final RootWindowContainer mRootWindowContainer; private final ActivityTaskSupervisor mSupervisor; @@ -1859,7 +1868,7 @@ class ActivityStarter { } if (!checkActivitySecurityModel(r, newTask, targetTask)) { - return START_SUCCESS; + return START_ABORTED; } return START_SUCCESS; @@ -1925,11 +1934,6 @@ class ActivityStarter { : targetTask.getActivity(ar -> !ar.isState(FINISHING) && !ar.isAlwaysOnTop()); - Slog.i(TAG, "Launching r: " + r - + " from background: " + mSourceRecord - + ". New task: " + newTask - + ". Top activity: " + targetTopActivity); - int action = newTask || mSourceRecord == null ? FrameworkStatsLog.ACTIVITY_ACTION_BLOCKED__ACTION__ACTIVITY_START_NEW_TASK : (mSourceRecord.getTask().equals(targetTask) @@ -1965,7 +1969,29 @@ class ActivityStarter { && !targetTask.equals(mSourceRecord.getTask()) && targetTask.isVisible() ); - return false; + boolean shouldBlockActivityStart = + ActivitySecurityModelFeatureFlags.shouldBlockActivityStart(mCallingUid); + + if (ActivitySecurityModelFeatureFlags.shouldShowToast(mCallingUid)) { + UiThread.getHandler().post(() -> Toast.makeText(mService.mContext, + (shouldBlockActivityStart + ? "Activity start blocked by " + : "Activity start would be blocked by ") + + ActivitySecurityModelFeatureFlags.DOC_LINK, + Toast.LENGTH_SHORT).show()); + } + + + if (shouldBlockActivityStart) { + Slog.e(TAG, "Abort Launching r: " + r + + " as source: " + mSourceRecord + + "is in background. New task: " + newTask + + ". Top activity: " + targetTopActivity); + + return false; + } + + return true; } /** @@ -2889,7 +2915,7 @@ class ActivityStarter { if (taskFragment.isOrganized()) { mService.mWindowOrganizerController.sendTaskFragmentOperationFailure( taskFragment.getTaskFragmentOrganizer(), mRequest.errorCallbackToken, - taskFragment, HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT, + taskFragment, OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT, new SecurityException(errMsg)); } else { // If the taskFragment is not organized, just dump error message as warning logs. diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index fd6d6062fbf2..9a8ef19d1637 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -859,6 +859,8 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { mRecentTasks.onSystemReadyLocked(); mTaskSupervisor.onSystemReady(); mActivityClientController.onSystemReady(); + // TODO(b/258792202) Cleanup once ASM is ready to launch + ActivitySecurityModelFeatureFlags.initialize(mContext.getMainExecutor(), pm); } } @@ -1495,7 +1497,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { a.persistableMode = ActivityInfo.PERSIST_NEVER; a.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; a.colorMode = ActivityInfo.COLOR_MODE_DEFAULT; - a.flags |= ActivityInfo.FLAG_EXCLUDE_FROM_RECENTS; + a.flags |= ActivityInfo.FLAG_EXCLUDE_FROM_RECENTS | ActivityInfo.FLAG_NO_HISTORY; a.resizeMode = RESIZE_MODE_UNRESIZEABLE; a.configChanges = 0xffffffff; diff --git a/services/core/java/com/android/server/wm/AppTransitionController.java b/services/core/java/com/android/server/wm/AppTransitionController.java index 74d52b2c1458..d65c2f96e1ce 100644 --- a/services/core/java/com/android/server/wm/AppTransitionController.java +++ b/services/core/java/com/android/server/wm/AppTransitionController.java @@ -900,7 +900,7 @@ public class AppTransitionController { * * TODO(b/213312721): Remove this predicate and its callers once ShellTransition is enabled. */ - private static boolean isTaskViewTask(WindowContainer wc) { + static boolean isTaskViewTask(WindowContainer wc) { // We use Task#mRemoveWithTaskOrganizer to identify an embedded Task, but this is a hack and // it is not guaranteed to work this logic in the future version. return wc instanceof Task && ((Task) wc).mRemoveWithTaskOrganizer; diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index e7a5ee7f01d9..eadb11e7c6e0 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -4509,11 +4509,35 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp mImeWindowsContainer.getParent().mSurfaceControl)); updateImeControlTarget(forceUpdateImeParent); } - // Unfreeze IME insets after the new target updated, in case updateAboveInsetsState may - // deliver unrelated IME insets change to the non-IME requester. - if (target != null) { - target.unfreezeInsetsAfterStartInput(); + } + + /** + * Callback from {@link ImeInsetsSourceProvider#updateClientVisibility} for the system to + * judge whether or not to notify the IME insets provider to dispatch this reported IME client + * visibility state to the app clients when needed. + */ + boolean onImeInsetsClientVisibilityUpdate() { + boolean[] changed = new boolean[1]; + + // Unlike the IME layering target or the control target can be updated during the layout + // change, the IME input target requires to be changed after gaining the input focus. + // In case unfreezing IME insets state may too early during IME focus switching, we unfreeze + // when activities going to be visible until the input target changed, or the + // activity was the current input target that has to unfreeze after updating the IME + // client visibility. + final ActivityRecord inputTargetActivity = + mImeInputTarget != null ? mImeInputTarget.getActivityRecord() : null; + final boolean targetChanged = mImeInputTarget != mLastImeInputTarget; + if (targetChanged || inputTargetActivity != null && inputTargetActivity.isVisibleRequested() + && inputTargetActivity.mImeInsetsFrozenUntilStartInput) { + forAllActivities(r -> { + if (r.mImeInsetsFrozenUntilStartInput && r.isVisibleRequested()) { + r.mImeInsetsFrozenUntilStartInput = false; + changed[0] = true; + } + }); } + return changed[0]; } void updateImeControlTarget() { diff --git a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java index ba0413df6325..c6037dab6568 100644 --- a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java @@ -203,8 +203,11 @@ final class DisplayRotationCompatPolicy { || !shouldRefreshActivity(activity, newConfig, lastReportedConfig)) { return; } - boolean cycleThroughStop = mWmService.mLetterboxConfiguration - .isCameraCompatRefreshCycleThroughStopEnabled(); + boolean cycleThroughStop = + mWmService.mLetterboxConfiguration + .isCameraCompatRefreshCycleThroughStopEnabled() + && !activity.mLetterboxUiController + .shouldRefreshActivityViaPauseForCameraCompat(); try { activity.mLetterboxUiController.setIsRefreshAfterRotationRequested(true); ProtoLog.v(WM_DEBUG_STATES, @@ -255,7 +258,8 @@ final class DisplayRotationCompatPolicy { Configuration lastReportedConfig) { return newConfig.windowConfiguration.getDisplayRotation() != lastReportedConfig.windowConfiguration.getDisplayRotation() - && isTreatmentEnabledForActivity(activity); + && isTreatmentEnabledForActivity(activity) + && activity.mLetterboxUiController.shouldRefreshActivityForCameraCompat(); } /** @@ -294,7 +298,8 @@ final class DisplayRotationCompatPolicy { // handle dynamic changes so we shouldn't force rotate them. && activity.getRequestedOrientation() != SCREEN_ORIENTATION_NOSENSOR && activity.getRequestedOrientation() != SCREEN_ORIENTATION_LOCKED - && mCameraIdPackageBiMap.containsPackageName(activity.packageName); + && mCameraIdPackageBiMap.containsPackageName(activity.packageName) + && activity.mLetterboxUiController.shouldForceRotateForCameraCompat(); } private synchronized void notifyCameraOpened( diff --git a/services/core/java/com/android/server/wm/EmbeddedWindowController.java b/services/core/java/com/android/server/wm/EmbeddedWindowController.java index d54f77a73661..c3c727a1d879 100644 --- a/services/core/java/com/android/server/wm/EmbeddedWindowController.java +++ b/services/core/java/com/android/server/wm/EmbeddedWindowController.java @@ -335,10 +335,6 @@ class EmbeddedWindowController { } @Override - public void unfreezeInsetsAfterStartInput() { - } - - @Override public InsetsControlTarget getImeControlTarget() { return mWmService.getDefaultDisplayContentLocked().mRemoteInsetsControlTarget; } diff --git a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java index 90d0f16e6478..85938e3bfd71 100644 --- a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java +++ b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java @@ -140,10 +140,14 @@ final class ImeInsetsSourceProvider extends WindowContainerInsetsSourceProvider @Override protected boolean updateClientVisibility(InsetsControlTarget caller) { + if (caller != getControlTarget()) { + return false; + } boolean changed = super.updateClientVisibility(caller); if (changed && caller.isRequestedVisible(mSource.getType())) { reportImeDrawnForOrganizer(caller); } + changed |= mDisplayContent.onImeInsetsClientVisibilityUpdate(); return changed; } diff --git a/services/core/java/com/android/server/wm/InputTarget.java b/services/core/java/com/android/server/wm/InputTarget.java index b5ab62b6e03f..653f5f5a74e9 100644 --- a/services/core/java/com/android/server/wm/InputTarget.java +++ b/services/core/java/com/android/server/wm/InputTarget.java @@ -16,8 +16,8 @@ package com.android.server.wm; -import android.view.IWindow; import android.util.proto.ProtoOutputStream; +import android.view.IWindow; /** * Common interface between focusable objects. @@ -58,7 +58,6 @@ interface InputTarget { boolean canScreenshotIme(); ActivityRecord getActivityRecord(); - void unfreezeInsetsAfterStartInput(); boolean isInputMethodClientFocus(int uid, int pid); diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java index 0c8a6453e6fb..75ba2146267d 100644 --- a/services/core/java/com/android/server/wm/LetterboxUiController.java +++ b/services/core/java/com/android/server/wm/LetterboxUiController.java @@ -17,12 +17,18 @@ package com.android.server.wm; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION; +import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH; +import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE; import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; import static android.content.pm.ActivityInfo.screenOrientationToString; import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; +import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION; +import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH; +import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE; import static android.view.WindowManager.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION; import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__BOTTOM; @@ -132,6 +138,15 @@ final class LetterboxUiController { @Nullable private Letterbox mLetterbox; + @Nullable + private final Boolean mBooleanPropertyCameraCompatAllowForceRotation; + + @Nullable + private final Boolean mBooleanPropertyCameraCompatAllowRefresh; + + @Nullable + private final Boolean mBooleanPropertyCameraCompatEnableRefreshViaPause; + // Whether activity "refresh" was requested but not finished in // ActivityRecord#activityResumedLocked following the camera compat force rotation in // DisplayRotationCompatPolicy. @@ -154,8 +169,33 @@ final class LetterboxUiController { readComponentProperty(packageManager, mActivityRecord.packageName, mLetterboxConfiguration::isPolicyForIgnoringRequestedOrientationEnabled, PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION); + mBooleanPropertyCameraCompatAllowForceRotation = + readComponentProperty(packageManager, mActivityRecord.packageName, + () -> mLetterboxConfiguration.isCameraCompatTreatmentEnabled( + /* checkDeviceConfig */ true), + PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION); + mBooleanPropertyCameraCompatAllowRefresh = + readComponentProperty(packageManager, mActivityRecord.packageName, + () -> mLetterboxConfiguration.isCameraCompatTreatmentEnabled( + /* checkDeviceConfig */ true), + PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH); + mBooleanPropertyCameraCompatEnableRefreshViaPause = + readComponentProperty(packageManager, mActivityRecord.packageName, + () -> mLetterboxConfiguration.isCameraCompatTreatmentEnabled( + /* checkDeviceConfig */ true), + PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE); } + /** + * Reads a {@link Boolean} component property fot a given {@code packageName} and a {@code + * propertyName}. Returns {@code null} if {@code gatingCondition} is {@code false} or if the + * property isn't specified for the package. + * + * <p>Return value is {@link Boolean} rather than {@code boolean} so we can know when the + * property is unset. Particularly, when this returns {@code null}, {@link + * #shouldEnableWithOverrideAndProperty} will check the value of override for the final + * decision. + */ @Nullable private static Boolean readComponentProperty(PackageManager packageManager, String packageName, BooleanSupplier gatingCondition, String propertyName) { @@ -210,15 +250,11 @@ final class LetterboxUiController { * </ul> */ boolean shouldIgnoreRequestedOrientation(@ScreenOrientation int requestedOrientation) { - if (!mLetterboxConfiguration.isPolicyForIgnoringRequestedOrientationEnabled()) { - return false; - } - if (Boolean.FALSE.equals(mBooleanPropertyIgnoreRequestedOrientation)) { - return false; - } - if (!Boolean.TRUE.equals(mBooleanPropertyIgnoreRequestedOrientation) - && !mActivityRecord.info.isChangeEnabled( - OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION)) { + if (!shouldEnableWithOverrideAndProperty( + /* gatingCondition */ mLetterboxConfiguration + ::isPolicyForIgnoringRequestedOrientationEnabled, + OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION, + mBooleanPropertyIgnoreRequestedOrientation)) { return false; } if (mIsRelauchingAfterRequestedOrientationChanged) { @@ -262,6 +298,109 @@ final class LetterboxUiController { mIsRefreshAfterRotationRequested = isRequested; } + /** + * Whether activity is eligible for activity "refresh" after camera compat force rotation + * treatment. See {@link DisplayRotationCompatPolicy} for context. + * + * <p>This treatment is enabled when the following conditions are met: + * <ul> + * <li>Flag gating the camera compat treatment is enabled. + * <li>Activity isn't opted out by the device manufacturer with override or by the app + * developers with the component property. + * </ul> + */ + boolean shouldRefreshActivityForCameraCompat() { + return shouldEnableWithOptOutOverrideAndProperty( + /* gatingCondition */ () -> mLetterboxConfiguration + .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true), + OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH, + mBooleanPropertyCameraCompatAllowRefresh); + } + + /** + * Whether activity should be "refreshed" after the camera compat force rotation treatment + * using the "resumed -> paused -> resumed" cycle rather than the "resumed -> ... -> stopped + * -> ... -> resumed" cycle. See {@link DisplayRotationCompatPolicy} for context. + * + * <p>This treatment is enabled when the following conditions are met: + * <ul> + * <li>Flag gating the camera compat treatment is enabled. + * <li>Activity "refresh" via "resumed -> paused -> resumed" cycle isn't disabled with the + * component property by the app developers. + * <li>Activity "refresh" via "resumed -> paused -> resumed" cycle is enabled by the device + * manufacturer with override / by the app developers with the component property. + * </ul> + */ + boolean shouldRefreshActivityViaPauseForCameraCompat() { + return shouldEnableWithOverrideAndProperty( + /* gatingCondition */ () -> mLetterboxConfiguration + .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true), + OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE, + mBooleanPropertyCameraCompatEnableRefreshViaPause); + } + + /** + * Whether activity is eligible for camera compat force rotation treatment. See {@link + * DisplayRotationCompatPolicy} for context. + * + * <p>This treatment is enabled when the following conditions are met: + * <ul> + * <li>Flag gating the camera compat treatment is enabled. + * <li>Activity isn't opted out by the device manufacturer with override or by the app + * developers with the component property. + * </ul> + */ + boolean shouldForceRotateForCameraCompat() { + return shouldEnableWithOptOutOverrideAndProperty( + /* gatingCondition */ () -> mLetterboxConfiguration + .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true), + OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION, + mBooleanPropertyCameraCompatAllowForceRotation); + } + + /** + * Returns {@code true} when the following conditions are met: + * <ul> + * <li>{@code gatingCondition} isn't {@code false} + * <li>OEM didn't opt out with a {@code overrideChangeId} override + * <li>App developers didn't opt out with a component {@code property} + * </ul> + * + * <p>This is used for the treatments that are enabled based with the heuristic but can be + * disabled on per-app basis by OEMs or app developers. + */ + private boolean shouldEnableWithOptOutOverrideAndProperty(BooleanSupplier gatingCondition, + long overrideChangeId, Boolean property) { + if (!gatingCondition.getAsBoolean()) { + return false; + } + return !Boolean.FALSE.equals(property) + && !mActivityRecord.info.isChangeEnabled(overrideChangeId); + } + + /** + * Returns {@code true} when the following conditions are met: + * <ul> + * <li>{@code gatingCondition} isn't {@code false} + * <li>App developers didn't opt out with a component {@code property} + * <li>App developers opted in with a component {@code property} or an OEM opted in with a + * component {@code property} + * </ul> + * + * <p>This is used for the treatments that are enabled only on per-app basis. + */ + private boolean shouldEnableWithOverrideAndProperty(BooleanSupplier gatingCondition, + long overrideChangeId, Boolean property) { + if (!gatingCondition.getAsBoolean()) { + return false; + } + if (Boolean.FALSE.equals(property)) { + return false; + } + return Boolean.TRUE.equals(property) + || mActivityRecord.info.isChangeEnabled(overrideChangeId); + } + boolean hasWallpaperBackgroundForLetterbox() { return mShowWallpaperForLetterboxBackground; } diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 852c9b297040..e253ce03e46d 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -6265,6 +6265,11 @@ class Task extends TaskFragment { return this; } + Builder setRemoveWithTaskOrganizer(boolean removeWithTaskOrganizer) { + mRemoveWithTaskOrganizer = removeWithTaskOrganizer; + return this; + } + private Builder setUserId(int userId) { mUserId = userId; return this; @@ -6462,7 +6467,7 @@ class Task extends TaskFragment { mCallingPackage = mActivityInfo.packageName; mResizeMode = mActivityInfo.resizeMode; mSupportsPictureInPicture = mActivityInfo.supportsPictureInPicture(); - if (mActivityOptions != null) { + if (!mRemoveWithTaskOrganizer && mActivityOptions != null) { mRemoveWithTaskOrganizer = mActivityOptions.getRemoveWithTaskOranizer(); } diff --git a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java index 90a0dffa25f2..5f186a178f2d 100644 --- a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java +++ b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java @@ -49,6 +49,7 @@ import android.view.WindowManager; import android.window.ITaskFragmentOrganizer; import android.window.ITaskFragmentOrganizerController; import android.window.TaskFragmentInfo; +import android.window.TaskFragmentOperation; import android.window.TaskFragmentParentInfo; import android.window.TaskFragmentTransaction; import android.window.WindowContainerTransaction; @@ -297,7 +298,7 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr @NonNull TaskFragmentTransaction.Change prepareTaskFragmentError( @Nullable IBinder errorCallbackToken, @Nullable TaskFragment taskFragment, - int opType, @NonNull Throwable exception) { + @TaskFragmentOperation.OperationType int opType, @NonNull Throwable exception) { ProtoLog.v(WM_DEBUG_WINDOW_ORGANIZER, "Sending TaskFragment error exception=%s", exception.toString()); final TaskFragmentInfo info = @@ -629,7 +630,7 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr void onTaskFragmentError(@NonNull ITaskFragmentOrganizer organizer, @Nullable IBinder errorCallbackToken, @Nullable TaskFragment taskFragment, - int opType, @NonNull Throwable exception) { + @TaskFragmentOperation.OperationType int opType, @NonNull Throwable exception) { if (taskFragment != null && taskFragment.mTaskFragmentVanishedSent) { return; } @@ -803,6 +804,7 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr // Set when the event is deferred due to the host task is invisible. The defer time will // be the last active time of the host task. private long mDeferTime; + @TaskFragmentOperation.OperationType private int mOpType; private PendingTaskFragmentEvent(@EventType int eventType, @@ -812,7 +814,7 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr @Nullable Throwable exception, @Nullable ActivityRecord activity, @Nullable Task task, - int opType) { + @TaskFragmentOperation.OperationType int opType) { mEventType = eventType; mTaskFragmentOrg = taskFragmentOrg; mTaskFragment = taskFragment; @@ -853,6 +855,7 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr private ActivityRecord mActivity; @Nullable private Task mTask; + @TaskFragmentOperation.OperationType private int mOpType; Builder(@EventType int eventType, @NonNull ITaskFragmentOrganizer taskFragmentOrg) { @@ -885,7 +888,7 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr return this; } - Builder setOpType(int opType) { + Builder setOpType(@TaskFragmentOperation.OperationType int opType) { mOpType = opType; return this; } diff --git a/services/core/java/com/android/server/wm/TaskOrganizerController.java b/services/core/java/com/android/server/wm/TaskOrganizerController.java index 274d7ff4671a..8570db275a08 100644 --- a/services/core/java/com/android/server/wm/TaskOrganizerController.java +++ b/services/core/java/com/android/server/wm/TaskOrganizerController.java @@ -783,7 +783,8 @@ class TaskOrganizerController extends ITaskOrganizerController.Stub { } @Override - public void createRootTask(int displayId, int windowingMode, @Nullable IBinder launchCookie) { + public void createRootTask(int displayId, int windowingMode, @Nullable IBinder launchCookie, + boolean removeWithTaskOrganizer) { enforceTaskPermission("createRootTask()"); final long origId = Binder.clearCallingIdentity(); try { @@ -795,7 +796,7 @@ class TaskOrganizerController extends ITaskOrganizerController.Stub { return; } - createRootTask(display, windowingMode, launchCookie); + createRootTask(display, windowingMode, launchCookie, removeWithTaskOrganizer); } } finally { Binder.restoreCallingIdentity(origId); @@ -804,6 +805,12 @@ class TaskOrganizerController extends ITaskOrganizerController.Stub { @VisibleForTesting Task createRootTask(DisplayContent display, int windowingMode, @Nullable IBinder launchCookie) { + return createRootTask(display, windowingMode, launchCookie, + false /* removeWithTaskOrganizer */); + } + + Task createRootTask(DisplayContent display, int windowingMode, @Nullable IBinder launchCookie, + boolean removeWithTaskOrganizer) { ProtoLog.v(WM_DEBUG_WINDOW_ORGANIZER, "Create root task displayId=%d winMode=%d", display.mDisplayId, windowingMode); // We want to defer the task appear signal until the task is fully created and attached to @@ -816,6 +823,7 @@ class TaskOrganizerController extends ITaskOrganizerController.Stub { .setDeferTaskAppear(true) .setLaunchCookie(launchCookie) .setParent(display.getDefaultTaskDisplayArea()) + .setRemoveWithTaskOrganizer(removeWithTaskOrganizer) .build(); task.setDeferTaskAppear(false /* deferTaskAppear */); return task; diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java index 7f9e808c4c93..16541c10d9db 100644 --- a/services/core/java/com/android/server/wm/WallpaperController.java +++ b/services/core/java/com/android/server/wm/WallpaperController.java @@ -370,7 +370,7 @@ class WallpaperController { boolean updateWallpaperOffset(WindowState wallpaperWin, boolean sync) { // Size of the display the wallpaper is rendered on. - final Rect lastWallpaperBounds = wallpaperWin.getLastReportedBounds(); + final Rect lastWallpaperBounds = wallpaperWin.getParentFrame(); // Full size of the wallpaper (usually larger than bounds above to parallax scroll when // swiping through Launcher pages). final Rect wallpaperFrame = wallpaperWin.getFrame(); diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index 0ab4faf9d4bd..63bb5c34492d 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -3237,11 +3237,11 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< private Animation loadAnimation(WindowManager.LayoutParams lp, int transit, boolean enter, boolean isVoiceInteraction) { - if (isOrganized() + if (AppTransitionController.isTaskViewTask(this) || (isOrganized() // TODO(b/161711458): Clean-up when moved to shell. && getWindowingMode() != WINDOWING_MODE_FULLSCREEN && getWindowingMode() != WINDOWING_MODE_FREEFORM - && getWindowingMode() != WINDOWING_MODE_MULTI_WINDOW) { + && getWindowingMode() != WINDOWING_MODE_MULTI_WINDOW)) { return null; } diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index aac5ef6868df..dd70fca0ab0c 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -21,12 +21,19 @@ import static android.app.ActivityManager.isStartResultSuccessful; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.app.WindowConfiguration.WINDOW_CONFIG_BOUNDS; import static android.view.Display.DEFAULT_DISPLAY; +import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_TASK_FRAGMENT; +import static android.window.TaskFragmentOperation.OP_TYPE_DELETE_TASK_FRAGMENT; +import static android.window.TaskFragmentOperation.OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT; +import static android.window.TaskFragmentOperation.OP_TYPE_REQUEST_FOCUS_ON_TASK_FRAGMENT; +import static android.window.TaskFragmentOperation.OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS; import static android.window.TaskFragmentOperation.OP_TYPE_SET_ANIMATION_PARAMS; +import static android.window.TaskFragmentOperation.OP_TYPE_SET_COMPANION_TASK_FRAGMENT; +import static android.window.TaskFragmentOperation.OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT; +import static android.window.TaskFragmentOperation.OP_TYPE_UNKNOWN; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_ADD_RECT_INSETS_PROVIDER; +import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_ADD_TASK_FRAGMENT_OPERATION; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_CHILDREN_TASKS_REPARENT; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_CLEAR_ADJACENT_ROOTS; -import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT; -import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_DELETE_TASK_FRAGMENT; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_FINISH_ACTIVITY; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_LAUNCH_TASK; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_PENDING_INTENT; @@ -34,19 +41,12 @@ import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REMOVE_TASK; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REPARENT; -import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT; -import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REPARENT_CHILDREN; -import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REQUEST_FOCUS_ON_TASK_FRAGMENT; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_RESTORE_TRANSIENT_ORDER; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_ADJACENT_ROOTS; -import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_ALWAYS_ON_TOP; -import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_COMPANION_TASK_FRAGMENT; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_LAUNCH_ADJACENT_FLAG_ROOT; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_REPARENT_LEAF_TASK_IF_RELAUNCH; -import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_TASK_FRAGMENT_OPERATION; -import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_START_SHORTCUT; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WINDOW_ORGANIZER; @@ -119,6 +119,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub private static final String TAG = "WindowOrganizerController"; + private static final int TRANSACT_EFFECTS_NONE = 0; /** Flag indicating that an applied transaction may have effected lifecycle */ private static final int TRANSACT_EFFECTS_CLIENT_CONFIG = 1; private static final int TRANSACT_EFFECTS_LIFECYCLE = 1 << 1; @@ -488,7 +489,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub private void applyTransaction(@NonNull WindowContainerTransaction t, int syncId, @Nullable Transition transition, @NonNull CallerInfo caller, @Nullable Transition finishTransition) { - int effects = 0; + int effects = TRANSACT_EFFECTS_NONE; ProtoLog.v(WM_DEBUG_WINDOW_ORGANIZER, "Apply window transaction, syncId=%d", syncId); mService.deferWindowLayout(); mService.mTaskSupervisor.setDeferRootVisibilityUpdate(true /* deferUpdate */); @@ -630,7 +631,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub // masks here. final int configMask = change.getConfigSetMask() & CONTROLLABLE_CONFIGS; final int windowMask = change.getWindowSetMask() & CONTROLLABLE_WINDOW_CONFIGS; - int effects = 0; + int effects = TRANSACT_EFFECTS_NONE; final int windowingMode = change.getWindowingMode(); if (configMask != 0) { @@ -795,7 +796,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub @NonNull WindowContainerTransaction.Change c, @Nullable IBinder errorCallbackToken) { if (taskFragment.isEmbeddedTaskFragmentInPip()) { // No override from organizer for embedded TaskFragment in a PIP Task. - return 0; + return TRANSACT_EFFECTS_NONE; } // When the TaskFragment is resized, we may want to create a change transition for it, for @@ -861,197 +862,6 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub effects |= clearAdjacentRootsHierarchyOp(hop); break; } - case HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT: { - final TaskFragmentCreationParams taskFragmentCreationOptions = - hop.getTaskFragmentCreationOptions(); - createTaskFragment(taskFragmentCreationOptions, errorCallbackToken, caller, - transition); - break; - } - case HIERARCHY_OP_TYPE_DELETE_TASK_FRAGMENT: { - final WindowContainer wc = WindowContainer.fromBinder(hop.getContainer()); - if (wc == null || !wc.isAttached()) { - Slog.e(TAG, "Attempt to operate on unknown or detached container: " + wc); - break; - } - final TaskFragment taskFragment = wc.asTaskFragment(); - if (taskFragment == null || taskFragment.asTask() != null) { - throw new IllegalArgumentException( - "Can only delete organized TaskFragment, but not Task."); - } - if (isInLockTaskMode) { - final ActivityRecord bottomActivity = taskFragment.getActivity( - a -> !a.finishing, false /* traverseTopToBottom */); - if (bottomActivity != null - && mService.getLockTaskController().activityBlockedFromFinish( - bottomActivity)) { - Slog.w(TAG, "Skip removing TaskFragment due in lock task mode."); - sendTaskFragmentOperationFailure(organizer, errorCallbackToken, - taskFragment, type, new IllegalStateException( - "Not allow to delete task fragment in lock task mode.")); - break; - } - } - effects |= deleteTaskFragment(taskFragment, organizer, errorCallbackToken, - transition); - break; - } - case HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT: { - final IBinder fragmentToken = hop.getContainer(); - final TaskFragment tf = mLaunchTaskFragments.get(fragmentToken); - if (tf == null) { - final Throwable exception = new IllegalArgumentException( - "Not allowed to operate with invalid fragment token"); - sendTaskFragmentOperationFailure(organizer, errorCallbackToken, tf, type, - exception); - break; - } - if (tf.isEmbeddedTaskFragmentInPip()) { - final Throwable exception = new IllegalArgumentException( - "Not allowed to start activity in PIP TaskFragment"); - sendTaskFragmentOperationFailure(organizer, errorCallbackToken, tf, type, - exception); - break; - } - final Intent activityIntent = hop.getActivityIntent(); - final Bundle activityOptions = hop.getLaunchOptions(); - final int result = mService.getActivityStartController() - .startActivityInTaskFragment(tf, activityIntent, activityOptions, - hop.getCallingActivity(), caller.mUid, caller.mPid, - errorCallbackToken); - if (!isStartResultSuccessful(result)) { - sendTaskFragmentOperationFailure(organizer, errorCallbackToken, tf, type, - convertStartFailureToThrowable(result, activityIntent)); - } else { - effects |= TRANSACT_EFFECTS_LIFECYCLE; - } - break; - } - case HIERARCHY_OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT: { - final IBinder fragmentToken = hop.getNewParent(); - final IBinder activityToken = hop.getContainer(); - ActivityRecord activity = ActivityRecord.forTokenLocked(activityToken); - if (activity == null) { - // The token may be a temporary token if the activity doesn't belong to - // the organizer process. - activity = mTaskFragmentOrganizerController - .getReparentActivityFromTemporaryToken(organizer, activityToken); - } - final TaskFragment parent = mLaunchTaskFragments.get(fragmentToken); - if (parent == null || activity == null) { - final Throwable exception = new IllegalArgumentException( - "Not allowed to operate with invalid fragment token or activity."); - sendTaskFragmentOperationFailure(organizer, errorCallbackToken, parent, type, - exception); - break; - } - if (parent.isEmbeddedTaskFragmentInPip()) { - final Throwable exception = new IllegalArgumentException( - "Not allowed to reparent activity to PIP TaskFragment"); - sendTaskFragmentOperationFailure(organizer, errorCallbackToken, parent, type, - exception); - break; - } - if (parent.isAllowedToEmbedActivity(activity) != EMBEDDING_ALLOWED) { - final Throwable exception = new SecurityException( - "The task fragment is not allowed to embed the given activity."); - sendTaskFragmentOperationFailure(organizer, errorCallbackToken, parent, type, - exception); - break; - } - if (parent.getTask() != activity.getTask()) { - final Throwable exception = new SecurityException("The reparented activity is" - + " not in the same Task as the target TaskFragment."); - sendTaskFragmentOperationFailure(organizer, errorCallbackToken, parent, type, - exception); - break; - } - - if (transition != null) { - transition.collect(activity); - if (activity.getParent() != null) { - // Collect the current parent. Its visibility may change as a result of - // this reparenting. - transition.collect(activity.getParent()); - } - transition.collect(parent); - } - activity.reparent(parent, POSITION_TOP); - effects |= TRANSACT_EFFECTS_LIFECYCLE; - break; - } - case HIERARCHY_OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS: { - final IBinder fragmentToken = hop.getContainer(); - final IBinder adjacentFragmentToken = hop.getAdjacentRoot(); - final TaskFragment tf1 = mLaunchTaskFragments.get(fragmentToken); - final TaskFragment tf2 = adjacentFragmentToken != null - ? mLaunchTaskFragments.get(adjacentFragmentToken) - : null; - if (tf1 == null || (adjacentFragmentToken != null && tf2 == null)) { - final Throwable exception = new IllegalArgumentException( - "Not allowed to set adjacent on invalid fragment tokens"); - sendTaskFragmentOperationFailure(organizer, errorCallbackToken, tf1, type, - exception); - break; - } - if (tf1.isEmbeddedTaskFragmentInPip() - || (tf2 != null && tf2.isEmbeddedTaskFragmentInPip())) { - final Throwable exception = new IllegalArgumentException( - "Not allowed to set adjacent on TaskFragment in PIP Task"); - sendTaskFragmentOperationFailure(organizer, errorCallbackToken, tf1, type, - exception); - break; - } - tf1.setAdjacentTaskFragment(tf2); - effects |= TRANSACT_EFFECTS_LIFECYCLE; - - // Clear the focused app if the focused app is no longer visible after reset the - // adjacent TaskFragments. - if (tf2 == null && tf1.getDisplayContent().mFocusedApp != null - && tf1.hasChild(tf1.getDisplayContent().mFocusedApp) - && !tf1.shouldBeVisible(null /* starting */)) { - tf1.getDisplayContent().setFocusedApp(null); - } - - final Bundle bundle = hop.getLaunchOptions(); - final WindowContainerTransaction.TaskFragmentAdjacentParams adjacentParams = - bundle != null ? new WindowContainerTransaction.TaskFragmentAdjacentParams( - bundle) : null; - if (adjacentParams == null) { - break; - } - - tf1.setDelayLastActivityRemoval( - adjacentParams.shouldDelayPrimaryLastActivityRemoval()); - if (tf2 != null) { - tf2.setDelayLastActivityRemoval( - adjacentParams.shouldDelaySecondaryLastActivityRemoval()); - } - break; - } - case HIERARCHY_OP_TYPE_REQUEST_FOCUS_ON_TASK_FRAGMENT: { - final TaskFragment tf = mLaunchTaskFragments.get(hop.getContainer()); - if (tf == null || !tf.isAttached()) { - Slog.e(TAG, "Attempt to operate on detached container: " + tf); - break; - } - final ActivityRecord curFocus = tf.getDisplayContent().mFocusedApp; - if (curFocus != null && curFocus.getTaskFragment() == tf) { - Slog.d(TAG, "The requested TaskFragment already has the focus."); - break; - } - if (curFocus != null && curFocus.getTask() != tf.getTask()) { - Slog.d(TAG, "The Task of the requested TaskFragment doesn't have focus."); - break; - } - final ActivityRecord targetFocus = tf.getTopResumedActivity(); - if (targetFocus == null) { - Slog.d(TAG, "There is no resumed activity in the requested TaskFragment."); - break; - } - tf.getDisplayContent().setFocusedApp(targetFocus); - break; - } case HIERARCHY_OP_TYPE_CHILDREN_TASKS_REPARENT: { effects |= reparentChildrenTasksHierarchyOp(hop, transition, syncId, isInLockTaskMode); @@ -1126,24 +936,9 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub effects |= sanitizeAndApplyHierarchyOp(wc, hop); break; } - case HIERARCHY_OP_TYPE_SET_COMPANION_TASK_FRAGMENT: { - final IBinder fragmentToken = hop.getContainer(); - final IBinder companionToken = hop.getCompanionContainer(); - final TaskFragment fragment = mLaunchTaskFragments.get(fragmentToken); - final TaskFragment companion = companionToken != null ? mLaunchTaskFragments.get( - companionToken) : null; - if (fragment == null || !fragment.isAttached()) { - final Throwable exception = new IllegalArgumentException( - "Not allowed to set companion on invalid fragment tokens"); - sendTaskFragmentOperationFailure(organizer, errorCallbackToken, fragment, type, - exception); - break; - } - fragment.setCompanionTaskFragment(companion); - break; - } - case HIERARCHY_OP_TYPE_SET_TASK_FRAGMENT_OPERATION: { - effects |= applyTaskFragmentOperation(hop, errorCallbackToken, organizer); + case HIERARCHY_OP_TYPE_ADD_TASK_FRAGMENT_OPERATION: { + effects |= applyTaskFragmentOperation(hop, transition, isInLockTaskMode, caller, + errorCallbackToken, organizer); break; } default: { @@ -1203,22 +998,6 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub } break; } - case HIERARCHY_OP_TYPE_REPARENT_CHILDREN: { - final WindowContainer oldParent = WindowContainer.fromBinder(hop.getContainer()); - final WindowContainer newParent = hop.getNewParent() != null - ? WindowContainer.fromBinder(hop.getNewParent()) - : null; - if (oldParent == null || oldParent.asTaskFragment() == null - || !oldParent.isAttached()) { - Slog.e(TAG, "Attempt to operate on unknown or detached container: " - + oldParent); - break; - } - reparentTaskFragment(oldParent.asTaskFragment(), newParent, organizer, - errorCallbackToken, transition); - effects |= TRANSACT_EFFECTS_LIFECYCLE; - break; - } case HIERARCHY_OP_TYPE_RESTORE_TRANSIENT_ORDER: { if (finishTransition == null) break; final WindowContainer container = WindowContainer.fromBinder(hop.getContainer()); @@ -1278,45 +1057,241 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub return effects; } - /** Applies change set through {@link WindowContainerTransaction#setTaskFragmentOperation}. */ + /** + * Applies change set through {@link WindowContainerTransaction#addTaskFragmentOperation}. + * @return an int to represent the transaction effects, such as {@link #TRANSACT_EFFECTS_NONE}, + * {@link #TRANSACT_EFFECTS_LIFECYCLE} or {@link #TRANSACT_EFFECTS_CLIENT_CONFIG}. + */ private int applyTaskFragmentOperation(@NonNull WindowContainerTransaction.HierarchyOp hop, + @Nullable Transition transition, boolean isInLockTaskMode, @NonNull CallerInfo caller, @Nullable IBinder errorCallbackToken, @Nullable ITaskFragmentOrganizer organizer) { + if (!validateTaskFragmentOperation(hop, errorCallbackToken, organizer)) { + return TRANSACT_EFFECTS_NONE; + } final IBinder fragmentToken = hop.getContainer(); final TaskFragment taskFragment = mLaunchTaskFragments.get(fragmentToken); final TaskFragmentOperation operation = hop.getTaskFragmentOperation(); - if (operation == null) { - final Throwable exception = new IllegalArgumentException( - "TaskFragmentOperation must be non-null"); - sendTaskFragmentOperationFailure(organizer, errorCallbackToken, taskFragment, - HIERARCHY_OP_TYPE_SET_TASK_FRAGMENT_OPERATION, exception); - return 0; - } final int opType = operation.getOpType(); - if (taskFragment == null || !taskFragment.isAttached()) { - final Throwable exception = new IllegalArgumentException( - "Not allowed to apply operation on invalid fragment tokens opType=" + opType); - sendTaskFragmentOperationFailure(organizer, errorCallbackToken, taskFragment, - HIERARCHY_OP_TYPE_SET_TASK_FRAGMENT_OPERATION, exception); - return 0; - } - int effect = 0; + int effects = TRANSACT_EFFECTS_NONE; switch (opType) { + case OP_TYPE_CREATE_TASK_FRAGMENT: { + final TaskFragmentCreationParams taskFragmentCreationParams = + operation.getTaskFragmentCreationParams(); + if (taskFragmentCreationParams == null) { + final Throwable exception = new IllegalArgumentException( + "TaskFragmentCreationParams must be non-null"); + sendTaskFragmentOperationFailure(organizer, errorCallbackToken, taskFragment, + opType, exception); + break; + } + createTaskFragment(taskFragmentCreationParams, errorCallbackToken, caller, + transition); + break; + } + case OP_TYPE_DELETE_TASK_FRAGMENT: { + if (isInLockTaskMode) { + final ActivityRecord bottomActivity = taskFragment.getActivity( + a -> !a.finishing, false /* traverseTopToBottom */); + if (bottomActivity != null + && mService.getLockTaskController().activityBlockedFromFinish( + bottomActivity)) { + Slog.w(TAG, "Skip removing TaskFragment due in lock task mode."); + sendTaskFragmentOperationFailure(organizer, errorCallbackToken, + taskFragment, opType, new IllegalStateException( + "Not allow to delete task fragment in lock task mode.")); + break; + } + } + effects |= deleteTaskFragment(taskFragment, transition); + break; + } + case OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT: { + final IBinder callerActivityToken = operation.getActivityToken(); + final Intent activityIntent = operation.getActivityIntent(); + final Bundle activityOptions = operation.getBundle(); + final int result = mService.getActivityStartController() + .startActivityInTaskFragment(taskFragment, activityIntent, activityOptions, + callerActivityToken, caller.mUid, caller.mPid, + errorCallbackToken); + if (!isStartResultSuccessful(result)) { + sendTaskFragmentOperationFailure(organizer, errorCallbackToken, taskFragment, + opType, convertStartFailureToThrowable(result, activityIntent)); + } else { + effects |= TRANSACT_EFFECTS_LIFECYCLE; + } + break; + } + case OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT: { + final IBinder activityToken = operation.getActivityToken(); + ActivityRecord activity = ActivityRecord.forTokenLocked(activityToken); + if (activity == null) { + // The token may be a temporary token if the activity doesn't belong to + // the organizer process. + activity = mTaskFragmentOrganizerController + .getReparentActivityFromTemporaryToken(organizer, activityToken); + } + if (activity == null) { + final Throwable exception = new IllegalArgumentException( + "Not allowed to operate with invalid activity."); + sendTaskFragmentOperationFailure(organizer, errorCallbackToken, taskFragment, + opType, exception); + break; + } + if (taskFragment.isAllowedToEmbedActivity(activity) != EMBEDDING_ALLOWED) { + final Throwable exception = new SecurityException( + "The task fragment is not allowed to embed the given activity."); + sendTaskFragmentOperationFailure(organizer, errorCallbackToken, taskFragment, + opType, exception); + break; + } + if (taskFragment.getTask() != activity.getTask()) { + final Throwable exception = new SecurityException("The reparented activity is" + + " not in the same Task as the target TaskFragment."); + sendTaskFragmentOperationFailure(organizer, errorCallbackToken, taskFragment, + opType, exception); + break; + } + if (transition != null) { + transition.collect(activity); + if (activity.getParent() != null) { + // Collect the current parent. Its visibility may change as a result of + // this reparenting. + transition.collect(activity.getParent()); + } + transition.collect(taskFragment); + } + activity.reparent(taskFragment, POSITION_TOP); + effects |= TRANSACT_EFFECTS_LIFECYCLE; + break; + } + case OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS: { + final IBinder secondaryFragmentToken = operation.getSecondaryFragmentToken(); + final TaskFragment secondaryTaskFragment = secondaryFragmentToken != null + ? mLaunchTaskFragments.get(secondaryFragmentToken) + : null; + taskFragment.setAdjacentTaskFragment(secondaryTaskFragment); + effects |= TRANSACT_EFFECTS_LIFECYCLE; + + // Clear the focused app if the focused app is no longer visible after reset the + // adjacent TaskFragments. + if (secondaryTaskFragment == null + && taskFragment.getDisplayContent().mFocusedApp != null + && taskFragment.hasChild(taskFragment.getDisplayContent().mFocusedApp) + && !taskFragment.shouldBeVisible(null /* starting */)) { + taskFragment.getDisplayContent().setFocusedApp(null); + } + + final Bundle bundle = hop.getLaunchOptions(); + final WindowContainerTransaction.TaskFragmentAdjacentParams adjacentParams = + bundle != null ? new WindowContainerTransaction.TaskFragmentAdjacentParams( + bundle) : null; + if (adjacentParams == null) { + break; + } + + taskFragment.setDelayLastActivityRemoval( + adjacentParams.shouldDelayPrimaryLastActivityRemoval()); + if (secondaryTaskFragment != null) { + secondaryTaskFragment.setDelayLastActivityRemoval( + adjacentParams.shouldDelaySecondaryLastActivityRemoval()); + } + break; + } + case OP_TYPE_REQUEST_FOCUS_ON_TASK_FRAGMENT: { + final ActivityRecord curFocus = taskFragment.getDisplayContent().mFocusedApp; + if (curFocus != null && curFocus.getTaskFragment() == taskFragment) { + Slog.d(TAG, "The requested TaskFragment already has the focus."); + break; + } + if (curFocus != null && curFocus.getTask() != taskFragment.getTask()) { + Slog.d(TAG, "The Task of the requested TaskFragment doesn't have focus."); + break; + } + final ActivityRecord targetFocus = taskFragment.getTopResumedActivity(); + if (targetFocus == null) { + Slog.d(TAG, "There is no resumed activity in the requested TaskFragment."); + break; + } + taskFragment.getDisplayContent().setFocusedApp(targetFocus); + break; + } + case OP_TYPE_SET_COMPANION_TASK_FRAGMENT: { + final IBinder companionFragmentToken = operation.getSecondaryFragmentToken(); + final TaskFragment companionTaskFragment = companionFragmentToken != null + ? mLaunchTaskFragments.get(companionFragmentToken) + : null; + taskFragment.setCompanionTaskFragment(companionTaskFragment); + break; + } case OP_TYPE_SET_ANIMATION_PARAMS: { final TaskFragmentAnimationParams animationParams = operation.getAnimationParams(); if (animationParams == null) { final Throwable exception = new IllegalArgumentException( "TaskFragmentAnimationParams must be non-null"); sendTaskFragmentOperationFailure(organizer, errorCallbackToken, taskFragment, - HIERARCHY_OP_TYPE_SET_TASK_FRAGMENT_OPERATION, exception); + opType, exception); break; } taskFragment.setAnimationParams(animationParams); break; } - // TODO(b/263436063): move other TaskFragment related operation here. } - return effect; + return effects; + } + + private boolean validateTaskFragmentOperation( + @NonNull WindowContainerTransaction.HierarchyOp hop, + @Nullable IBinder errorCallbackToken, @Nullable ITaskFragmentOrganizer organizer) { + final TaskFragmentOperation operation = hop.getTaskFragmentOperation(); + final IBinder fragmentToken = hop.getContainer(); + final TaskFragment taskFragment = mLaunchTaskFragments.get(fragmentToken); + if (operation == null) { + final Throwable exception = new IllegalArgumentException( + "TaskFragmentOperation must be non-null"); + sendTaskFragmentOperationFailure(organizer, errorCallbackToken, taskFragment, + OP_TYPE_UNKNOWN, exception); + return false; + } + final int opType = operation.getOpType(); + if (opType == OP_TYPE_CREATE_TASK_FRAGMENT) { + // No need to check TaskFragment. + return true; + } + + if (!validateTaskFragment(taskFragment, opType, errorCallbackToken, organizer)) { + return false; + } + + final IBinder secondaryFragmentToken = operation.getSecondaryFragmentToken(); + return secondaryFragmentToken == null + || validateTaskFragment(mLaunchTaskFragments.get(secondaryFragmentToken), opType, + errorCallbackToken, organizer); + } + + private boolean validateTaskFragment(@Nullable TaskFragment taskFragment, + @TaskFragmentOperation.OperationType int opType, @Nullable IBinder errorCallbackToken, + @Nullable ITaskFragmentOrganizer organizer) { + if (taskFragment == null || !taskFragment.isAttached()) { + // TaskFragment doesn't exist. + final Throwable exception = new IllegalArgumentException( + "Not allowed to apply operation on invalid fragment tokens opType=" + opType); + sendTaskFragmentOperationFailure(organizer, errorCallbackToken, taskFragment, + opType, exception); + return false; + } + if (taskFragment.isEmbeddedTaskFragmentInPip() + && (opType != OP_TYPE_DELETE_TASK_FRAGMENT + // When the Task enters PiP before the organizer removes the empty TaskFragment, we + // should allow it to delete the TaskFragment for cleanup. + || taskFragment.getTopNonFinishingActivity() != null)) { + final Throwable exception = new IllegalArgumentException( + "Not allowed to apply operation on PIP TaskFragment"); + sendTaskFragmentOperationFailure(organizer, errorCallbackToken, taskFragment, + opType, exception); + return false; + } + return true; } /** A helper method to send minimum dimension violation error to the client. */ @@ -1329,7 +1304,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub + taskFragment.getBounds() + " does not satisfy minimum dimensions:" + minDimensions + " " + reason); sendTaskFragmentOperationFailure(taskFragment.getTaskFragmentOrganizer(), - errorCallbackToken, taskFragment, -1 /* opType */, exception); + errorCallbackToken, taskFragment, OP_TYPE_UNKNOWN, exception); } /** @@ -1366,7 +1341,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub final DisplayContent dc = task.getDisplayContent(); if (dc == null) { Slog.w(TAG, "Container is no longer attached: " + task); - return 0; + return TRANSACT_EFFECTS_NONE; } final Task as = task; @@ -1379,7 +1354,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub : WindowContainer.fromBinder(hop.getNewParent()); if (newParent == null) { Slog.e(TAG, "Can't resolve parent window from token"); - return 0; + return TRANSACT_EFFECTS_NONE; } if (task.getParent() != newParent) { if (newParent.asTaskDisplayArea() != null) { @@ -1390,14 +1365,14 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub if (newParent.inPinnedWindowingMode()) { Slog.w(TAG, "Can't support moving a task to another PIP window..." + " newParent=" + newParent + " task=" + task); - return 0; + return TRANSACT_EFFECTS_NONE; } if (!task.supportsMultiWindowInDisplayArea( newParent.asTask().getDisplayArea())) { Slog.w(TAG, "Can't support task that doesn't support multi-window" + " mode in multi-window mode... newParent=" + newParent + " task=" + task); - return 0; + return TRANSACT_EFFECTS_NONE; } } task.reparent((Task) newParent, @@ -1459,22 +1434,22 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub if (currentParent == newParent) { Slog.e(TAG, "reparentChildrenTasksHierarchyOp parent not changing: " + hop); - return 0; + return TRANSACT_EFFECTS_NONE; } if (!currentParent.isAttached()) { Slog.e(TAG, "reparentChildrenTasksHierarchyOp currentParent detached=" + currentParent + " hop=" + hop); - return 0; + return TRANSACT_EFFECTS_NONE; } if (!newParent.isAttached()) { Slog.e(TAG, "reparentChildrenTasksHierarchyOp newParent detached=" + newParent + " hop=" + hop); - return 0; + return TRANSACT_EFFECTS_NONE; } if (newParent.inPinnedWindowingMode()) { Slog.e(TAG, "reparentChildrenTasksHierarchyOp newParent in PIP=" + newParent + " hop=" + hop); - return 0; + return TRANSACT_EFFECTS_NONE; } final boolean newParentInMultiWindow = newParent.inMultiWindowMode(); @@ -1553,10 +1528,6 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub throw new IllegalArgumentException("setAdjacentRootsHierarchyOp: Not created by" + " organizer root1=" + root1 + " root2=" + root2); } - if (root1.isEmbeddedTaskFragmentInPip() || root2.isEmbeddedTaskFragmentInPip()) { - Slog.e(TAG, "Attempt to set adjacent TaskFragment in PIP Task"); - return 0; - } root1.setAdjacentTaskFragment(root2); return TRANSACT_EFFECTS_LIFECYCLE; } @@ -1725,52 +1696,12 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub final int type = hop.getType(); // Check for each type of the operations that are allowed for TaskFragmentOrganizer. switch (type) { - case HIERARCHY_OP_TYPE_DELETE_TASK_FRAGMENT: - enforceTaskFragmentOrganized(func, - WindowContainer.fromBinder(hop.getContainer()), organizer); - break; - case HIERARCHY_OP_TYPE_SET_ADJACENT_ROOTS: - enforceTaskFragmentOrganized(func, - WindowContainer.fromBinder(hop.getContainer()), organizer); - enforceTaskFragmentOrganized(func, - WindowContainer.fromBinder(hop.getAdjacentRoot()), - organizer); - break; - case HIERARCHY_OP_TYPE_CLEAR_ADJACENT_ROOTS: - enforceTaskFragmentOrganized(func, - WindowContainer.fromBinder(hop.getContainer()), organizer); - break; - case HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT: - // We are allowing organizer to create TaskFragment. We will check the - // ownerToken in #createTaskFragment, and trigger error callback if that is not - // valid. - break; - case HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT: - case HIERARCHY_OP_TYPE_REQUEST_FOCUS_ON_TASK_FRAGMENT: - case HIERARCHY_OP_TYPE_SET_TASK_FRAGMENT_OPERATION: - enforceTaskFragmentOrganized(func, hop.getContainer(), organizer); - break; - case HIERARCHY_OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT: - enforceTaskFragmentOrganized(func, hop.getNewParent(), organizer); - break; - case HIERARCHY_OP_TYPE_SET_COMPANION_TASK_FRAGMENT: + case HIERARCHY_OP_TYPE_ADD_TASK_FRAGMENT_OPERATION: enforceTaskFragmentOrganized(func, hop.getContainer(), organizer); - if (hop.getCompanionContainer() != null) { - enforceTaskFragmentOrganized(func, hop.getCompanionContainer(), organizer); - } - break; - case HIERARCHY_OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS: - enforceTaskFragmentOrganized(func, hop.getContainer(), organizer); - if (hop.getAdjacentRoot() != null) { - enforceTaskFragmentOrganized(func, hop.getAdjacentRoot(), organizer); - } - break; - case HIERARCHY_OP_TYPE_REPARENT_CHILDREN: - enforceTaskFragmentOrganized(func, - WindowContainer.fromBinder(hop.getContainer()), organizer); - if (hop.getNewParent() != null) { + if (hop.getTaskFragmentOperation() != null + && hop.getTaskFragmentOperation().getSecondaryFragmentToken() != null) { enforceTaskFragmentOrganized(func, - WindowContainer.fromBinder(hop.getNewParent()), + hop.getTaskFragmentOperation().getSecondaryFragmentToken(), organizer); } break; @@ -1917,21 +1848,21 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub final Throwable exception = new IllegalArgumentException("TaskFragment token must be unique"); sendTaskFragmentOperationFailure(organizer, errorCallbackToken, null /* taskFragment */, - HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT, exception); + OP_TYPE_CREATE_TASK_FRAGMENT, exception); return; } if (ownerActivity == null || ownerActivity.getTask() == null) { final Throwable exception = new IllegalArgumentException("Not allowed to operate with invalid ownerToken"); sendTaskFragmentOperationFailure(organizer, errorCallbackToken, null /* taskFragment */, - HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT, exception); + OP_TYPE_CREATE_TASK_FRAGMENT, exception); return; } if (!ownerActivity.isResizeable()) { final IllegalArgumentException exception = new IllegalArgumentException("Not allowed" + " to operate with non-resizable owner Activity"); sendTaskFragmentOperationFailure(organizer, errorCallbackToken, null /* taskFragment */, - HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT, exception); + OP_TYPE_CREATE_TASK_FRAGMENT, exception); return; } // The ownerActivity has to belong to the same app as the target Task. @@ -1942,14 +1873,14 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub new SecurityException("Not allowed to operate with the ownerToken while " + "the root activity of the target task belong to the different app"); sendTaskFragmentOperationFailure(organizer, errorCallbackToken, null /* taskFragment */, - HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT, exception); + OP_TYPE_CREATE_TASK_FRAGMENT, exception); return; } if (ownerTask.inPinnedWindowingMode()) { final Throwable exception = new IllegalArgumentException( "Not allowed to create TaskFragment in PIP Task"); sendTaskFragmentOperationFailure(organizer, errorCallbackToken, null /* taskFragment */, - HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT, exception); + OP_TYPE_CREATE_TASK_FRAGMENT, exception); return; } final TaskFragment taskFragment = new TaskFragment(mService, @@ -1984,91 +1915,11 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub if (transition != null) transition.collectExistenceChange(taskFragment); } - private void reparentTaskFragment(@NonNull TaskFragment oldParent, - @Nullable WindowContainer<?> newParent, @Nullable ITaskFragmentOrganizer organizer, - @Nullable IBinder errorCallbackToken, @Nullable Transition transition) { - final TaskFragment newParentTF; - if (newParent == null) { - // Use the old parent's parent if the caller doesn't specify the new parent. - newParentTF = oldParent.getTask(); - } else { - newParentTF = newParent.asTaskFragment(); - } - if (newParentTF == null) { - final Throwable exception = - new IllegalArgumentException("Not allowed to operate with invalid container"); - sendTaskFragmentOperationFailure(organizer, errorCallbackToken, newParentTF, - HIERARCHY_OP_TYPE_REPARENT_CHILDREN, exception); - return; - } - if (newParentTF.getTaskFragmentOrganizer() != null) { - // We are reparenting activities to a new embedded TaskFragment, this operation is only - // allowed if the new parent is trusted by all reparent activities. - final boolean isEmbeddingDisallowed = oldParent.forAllActivities(activity -> - newParentTF.isAllowedToEmbedActivity(activity) != EMBEDDING_ALLOWED); - if (isEmbeddingDisallowed) { - final Throwable exception = new SecurityException( - "The new parent is not allowed to embed the activities."); - sendTaskFragmentOperationFailure(organizer, errorCallbackToken, newParentTF, - HIERARCHY_OP_TYPE_REPARENT_CHILDREN, exception); - return; - } - } - if (newParentTF.isEmbeddedTaskFragmentInPip() || oldParent.isEmbeddedTaskFragmentInPip()) { - final Throwable exception = new SecurityException( - "Not allow to reparent in TaskFragment in PIP Task."); - sendTaskFragmentOperationFailure(organizer, errorCallbackToken, newParentTF, - HIERARCHY_OP_TYPE_REPARENT_CHILDREN, exception); - return; - } - if (newParentTF.getTask() != oldParent.getTask()) { - final Throwable exception = new SecurityException( - "The new parent is not in the same Task as the old parent."); - sendTaskFragmentOperationFailure(organizer, errorCallbackToken, newParentTF, - HIERARCHY_OP_TYPE_REPARENT_CHILDREN, exception); - return; - } - if (transition != null) { - // Collect the current parent. It's visibility may change as a result of this - // reparenting. - transition.collect(oldParent); - transition.collect(newParentTF); - } - while (oldParent.hasChild()) { - final WindowContainer child = oldParent.getChildAt(0); - if (transition != null) { - transition.collect(child); - } - child.reparent(newParentTF, POSITION_TOP); - } - } - private int deleteTaskFragment(@NonNull TaskFragment taskFragment, - @Nullable ITaskFragmentOrganizer organizer, @Nullable IBinder errorCallbackToken, @Nullable Transition transition) { - final int index = mLaunchTaskFragments.indexOfValue(taskFragment); - if (index < 0) { - final Throwable exception = - new IllegalArgumentException("Not allowed to operate with invalid " - + "taskFragment"); - sendTaskFragmentOperationFailure(organizer, errorCallbackToken, taskFragment, - HIERARCHY_OP_TYPE_DELETE_TASK_FRAGMENT, exception); - return 0; - } - if (taskFragment.isEmbeddedTaskFragmentInPip() - // When the Task enters PiP before the organizer removes the empty TaskFragment, we - // should allow it to do the cleanup. - && taskFragment.getTopNonFinishingActivity() != null) { - final Throwable exception = new IllegalArgumentException( - "Not allowed to delete TaskFragment in PIP Task"); - sendTaskFragmentOperationFailure(organizer, errorCallbackToken, taskFragment, - HIERARCHY_OP_TYPE_DELETE_TASK_FRAGMENT, exception); - return 0; - } - if (transition != null) transition.collectExistenceChange(taskFragment); - mLaunchTaskFragments.removeAt(index); + mLaunchTaskFragments.remove(taskFragment.getFragmentToken()); taskFragment.remove(true /* withTransition */, "deleteTaskFragment"); return TRANSACT_EFFECTS_LIFECYCLE; } @@ -2093,8 +1944,8 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub } void sendTaskFragmentOperationFailure(@NonNull ITaskFragmentOrganizer organizer, - @Nullable IBinder errorCallbackToken, @Nullable TaskFragment taskFragment, int opType, - @NonNull Throwable exception) { + @Nullable IBinder errorCallbackToken, @Nullable TaskFragment taskFragment, + @TaskFragmentOperation.OperationType int opType, @NonNull Throwable exception) { if (organizer == null) { throw new IllegalArgumentException("Not allowed to operate with invalid organizer"); } diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 096f8f67d494..e08baccc2da8 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -3068,12 +3068,6 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP return mLastReportedConfiguration.getMergedConfiguration(); } - /** Returns the last window configuration bounds reported to the client. */ - Rect getLastReportedBounds() { - final Rect bounds = getLastReportedConfiguration().windowConfiguration.getBounds(); - return !bounds.isEmpty() ? bounds : getBounds(); - } - void adjustStartingWindowFlags() { if (mAttrs.type == TYPE_BASE_APPLICATION && mActivityRecord != null && mActivityRecord.mStartingWindow != null) { @@ -4390,6 +4384,9 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP pw.print("null"); } + if (mXOffset != 0 || mYOffset != 0) { + pw.println(prefix + "mXOffset=" + mXOffset + " mYOffset=" + mYOffset); + } if (mHScale != 1 || mVScale != 1) { pw.println(prefix + "mHScale=" + mHScale + " mVScale=" + mVScale); @@ -5527,7 +5524,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP mSurfacePosition); if (mWallpaperScale != 1f) { - final Rect bounds = getLastReportedBounds(); + final Rect bounds = getParentFrame(); Matrix matrix = mTmpMatrix; matrix.setTranslate(mXOffset, mYOffset); matrix.postScale(mWallpaperScale, mWallpaperScale, bounds.exactCenterX(), @@ -5640,6 +5637,14 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP && imeTarget.compareTo(this) <= 0; return inTokenWithAndAboveImeTarget; } + + // The condition is for the system dialog not belonging to any Activity. + // (^FLAG_NOT_FOCUSABLE & FLAG_ALT_FOCUSABLE_IM) means the dialog is still focusable but + // should be placed above the IME window. + if ((mAttrs.flags & (FLAG_NOT_FOCUSABLE | FLAG_ALT_FOCUSABLE_IM)) + == FLAG_ALT_FOCUSABLE_IM && isTrustedOverlay() && canAddInternalSystemWindow()) { + return true; + } return false; } @@ -6262,13 +6267,6 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP } @Override - public void unfreezeInsetsAfterStartInput() { - if (mActivityRecord != null) { - mActivityRecord.mImeInsetsFrozenUntilStartInput = false; - } - } - - @Override public boolean isInputMethodClientFocus(int uid, int pid) { return getDisplayContent().isInputMethodClientFocus(uid, pid); } diff --git a/services/core/jni/com_android_server_companion_virtual_InputController.cpp b/services/core/jni/com_android_server_companion_virtual_InputController.cpp index b7a4fd1be261..4cb7a8fc04de 100644 --- a/services/core/jni/com_android_server_companion_virtual_InputController.cpp +++ b/services/core/jni/com_android_server_companion_virtual_InputController.cpp @@ -81,7 +81,7 @@ static std::map<int, int> TOOL_TYPE_MAPPING = { static std::map<int, int> DPAD_KEY_CODE_MAPPING = { {AKEYCODE_DPAD_DOWN, KEY_DOWN}, {AKEYCODE_DPAD_UP, KEY_UP}, {AKEYCODE_DPAD_LEFT, KEY_LEFT}, {AKEYCODE_DPAD_RIGHT, KEY_RIGHT}, - {AKEYCODE_DPAD_CENTER, KEY_SELECT}, + {AKEYCODE_DPAD_CENTER, KEY_SELECT}, {AKEYCODE_BACK, KEY_BACK}, }; // Keycode mapping from https://source.android.com/devices/input/keyboard-devices @@ -378,7 +378,7 @@ static bool writeKeyEvent(jint fd, jint androidKeyCode, jint action, const std::map<int, int>& keyCodeMapping) { auto keyCodeIterator = keyCodeMapping.find(androidKeyCode); if (keyCodeIterator == keyCodeMapping.end()) { - ALOGE("No supportive native keycode for androidKeyCode %d", androidKeyCode); + ALOGE("Unsupported native keycode for androidKeyCode %d", androidKeyCode); return false; } auto actionIterator = KEY_ACTION_MAPPING.find(action); @@ -512,4 +512,4 @@ int register_android_server_companion_virtual_InputController(JNIEnv* env) { methods, NELEM(methods)); } -} // namespace android
\ No newline at end of file +} // namespace android diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java index 50ce1c3dafc5..fa7661da02da 100644 --- a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java +++ b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java @@ -50,6 +50,7 @@ import android.service.credentials.CredentialProviderInfo; import android.text.TextUtils; import android.util.Log; import android.util.Slog; +import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; import com.android.server.infra.AbstractMasterSystemService; @@ -73,6 +74,16 @@ public final class CredentialManagerService private static final String TAG = "CredManSysService"; + private final Context mContext; + + /** + * Cache of system service list per user id. + */ + @GuardedBy("mLock") + private final SparseArray<List<CredentialManagerServiceImpl>> mSystemServicesCacheList = + new SparseArray<>(); + + public CredentialManagerService(@NonNull Context context) { super( context, @@ -80,6 +91,20 @@ public final class CredentialManagerService context, Settings.Secure.CREDENTIAL_SERVICE, /* isMultipleMode= */ true), null, PACKAGE_UPDATE_POLICY_REFRESH_EAGER); + mContext = context; + } + + @NonNull + @GuardedBy("mLock") + private List<CredentialManagerServiceImpl> constructSystemServiceListLocked( + int resolvedUserId) { + List<CredentialManagerServiceImpl> services = new ArrayList<>(); + List<CredentialProviderInfo> credentialProviderInfos = + CredentialProviderInfo.getAvailableSystemServices(mContext, resolvedUserId); + credentialProviderInfos.forEach(info -> { + services.add(new CredentialManagerServiceImpl(this, mLock, resolvedUserId, info)); + }); + return services; } @Override @@ -105,8 +130,10 @@ public final class CredentialManagerService } @Override // from AbstractMasterSystemService + @GuardedBy("mLock") protected List<CredentialManagerServiceImpl> newServiceListLocked( int resolvedUserId, boolean disabled, String[] serviceNames) { + getOrConstructSystemServiceListLock(resolvedUserId); if (serviceNames == null || serviceNames.length == 0) { Slog.i(TAG, "serviceNames sent in newServiceListLocked is null, or empty"); return new ArrayList<>(); @@ -155,13 +182,24 @@ public final class CredentialManagerService // TODO("Iterate over system services and remove if needed") } + @GuardedBy("mLock") + private List<CredentialManagerServiceImpl> getOrConstructSystemServiceListLock( + int resolvedUserId) { + List<CredentialManagerServiceImpl> services = mSystemServicesCacheList.get(resolvedUserId); + if (services == null || services.size() == 0) { + services = constructSystemServiceListLocked(resolvedUserId); + mSystemServicesCacheList.put(resolvedUserId, services); + } + return services; + } + private void runForUser(@NonNull final Consumer<CredentialManagerServiceImpl> c) { final int userId = UserHandle.getCallingUserId(); final long origId = Binder.clearCallingIdentity(); try { synchronized (mLock) { final List<CredentialManagerServiceImpl> services = - getServiceListForUserLocked(userId); + getAllCredentialProviderServicesLocked(userId); for (CredentialManagerServiceImpl s : services) { c.accept(s); } @@ -171,6 +209,19 @@ public final class CredentialManagerService } } + @GuardedBy("mLock") + private List<CredentialManagerServiceImpl> getAllCredentialProviderServicesLocked( + int userId) { + List<CredentialManagerServiceImpl> concatenatedServices = new ArrayList<>(); + List<CredentialManagerServiceImpl> userConfigurableServices = + getServiceListForUserLocked(userId); + if (userConfigurableServices != null && !userConfigurableServices.isEmpty()) { + concatenatedServices.addAll(userConfigurableServices); + } + concatenatedServices.addAll(getOrConstructSystemServiceListLock(userId)); + return concatenatedServices; + } + @SuppressWarnings("GuardedBy") // ErrorProne requires initiateProviderSessionForRequestLocked // to be guarded by 'service.mLock', which is the same as mLock. private List<ProviderSession> initiateProviderSessions( @@ -248,14 +299,10 @@ public final class CredentialManagerService // Iterate over all provider sessions and invoke the request providerSessions.forEach( - providerGetSession -> { - providerGetSession - .getRemoteCredentialService() - .onBeginGetCredential( - (BeginGetCredentialRequest) - providerGetSession.getProviderRequest(), - /* callback= */ providerGetSession); - }); + providerGetSession -> providerGetSession + .getRemoteCredentialService().onBeginGetCredential( + (BeginGetCredentialRequest) providerGetSession.getProviderRequest(), + /*callback=*/providerGetSession)); return cancelTransport; } @@ -297,14 +344,12 @@ public final class CredentialManagerService // Iterate over all provider sessions and invoke the request providerSessions.forEach( - providerCreateSession -> { - providerCreateSession - .getRemoteCredentialService() - .onCreateCredential( - (BeginCreateCredentialRequest) - providerCreateSession.getProviderRequest(), - /* callback= */ providerCreateSession); - }); + providerCreateSession -> providerCreateSession + .getRemoteCredentialService() + .onCreateCredential( + (BeginCreateCredentialRequest) + providerCreateSession.getProviderRequest(), + /* callback= */ providerCreateSession)); return cancelTransport; } diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java b/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java index 0fd1f1929cae..546c48fe05f4 100644 --- a/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java +++ b/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java @@ -58,7 +58,18 @@ public final class CredentialManagerServiceImpl extends return mInfo.getServiceInfo().getComponentName(); } - @Override // from PerUserSystemService + CredentialManagerServiceImpl( + @NonNull CredentialManagerService master, + @NonNull Object lock, int userId, CredentialProviderInfo providerInfo) { + super(master, lock, userId); + Log.i(TAG, "in CredentialManagerServiceImpl constructed with system constructor: " + + providerInfo.isSystemProvider() + + " , " + providerInfo.getServiceInfo() == null ? "" : + providerInfo.getServiceInfo().getComponentName().flattenToString()); + mInfo = providerInfo; + } + + @Override // from PerUserSystemService when a new setting based service is to be created @GuardedBy("mLock") protected ServiceInfo newServiceInfoLocked(@NonNull ComponentName serviceComponent) throws PackageManager.NameNotFoundException { @@ -71,7 +82,9 @@ public final class CredentialManagerServiceImpl extends Log.i(TAG, "newServiceInfoLocked with null mInfo , " + serviceComponent.getPackageName()); } - mInfo = new CredentialProviderInfo(getContext(), serviceComponent, mUserId); + mInfo = new CredentialProviderInfo( + getContext(), serviceComponent, + mUserId, /*isSystemProvider=*/false); return mInfo.getServiceInfo(); } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index cdb2e08e80e3..8be3df4fbe82 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -8742,21 +8742,6 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { } } - private boolean isDeviceOwnerPackage(String packageName, int userId) { - synchronized (getLockObject()) { - return mOwners.hasDeviceOwner() - && mOwners.getDeviceOwnerUserId() == userId - && mOwners.getDeviceOwnerPackageName().equals(packageName); - } - } - - private boolean isProfileOwnerPackage(String packageName, int userId) { - synchronized (getLockObject()) { - return mOwners.hasProfileOwner(userId) - && mOwners.getProfileOwnerPackage(userId).equals(packageName); - } - } - public boolean isProfileOwner(ComponentName who, int userId) { final ComponentName profileOwner = mInjector.binderWithCleanCallingIdentity(() -> getProfileOwnerAsUser(userId)); @@ -9315,7 +9300,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { boolean hasProfileOwner = mOwners.hasProfileOwner(userId); if (!hasProfileOwner) { int managedUserId = getManagedUserId(userId); - if (managedUserId == -1 && newState != STATE_USER_UNMANAGED) { + if (managedUserId < 0 && newState != STATE_USER_UNMANAGED) { // No managed device, user or profile, so setting provisioning state makes // no sense. String error = "Not allowed to change provisioning state unless a " @@ -12524,7 +12509,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { /** * @return the user ID of the managed user that is linked to the current user, if any. - * Otherwise -1. + * Otherwise UserHandle.USER_NULL (-10000). */ public int getManagedUserId(@UserIdInt int callingUserId) { if (VERBOSE_LOG) Slogf.v(LOG_TAG, "getManagedUserId: callingUserId=%d", callingUserId); @@ -12537,7 +12522,26 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { return ui.id; } if (VERBOSE_LOG) Slogf.v(LOG_TAG, "Managed user not found."); - return -1; + return UserHandle.USER_NULL; + } + + /** + * Returns the userId of the managed profile on the device. + * If none exists, return {@link UserHandle#USER_NULL}. + * + * We assume there is only one managed profile across all users + * on the device, which is true for now (HSUM or not) but could + * change in future. + */ + private @UserIdInt int getManagedUserId() { + // On HSUM, there is only one main user and only the main user + // can have a managed profile (for now). On non-HSUM, only user 0 + // can host the managed profile and user 0 is the main user. + // So in both cases, we could just get the main user and + // search for the profile user under it. + UserHandle mainUser = mUserManager.getMainUser(); + if (mainUser == null) return UserHandle.USER_NULL; + return getManagedUserId(mainUser.getIdentifier()); } @Override @@ -16187,7 +16191,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { return mOwners.getDeviceOwnerUserId(); } else { return mInjector.binderWithCleanCallingIdentity( - () -> getManagedUserId(UserHandle.USER_SYSTEM)); + () -> getManagedUserId()); } } } diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 5ebf6cec4d5c..5c5442d0b9c3 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -108,7 +108,6 @@ import com.android.internal.widget.LockSettingsInternal; import com.android.server.am.ActivityManagerService; import com.android.server.ambientcontext.AmbientContextManagerService; import com.android.server.appbinding.AppBindingService; -import com.android.server.art.ArtManagerLocal; import com.android.server.art.ArtModuleServiceInitializer; import com.android.server.art.DexUseManagerLocal; import com.android.server.attention.AttentionManagerService; @@ -132,8 +131,8 @@ import com.android.server.display.DisplayManagerService; import com.android.server.display.color.ColorDisplayService; import com.android.server.dreams.DreamManagerService; import com.android.server.emergency.EmergencyAffordanceService; -import com.android.server.grammaticalinflection.GrammaticalInflectionService; import com.android.server.gpu.GpuService; +import com.android.server.grammaticalinflection.GrammaticalInflectionService; import com.android.server.graphics.fonts.FontManagerService; import com.android.server.hdmi.HdmiControlService; import com.android.server.incident.IncidentCompanionService; @@ -147,6 +146,7 @@ import com.android.server.logcat.LogcatManagerService; import com.android.server.media.MediaRouterService; import com.android.server.media.metrics.MediaMetricsManagerService; import com.android.server.media.projection.MediaProjectionManagerService; +import com.android.server.net.NetworkManagementService; import com.android.server.net.NetworkPolicyManagerService; import com.android.server.net.watchlist.NetworkWatchlistService; import com.android.server.notification.NotificationManagerService; @@ -163,6 +163,7 @@ import com.android.server.pm.ApexSystemServiceInfo; import com.android.server.pm.BackgroundInstallControlService; import com.android.server.pm.CrossProfileAppsService; import com.android.server.pm.DataLoaderManagerService; +import com.android.server.pm.DexOptHelper; import com.android.server.pm.DynamicCodeLoggingService; import com.android.server.pm.Installer; import com.android.server.pm.LauncherAppsService; @@ -2770,7 +2771,7 @@ public final class SystemServer implements Dumpable { t.traceEnd(); t.traceBegin("ArtManagerLocal"); - LocalManagerRegistry.addManager(ArtManagerLocal.class, new ArtManagerLocal(context)); + DexOptHelper.initializeArtManagerLocal(context, mPackageManagerService); t.traceEnd(); if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_UWB)) { diff --git a/services/permission/java/com/android/server/permission/access/collection/IndexedMap.kt b/services/permission/java/com/android/server/permission/access/collection/IndexedMap.kt index f4e362ceb2c7..998d2067e070 100644 --- a/services/permission/java/com/android/server/permission/access/collection/IndexedMap.kt +++ b/services/permission/java/com/android/server/permission/access/collection/IndexedMap.kt @@ -148,6 +148,13 @@ inline fun <K, V> IndexedMap<K, V>.retainAllIndexed(predicate: (Int, K, V) -> Bo return isChanged } +inline fun <K, V, R> IndexedMap<K, V>.mapIndexed(transform: (Int, K, V) -> R): IndexedList<R> = + IndexedList<R>().also { destination -> + forEachIndexed { index, key, value -> + transform(index, key, value).let { destination += it } + } + } + inline fun <K, V, R> IndexedMap<K, V>.mapNotNullIndexed( transform: (Int, K, V) -> R? ): IndexedList<R> = diff --git a/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt b/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt index acd0a3cbbb98..903fad33055f 100644 --- a/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt +++ b/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt @@ -73,6 +73,7 @@ import com.android.server.pm.UserManagerInternal import com.android.server.pm.UserManagerService import com.android.server.pm.parsing.pkg.AndroidPackageUtils import com.android.server.pm.permission.LegacyPermission +import com.android.server.pm.permission.Permission as LegacyPermission2 import com.android.server.pm.permission.LegacyPermissionSettings import com.android.server.pm.permission.LegacyPermissionState import com.android.server.pm.permission.PermissionManagerServiceInterface @@ -1673,40 +1674,77 @@ class PermissionService( context.getSystemService(PermissionControllerManager::class.java)!!.dump(fd, args) } - override fun getPermissionTEMP( - permissionName: String - ): com.android.server.pm.permission.Permission? { - // TODO("Not yet implemented") - return null - } + override fun getPermissionTEMP(permissionName: String): LegacyPermission2? { + val permission = service.getState { + with(policy) { getPermissions()[permissionName] } + } ?: return null - override fun getLegacyPermissions(): List<LegacyPermission> { - // TODO("Not yet implemented") - return emptyList() + return LegacyPermission2( + permission.permissionInfo, permission.type, permission.isReconciled, permission.appId, + permission.gids, permission.areGidsPerUser + ) } + override fun getLegacyPermissions(): List<LegacyPermission> = + service.getState { + with(policy) { getPermissions() } + }.mapIndexed { _, _, permission -> + LegacyPermission( + permission.permissionInfo, permission.type, permission.appId, permission.gids + ) + } + override fun readLegacyPermissionsTEMP(legacyPermissionSettings: LegacyPermissionSettings) { // Package settings has been read when this method is called. service.initialize() - // TODO("Not yet implemented") } override fun writeLegacyPermissionsTEMP(legacyPermissionSettings: LegacyPermissionSettings) { - // TODO("Not yet implemented") + service.getState { + val permissions = with(policy) { getPermissions() } + legacyPermissionSettings.replacePermissions(toLegacyPermissions(permissions)) + val permissionTrees = with(policy) { getPermissionTrees() } + legacyPermissionSettings.replacePermissionTrees(toLegacyPermissions(permissionTrees)) + } } + private fun toLegacyPermissions( + permissions: IndexedMap<String, Permission> + ): List<LegacyPermission> = + permissions.mapIndexed { _, _, permission -> + // We don't need to provide UID and GIDs, which are only retrieved when dumping. + LegacyPermission( + permission.permissionInfo, permission.type, 0, EmptyArray.INT + ) + } + override fun getLegacyPermissionState(appId: Int): LegacyPermissionState { - // TODO("Not yet implemented") - return LegacyPermissionState() - } + val legacyState = LegacyPermissionState() + val userIds = userManagerService.userIdsIncludingPreCreated + service.getState { + val permissions = with(policy) { getPermissions() } + userIds.forEachIndexed { _, userId -> + val permissionFlags = with(policy) { getUidPermissionFlags(appId, userId) } + ?: return@forEachIndexed - override fun readLegacyPermissionStateTEMP() { - // TODO("Not yet implemented") + permissionFlags.forEachIndexed permissionFlags@{ _, permissionName, flags -> + val permission = permissions[permissionName] ?: return@permissionFlags + val legacyPermissionState = LegacyPermissionState.PermissionState( + permissionName, + permission.isRuntime, + PermissionFlags.isPermissionGranted(flags), + PermissionFlags.toApiFlags(flags) + ) + legacyState.putPermissionState(legacyPermissionState, userId) + } + } + } + return legacyState } - override fun writeLegacyPermissionStateTEMP() { - // TODO("Not yet implemented") - } + override fun readLegacyPermissionStateTEMP() {} + + override fun writeLegacyPermissionStateTEMP() {} override fun onSystemReady() { // TODO STOPSHIP privappPermissionsViolationsfix check diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/AppsFilterImplTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/AppsFilterImplTest.java index 5e5e7e3a98a9..56cd7a924acd 100644 --- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/AppsFilterImplTest.java +++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/AppsFilterImplTest.java @@ -1024,7 +1024,10 @@ public class AppsFilterImplTest { DUMMY_TARGET_APPID); PackageSetting calling = simulateAddPackage(appsFilter, pkg("com.some.other.package"), DUMMY_CALLING_APPID, - withInstallSource(target.getPackageName(), null, null, INVALID_UID, null, false)); + withInstallSource(target.getPackageName(), null /* originatingPackageName */, + null /* installerPackageName */, INVALID_UID, + null /* updateOwnerPackageName */, null /* installerAttributionTag */, + false /* isInitiatingPackageUninstalled */)); assertFalse( appsFilter.shouldFilterApplication(mSnapshot, DUMMY_CALLING_APPID, calling, target, @@ -1043,7 +1046,10 @@ public class AppsFilterImplTest { DUMMY_TARGET_APPID); PackageSetting calling = simulateAddPackage(appsFilter, pkg("com.some.other.package"), DUMMY_CALLING_APPID, - withInstallSource(target.getPackageName(), null, null, INVALID_UID, null, true)); + withInstallSource(target.getPackageName(), null /* originatingPackageName */, + null /* installerPackageName */, INVALID_UID, + null /* updateOwnerPackageName */, null /* installerAttributionTag */, + true /* isInitiatingPackageUninstalled */)); assertTrue( appsFilter.shouldFilterApplication(mSnapshot, DUMMY_CALLING_APPID, calling, target, @@ -1066,8 +1072,10 @@ public class AppsFilterImplTest { DUMMY_TARGET_APPID); watcher.verifyChangeReported("add package"); PackageSetting calling = simulateAddPackage(appsFilter, pkg("com.some.other.package"), - DUMMY_CALLING_APPID, withInstallSource(null, target.getPackageName(), null, - INVALID_UID, null, false)); + DUMMY_CALLING_APPID, withInstallSource(null /* initiatingPackageName */, + target.getPackageName(), null /* installerPackageName */, INVALID_UID, + null /* updateOwnerPackageName */, null /* installerAttributionTag */, + false /* isInitiatingPackageUninstalled */)); watcher.verifyChangeReported("add package"); assertTrue( @@ -1092,8 +1100,11 @@ public class AppsFilterImplTest { DUMMY_TARGET_APPID); watcher.verifyChangeReported("add package"); PackageSetting calling = simulateAddPackage(appsFilter, pkg("com.some.other.package"), - DUMMY_CALLING_APPID, withInstallSource(null, null, target.getPackageName(), - DUMMY_TARGET_APPID, null, false)); + DUMMY_CALLING_APPID, withInstallSource(null /* initiatingPackageName */, + null /* originatingPackageName */, target.getPackageName(), + DUMMY_TARGET_APPID, null /* updateOwnerPackageName */, + null /* installerAttributionTag */, + false /* isInitiatingPackageUninstalled */)); watcher.verifyChangeReported("add package"); assertFalse( @@ -1679,10 +1690,12 @@ public class AppsFilterImplTest { private WithSettingBuilder withInstallSource(String initiatingPackageName, String originatingPackageName, String installerPackageName, int installerPackageUid, - String installerAttributionTag, boolean isInitiatingPackageUninstalled) { + String updateOwnerPackageName, String installerAttributionTag, + boolean isInitiatingPackageUninstalled) { final InstallSource installSource = InstallSource.create(initiatingPackageName, originatingPackageName, installerPackageName, installerPackageUid, - installerAttributionTag, /* isOrphaned= */ false, isInitiatingPackageUninstalled); + updateOwnerPackageName, installerAttributionTag, /* isOrphaned= */ false, + isInitiatingPackageUninstalled); return setting -> setting.setInstallSource(installSource); } } diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageInstallerSessionTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageInstallerSessionTest.java index 4da082e0370b..98655c895aff 100644 --- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageInstallerSessionTest.java +++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageInstallerSessionTest.java @@ -167,8 +167,8 @@ public class PackageInstallerSessionTest { params.isMultiPackage = true; } InstallSource installSource = InstallSource.create("testInstallInitiator", - "testInstallOriginator", "testInstaller", -1, "testAttributionTag", - PackageInstaller.PACKAGE_SOURCE_UNSPECIFIED); + "testInstallOriginator", "testInstaller", -1, "testUpdateOwner", + "testAttributionTag", PackageInstaller.PACKAGE_SOURCE_UNSPECIFIED); return new PackageInstallerSession( /* callback */ null, /* context */null, 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 8e1ca3c02264..0b7020c74f66 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 @@ -218,6 +218,7 @@ class AndroidPackageTest : ParcelableComponentTest(AndroidPackage::class, Packag AndroidPackage::isAllowClearUserDataOnFailedRestore, AndroidPackage::isAllowNativeHeapPointerTagging, AndroidPackage::isAllowTaskReparenting, + AndroidPackage::isAllowUpdateOwnership, AndroidPackage::isBackupInForeground, AndroidPackage::isHardwareAccelerated, AndroidPackage::isCantSaveState, diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/MockSystem.kt b/services/tests/mockingservicestests/src/com/android/server/pm/MockSystem.kt index 9935a2f2a0ba..06ba5dd6069b 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/MockSystem.kt +++ b/services/tests/mockingservicestests/src/com/android/server/pm/MockSystem.kt @@ -63,6 +63,7 @@ import com.android.server.SystemServerInitThreadPool import com.android.server.compat.PlatformCompat import com.android.server.extendedtestutils.wheneverStatic import com.android.server.pm.dex.DexManager +import com.android.server.pm.dex.DynamicCodeLogger import com.android.server.pm.parsing.PackageParser2 import com.android.server.pm.parsing.pkg.PackageImpl import com.android.server.pm.parsing.pkg.ParsedPackage @@ -208,6 +209,7 @@ class MockSystem(withSession: (StaticMockitoSessionBuilder) -> Unit = {}) { whenever(snapshot()) { appsFilterSnapshot } } val dexManager: DexManager = mock() + val dynamicCodeLogger: DynamicCodeLogger = mock() val installer: Installer = mock() val displayMetrics: DisplayMetrics = mock() val domainVerificationManagerInternal: DomainVerificationManagerInternal = mock() @@ -285,6 +287,7 @@ class MockSystem(withSession: (StaticMockitoSessionBuilder) -> Unit = {}) { whenever(mocks.injector.crossProfileIntentFilterHelper) .thenReturn(mocks.crossProfileIntentFilterHelper) whenever(mocks.injector.dexManager).thenReturn(mocks.dexManager) + whenever(mocks.injector.dynamicCodeLogger).thenReturn(mocks.dynamicCodeLogger) whenever(mocks.injector.systemConfig).thenReturn(mocks.systemConfig) whenever(mocks.injector.apexManager).thenReturn(mocks.apexManager) whenever(mocks.injector.scanningCachingPackageParser).thenReturn(mocks.packageParser) diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java index cfd5279a0fa9..d2547a3ff336 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java @@ -723,8 +723,8 @@ public class StagingManagerTest { params.isStaged = true; InstallSource installSource = InstallSource.create("testInstallInitiator", - "testInstallOriginator", "testInstaller", 100, "testAttributionTag", - PackageInstaller.PACKAGE_SOURCE_UNSPECIFIED); + "testInstallOriginator", "testInstaller", 100, "testUpdateOwner", + "testAttributionTag", PackageInstaller.PACKAGE_SOURCE_UNSPECIFIED); PackageInstallerSession session = new PackageInstallerSession( /* callback */ null, diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorSUSDTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorSUSDTest.java index 88709e164d79..b9ba780ff685 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorSUSDTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorSUSDTest.java @@ -49,6 +49,7 @@ public final class UserVisibilityMediatorSUSDTest extends UserVisibilityMediator int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, FG, DEFAULT_DISPLAY); assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE); + expectUserCannotBeUnassignedFromDisplay(USER_ID, DEFAULT_DISPLAY); expectUserIsVisible(USER_ID); expectUserIsNotVisibleOnDisplay(USER_ID, INVALID_DISPLAY); @@ -80,6 +81,7 @@ public final class UserVisibilityMediatorSUSDTest extends UserVisibilityMediator int result = mMediator.assignUserToDisplayOnStart(currentUserId, currentUserId, FG, DEFAULT_DISPLAY); assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE); + expectUserCannotBeUnassignedFromDisplay(currentUserId, DEFAULT_DISPLAY); expectUserIsVisible(currentUserId); expectUserIsNotVisibleOnDisplay(currentUserId, INVALID_DISPLAY); @@ -110,6 +112,7 @@ public final class UserVisibilityMediatorSUSDTest extends UserVisibilityMediator int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID, BG_VISIBLE, DEFAULT_DISPLAY); assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE); + expectUserCannotBeUnassignedFromDisplay(PROFILE_USER_ID, DEFAULT_DISPLAY); expectUserIsVisible(PROFILE_USER_ID); expectUserIsNotVisibleOnDisplay(PROFILE_USER_ID, INVALID_DISPLAY); diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorTestCase.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorTestCase.java index e4664d2c2c46..c59834bea6ca 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorTestCase.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorTestCase.java @@ -165,12 +165,16 @@ abstract class UserVisibilityMediatorTestCase extends ExtendedMockitoTestCase { expectNoDisplayAssignedToUser(USER_ID); expectInitialCurrentUserAssignedToDisplay(DEFAULT_DISPLAY); + assertInvisibleUserCannotBeAssignedExtraDisplay(USER_ID, SECONDARY_DISPLAY_ID); + listener.verify(); } @Test public final void testStartVisibleBgUser_onDefaultDisplay() throws Exception { visibleBgUserCannotBeStartedOnDefaultDisplayTest(); + + assertInvisibleUserCannotBeAssignedExtraDisplay(USER_ID, SECONDARY_DISPLAY_ID); } protected final void visibleBgUserCannotBeStartedOnDefaultDisplayTest() throws Exception { @@ -180,8 +184,8 @@ abstract class UserVisibilityMediatorTestCase extends ExtendedMockitoTestCase { DEFAULT_DISPLAY); assertStartUserResult(result, USER_ASSIGNMENT_RESULT_FAILURE); - expectUserIsNotVisibleAtAll(PROFILE_USER_ID); - expectNoDisplayAssignedToUser(PROFILE_USER_ID); + expectUserIsNotVisibleAtAll(USER_ID); + expectNoDisplayAssignedToUser(USER_ID); listener.verify(); } @@ -194,8 +198,11 @@ abstract class UserVisibilityMediatorTestCase extends ExtendedMockitoTestCase { SECONDARY_DISPLAY_ID); assertStartUserResult(result, USER_ASSIGNMENT_RESULT_FAILURE); - expectUserIsNotVisibleAtAll(PROFILE_USER_ID); - expectNoDisplayAssignedToUser(PROFILE_USER_ID); + expectUserIsNotVisibleAtAll(USER_ID); + expectNoDisplayAssignedToUser(USER_ID); + + assertInvisibleUserCannotBeAssignedExtraDisplay(USER_ID, SECONDARY_DISPLAY_ID); + assertInvisibleUserCannotBeAssignedExtraDisplay(USER_ID, OTHER_SECONDARY_DISPLAY_ID); listener.verify(); } @@ -217,6 +224,9 @@ abstract class UserVisibilityMediatorTestCase extends ExtendedMockitoTestCase { expectNoDisplayAssignedToUser(USER_SYSTEM); expectUserAssignedToDisplay(SECONDARY_DISPLAY_ID, USER_ID); + assertUserCannotBeAssignedExtraDisplay(USER_SYSTEM, SECONDARY_DISPLAY_ID); + assertUserCannotBeAssignedExtraDisplay(USER_SYSTEM, OTHER_SECONDARY_DISPLAY_ID); + listener.verify(); } @@ -256,6 +266,8 @@ abstract class UserVisibilityMediatorTestCase extends ExtendedMockitoTestCase { expectNoDisplayAssignedToUser(PROFILE_USER_ID); expectUserAssignedToDisplay(DEFAULT_DISPLAY, OTHER_USER_ID); + assertUserCannotBeAssignedExtraDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID); + listener.verify(); } @@ -289,6 +301,8 @@ abstract class UserVisibilityMediatorTestCase extends ExtendedMockitoTestCase { expectNoDisplayAssignedToUser(PROFILE_USER_ID); expectInitialCurrentUserAssignedToDisplay(DEFAULT_DISPLAY); + assertUserCannotBeAssignedExtraDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID); + listener.verify(); } @@ -305,6 +319,10 @@ abstract class UserVisibilityMediatorTestCase extends ExtendedMockitoTestCase { expectNoDisplayAssignedToUser(PROFILE_USER_ID); expectInitialCurrentUserAssignedToDisplay(SECONDARY_DISPLAY_ID); + assertInvisibleUserCannotBeAssignedExtraDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID); + assertInvisibleUserCannotBeAssignedExtraDisplay(PROFILE_USER_ID, + OTHER_SECONDARY_DISPLAY_ID); + listener.verify(); } @@ -320,6 +338,10 @@ abstract class UserVisibilityMediatorTestCase extends ExtendedMockitoTestCase { expectNoDisplayAssignedToUser(PROFILE_USER_ID); expectInitialCurrentUserAssignedToDisplay(SECONDARY_DISPLAY_ID); + assertInvisibleUserCannotBeAssignedExtraDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID); + assertInvisibleUserCannotBeAssignedExtraDisplay(PROFILE_USER_ID, + OTHER_SECONDARY_DISPLAY_ID); + listener.verify(); } @@ -336,6 +358,9 @@ abstract class UserVisibilityMediatorTestCase extends ExtendedMockitoTestCase { expectNoDisplayAssignedToUser(PROFILE_USER_ID); expectInitialCurrentUserAssignedToDisplay(DEFAULT_DISPLAY); + assertInvisibleUserCannotBeAssignedExtraDisplay(PROFILE_USER_ID, DEFAULT_DISPLAY); + assertInvisibleUserCannotBeAssignedExtraDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID); + listener.verify(); } @@ -351,6 +376,10 @@ abstract class UserVisibilityMediatorTestCase extends ExtendedMockitoTestCase { expectNoDisplayAssignedToUser(PROFILE_USER_ID); expectInitialCurrentUserAssignedToDisplay(SECONDARY_DISPLAY_ID); + assertInvisibleUserCannotBeAssignedExtraDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID); + assertInvisibleUserCannotBeAssignedExtraDisplay(PROFILE_USER_ID, + OTHER_SECONDARY_DISPLAY_ID); + listener.verify(); } @@ -481,6 +510,63 @@ abstract class UserVisibilityMediatorTestCase extends ExtendedMockitoTestCase { .that(actualResult).isEqualTo(expectedResult); } + protected void assertBgUserBecomesInvisibleOnStop(@UserIdInt int userId) { + Log.d(TAG, "Stopping user " + userId); + mMediator.unassignUserFromDisplayOnStop(userId); + expectUserIsNotVisibleAtAll(userId); + } + + /** + * Assigns and unassigns the user to / from an extra display, asserting the visibility state in + * between. + * + * <p>It assumes the user was not visible in the display beforehand. + */ + protected void assertUserCanBeAssignedExtraDisplay(@UserIdInt int userId, int displayId) { + assertUserCanBeAssignedExtraDisplay(userId, displayId, /* unassign= */ true); + } + + protected void assertUserCanBeAssignedExtraDisplay(@UserIdInt int userId, int displayId, + boolean unassign) { + + expectUserIsNotVisibleOnDisplay(userId, displayId); + + Log.d(TAG, "Calling assignUserToExtraDisplay(" + userId + ", " + displayId + ")"); + assertWithMessage("assignUserToExtraDisplay(%s, %s)", userId, displayId) + .that(mMediator.assignUserToExtraDisplay(userId, displayId)) + .isTrue(); + expectUserIsVisibleOnDisplay(userId, displayId); + + if (unassign) { + Log.d(TAG, "Calling unassignUserFromExtraDisplay(" + userId + ", " + displayId + ")"); + assertWithMessage("unassignUserFromExtraDisplay(%s, %s)", userId, displayId) + .that(mMediator.unassignUserFromExtraDisplay(userId, displayId)) + .isTrue(); + expectUserIsNotVisibleOnDisplay(userId, displayId); + } + } + + /** + * Asserts that a user (already visible or not) cannot be assigned to an extra display (and + * hence won't be visible on that display). + */ + protected void assertUserCannotBeAssignedExtraDisplay(@UserIdInt int userId, int displayId) { + expectWithMessage("assignUserToExtraDisplay(%s, %s)", userId, displayId) + .that(mMediator.assignUserToExtraDisplay(userId, displayId)) + .isFalse(); + expectUserIsNotVisibleOnDisplay(userId, displayId); + } + + /** + * Asserts that an invisible user cannot be assigned to an extra display. + */ + protected void assertInvisibleUserCannotBeAssignedExtraDisplay(@UserIdInt int userId, + int displayId) { + assertUserCannotBeAssignedExtraDisplay(userId, displayId); + expectNoDisplayAssignedToUser(userId); + expectInitialCurrentUserAssignedToDisplay(displayId); + } + protected void expectUserIsVisible(@UserIdInt int userId) { expectWithMessage("isUserVisible(%s)", userId) .that(mMediator.isUserVisible(userId)) @@ -534,6 +620,11 @@ abstract class UserVisibilityMediatorTestCase extends ExtendedMockitoTestCase { .that(mMediator.getDisplayAssignedToUser(userId)).isEqualTo(INVALID_DISPLAY); } + protected void expectUserCannotBeUnassignedFromDisplay(@UserIdInt int userId, int displayId) { + expectWithMessage("unassignUserFromExtraDisplay(%s, %s)", userId, displayId) + .that(mMediator.unassignUserFromExtraDisplay(userId, displayId)).isFalse(); + } + protected void expectUserAssignedToDisplay(int displayId, @UserIdInt int userId) { expectWithMessage("getUserAssignedToDisplay(%s)", displayId) .that(mMediator.getUserAssignedToDisplay(displayId)).isEqualTo(userId); diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorVisibleBackgroundUserTestCase.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorVisibleBackgroundUserTestCase.java index 66d7eb6e603e..627553bcfa18 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorVisibleBackgroundUserTestCase.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorVisibleBackgroundUserTestCase.java @@ -52,6 +52,7 @@ abstract class UserVisibilityMediatorVisibleBackgroundUserTestCase int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, FG, DEFAULT_DISPLAY); assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE); + expectUserCannotBeUnassignedFromDisplay(USER_ID, DEFAULT_DISPLAY); expectUserIsVisible(USER_ID); expectUserIsVisibleOnDisplay(USER_ID, DEFAULT_DISPLAY); @@ -64,7 +65,9 @@ abstract class UserVisibilityMediatorVisibleBackgroundUserTestCase expectUserAssignedToDisplay(INVALID_DISPLAY, USER_ID); expectUserAssignedToDisplay(SECONDARY_DISPLAY_ID, USER_ID); - expectDisplayAssignedToUser(USER_NULL, INVALID_DISPLAY); + expectNoDisplayAssignedToUser(USER_NULL); + + assertUserCanBeAssignedExtraDisplay(USER_ID, SECONDARY_DISPLAY_ID); listener.verify(); } @@ -83,6 +86,7 @@ abstract class UserVisibilityMediatorVisibleBackgroundUserTestCase int result = mMediator.assignUserToDisplayOnStart(currentUserId, currentUserId, FG, DEFAULT_DISPLAY); assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE); + expectUserCannotBeUnassignedFromDisplay(currentUserId, DEFAULT_DISPLAY); expectUserIsVisible(currentUserId); expectUserIsVisibleOnDisplay(currentUserId, DEFAULT_DISPLAY); @@ -98,6 +102,8 @@ abstract class UserVisibilityMediatorVisibleBackgroundUserTestCase expectUserIsNotVisibleAtAll(previousCurrentUserId); expectNoDisplayAssignedToUser(previousCurrentUserId); + assertUserCanBeAssignedExtraDisplay(USER_ID, SECONDARY_DISPLAY_ID); + listener.verify(); } @@ -113,6 +119,7 @@ abstract class UserVisibilityMediatorVisibleBackgroundUserTestCase int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID, BG_VISIBLE, DEFAULT_DISPLAY); assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE); + expectUserCannotBeUnassignedFromDisplay(PROFILE_USER_ID, DEFAULT_DISPLAY); expectUserIsVisible(PROFILE_USER_ID); expectUserIsNotVisibleOnDisplay(PROFILE_USER_ID, INVALID_DISPLAY); @@ -123,6 +130,8 @@ abstract class UserVisibilityMediatorVisibleBackgroundUserTestCase expectDisplayAssignedToUser(PROFILE_USER_ID, DEFAULT_DISPLAY); expectUserAssignedToDisplay(DEFAULT_DISPLAY, PARENT_USER_ID); + assertUserCannotBeAssignedExtraDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID); + listener.verify(); } @@ -134,6 +143,9 @@ abstract class UserVisibilityMediatorVisibleBackgroundUserTestCase assertStartUserResult(result, USER_ASSIGNMENT_RESULT_FAILURE); + assertInvisibleUserCannotBeAssignedExtraDisplay(USER_ID, DEFAULT_DISPLAY); + assertInvisibleUserCannotBeAssignedExtraDisplay(USER_ID, SECONDARY_DISPLAY_ID); + listener.verify(); } @@ -148,6 +160,9 @@ abstract class UserVisibilityMediatorVisibleBackgroundUserTestCase expectUserIsNotVisibleAtAll(USER_ID); + assertInvisibleUserCannotBeAssignedExtraDisplay(USER_ID, DEFAULT_DISPLAY); + assertInvisibleUserCannotBeAssignedExtraDisplay(USER_ID, SECONDARY_DISPLAY_ID); + listener.verify(); } @@ -159,6 +174,7 @@ abstract class UserVisibilityMediatorVisibleBackgroundUserTestCase int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG_VISIBLE, SECONDARY_DISPLAY_ID); assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE); + expectUserCannotBeUnassignedFromDisplay(USER_ID, SECONDARY_DISPLAY_ID); expectUserIsVisible(USER_ID); expectUserIsVisibleOnDisplay(USER_ID, SECONDARY_DISPLAY_ID); @@ -169,7 +185,16 @@ abstract class UserVisibilityMediatorVisibleBackgroundUserTestCase expectDisplayAssignedToUser(USER_ID, SECONDARY_DISPLAY_ID); expectUserAssignedToDisplay(SECONDARY_DISPLAY_ID, USER_ID); - listener.verify(); + assertUserCanBeAssignedExtraDisplay(USER_ID, OTHER_SECONDARY_DISPLAY_ID); + + // Assign again, without unassigning (to make sure it becomes invisible on stop) + AsyncUserVisibilityListener listener2 = addListenerForEvents(onInvisible(USER_ID)); + assertUserCanBeAssignedExtraDisplay(USER_ID, OTHER_SECONDARY_DISPLAY_ID, + /* unassign= */ false); + + assertBgUserBecomesInvisibleOnStop(USER_ID); + + listener2.verify(); } @Test @@ -203,6 +228,8 @@ abstract class UserVisibilityMediatorVisibleBackgroundUserTestCase expectNoDisplayAssignedToUser(USER_ID); expectUserAssignedToDisplay(SECONDARY_DISPLAY_ID, OTHER_USER_ID); + assertUserCannotBeAssignedExtraDisplay(USER_ID, SECONDARY_DISPLAY_ID); + listener.verify(); } @@ -226,7 +253,18 @@ abstract class UserVisibilityMediatorVisibleBackgroundUserTestCase expectDisplayAssignedToUser(USER_ID, OTHER_SECONDARY_DISPLAY_ID); expectUserAssignedToDisplay(OTHER_SECONDARY_DISPLAY_ID, USER_ID); + assertUserCanBeAssignedExtraDisplay(USER_ID, SECONDARY_DISPLAY_ID); + listener.verify(); + + // Assign again, without unassigning (to make sure it becomes invisible on stop) + AsyncUserVisibilityListener listener2 = addListenerForEvents(onInvisible(USER_ID)); + assertUserCanBeAssignedExtraDisplay(USER_ID, SECONDARY_DISPLAY_ID, + /* unassign= */ false); + + assertBgUserBecomesInvisibleOnStop(USER_ID); + + listener2.verify(); } @Test @@ -244,12 +282,14 @@ abstract class UserVisibilityMediatorVisibleBackgroundUserTestCase expectNoDisplayAssignedToUser(PROFILE_USER_ID); expectUserAssignedToDisplay(OTHER_SECONDARY_DISPLAY_ID, PARENT_USER_ID); + assertInvisibleUserCannotBeAssignedExtraDisplay(PROFILE_USER_ID, SECONDARY_DISPLAY_ID); + listener.verify(); } + // Conditions below are asserted on other tests, but they're explicitly checked in the 2 + // tests below (which call this method) as well private void currentUserVisibilityWhenNoDisplayIsAssignedTest(@UserIdInt int currentUserId) { - // Conditions below are asserted on other tests, but they're explicitly checked in the 2 - // tests below as well expectUserIsVisible(currentUserId); expectUserIsVisibleOnDisplay(currentUserId, DEFAULT_DISPLAY); expectUserIsNotVisibleOnDisplay(currentUserId, SECONDARY_DISPLAY_ID); @@ -277,4 +317,13 @@ abstract class UserVisibilityMediatorVisibleBackgroundUserTestCase expectUserIsNotVisibleAtAll(INITIAL_CURRENT_USER_ID); expectDisplayAssignedToUser(INITIAL_CURRENT_USER_ID, INVALID_DISPLAY); } + + @Test + public final void testAssignUserToExtraDisplay_invalidDisplays() throws Exception { + expectWithMessage("assignUserToExtraDisplay(%s, %s)", USER_ID, INVALID_DISPLAY) + .that(mMediator.assignUserToExtraDisplay(USER_ID, INVALID_DISPLAY)).isFalse(); + // DEFAULT_DISPLAY is always assigned to the current user + expectWithMessage("assignUserToExtraDisplay(%s, %s)", USER_ID, DEFAULT_DISPLAY) + .that(mMediator.assignUserToExtraDisplay(USER_ID, DEFAULT_DISPLAY)).isFalse(); + } } diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/dex/DexManagerTests.java b/services/tests/mockingservicestests/src/com/android/server/pm/dex/DexManagerTests.java index fb9cbb00255c..7dae23529fc6 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/dex/DexManagerTests.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/dex/DexManagerTests.java @@ -85,6 +85,7 @@ public class DexManagerTests { private final Object mInstallLock = new Object(); + private DynamicCodeLogger mDynamicCodeLogger; private DexManager mDexManager; private TestData mFooUser0; @@ -158,8 +159,9 @@ public class DexManagerTests { .when(mockContext) .getSystemService(PowerManager.class); - mDexManager = new DexManager(mockContext, /*PackageDexOptimizer*/ null, - mInstaller, mInstallLock, mPM); + mDynamicCodeLogger = new DynamicCodeLogger(mInstaller); + mDexManager = new DexManager(mockContext, /*PackageDexOptimizer*/ null, mInstaller, + mInstallLock, mDynamicCodeLogger, mPM); // Foo and Bar are available to user0. // Only Bar is available to user1; @@ -452,6 +454,7 @@ public class DexManagerTests { notifyDexLoad(mBarUser1, mBarUser1.getSecondaryDexPaths(), mUser1); mDexManager.notifyPackageDataDestroyed(mBarUser0.getPackageName(), mUser0); + mDynamicCodeLogger.notifyPackageDataDestroyed(mBarUser0.getPackageName(), mUser0); // Data for user 1 should still be present PackageUseInfo pui = getPackageUseInfo(mBarUser1); @@ -474,6 +477,7 @@ public class DexManagerTests { notifyDexLoad(mBarUser0, mFooUser0.getBaseAndSplitDexPaths(), mUser0); mDexManager.notifyPackageDataDestroyed(mFooUser0.getPackageName(), mUser0); + mDynamicCodeLogger.notifyPackageDataDestroyed(mFooUser0.getPackageName(), mUser0); // Foo should still be around since it's used by other apps but with no // secondary dex info. @@ -491,6 +495,7 @@ public class DexManagerTests { notifyDexLoad(mFooUser0, fooSecondaries, mUser0); mDexManager.notifyPackageDataDestroyed(mFooUser0.getPackageName(), mUser0); + mDynamicCodeLogger.notifyPackageDataDestroyed(mFooUser0.getPackageName(), mUser0); // Foo should not be around since all its secondary dex info were deleted // and it is not used by other apps. @@ -505,6 +510,8 @@ public class DexManagerTests { notifyDexLoad(mBarUser1, mBarUser1.getSecondaryDexPaths(), mUser1); mDexManager.notifyPackageDataDestroyed(mBarUser0.getPackageName(), UserHandle.USER_ALL); + mDynamicCodeLogger.notifyPackageDataDestroyed( + mBarUser0.getPackageName(), UserHandle.USER_ALL); // Bar should not be around since it was removed for all users. assertNoUseInfo(mBarUser0); @@ -906,8 +913,7 @@ public class DexManagerTests { } private PackageDynamicCode getPackageDynamicCodeInfo(TestData testData) { - return mDexManager.getDynamicCodeLogger() - .getPackageDynamicCodeInfo(testData.getPackageName()); + return mDynamicCodeLogger.getPackageDynamicCodeInfo(testData.getPackageName()); } private void assertNoUseInfo(TestData testData) { diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java index d056348318a5..faa7ee022b54 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java @@ -106,7 +106,8 @@ public class AccessibilityManagerServiceTest { private static final String INTENT_ACTION = "TESTACTION"; private static final String DESCRIPTION = "description"; private static final PendingIntent TEST_PENDING_INTENT = PendingIntent.getBroadcast( - ApplicationProvider.getApplicationContext(), 0, new Intent(INTENT_ACTION), + ApplicationProvider.getApplicationContext(), 0, new Intent(INTENT_ACTION) + .setPackage(ApplicationProvider.getApplicationContext().getPackageName()), PendingIntent.FLAG_MUTABLE_UNAUDITED); private static final RemoteAction TEST_ACTION = new RemoteAction( Icon.createWithContentUri("content://test"), diff --git a/services/tests/servicestests/src/com/android/server/accessibility/SystemActionPerformerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/SystemActionPerformerTest.java index bbcf77bf15d7..13d93cbbfde4 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/SystemActionPerformerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/SystemActionPerformerTest.java @@ -76,14 +76,18 @@ public class SystemActionPerformerTest { private static final String DESCRIPTION1 = "description1"; private static final String DESCRIPTION2 = "description2"; private static final PendingIntent TEST_PENDING_INTENT_1 = PendingIntent.getBroadcast( - InstrumentationRegistry.getTargetContext(), 0, new Intent(INTENT_ACTION1), PendingIntent.FLAG_MUTABLE_UNAUDITED); + InstrumentationRegistry.getTargetContext(), 0, new Intent(INTENT_ACTION1) + .setPackage(InstrumentationRegistry.getTargetContext().getPackageName()), + PendingIntent.FLAG_MUTABLE_UNAUDITED); private static final RemoteAction NEW_TEST_ACTION_1 = new RemoteAction( Icon.createWithContentUri("content://test"), LABEL_1, DESCRIPTION1, TEST_PENDING_INTENT_1); private static final PendingIntent TEST_PENDING_INTENT_2 = PendingIntent.getBroadcast( - InstrumentationRegistry.getTargetContext(), 0, new Intent(INTENT_ACTION2), PendingIntent.FLAG_MUTABLE_UNAUDITED); + InstrumentationRegistry.getTargetContext(), 0, new Intent(INTENT_ACTION2) + .setPackage(InstrumentationRegistry.getTargetContext().getPackageName()), + PendingIntent.FLAG_MUTABLE_UNAUDITED); private static final RemoteAction NEW_TEST_ACTION_2 = new RemoteAction( Icon.createWithContentUri("content://test"), LABEL_2, diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java index eac86715e4c7..2a80ce05d4e0 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationControllerTest.java @@ -26,13 +26,13 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; @@ -40,26 +40,33 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.accessibilityservice.MagnificationConfig; +import android.animation.ValueAnimator; import android.content.Context; import android.content.pm.PackageManager; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.Region; +import android.hardware.display.DisplayManagerInternal; +import android.os.Looper; import android.os.RemoteException; import android.os.UserHandle; import android.provider.Settings; import android.test.mock.MockContentResolver; import android.testing.DexmakerShareClassLoaderRule; import android.view.Display; +import android.view.DisplayInfo; import android.view.accessibility.IRemoteMagnificationAnimationCallback; import android.view.accessibility.MagnificationAnimationCallback; +import androidx.annotation.NonNull; +import androidx.test.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; import com.android.internal.util.test.FakeSettingsProvider; import com.android.server.LocalServices; import com.android.server.accessibility.AccessibilityManagerService; import com.android.server.accessibility.AccessibilityTraceManager; +import com.android.server.accessibility.test.MessageCapturingHandler; import com.android.server.wm.WindowManagerInternal; import org.junit.After; @@ -70,7 +77,6 @@ import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; -import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.mockito.stubbing.Answer; @@ -82,6 +88,8 @@ public class MagnificationControllerTest { private static final int TEST_DISPLAY = Display.DEFAULT_DISPLAY; private static final int TEST_SERVICE_ID = 1; + private static final Region INITIAL_SCREEN_MAGNIFICATION_REGION = + new Region(0, 0, 500, 600); private static final Rect TEST_RECT = new Rect(0, 50, 100, 51); private static final float MAGNIFIED_CENTER_X = 100; private static final float MAGNIFIED_CENTER_Y = 200; @@ -101,9 +109,20 @@ public class MagnificationControllerTest { @Mock private Context mContext; @Mock - PackageManager mPackageManager; + private PackageManager mPackageManager; + + @Mock + private FullScreenMagnificationController.ControllerContext mControllerCtx; + @Mock + private ValueAnimator mValueAnimator; @Mock + private MessageCapturingHandler mMessageCapturingHandler; + private FullScreenMagnificationController mScreenMagnificationController; + private final FullScreenMagnificationCtrInfoChangedCallbackDelegate + mScreenMagnificationInfoChangedCallbackDelegate = + new FullScreenMagnificationCtrInfoChangedCallbackDelegate(); + private MagnificationScaleProvider mScaleProvider; @Captor private ArgumentCaptor<MagnificationAnimationCallback> mCallbackArgumentCaptor; @@ -112,13 +131,17 @@ public class MagnificationControllerTest { private WindowMagnificationManager mWindowMagnificationManager; private MockContentResolver mMockResolver; private MagnificationController mMagnificationController; - private final WindowMagnificationMgrCallbackDelegate mCallbackDelegate = + private final WindowMagnificationMgrCallbackDelegate + mWindowMagnificationCallbackDelegate = new WindowMagnificationMgrCallbackDelegate(); @Mock - private WindowManagerInternal mMockWindowManagerInternal; + private WindowManagerInternal mWindowManagerInternal; + @Mock + private WindowManagerInternal.AccessibilityControllerInternal mA11yController; + @Mock - private WindowManagerInternal.AccessibilityControllerInternal mMockA11yController; + private DisplayManagerInternal mDisplayManagerInternal; // To mock package-private class @Rule @@ -132,31 +155,60 @@ public class MagnificationControllerTest { final Object globalLock = new Object(); LocalServices.removeServiceForTest(WindowManagerInternal.class); - LocalServices.addService(WindowManagerInternal.class, mMockWindowManagerInternal); - when(mMockWindowManagerInternal.getAccessibilityController()).thenReturn( - mMockA11yController); + LocalServices.addService(WindowManagerInternal.class, mWindowManagerInternal); + when(mWindowManagerInternal.getAccessibilityController()).thenReturn( + mA11yController); + when(mWindowManagerInternal.setMagnificationCallbacks(eq(TEST_DISPLAY), any())) + .thenReturn(true); + doAnswer((Answer<Void>) invocationOnMock -> { + Object[] args = invocationOnMock.getArguments(); + Region regionArg = (Region) args[1]; + regionArg.set(INITIAL_SCREEN_MAGNIFICATION_REGION); + return null; + }).when(mWindowManagerInternal).getMagnificationRegion(anyInt(), any(Region.class)); mMockResolver = new MockContentResolver(); mMockResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider()); + Looper looper = InstrumentationRegistry.getContext().getMainLooper(); + // Pretending ID of the Thread associated with looper as main thread ID in controller + when(mContext.getMainLooper()).thenReturn(looper); when(mContext.getContentResolver()).thenReturn(mMockResolver); when(mContext.getPackageManager()).thenReturn(mPackageManager); Settings.Secure.putFloatForUser(mMockResolver, Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, DEFAULT_SCALE, CURRENT_USER_ID); mScaleProvider = spy(new MagnificationScaleProvider(mContext)); - mWindowMagnificationManager = Mockito.spy( - new WindowMagnificationManager(mContext, globalLock, - mCallbackDelegate, mTraceManager, mScaleProvider)); + + when(mControllerCtx.getContext()).thenReturn(mContext); + when(mControllerCtx.getTraceManager()).thenReturn(mTraceManager); + when(mControllerCtx.getWindowManager()).thenReturn(mWindowManagerInternal); + when(mControllerCtx.getHandler()).thenReturn(mMessageCapturingHandler); + when(mControllerCtx.getAnimationDuration()).thenReturn(1000L); + when(mControllerCtx.newValueAnimator()).thenReturn(mValueAnimator); + + final DisplayInfo displayInfo = new DisplayInfo(); + displayInfo.logicalDensityDpi = 300; + doReturn(displayInfo).when(mDisplayManagerInternal).getDisplayInfo(anyInt()); + LocalServices.removeServiceForTest(DisplayManagerInternal.class); + LocalServices.addService(DisplayManagerInternal.class, mDisplayManagerInternal); + + mScreenMagnificationController = spy(new FullScreenMagnificationController( + mControllerCtx, new Object(), + mScreenMagnificationInfoChangedCallbackDelegate, mScaleProvider)); + mScreenMagnificationController.register(TEST_DISPLAY); + + mWindowMagnificationManager = spy(new WindowMagnificationManager(mContext, globalLock, + mWindowMagnificationCallbackDelegate, mTraceManager, mScaleProvider)); mMockConnection = new MockWindowMagnificationConnection(true); mWindowMagnificationManager.setConnection(mMockConnection.getConnection()); + mMagnificationController = new MagnificationController(mService, globalLock, mContext, mScreenMagnificationController, mWindowMagnificationManager, mScaleProvider); - new FullScreenMagnificationControllerStubber(mScreenMagnificationController, - mMagnificationController); - mMagnificationController.setMagnificationCapabilities( Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_ALL); - mCallbackDelegate.setDelegate(mMagnificationController); + + mScreenMagnificationInfoChangedCallbackDelegate.setDelegate(mMagnificationController); + mWindowMagnificationCallbackDelegate.setDelegate(mMagnificationController); } @After @@ -213,8 +265,8 @@ public class MagnificationControllerTest { verify(mTransitionCallBack).onResult(TEST_DISPLAY, false); final ArgumentCaptor<MagnificationConfig> configCaptor = ArgumentCaptor.forClass( MagnificationConfig.class); - // The first time is for notifying full-screen enabled and the second time is for notifying - // the target mode transitions failed. + // The first time is for notifying full-screen enabled. + // The second time is for notifying the target mode transitions failed. verify(mService, times(2)).notifyMagnificationChanged(eq(TEST_DISPLAY), any(Region.class), configCaptor.capture()); final MagnificationConfig actualConfig = configCaptor.getValue(); @@ -252,9 +304,9 @@ public class MagnificationControllerTest { MODE_WINDOW, mTransitionCallBack); - // The first time is triggered when window mode is activated, the second time is triggered - // when activating the window mode again. The third time is triggered when the transition is - // completed. + // The first time is triggered when window mode is activated. + // The second time is triggered when activating the window mode again. + // The third time is triggered when the transition is completed. verify(mWindowMagnificationManager, times(3)).showMagnificationButton(eq(TEST_DISPLAY), eq(MODE_WINDOW)); } @@ -279,8 +331,7 @@ public class MagnificationControllerTest { @Test public void transitionToFullScreen_centerNotInTheBounds_magnifyBoundsCenter() throws RemoteException { - final Rect magnificationBounds = - FullScreenMagnificationControllerStubber.MAGNIFICATION_REGION.getBounds(); + final Rect magnificationBounds = INITIAL_SCREEN_MAGNIFICATION_REGION.getBounds(); final PointF magnifiedCenter = new PointF(magnificationBounds.right + 100, magnificationBounds.bottom + 100); setMagnificationEnabled(MODE_WINDOW, magnifiedCenter.x, magnifiedCenter.y); @@ -436,17 +487,19 @@ public class MagnificationControllerTest { public void magnifyThroughExternalRequest_showMagnificationButton() { mScreenMagnificationController.setScaleAndCenter(TEST_DISPLAY, DEFAULT_SCALE, MAGNIFIED_CENTER_X, MAGNIFIED_CENTER_Y, false, TEST_SERVICE_ID); - mMagnificationController.onRequestMagnificationSpec(TEST_DISPLAY, TEST_SERVICE_ID); - verify(mWindowMagnificationManager).showMagnificationButton(eq(TEST_DISPLAY), + // The first time is trigger when fullscreen mode is activated. + // The second time is triggered when magnification spec is changed. + verify(mWindowMagnificationManager, times(2)).showMagnificationButton(eq(TEST_DISPLAY), eq(MODE_FULLSCREEN)); } @Test - public void setScaleOneThroughExternalRequest_removeMagnificationButton() { + public void setScaleOneThroughExternalRequest_fullScreenEnabled_removeMagnificationButton() + throws RemoteException { + setMagnificationEnabled(MODE_FULLSCREEN); mScreenMagnificationController.setScaleAndCenter(TEST_DISPLAY, 1.0f, MAGNIFIED_CENTER_X, MAGNIFIED_CENTER_Y, false, TEST_SERVICE_ID); - mMagnificationController.onRequestMagnificationSpec(TEST_DISPLAY, TEST_SERVICE_ID); verify(mWindowMagnificationManager).removeMagnificationButton(eq(TEST_DISPLAY)); } @@ -490,12 +543,12 @@ public class MagnificationControllerTest { config.getScale(), config.getCenterX(), config.getCenterY(), true, TEST_SERVICE_ID); - // The first time is triggered when setting magnification enabled. And the second time is - // triggered when calling setScaleAndCenter. + // The notify method is triggered when setting magnification enabled. + // The setScaleAndCenter call should not trigger notify method due to same scale and center. final ArgumentCaptor<MagnificationConfig> configCaptor = ArgumentCaptor.forClass( MagnificationConfig.class); - verify(mService, times(2)).notifyMagnificationChanged(eq(TEST_DISPLAY), - eq(FullScreenMagnificationControllerStubber.MAGNIFICATION_REGION), + verify(mService).notifyMagnificationChanged(eq(TEST_DISPLAY), + eq(INITIAL_SCREEN_MAGNIFICATION_REGION), configCaptor.capture()); final MagnificationConfig actualConfig = configCaptor.getValue(); assertEquals(config.getCenterX(), actualConfig.getCenterX(), 0); @@ -514,8 +567,8 @@ public class MagnificationControllerTest { final ArgumentCaptor<MagnificationConfig> configCaptor = ArgumentCaptor.forClass( MagnificationConfig.class); - // The first time is for notifying window enabled and the second time is for notifying - // the target mode transitions. + // The first time is for notifying window enabled. + // The second time is for notifying the target mode transitions. verify(mService, times(2)).notifyMagnificationChanged(eq(TEST_DISPLAY), any(Region.class), configCaptor.capture()); final MagnificationConfig actualConfig = configCaptor.getValue(); @@ -534,8 +587,8 @@ public class MagnificationControllerTest { final ArgumentCaptor<MagnificationConfig> configCaptor = ArgumentCaptor.forClass( MagnificationConfig.class); - // The first time is for notifying window enabled and the second time is for notifying - // the target mode transitions. + // The first time is for notifying window enabled. + // The second time is for notifying the target mode transitions. verify(mService, times(2)).notifyMagnificationChanged(eq(TEST_DISPLAY), any(Region.class), configCaptor.capture()); final MagnificationConfig actualConfig = configCaptor.getValue(); @@ -558,8 +611,8 @@ public class MagnificationControllerTest { final ArgumentCaptor<MagnificationConfig> configCaptor = ArgumentCaptor.forClass( MagnificationConfig.class); - // The first time is for notifying full-screen enabled and the second time is for notifying - // the target mode transitions. + // The first time is for notifying full-screen enabled. + // The second time is for notifying the target mode transitions. verify(mService, times(2)).notifyMagnificationChanged(eq(TEST_DISPLAY), any(Region.class), configCaptor.capture()); final MagnificationConfig actualConfig = configCaptor.getValue(); @@ -578,8 +631,8 @@ public class MagnificationControllerTest { final ArgumentCaptor<MagnificationConfig> configCaptor = ArgumentCaptor.forClass( MagnificationConfig.class); - // The first time is for notifying full-screen enabled and the second time is for notifying - // the target mode transitions. + // The first time is for notifying full-screen enabled. + // The second time is for notifying the target mode transitions. verify(mService, times(2)).notifyMagnificationChanged(eq(TEST_DISPLAY), any(Region.class), configCaptor.capture()); final MagnificationConfig actualConfig = configCaptor.getValue(); @@ -597,6 +650,7 @@ public class MagnificationControllerTest { mMagnificationController.onAccessibilityActionPerformed(TEST_DISPLAY); // The first time is triggered when window mode is activated. + // The second time is triggered when accessibility action performed. verify(mWindowMagnificationManager, times(2)).showMagnificationButton(eq(TEST_DISPLAY), eq(MODE_WINDOW)); } @@ -611,6 +665,7 @@ public class MagnificationControllerTest { mMagnificationController.onAccessibilityActionPerformed(TEST_DISPLAY); // The first time is triggered when window mode is activated. + // The second time is triggered when accessibility action performed. verify(mWindowMagnificationManager, times(2)).removeMagnificationButton(eq(TEST_DISPLAY)); } @@ -767,7 +822,10 @@ public class MagnificationControllerTest { mMagnificationController.onTouchInteractionStart(TEST_DISPLAY, MODE_FULLSCREEN); - verify(mWindowMagnificationManager).showMagnificationButton(eq(TEST_DISPLAY), + // The first time is triggered when fullscreen mode is activated. + // The second time is triggered when magnification spec is changed. + // The third time is triggered when user interaction changed. + verify(mWindowMagnificationManager, times(3)).showMagnificationButton(eq(TEST_DISPLAY), eq(MODE_FULLSCREEN)); } @@ -778,7 +836,10 @@ public class MagnificationControllerTest { mMagnificationController.onTouchInteractionEnd(TEST_DISPLAY, MODE_FULLSCREEN); - verify(mWindowMagnificationManager).showMagnificationButton(eq(TEST_DISPLAY), + // The first time is triggered when fullscreen mode is activated. + // The second time is triggered when magnification spec is changed. + // The third time is triggered when user interaction changed. + verify(mWindowMagnificationManager, times(3)).showMagnificationButton(eq(TEST_DISPLAY), eq(MODE_FULLSCREEN)); } @@ -790,6 +851,7 @@ public class MagnificationControllerTest { mMagnificationController.onTouchInteractionStart(TEST_DISPLAY, MODE_WINDOW); // The first time is triggered when the window mode is activated. + // The second time is triggered when user interaction changed. verify(mWindowMagnificationManager, times(2)).showMagnificationButton(eq(TEST_DISPLAY), eq(MODE_WINDOW)); } @@ -802,6 +864,7 @@ public class MagnificationControllerTest { mMagnificationController.onTouchInteractionEnd(TEST_DISPLAY, MODE_WINDOW); // The first time is triggered when the window mode is activated. + // The second time is triggered when user interaction changed. verify(mWindowMagnificationManager, times(2)).showMagnificationButton(eq(TEST_DISPLAY), eq(MODE_WINDOW)); } @@ -849,7 +912,10 @@ public class MagnificationControllerTest { mMagnificationController.onFullScreenMagnificationActivationState(TEST_DISPLAY, true); - verify(mWindowMagnificationManager).showMagnificationButton(eq(TEST_DISPLAY), + // The first time is triggered when fullscreen mode is activated. + // The second time is triggered when magnification spec is changed. + // The third time is triggered when fullscreen mode activation state is updated. + verify(mWindowMagnificationManager, times(3)).showMagnificationButton(eq(TEST_DISPLAY), eq(MODE_FULLSCREEN)); } @@ -867,11 +933,7 @@ public class MagnificationControllerTest { public void onFullScreenDeactivated_fullScreenEnabled_removeMagnificationButton() throws RemoteException { setMagnificationEnabled(MODE_FULLSCREEN); - mScreenMagnificationController.setScaleAndCenter(TEST_DISPLAY, - /* scale= */ 1, MAGNIFIED_CENTER_X, MAGNIFIED_CENTER_Y, - true, TEST_SERVICE_ID); - - mMagnificationController.onFullScreenMagnificationActivationState(TEST_DISPLAY, false); + mScreenMagnificationController.reset(TEST_DISPLAY, /* animate= */ true); verify(mWindowMagnificationManager).removeMagnificationButton(eq(TEST_DISPLAY)); } @@ -885,7 +947,10 @@ public class MagnificationControllerTest { MODE_FULLSCREEN, mTransitionCallBack); mMockConnection.invokeCallbacks(); - verify(mWindowMagnificationManager).showMagnificationButton(eq(TEST_DISPLAY), + // The first time is triggered when fullscreen mode is activated. + // The second time is triggered when magnification spec is changed. + // The third time is triggered when the disable-magnification callback is triggered. + verify(mWindowMagnificationManager, times(3)).showMagnificationButton(eq(TEST_DISPLAY), eq(MODE_FULLSCREEN)); } @@ -902,8 +967,8 @@ public class MagnificationControllerTest { mCallbackArgumentCaptor.getValue().onResult(true); mMockConnection.invokeCallbacks(); - // The first time is triggered when window mode is activated, the second time is triggered - // when the disable-magnification callback is triggered. + // The first time is triggered when window mode is activated. + // The second time is triggered when the disable-magnification callback is triggered. verify(mWindowMagnificationManager, times(2)).showMagnificationButton(eq(TEST_DISPLAY), eq(MODE_WINDOW)); } @@ -1023,8 +1088,7 @@ public class MagnificationControllerTest { .UiChangesForAccessibilityCallbacks> captor = ArgumentCaptor.forClass( WindowManagerInternal.AccessibilityControllerInternal .UiChangesForAccessibilityCallbacks.class); - verify(mMockWindowManagerInternal.getAccessibilityController()) - .setUiChangesForAccessibilityCallbacks(captor.capture()); + verify(mA11yController).setUiChangesForAccessibilityCallbacks(captor.capture()); return captor.getValue(); } @@ -1072,95 +1136,42 @@ public class MagnificationControllerTest { } } - /** - * Stubs public methods to simulate the real behaviours. - */ - private static class FullScreenMagnificationControllerStubber { - private static final Region MAGNIFICATION_REGION = new Region(0, 0, 500, 600); - private final FullScreenMagnificationController mScreenMagnificationController; - private final FullScreenMagnificationController.MagnificationInfoChangedCallback - mMagnificationChangedCallback; - private boolean mIsMagnifying = false; - private float mScale = 1.0f; - private float mCenterX = MAGNIFICATION_REGION.getBounds().exactCenterX(); - private float mCenterY = MAGNIFICATION_REGION.getBounds().exactCenterY(); - private int mServiceId = -1; - - FullScreenMagnificationControllerStubber( - FullScreenMagnificationController screenMagnificationController, + private static class FullScreenMagnificationCtrInfoChangedCallbackDelegate implements + FullScreenMagnificationController.MagnificationInfoChangedCallback { + private FullScreenMagnificationController.MagnificationInfoChangedCallback mCallback; + + public void setDelegate( FullScreenMagnificationController.MagnificationInfoChangedCallback callback) { - mScreenMagnificationController = screenMagnificationController; - mMagnificationChangedCallback = callback; - stubMethods(); + mCallback = callback; + } + + @Override + public void onRequestMagnificationSpec(int displayId, int serviceId) { + if (mCallback != null) { + mCallback.onRequestMagnificationSpec(displayId, serviceId); + } + } + + @Override + public void onFullScreenMagnificationActivationState(int displayId, boolean activated) { + if (mCallback != null) { + mCallback.onFullScreenMagnificationActivationState(displayId, activated); + } } - private void stubMethods() { - doAnswer(invocation -> mIsMagnifying).when(mScreenMagnificationController).isMagnifying( - TEST_DISPLAY); - doAnswer(invocation -> mIsMagnifying).when( - mScreenMagnificationController).isForceShowMagnifiableBounds(TEST_DISPLAY); - doAnswer(invocation -> mScale).when(mScreenMagnificationController).getPersistedScale( - TEST_DISPLAY); - doAnswer(invocation -> mScale).when(mScreenMagnificationController).getScale( - TEST_DISPLAY); - doAnswer(invocation -> mCenterX).when(mScreenMagnificationController).getCenterX( - TEST_DISPLAY); - doAnswer(invocation -> mCenterY).when(mScreenMagnificationController).getCenterY( - TEST_DISPLAY); - doAnswer(invocation -> mServiceId).when( - mScreenMagnificationController).getIdOfLastServiceToMagnify(TEST_DISPLAY); - - doAnswer(invocation -> { - final Region outRegion = invocation.getArgument(1); - outRegion.set(MAGNIFICATION_REGION); - return null; - }).when(mScreenMagnificationController).getMagnificationRegion(anyInt(), - any(Region.class)); - - Answer setScaleAndCenterStubAnswer = invocation -> { - final float scale = invocation.getArgument(1); - mScale = Float.isNaN(scale) ? mScale : scale; - mIsMagnifying = mScale > 1.0f; - if (mIsMagnifying) { - mCenterX = invocation.getArgument(2); - mCenterY = invocation.getArgument(3); - mServiceId = invocation.getArgument(5); - } else { - reset(); - } - - final MagnificationConfig config = new MagnificationConfig.Builder().setMode( - MODE_FULLSCREEN).setScale(mScale).setCenterX(mCenterX).setCenterY( - mCenterY).build(); - mMagnificationChangedCallback.onFullScreenMagnificationChanged(TEST_DISPLAY, - FullScreenMagnificationControllerStubber.MAGNIFICATION_REGION, - config); - return true; - }; - doAnswer(setScaleAndCenterStubAnswer).when( - mScreenMagnificationController).setScaleAndCenter(eq(TEST_DISPLAY), - anyFloat(), anyFloat(), anyFloat(), any(), anyInt()); - - doAnswer(setScaleAndCenterStubAnswer).when( - mScreenMagnificationController).setScaleAndCenter(eq(TEST_DISPLAY), - anyFloat(), anyFloat(), anyFloat(), anyBoolean(), anyInt()); - - Answer resetStubAnswer = invocation -> { - reset(); - return true; - }; - doAnswer(resetStubAnswer).when(mScreenMagnificationController).reset(eq(TEST_DISPLAY), - any(MagnificationAnimationCallback.class)); - doAnswer(resetStubAnswer).when(mScreenMagnificationController).reset(eq(TEST_DISPLAY), - anyBoolean()); + @Override + public void onImeWindowVisibilityChanged(int displayId, boolean shown) { + if (mCallback != null) { + mCallback.onImeWindowVisibilityChanged(displayId, shown); + } } - private void reset() { - mScale = 1.0f; - mIsMagnifying = false; - mServiceId = -1; - mCenterX = MAGNIFICATION_REGION.getBounds().exactCenterX(); - mCenterY = MAGNIFICATION_REGION.getBounds().exactCenterY(); + @Override + public void onFullScreenMagnificationChanged(int displayId, @NonNull Region region, + @NonNull MagnificationConfig config) { + if (mCallback != null) { + mCallback.onFullScreenMagnificationChanged(displayId, region, config); + } } } } diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java index 759b0497044f..eb99e30b58ec 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java @@ -369,6 +369,21 @@ public class VirtualDeviceManagerServiceTest { } @Test + public void isDeviceIdValid_defaultDeviceId_returnsFalse() { + assertThat(mVdm.isValidVirtualDeviceId(DEVICE_ID_DEFAULT)).isFalse(); + } + + @Test + public void isDeviceIdValid_validVirtualDeviceId_returnsTrue() { + assertThat(mVdm.isValidVirtualDeviceId(mDeviceImpl.getDeviceId())).isTrue(); + } + + @Test + public void isDeviceIdValid_nonExistentDeviceId_returnsFalse() { + assertThat(mVdm.isValidVirtualDeviceId(mDeviceImpl.getDeviceId() + 1)).isFalse(); + } + + @Test public void getDevicePolicy_invalidDeviceId_returnsDefault() { assertThat(mVdm.getDevicePolicy(DEVICE_ID_INVALID, POLICY_TYPE_SENSORS)) .isEqualTo(DEVICE_POLICY_DEFAULT); diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/MockSystemServices.java b/services/tests/servicestests/src/com/android/server/devicepolicy/MockSystemServices.java index 2a6a97991135..4163f33e94e9 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/MockSystemServices.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/MockSystemServices.java @@ -219,8 +219,10 @@ public class MockSystemServices { // Add the system user with a fake profile group already set up (this can happen in the real // world if a managed profile is added and then removed). - systemUserDataDir = addUser(UserHandle.USER_SYSTEM, UserInfo.FLAG_PRIMARY, + systemUserDataDir = addUser(UserHandle.USER_SYSTEM, + UserInfo.FLAG_PRIMARY | UserInfo.FLAG_MAIN, UserManager.USER_TYPE_FULL_SYSTEM, UserHandle.USER_SYSTEM); + when(userManager.getMainUser()).thenReturn(UserHandle.SYSTEM); // System user is always running. setUserRunning(UserHandle.USER_SYSTEM, true); diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java index f676a3f84c0f..2d252cbbbd9c 100644 --- a/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java @@ -39,6 +39,8 @@ import static org.mockito.Mockito.when; import android.app.PropertyInvalidatedCache; import android.companion.virtual.IVirtualDevice; +import android.companion.virtual.IVirtualDeviceManager; +import android.companion.virtual.VirtualDeviceManager; import android.compat.testing.PlatformCompatChangeRule; import android.content.Context; import android.content.ContextWrapper; @@ -173,6 +175,7 @@ public class DisplayManagerServiceTest { private final DisplayManagerService.Injector mBasicInjector = new BasicInjector(); + @Mock IVirtualDeviceManager mIVirtualDeviceManager; @Mock InputManagerInternal mMockInputManagerInternal; @Mock VirtualDeviceManagerInternal mMockVirtualDeviceManagerInternal; @Mock IVirtualDisplayCallback.Stub mMockAppToken; @@ -202,6 +205,8 @@ public class DisplayManagerServiceTest { mContext = spy(new ContextWrapper(ApplicationProvider.getApplicationContext())); + VirtualDeviceManager vdm = new VirtualDeviceManager(mIVirtualDeviceManager, mContext); + when(mContext.getSystemService(VirtualDeviceManager.class)).thenReturn(vdm); // Disable binder caches in this process. PropertyInvalidatedCache.disableForTestMode(); setUpDisplay(); @@ -727,10 +732,8 @@ public class DisplayManagerServiceTest { when(mMockAppToken.asBinder()).thenReturn(mMockAppToken); IVirtualDevice virtualDevice = mock(IVirtualDevice.class); - when(mMockVirtualDeviceManagerInternal.isValidVirtualDevice(virtualDevice)) - .thenReturn(true); when(virtualDevice.getDeviceId()).thenReturn(1); - + when(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true); // Create a first virtual display. A display group should be created for this display on the // virtual device. final VirtualDisplayConfig.Builder builder1 = @@ -780,9 +783,8 @@ public class DisplayManagerServiceTest { when(mMockAppToken.asBinder()).thenReturn(mMockAppToken); IVirtualDevice virtualDevice = mock(IVirtualDevice.class); - when(mMockVirtualDeviceManagerInternal.isValidVirtualDevice(virtualDevice)) - .thenReturn(true); when(virtualDevice.getDeviceId()).thenReturn(1); + when(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true); // Create a first virtual display. A display group should be created for this display on the // virtual device. @@ -806,6 +808,8 @@ public class DisplayManagerServiceTest { .setFlags(VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP) .setUniqueId("uniqueId --- own display group"); + when(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true); + int displayId2 = localService.createVirtualDisplay( builder2.build(), @@ -832,9 +836,8 @@ public class DisplayManagerServiceTest { when(mMockAppToken.asBinder()).thenReturn(mMockAppToken); IVirtualDevice virtualDevice = mock(IVirtualDevice.class); - when(mMockVirtualDeviceManagerInternal.isValidVirtualDevice(virtualDevice)) - .thenReturn(true); when(virtualDevice.getDeviceId()).thenReturn(1); + when(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true); // Allow an ALWAYS_UNLOCKED display to be created. when(mContext.checkCallingPermission(ADD_TRUSTED_DISPLAY)) @@ -1062,7 +1065,7 @@ public class DisplayManagerServiceTest { * a virtual device, even if ADD_TRUSTED_DISPLAY is not granted. */ @Test - public void testOwnDisplayGroup_allowCreationWithVirtualDevice() { + public void testOwnDisplayGroup_allowCreationWithVirtualDevice() throws Exception { DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); DisplayManagerInternal localService = displayManager.new LocalService(); @@ -1081,8 +1084,8 @@ public class DisplayManagerServiceTest { builder.setUniqueId("uniqueId --- OWN_DISPLAY_GROUP"); IVirtualDevice virtualDevice = mock(IVirtualDevice.class); - when(mMockVirtualDeviceManagerInternal.isValidVirtualDevice(virtualDevice)) - .thenReturn(true); + when(virtualDevice.getDeviceId()).thenReturn(1); + when(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true); int displayId = localService.createVirtualDisplay(builder.build(), mMockAppToken /* callback */, virtualDevice /* virtualDeviceToken */, diff --git a/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt b/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt index ecd9d893330a..3ce747f145dc 100644 --- a/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt +++ b/services/tests/servicestests/src/com/android/server/input/BatteryControllerTests.kt @@ -16,7 +16,9 @@ package com.android.server.input +import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothManager import android.content.Context import android.content.ContextWrapper import android.hardware.BatteryState.STATUS_CHARGING @@ -246,6 +248,11 @@ class BatteryControllerTests { notifyDeviceChanged(deviceId, hasBattery, supportsUsi) } + private fun createBluetoothDevice(address: String): BluetoothDevice { + return context.getSystemService(BluetoothManager::class.java)!! + .adapter.getRemoteDevice(address) + } + @After fun tearDown() { InputManager.clearInstance() @@ -656,29 +663,31 @@ class BatteryControllerTests { addInputDevice(SECOND_BT_DEVICE_ID) testLooper.dispatchNext() - // Ensure that a BT battery listener is not added when there are no monitored BT devices. - verify(bluetoothBatteryManager, never()).addListener(any()) + // Listen to a non-Bluetooth device and ensure that the BT battery listener is not added + // when there are no monitored BT devices. + val listener = createMockListener() + batteryController.registerBatteryListener(DEVICE_ID, listener, PID) + verify(bluetoothBatteryManager, never()).addBatteryListener(any()) val bluetoothListener = ArgumentCaptor.forClass(BluetoothBatteryListener::class.java) - val listener = createMockListener() // The BT battery listener is added when the first BT input device is monitored. batteryController.registerBatteryListener(BT_DEVICE_ID, listener, PID) - verify(bluetoothBatteryManager).addListener(bluetoothListener.capture()) + verify(bluetoothBatteryManager).addBatteryListener(bluetoothListener.capture()) // The BT listener is only added once for all BT devices. batteryController.registerBatteryListener(SECOND_BT_DEVICE_ID, listener, PID) - verify(bluetoothBatteryManager, times(1)).addListener(any()) + verify(bluetoothBatteryManager, times(1)).addBatteryListener(any()) // The BT listener is only removed when there are no monitored BT devices. batteryController.unregisterBatteryListener(BT_DEVICE_ID, listener, PID) - verify(bluetoothBatteryManager, never()).removeListener(any()) + verify(bluetoothBatteryManager, never()).removeBatteryListener(any()) `when`(iInputManager.getInputDeviceBluetoothAddress(SECOND_BT_DEVICE_ID)) .thenReturn(null) notifyDeviceChanged(SECOND_BT_DEVICE_ID) testLooper.dispatchNext() - verify(bluetoothBatteryManager).removeListener(bluetoothListener.value) + verify(bluetoothBatteryManager).removeBatteryListener(bluetoothListener.value) } @Test @@ -690,15 +699,14 @@ class BatteryControllerTests { val bluetoothListener = ArgumentCaptor.forClass(BluetoothBatteryListener::class.java) val listener = createMockListener() batteryController.registerBatteryListener(BT_DEVICE_ID, listener, PID) - verify(bluetoothBatteryManager).addListener(bluetoothListener.capture()) + verify(bluetoothBatteryManager).addBatteryListener(bluetoothListener.capture()) listener.verifyNotified(BT_DEVICE_ID, capacity = 0.21f) // When the state has not changed, the listener is not notified again. - bluetoothListener.value!!.onBluetoothBatteryChanged(TIMESTAMP, "AA:BB:CC:DD:EE:FF") + bluetoothListener.value!!.onBluetoothBatteryChanged(TIMESTAMP, "AA:BB:CC:DD:EE:FF", 21) listener.verifyNotified(BT_DEVICE_ID, mode = times(1), capacity = 0.21f) - `when`(bluetoothBatteryManager.getBatteryLevel(eq("AA:BB:CC:DD:EE:FF"))).thenReturn(25) - bluetoothListener.value!!.onBluetoothBatteryChanged(TIMESTAMP, "AA:BB:CC:DD:EE:FF") + bluetoothListener.value!!.onBluetoothBatteryChanged(TIMESTAMP, "AA:BB:CC:DD:EE:FF", 25) listener.verifyNotified(BT_DEVICE_ID, capacity = 0.25f) } @@ -717,7 +725,7 @@ class BatteryControllerTests { // When the device is first monitored and both native and BT battery is available, // the latter is used. batteryController.registerBatteryListener(BT_DEVICE_ID, listener, PID) - verify(bluetoothBatteryManager).addListener(bluetoothListener.capture()) + verify(bluetoothBatteryManager).addBatteryListener(bluetoothListener.capture()) verify(uEventManager).addListener(uEventListener.capture(), any()) listener.verifyNotified(BT_DEVICE_ID, capacity = 0.21f) assertThat("battery state matches", batteryController.getBatteryState(BT_DEVICE_ID), @@ -744,25 +752,144 @@ class BatteryControllerTests { val uEventListener = ArgumentCaptor.forClass(UEventBatteryListener::class.java) batteryController.registerBatteryListener(BT_DEVICE_ID, listener, PID) - verify(bluetoothBatteryManager).addListener(bluetoothListener.capture()) + verify(bluetoothBatteryManager).addBatteryListener(bluetoothListener.capture()) verify(uEventManager).addListener(uEventListener.capture(), any()) listener.verifyNotified(BT_DEVICE_ID, capacity = 0.21f) // Fall back to the native state when BT is off. - `when`(bluetoothBatteryManager.getBatteryLevel(eq("AA:BB:CC:DD:EE:FF"))) - .thenReturn(BluetoothDevice.BATTERY_LEVEL_BLUETOOTH_OFF) - bluetoothListener.value!!.onBluetoothBatteryChanged(TIMESTAMP, "AA:BB:CC:DD:EE:FF") + bluetoothListener.value!!.onBluetoothBatteryChanged(TIMESTAMP, "AA:BB:CC:DD:EE:FF", + BluetoothDevice.BATTERY_LEVEL_BLUETOOTH_OFF) listener.verifyNotified(BT_DEVICE_ID, capacity = 0.98f) - `when`(bluetoothBatteryManager.getBatteryLevel(eq("AA:BB:CC:DD:EE:FF"))).thenReturn(22) - bluetoothListener.value!!.onBluetoothBatteryChanged(TIMESTAMP, "AA:BB:CC:DD:EE:FF") - verify(bluetoothBatteryManager).addListener(bluetoothListener.capture()) + bluetoothListener.value!!.onBluetoothBatteryChanged(TIMESTAMP, "AA:BB:CC:DD:EE:FF", 22) + verify(bluetoothBatteryManager).addBatteryListener(bluetoothListener.capture()) listener.verifyNotified(BT_DEVICE_ID, capacity = 0.22f) // Fall back to the native state when BT battery is unknown. - `when`(bluetoothBatteryManager.getBatteryLevel(eq("AA:BB:CC:DD:EE:FF"))) - .thenReturn(BluetoothDevice.BATTERY_LEVEL_UNKNOWN) - bluetoothListener.value!!.onBluetoothBatteryChanged(TIMESTAMP, "AA:BB:CC:DD:EE:FF") + bluetoothListener.value!!.onBluetoothBatteryChanged(TIMESTAMP, "AA:BB:CC:DD:EE:FF", + BluetoothDevice.BATTERY_LEVEL_UNKNOWN) listener.verifyNotified(BT_DEVICE_ID, mode = times(2), capacity = 0.98f) } + + @Test + fun testRegisterBluetoothMetadataListenerForMonitoredBluetoothDevices() { + `when`(iInputManager.getInputDeviceBluetoothAddress(BT_DEVICE_ID)) + .thenReturn("AA:BB:CC:DD:EE:FF") + `when`(iInputManager.getInputDeviceBluetoothAddress(SECOND_BT_DEVICE_ID)) + .thenReturn("11:22:33:44:55:66") + addInputDevice(BT_DEVICE_ID) + testLooper.dispatchNext() + addInputDevice(SECOND_BT_DEVICE_ID) + testLooper.dispatchNext() + + // Listen to a non-Bluetooth device and ensure that the metadata listener is not added when + // there are no monitored BT devices. + val listener = createMockListener() + batteryController.registerBatteryListener(DEVICE_ID, listener, PID) + verify(bluetoothBatteryManager, never()).addMetadataListener(any(), any()) + + val metadataListener1 = ArgumentCaptor.forClass( + BluetoothAdapter.OnMetadataChangedListener::class.java) + val metadataListener2 = ArgumentCaptor.forClass( + BluetoothAdapter.OnMetadataChangedListener::class.java) + + // The metadata listener is added when the first BT input device is monitored. + batteryController.registerBatteryListener(BT_DEVICE_ID, listener, PID) + verify(bluetoothBatteryManager) + .addMetadataListener(eq("AA:BB:CC:DD:EE:FF"), metadataListener1.capture()) + + // There is one metadata listener added for each BT device. + batteryController.registerBatteryListener(SECOND_BT_DEVICE_ID, listener, PID) + verify(bluetoothBatteryManager) + .addMetadataListener(eq("11:22:33:44:55:66"), metadataListener2.capture()) + + // The metadata listener is removed when the device is no longer monitored. + batteryController.unregisterBatteryListener(BT_DEVICE_ID, listener, PID) + verify(bluetoothBatteryManager) + .removeMetadataListener("AA:BB:CC:DD:EE:FF", metadataListener1.value) + + `when`(iInputManager.getInputDeviceBluetoothAddress(SECOND_BT_DEVICE_ID)) + .thenReturn(null) + notifyDeviceChanged(SECOND_BT_DEVICE_ID) + testLooper.dispatchNext() + verify(bluetoothBatteryManager) + .removeMetadataListener("11:22:33:44:55:66", metadataListener2.value) + } + + @Test + fun testNotifiesBluetoothMetadataBatteryChanges() { + `when`(iInputManager.getInputDeviceBluetoothAddress(BT_DEVICE_ID)) + .thenReturn("AA:BB:CC:DD:EE:FF") + `when`(bluetoothBatteryManager.getMetadata("AA:BB:CC:DD:EE:FF", + BluetoothDevice.METADATA_MAIN_BATTERY)) + .thenReturn("21".toByteArray()) + addInputDevice(BT_DEVICE_ID) + val metadataListener = ArgumentCaptor.forClass( + BluetoothAdapter.OnMetadataChangedListener::class.java) + val listener = createMockListener() + val bluetoothDevice = createBluetoothDevice("AA:BB:CC:DD:EE:FF") + batteryController.registerBatteryListener(BT_DEVICE_ID, listener, PID) + verify(bluetoothBatteryManager) + .addMetadataListener(eq("AA:BB:CC:DD:EE:FF"), metadataListener.capture()) + listener.verifyNotified(BT_DEVICE_ID, capacity = 0.21f, status = STATUS_UNKNOWN) + + // When the state has not changed, the listener is not notified again. + metadataListener.value!!.onMetadataChanged( + bluetoothDevice, BluetoothDevice.METADATA_MAIN_BATTERY, "21".toByteArray()) + listener.verifyNotified(BT_DEVICE_ID, mode = times(1), capacity = 0.21f) + + metadataListener.value!!.onMetadataChanged( + bluetoothDevice, BluetoothDevice.METADATA_MAIN_BATTERY, "25".toByteArray()) + listener.verifyNotified(BT_DEVICE_ID, capacity = 0.25f, status = STATUS_UNKNOWN) + + metadataListener.value!!.onMetadataChanged( + bluetoothDevice, BluetoothDevice.METADATA_MAIN_CHARGING, "true".toByteArray()) + listener.verifyNotified(BT_DEVICE_ID, capacity = 0.25f, status = STATUS_CHARGING) + + metadataListener.value!!.onMetadataChanged( + bluetoothDevice, BluetoothDevice.METADATA_MAIN_CHARGING, "false".toByteArray()) + listener.verifyNotified(BT_DEVICE_ID, capacity = 0.25f, status = STATUS_DISCHARGING) + + metadataListener.value!!.onMetadataChanged( + bluetoothDevice, BluetoothDevice.METADATA_MAIN_CHARGING, null) + listener.verifyNotified(BT_DEVICE_ID, mode = times(2), capacity = 0.25f, + status = STATUS_UNKNOWN) + } + + @Test + fun testBluetoothMetadataBatteryIsPrioritized() { + `when`(iInputManager.getInputDeviceBluetoothAddress(BT_DEVICE_ID)) + .thenReturn("AA:BB:CC:DD:EE:FF") + `when`(bluetoothBatteryManager.getBatteryLevel(eq("AA:BB:CC:DD:EE:FF"))).thenReturn(21) + `when`(bluetoothBatteryManager.getMetadata("AA:BB:CC:DD:EE:FF", + BluetoothDevice.METADATA_MAIN_BATTERY)) + .thenReturn("22".toByteArray()) + addInputDevice(BT_DEVICE_ID) + val bluetoothListener = ArgumentCaptor.forClass(BluetoothBatteryListener::class.java) + val metadataListener = ArgumentCaptor.forClass( + BluetoothAdapter.OnMetadataChangedListener::class.java) + val listener = createMockListener() + val bluetoothDevice = createBluetoothDevice("AA:BB:CC:DD:EE:FF") + batteryController.registerBatteryListener(BT_DEVICE_ID, listener, PID) + + verify(bluetoothBatteryManager).addBatteryListener(bluetoothListener.capture()) + verify(bluetoothBatteryManager) + .addMetadataListener(eq("AA:BB:CC:DD:EE:FF"), metadataListener.capture()) + listener.verifyNotified(BT_DEVICE_ID, capacity = 0.22f) + + // A change in the Bluetooth battery level has no effect while there is a valid battery + // level obtained through the metadata. + bluetoothListener.value!!.onBluetoothBatteryChanged(TIMESTAMP, "AA:BB:CC:DD:EE:FF", 23) + listener.verifyNotified(BT_DEVICE_ID, mode = never(), capacity = 0.23f) + + metadataListener.value!!.onMetadataChanged( + bluetoothDevice, BluetoothDevice.METADATA_MAIN_BATTERY, "24".toByteArray()) + listener.verifyNotified(BT_DEVICE_ID, capacity = 0.24f) + + // When the battery level from the metadata is no longer valid, we fall back to using the + // Bluetooth battery level. + metadataListener.value!!.onMetadataChanged( + bluetoothDevice, BluetoothDevice.METADATA_MAIN_BATTERY, null) + listener.verifyNotified(BT_DEVICE_ID, capacity = 0.23f) + } } diff --git a/services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java index 065aec5b2f64..07fda309f03e 100644 --- a/services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/locales/LocaleManagerServiceTest.java @@ -77,6 +77,7 @@ public class LocaleManagerServiceTest { /* initiatingPackageName = */ null, /* initiatingPackageSigningInfo = */ null, /* originatingPackageName = */ null, /* installingPackageName = */ DEFAULT_INSTALLER_PACKAGE_NAME, + /* updateOwnerPackageName = */ null, /* packageSource = */ PackageInstaller.PACKAGE_SOURCE_UNSPECIFIED); private LocaleManagerService mLocaleManagerService; diff --git a/services/tests/servicestests/src/com/android/server/locales/SystemAppUpdateTrackerTest.java b/services/tests/servicestests/src/com/android/server/locales/SystemAppUpdateTrackerTest.java index 494796ee48eb..9429462a6723 100644 --- a/services/tests/servicestests/src/com/android/server/locales/SystemAppUpdateTrackerTest.java +++ b/services/tests/servicestests/src/com/android/server/locales/SystemAppUpdateTrackerTest.java @@ -89,6 +89,7 @@ public class SystemAppUpdateTrackerTest { /* initiatingPackageName = */ null, /* initiatingPackageSigningInfo = */ null, /* originatingPackageName = */ null, /* installingPackageName = */ DEFAULT_INSTALLER_PACKAGE_NAME, + /* updateOwnerPackageName = */ null, /* packageSource = */ PackageInstaller.PACKAGE_SOURCE_UNSPECIFIED); @Mock diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManagerTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManagerTest.java index 281195de4b35..1b983f0bfb1b 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManagerTest.java @@ -1073,7 +1073,9 @@ public class RecoverableKeyStoreManagerTest { int uid = Binder.getCallingUid(); PendingIntent intent = PendingIntent.getBroadcast( InstrumentationRegistry.getTargetContext(), /*requestCode=*/1, - new Intent(), /*flags=*/ PendingIntent.FLAG_MUTABLE_UNAUDITED); + new Intent() + .setPackage(InstrumentationRegistry.getTargetContext().getPackageName()), + /*flags=*/ PendingIntent.FLAG_MUTABLE); mRecoverableKeyStoreManager.setSnapshotCreatedPendingIntent(intent); verify(mMockListenersStorage).setSnapshotListener(eq(uid), any(PendingIntent.class)); } diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverySnapshotListenersStorageTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverySnapshotListenersStorageTest.java index d9ebb4c26891..418d47452330 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverySnapshotListenersStorageTest.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/RecoverySnapshotListenersStorageTest.java @@ -41,7 +41,9 @@ public class RecoverySnapshotListenersStorageTest { int recoveryAgentUid = 1000; PendingIntent intent = PendingIntent.getBroadcast( InstrumentationRegistry.getTargetContext(), /*requestCode=*/ 1, - new Intent(), /*flags=*/ PendingIntent.FLAG_MUTABLE_UNAUDITED); + new Intent() + .setPackage(InstrumentationRegistry.getTargetContext().getPackageName()), + /*flags=*/ PendingIntent.FLAG_MUTABLE); mStorage.setSnapshotListener(recoveryAgentUid, intent); assertTrue(mStorage.hasListener(recoveryAgentUid)); @@ -54,7 +56,9 @@ public class RecoverySnapshotListenersStorageTest { int recoveryAgentUid = 1000; mStorage.recoverySnapshotAvailable(recoveryAgentUid); PendingIntent intent = PendingIntent.getBroadcast( - context, /*requestCode=*/ 0, new Intent(TEST_INTENT_ACTION), /*flags=*/PendingIntent.FLAG_MUTABLE_UNAUDITED); + context, /*requestCode=*/ 0, + new Intent(TEST_INTENT_ACTION).setPackage(context.getPackageName()), + /*flags=*/PendingIntent.FLAG_MUTABLE); CountDownLatch latch = new CountDownLatch(1); context.registerReceiver(new BroadcastReceiver() { @Override @@ -75,7 +79,9 @@ public class RecoverySnapshotListenersStorageTest { int recoveryAgentUid = 1000; mStorage.recoverySnapshotAvailable(recoveryAgentUid); PendingIntent intent = PendingIntent.getBroadcast( - context, /*requestCode=*/ 0, new Intent(TEST_INTENT_ACTION), /*flags=*/PendingIntent.FLAG_MUTABLE_UNAUDITED); + context, /*requestCode=*/ 0, + new Intent(TEST_INTENT_ACTION).setPackage(context.getPackageName()), + /*flags=*/PendingIntent.FLAG_MUTABLE); CountDownLatch latch = new CountDownLatch(2); BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { @Override diff --git a/services/tests/servicestests/src/com/android/server/NetworkManagementServiceTest.java b/services/tests/servicestests/src/com/android/server/net/NetworkManagementServiceTest.java index 17a587603009..d9cd77d8cd7c 100644 --- a/services/tests/servicestests/src/com/android/server/NetworkManagementServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/net/NetworkManagementServiceTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.server; +package com.android.server.net; import static android.net.ConnectivityManager.FIREWALL_CHAIN_DOZABLE; import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY; @@ -51,14 +51,13 @@ import android.os.PermissionEnforcer; import android.os.Process; import android.os.RemoteException; import android.permission.PermissionCheckerManager; +import android.platform.test.annotations.Presubmit; import android.test.suitebuilder.annotation.SmallTest; import android.util.ArrayMap; import androidx.test.runner.AndroidJUnit4; import com.android.internal.app.IBatteryStats; -import com.android.server.NetworkManagementService.Dependencies; -import com.android.server.net.BaseNetworkObserver; import org.junit.After; import org.junit.Before; @@ -76,6 +75,7 @@ import java.util.function.BiFunction; */ @RunWith(AndroidJUnit4.class) @SmallTest +@Presubmit public class NetworkManagementServiceTest { private NetworkManagementService mNMService; @Mock private Context mContext; @@ -92,7 +92,7 @@ public class NetworkManagementServiceTest { private final MockDependencies mDeps = new MockDependencies(); private final MockPermissionEnforcer mPermissionEnforcer = new MockPermissionEnforcer(); - private final class MockDependencies extends Dependencies { + private final class MockDependencies extends NetworkManagementService.Dependencies { @Override public IBinder getService(String name) { switch (name) { diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest8.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest8.java index 2293808a5d64..a85c7227b954 100644 --- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest8.java +++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest8.java @@ -327,7 +327,9 @@ public class ShortcutManagerTest8 extends BaseShortcutManagerTest { } private IntentSender makeResultIntent() { - return PendingIntent.getActivity(getTestContext(), 0, new Intent(), PendingIntent.FLAG_MUTABLE_UNAUDITED).getIntentSender(); + return PendingIntent.getActivity(getTestContext(), 0, + new Intent().setPackage(getTestContext().getPackageName()), + PendingIntent.FLAG_MUTABLE).getIntentSender(); } public void testRequestPinShortcut_withCallback() { diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest9.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest9.java index a47a8df51c9f..2fca3d07149e 100644 --- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest9.java +++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest9.java @@ -150,7 +150,9 @@ public class ShortcutManagerTest9 extends BaseShortcutManagerTest { public void testRequestPinAppWidget_withCallback() { final PendingIntent resultIntent = - PendingIntent.getActivity(getTestContext(), 0, new Intent(), PendingIntent.FLAG_MUTABLE_UNAUDITED); + PendingIntent.getActivity(getTestContext(), 0, + new Intent().setPackage(getTestContext().getPackageName()), + PendingIntent.FLAG_MUTABLE); checkRequestPinAppWidget(resultIntent); } diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java index 34dad0922468..1889d9a07692 100644 --- a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java @@ -175,14 +175,14 @@ public final class UserManagerTest { final UserProperties typeProps = userTypeDetails.getDefaultUserPropertiesReference(); // Test that only one clone user can be created - final int primaryUserId = mUserManager.getPrimaryUser().id; + final int mainUserId = mUserManager.getMainUser().getIdentifier(); UserInfo userInfo = createProfileForUser("Clone user1", UserManager.USER_TYPE_PROFILE_CLONE, - primaryUserId); + mainUserId); assertThat(userInfo).isNotNull(); UserInfo userInfo2 = createProfileForUser("Clone user2", UserManager.USER_TYPE_PROFILE_CLONE, - primaryUserId); + mainUserId); assertThat(userInfo2).isNull(); final Context userContext = mContext.createPackageContextAsUser("system", 0, @@ -212,12 +212,12 @@ public final class UserManagerTest { cloneUserProperties::getCrossProfileIntentResolutionStrategy); // Verify clone user parent - assertThat(mUserManager.getProfileParent(primaryUserId)).isNull(); + assertThat(mUserManager.getProfileParent(mainUserId)).isNull(); UserInfo parentProfileInfo = mUserManager.getProfileParent(userInfo.id); assertThat(parentProfileInfo).isNotNull(); - assertThat(primaryUserId).isEqualTo(parentProfileInfo.id); + assertThat(mainUserId).isEqualTo(parentProfileInfo.id); removeUser(userInfo.id); - assertThat(mUserManager.getProfileParent(primaryUserId)).isNull(); + assertThat(mUserManager.getProfileParent(mainUserId)).isNull(); } @MediumTest @@ -670,17 +670,16 @@ public final class UserManagerTest { @Test public void testGetProfileParent() throws Exception { assumeManagedUsersSupported(); - final int primaryUserId = mUserManager.getPrimaryUser().id; - + int mainUserId = mUserManager.getMainUser().getIdentifier(); UserInfo userInfo = createProfileForUser("Profile", - UserManager.USER_TYPE_PROFILE_MANAGED, primaryUserId); + UserManager.USER_TYPE_PROFILE_MANAGED, mainUserId); assertThat(userInfo).isNotNull(); - assertThat(mUserManager.getProfileParent(primaryUserId)).isNull(); + assertThat(mUserManager.getProfileParent(mainUserId)).isNull(); UserInfo parentProfileInfo = mUserManager.getProfileParent(userInfo.id); assertThat(parentProfileInfo).isNotNull(); - assertThat(primaryUserId).isEqualTo(parentProfileInfo.id); + assertThat(mainUserId).isEqualTo(parentProfileInfo.id); removeUser(userInfo.id); - assertThat(mUserManager.getProfileParent(primaryUserId)).isNull(); + assertThat(mUserManager.getProfileParent(mainUserId)).isNull(); } /** Test that UserManager returns the correct badge information for a managed profile. */ @@ -694,9 +693,9 @@ public final class UserManagerTest { .that(userTypeDetails).isNotNull(); assertThat(userTypeDetails.getName()).isEqualTo(UserManager.USER_TYPE_PROFILE_MANAGED); - final int primaryUserId = mUserManager.getPrimaryUser().id; + int mainUserId = mUserManager.getMainUser().getIdentifier(); UserInfo userInfo = createProfileForUser("Managed", - UserManager.USER_TYPE_PROFILE_MANAGED, primaryUserId); + UserManager.USER_TYPE_PROFILE_MANAGED, mainUserId); assertThat(userInfo).isNotNull(); final int userId = userInfo.id; @@ -739,9 +738,9 @@ public final class UserManagerTest { final UserProperties typeProps = userTypeDetails.getDefaultUserPropertiesReference(); // Create an actual user (of this user type) and get its properties. - final int primaryUserId = mUserManager.getPrimaryUser().id; + int mainUserId = mUserManager.getMainUser().getIdentifier(); final UserInfo userInfo = createProfileForUser("Managed", - UserManager.USER_TYPE_PROFILE_MANAGED, primaryUserId); + UserManager.USER_TYPE_PROFILE_MANAGED, mainUserId); assertThat(userInfo).isNotNull(); final int userId = userInfo.id; final UserProperties userProps = mUserManager.getUserProperties(UserHandle.of(userId)); @@ -762,11 +761,11 @@ public final class UserManagerTest { @Test public void testAddManagedProfile() throws Exception { assumeManagedUsersSupported(); - final int primaryUserId = mUserManager.getPrimaryUser().id; + int mainUserId = mUserManager.getMainUser().getIdentifier(); UserInfo userInfo1 = createProfileForUser("Managed 1", - UserManager.USER_TYPE_PROFILE_MANAGED, primaryUserId); + UserManager.USER_TYPE_PROFILE_MANAGED, mainUserId); UserInfo userInfo2 = createProfileForUser("Managed 2", - UserManager.USER_TYPE_PROFILE_MANAGED, primaryUserId); + UserManager.USER_TYPE_PROFILE_MANAGED, mainUserId); assertThat(userInfo1).isNotNull(); assertThat(userInfo2).isNull(); @@ -785,9 +784,9 @@ public final class UserManagerTest { @Test public void testAddManagedProfile_withDisallowedPackages() throws Exception { assumeManagedUsersSupported(); - final int primaryUserId = mUserManager.getPrimaryUser().id; + int mainUserId = mUserManager.getMainUser().getIdentifier(); UserInfo userInfo1 = createProfileForUser("Managed1", - UserManager.USER_TYPE_PROFILE_MANAGED, primaryUserId); + UserManager.USER_TYPE_PROFILE_MANAGED, mainUserId); // Verify that the packagesToVerify are installed by default. for (String pkg : PACKAGES) { if (!mPackageManager.isPackageAvailable(pkg)) { @@ -801,7 +800,7 @@ public final class UserManagerTest { removeUser(userInfo1.id); UserInfo userInfo2 = createProfileForUser("Managed2", - UserManager.USER_TYPE_PROFILE_MANAGED, primaryUserId, PACKAGES); + UserManager.USER_TYPE_PROFILE_MANAGED, mainUserId, PACKAGES); // Verify that the packagesToVerify are not installed by default. for (String pkg : PACKAGES) { if (!mPackageManager.isPackageAvailable(pkg)) { @@ -821,9 +820,9 @@ public final class UserManagerTest { @Test public void testAddManagedProfile_disallowedPackagesInstalledLater() throws Exception { assumeManagedUsersSupported(); - final int primaryUserId = mUserManager.getPrimaryUser().id; + final int mainUserId = mUserManager.getMainUser().getIdentifier(); UserInfo userInfo = createProfileForUser("Managed", - UserManager.USER_TYPE_PROFILE_MANAGED, primaryUserId, PACKAGES); + UserManager.USER_TYPE_PROFILE_MANAGED, mainUserId, PACKAGES); // Verify that the packagesToVerify are not installed by default. for (String pkg : PACKAGES) { if (!mPackageManager.isPackageAvailable(pkg)) { @@ -868,17 +867,17 @@ public final class UserManagerTest { @MediumTest @Test public void testCreateUser_disallowAddClonedUserProfile() throws Exception { - final int primaryUserId = ActivityManager.getCurrentUser(); - final UserHandle primaryUserHandle = asHandle(primaryUserId); + final int mainUserId = ActivityManager.getCurrentUser(); + final UserHandle mainUserHandle = asHandle(mainUserId); mUserManager.setUserRestriction(UserManager.DISALLOW_ADD_CLONE_PROFILE, - true, primaryUserHandle); + true, mainUserHandle); try { UserInfo cloneProfileUserInfo = createProfileForUser("Clone", - UserManager.USER_TYPE_PROFILE_CLONE, primaryUserId); + UserManager.USER_TYPE_PROFILE_CLONE, mainUserId); assertThat(cloneProfileUserInfo).isNull(); } finally { mUserManager.setUserRestriction(UserManager.DISALLOW_ADD_CLONE_PROFILE, false, - primaryUserHandle); + mainUserHandle); } } @@ -887,17 +886,17 @@ public final class UserManagerTest { @Test public void testCreateProfileForUser_disallowAddManagedProfile() throws Exception { assumeManagedUsersSupported(); - final int primaryUserId = mUserManager.getPrimaryUser().id; - final UserHandle primaryUserHandle = asHandle(primaryUserId); + final int mainUserId = mUserManager.getMainUser().getIdentifier(); + final UserHandle mainUserHandle = asHandle(mainUserId); mUserManager.setUserRestriction(UserManager.DISALLOW_ADD_MANAGED_PROFILE, true, - primaryUserHandle); + mainUserHandle); try { UserInfo userInfo = createProfileForUser("Managed", - UserManager.USER_TYPE_PROFILE_MANAGED, primaryUserId); + UserManager.USER_TYPE_PROFILE_MANAGED, mainUserId); assertThat(userInfo).isNull(); } finally { mUserManager.setUserRestriction(UserManager.DISALLOW_ADD_MANAGED_PROFILE, false, - primaryUserHandle); + mainUserHandle); } } @@ -906,17 +905,17 @@ public final class UserManagerTest { @Test public void testCreateProfileForUserEvenWhenDisallowed() throws Exception { assumeManagedUsersSupported(); - final int primaryUserId = mUserManager.getPrimaryUser().id; - final UserHandle primaryUserHandle = asHandle(primaryUserId); + final int mainUserId = mUserManager.getMainUser().getIdentifier(); + final UserHandle mainUserHandle = asHandle(mainUserId); mUserManager.setUserRestriction(UserManager.DISALLOW_ADD_MANAGED_PROFILE, true, - primaryUserHandle); + mainUserHandle); try { UserInfo userInfo = createProfileEvenWhenDisallowedForUser("Managed", - UserManager.USER_TYPE_PROFILE_MANAGED, primaryUserId); + UserManager.USER_TYPE_PROFILE_MANAGED, mainUserId); assertThat(userInfo).isNotNull(); } finally { mUserManager.setUserRestriction(UserManager.DISALLOW_ADD_MANAGED_PROFILE, false, - primaryUserHandle); + mainUserHandle); } } @@ -925,23 +924,23 @@ public final class UserManagerTest { @Test public void testCreateProfileForUser_disallowAddUser() throws Exception { assumeManagedUsersSupported(); - final int primaryUserId = mUserManager.getPrimaryUser().id; - final UserHandle primaryUserHandle = asHandle(primaryUserId); - mUserManager.setUserRestriction(UserManager.DISALLOW_ADD_USER, true, primaryUserHandle); + final int mainUserId = mUserManager.getMainUser().getIdentifier(); + final UserHandle mainUserHandle = asHandle(mainUserId); + mUserManager.setUserRestriction(UserManager.DISALLOW_ADD_USER, true, mainUserHandle); try { UserInfo userInfo = createProfileForUser("Managed", - UserManager.USER_TYPE_PROFILE_MANAGED, primaryUserId); + UserManager.USER_TYPE_PROFILE_MANAGED, mainUserId); assertThat(userInfo).isNotNull(); } finally { mUserManager.setUserRestriction(UserManager.DISALLOW_ADD_USER, false, - primaryUserHandle); + mainUserHandle); } } @MediumTest @Test public void testAddRestrictedProfile() throws Exception { - if (isAutomotive()) return; + if (isAutomotive() || UserManager.isHeadlessSystemUserMode()) return; assertWithMessage("There should be no associated restricted profiles before the test") .that(mUserManager.hasRestrictedProfiles()).isFalse(); UserInfo userInfo = createRestrictedProfile("Profile"); @@ -973,10 +972,10 @@ public final class UserManagerTest { @Test public void testGetManagedProfileCreationTime() throws Exception { assumeManagedUsersSupported(); - final int primaryUserId = mUserManager.getPrimaryUser().id; + final int mainUserId = mUserManager.getMainUser().getIdentifier(); final long startTime = System.currentTimeMillis(); UserInfo profile = createProfileForUser("Managed 1", - UserManager.USER_TYPE_PROFILE_MANAGED, primaryUserId); + UserManager.USER_TYPE_PROFILE_MANAGED, mainUserId); final long endTime = System.currentTimeMillis(); assertThat(profile).isNotNull(); if (System.currentTimeMillis() > EPOCH_PLUS_30_YEARS) { @@ -989,8 +988,8 @@ public final class UserManagerTest { assertThat(mUserManager.getUserCreationTime(asHandle(profile.id))) .isEqualTo(profile.creationTime); - long ownerCreationTime = mUserManager.getUserInfo(primaryUserId).creationTime; - assertThat(mUserManager.getUserCreationTime(asHandle(primaryUserId))) + long ownerCreationTime = mUserManager.getUserInfo(mainUserId).creationTime; + assertThat(mUserManager.getUserCreationTime(asHandle(mainUserId))) .isEqualTo(ownerCreationTime); } @@ -1226,14 +1225,14 @@ public final class UserManagerTest { @Test public void testCreateProfile_withContextUserId() throws Exception { assumeManagedUsersSupported(); - final int primaryUserId = mUserManager.getPrimaryUser().id; + final int mainUserId = mUserManager.getMainUser().getIdentifier(); UserInfo userProfile = createProfileForUser("Managed 1", - UserManager.USER_TYPE_PROFILE_MANAGED, primaryUserId); + UserManager.USER_TYPE_PROFILE_MANAGED, mainUserId); assertThat(userProfile).isNotNull(); UserManager um = (UserManager) mContext.createPackageContextAsUser( - "android", 0, mUserManager.getPrimaryUser().getUserHandle()) + "android", 0, mUserManager.getMainUser()) .getSystemService(Context.USER_SERVICE); List<UserHandle> profiles = um.getAllProfiles(); @@ -1245,10 +1244,10 @@ public final class UserManagerTest { @Test public void testSetUserName_withContextUserId() throws Exception { assumeManagedUsersSupported(); - final int primaryUserId = mUserManager.getPrimaryUser().id; + final int mainUserId = mUserManager.getMainUser().getIdentifier(); UserInfo userInfo1 = createProfileForUser("Managed 1", - UserManager.USER_TYPE_PROFILE_MANAGED, primaryUserId); + UserManager.USER_TYPE_PROFILE_MANAGED, mainUserId); assertThat(userInfo1).isNotNull(); UserManager um = (UserManager) mContext.createPackageContextAsUser( @@ -1294,10 +1293,10 @@ public final class UserManagerTest { @Test public void testGetUserIcon_withContextUserId() throws Exception { assumeManagedUsersSupported(); - final int primaryUserId = mUserManager.getPrimaryUser().id; + final int mainUserId = mUserManager.getMainUser().getIdentifier(); UserInfo userInfo1 = createProfileForUser("Managed 1", - UserManager.USER_TYPE_PROFILE_MANAGED, primaryUserId); + UserManager.USER_TYPE_PROFILE_MANAGED, mainUserId); assertThat(userInfo1).isNotNull(); UserManager um = (UserManager) mContext.createPackageContextAsUser( diff --git a/services/tests/servicestests/src/com/android/server/timedetector/FakeTimeDetectorStrategy.java b/services/tests/servicestests/src/com/android/server/timedetector/FakeTimeDetectorStrategy.java index 856df359b326..50040b70c47e 100644 --- a/services/tests/servicestests/src/com/android/server/timedetector/FakeTimeDetectorStrategy.java +++ b/services/tests/servicestests/src/com/android/server/timedetector/FakeTimeDetectorStrategy.java @@ -62,6 +62,15 @@ public class FakeTimeDetectorStrategy implements TimeDetectorStrategy { } @Override + public NetworkTimeSuggestion getLatestNetworkSuggestion() { + return null; + } + + @Override + public void clearLatestNetworkSuggestion() { + } + + @Override public void suggestGnssTime(GnssTimeSuggestion timeSuggestion) { } diff --git a/services/tests/servicestests/src/com/android/server/timedetector/NetworkTimeUpdateServiceTest.java b/services/tests/servicestests/src/com/android/server/timedetector/NetworkTimeUpdateServiceTest.java new file mode 100644 index 000000000000..08d08b65288a --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/timedetector/NetworkTimeUpdateServiceTest.java @@ -0,0 +1,439 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.timedetector; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.time.UnixEpochTime; +import android.net.Network; +import android.util.NtpTrustedTime; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.server.timedetector.NetworkTimeUpdateService.Engine.RefreshCallbacks; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.net.InetSocketAddress; +import java.util.function.Supplier; + +@RunWith(AndroidJUnit4.class) +public class NetworkTimeUpdateServiceTest { + + private static final InetSocketAddress FAKE_SERVER_ADDRESS = + InetSocketAddress.createUnresolved("test", 123); + private static final long ARBITRARY_ELAPSED_REALTIME_MILLIS = 100000000L; + private static final long ARBITRARY_UNIX_EPOCH_TIME_MILLIS = 5555555555L; + private static final int ARBITRARY_UNCERTAINTY_MILLIS = 999; + + private FakeElapsedRealtimeClock mFakeElapsedRealtimeClock; + private NtpTrustedTime mMockNtpTrustedTime; + private Network mDummyNetwork; + + @Before + public void setUp() { + mFakeElapsedRealtimeClock = new FakeElapsedRealtimeClock(); + mMockNtpTrustedTime = mock(NtpTrustedTime.class); + mDummyNetwork = mock(Network.class); + } + + @Test + public void engineImpl_refreshIfRequiredAndReschedule_success() { + mFakeElapsedRealtimeClock.setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS); + + int normalPollingIntervalMillis = 7777777; + int shortPollingIntervalMillis = 3333; + int tryAgainTimesMax = 5; + NetworkTimeUpdateService.Engine engine = new NetworkTimeUpdateService.EngineImpl( + mFakeElapsedRealtimeClock, + normalPollingIntervalMillis, shortPollingIntervalMillis, tryAgainTimesMax, + mMockNtpTrustedTime); + + // Simulated NTP client behavior: No cached time value available initially, then a + // successful refresh. + NtpTrustedTime.TimeResult timeResult = createNtpTimeResult( + mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() - 1); + when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(null, timeResult); + when(mMockNtpTrustedTime.forceRefresh(mDummyNetwork)).thenReturn(true); + + RefreshCallbacks mockCallback = mock(RefreshCallbacks.class); + // Trigger the engine's logic. + engine.refreshIfRequiredAndReschedule(mDummyNetwork, "Test", mockCallback); + + // Expect the refresh attempt to have been made. + verify(mMockNtpTrustedTime).forceRefresh(mDummyNetwork); + + // Check everything happened that was supposed to. + long expectedDelayMillis = calculateRefreshDelayMillisForTimeResult( + timeResult, normalPollingIntervalMillis); + verify(mockCallback).scheduleNextRefresh( + mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() + expectedDelayMillis); + + NetworkTimeSuggestion expectedSuggestion = createExpectedSuggestion(timeResult); + verify(mockCallback).submitSuggestion(expectedSuggestion); + } + + @Test + public void engineImpl_refreshIfRequiredAndReschedule_failThenFailRepeatedly() { + mFakeElapsedRealtimeClock.setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS); + + int normalPollingIntervalMillis = 7777777; + int shortPollingIntervalMillis = 3333; + int tryAgainTimesMax = 5; + NetworkTimeUpdateService.Engine engine = new NetworkTimeUpdateService.EngineImpl( + mFakeElapsedRealtimeClock, + normalPollingIntervalMillis, shortPollingIntervalMillis, tryAgainTimesMax, + mMockNtpTrustedTime); + + for (int i = 0; i < tryAgainTimesMax + 1; i++) { + // Simulated NTP client behavior: No cached time value available and failure to refresh. + when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(null); + when(mMockNtpTrustedTime.forceRefresh(mDummyNetwork)).thenReturn(false); + + RefreshCallbacks mockCallback = mock(RefreshCallbacks.class); + + // Trigger the engine's logic. + engine.refreshIfRequiredAndReschedule(mDummyNetwork, "Test", mockCallback); + + // Expect a refresh attempt each time: there's no currently cached result. + verify(mMockNtpTrustedTime).forceRefresh(mDummyNetwork); + + // Check everything happened that was supposed to. + long expectedDelayMillis; + if (i < tryAgainTimesMax) { + expectedDelayMillis = shortPollingIntervalMillis; + } else { + expectedDelayMillis = normalPollingIntervalMillis; + } + verify(mockCallback).scheduleNextRefresh( + mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() + expectedDelayMillis); + verify(mockCallback, never()).submitSuggestion(any()); + + reset(mMockNtpTrustedTime); + } + } + + @Test + public void engineImpl_refreshIfRequiredAndReschedule_successThenFailRepeatedly() { + mFakeElapsedRealtimeClock.setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS); + + int normalPollingIntervalMillis = 7777777; + int maxTimeResultAgeMillis = normalPollingIntervalMillis; + int shortPollingIntervalMillis = 3333; + int tryAgainTimesMax = 5; + NetworkTimeUpdateService.Engine engine = new NetworkTimeUpdateService.EngineImpl( + mFakeElapsedRealtimeClock, + normalPollingIntervalMillis, shortPollingIntervalMillis, tryAgainTimesMax, + mMockNtpTrustedTime); + + NtpTrustedTime.TimeResult timeResult = createNtpTimeResult( + mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() - 1); + NetworkTimeSuggestion expectedSuggestion = createExpectedSuggestion(timeResult); + + { + // Simulated NTP client behavior: No cached time value available initially, with a + // successful refresh. + when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(null, timeResult); + when(mMockNtpTrustedTime.forceRefresh(mDummyNetwork)).thenReturn(true); + + RefreshCallbacks mockCallback = mock(RefreshCallbacks.class); + + // Trigger the engine's logic. + engine.refreshIfRequiredAndReschedule(mDummyNetwork, "Test", mockCallback); + + // Expect the refresh attempt to have been made: there is no cached network time + // initially. + verify(mMockNtpTrustedTime).forceRefresh(mDummyNetwork); + + long expectedDelayMillis = calculateRefreshDelayMillisForTimeResult( + timeResult, normalPollingIntervalMillis); + verify(mockCallback).scheduleNextRefresh( + mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() + expectedDelayMillis); + verify(mockCallback, times(1)).submitSuggestion(expectedSuggestion); + reset(mMockNtpTrustedTime); + } + + // Increment the current time by enough so that an attempt to refresh the time should be + // made every time refreshIfRequiredAndReschedule() is called. + mFakeElapsedRealtimeClock.incrementMillis(maxTimeResultAgeMillis); + + // Test multiple follow-up calls. + for (int i = 0; i < tryAgainTimesMax + 1; i++) { + // Simulated NTP client behavior: (Too old) cached time value available, unsuccessful + // refresh. + when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(timeResult); + when(mMockNtpTrustedTime.forceRefresh(mDummyNetwork)).thenReturn(false); + + RefreshCallbacks mockCallback = mock(RefreshCallbacks.class); + + // Trigger the engine's logic. + engine.refreshIfRequiredAndReschedule(mDummyNetwork, "Test", mockCallback); + + // Expect a refresh attempt each time as the cached network time is too old. + verify(mMockNtpTrustedTime).forceRefresh(mDummyNetwork); + + // Check the scheduling. + long expectedDelayMillis; + if (i < tryAgainTimesMax) { + expectedDelayMillis = shortPollingIntervalMillis; + } else { + expectedDelayMillis = normalPollingIntervalMillis; + } + verify(mockCallback).scheduleNextRefresh( + mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() + expectedDelayMillis); + + // No valid time, no suggestion. + verify(mockCallback, never()).submitSuggestion(any()); + + reset(mMockNtpTrustedTime); + } + } + + @Test + public void engineImpl_refreshIfRequiredAndReschedule_successFailSuccess() { + mFakeElapsedRealtimeClock.setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS); + + int normalPollingIntervalMillis = 7777777; + int maxTimeResultAgeMillis = normalPollingIntervalMillis; + int shortPollingIntervalMillis = 3333; + int tryAgainTimesMax = 5; + NetworkTimeUpdateService.Engine engine = new NetworkTimeUpdateService.EngineImpl( + mFakeElapsedRealtimeClock, + normalPollingIntervalMillis, shortPollingIntervalMillis, tryAgainTimesMax, + mMockNtpTrustedTime); + + NtpTrustedTime.TimeResult timeResult1 = createNtpTimeResult( + mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() - 1); + { + // Simulated NTP client behavior: No cached time value available initially, with a + // successful refresh. + when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(null, timeResult1); + when(mMockNtpTrustedTime.forceRefresh(mDummyNetwork)).thenReturn(true); + + RefreshCallbacks mockCallback = mock(RefreshCallbacks.class); + + // Trigger the engine's logic. + engine.refreshIfRequiredAndReschedule(mDummyNetwork, "Test", mockCallback); + + // Expect the refresh attempt to have been made: there is no cached network time + // initially. + verify(mMockNtpTrustedTime).forceRefresh(mDummyNetwork); + + long expectedDelayMillis = calculateRefreshDelayMillisForTimeResult( + timeResult1, normalPollingIntervalMillis); + verify(mockCallback).scheduleNextRefresh( + mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() + expectedDelayMillis); + NetworkTimeSuggestion expectedSuggestion = createExpectedSuggestion(timeResult1); + verify(mockCallback, times(1)).submitSuggestion(expectedSuggestion); + reset(mMockNtpTrustedTime); + } + + // Increment the current time by enough so that the cached time result is too old and an + // attempt to refresh the time should be made every time refreshIfRequiredAndReschedule() is + // called. + mFakeElapsedRealtimeClock.incrementMillis(maxTimeResultAgeMillis); + + { + // Simulated NTP client behavior: (Old) cached time value available initially, with an + // unsuccessful refresh. + when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(timeResult1); + when(mMockNtpTrustedTime.forceRefresh(mDummyNetwork)).thenReturn(false); + + RefreshCallbacks mockCallback = mock(RefreshCallbacks.class); + + // Trigger the engine's logic. + engine.refreshIfRequiredAndReschedule(mDummyNetwork, "Test", mockCallback); + + // Expect the refresh attempt to have been made: the timeResult is too old. + verify(mMockNtpTrustedTime).forceRefresh(mDummyNetwork); + + long expectedDelayMillis = shortPollingIntervalMillis; + verify(mockCallback).scheduleNextRefresh( + mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() + expectedDelayMillis); + + // No valid time, no suggestion. + verify(mockCallback, never()).submitSuggestion(any()); + reset(mMockNtpTrustedTime); + } + + NtpTrustedTime.TimeResult timeResult2 = createNtpTimeResult( + mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() - 1); + + { + // Simulated NTP client behavior: (Old) cached time value available initially, with a + // successful refresh and a new cached time value. + when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(timeResult1, timeResult2); + when(mMockNtpTrustedTime.forceRefresh(mDummyNetwork)).thenReturn(true); + + RefreshCallbacks mockCallback = mock(RefreshCallbacks.class); + + // Trigger the engine's logic. + engine.refreshIfRequiredAndReschedule(mDummyNetwork, "Test", mockCallback); + + // Expect the refresh attempt to have been made: the timeResult is too old. + verify(mMockNtpTrustedTime).forceRefresh(mDummyNetwork); + + long expectedDelayMillis = calculateRefreshDelayMillisForTimeResult( + timeResult2, normalPollingIntervalMillis); + verify(mockCallback).scheduleNextRefresh( + mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() + expectedDelayMillis); + NetworkTimeSuggestion expectedSuggestion = createExpectedSuggestion(timeResult2); + verify(mockCallback, times(1)).submitSuggestion(expectedSuggestion); + reset(mMockNtpTrustedTime); + } + } + + /** + * Confirms that if a refreshIfRequiredAndReschedule() call is made, e.g. for reasons besides + * scheduled alerts, and the latest time is not too old, then an NTP refresh won't be attempted. + * A suggestion will still be made. + */ + @Test + public void engineImpl_refreshIfRequiredAndReschedule_noRefreshIfLatestIsNotTooOld() { + mFakeElapsedRealtimeClock.setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS); + + int normalPollingIntervalMillis = 7777777; + int maxTimeResultAgeMillis = normalPollingIntervalMillis; + int shortPollingIntervalMillis = 3333; + int tryAgainTimesMax = 5; + NetworkTimeUpdateService.Engine engine = new NetworkTimeUpdateService.EngineImpl( + mFakeElapsedRealtimeClock, + normalPollingIntervalMillis, shortPollingIntervalMillis, tryAgainTimesMax, + mMockNtpTrustedTime); + + // Simulated NTP client behavior: A cached time value is available, increment the clock, but + // not enough to consider the cached value too old. + NtpTrustedTime.TimeResult timeResult = createNtpTimeResult( + mFakeElapsedRealtimeClock.getElapsedRealtimeMillis()); + when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(timeResult); + mFakeElapsedRealtimeClock.incrementMillis(maxTimeResultAgeMillis - 1); + + RefreshCallbacks mockCallback = mock(RefreshCallbacks.class); + // Trigger the engine's logic. + engine.refreshIfRequiredAndReschedule(mDummyNetwork, "Test", mockCallback); + + // Expect no refresh attempt to have been made. + verify(mMockNtpTrustedTime, never()).forceRefresh(any()); + + // The next wake-up should be rescheduled for when the cached time value will become too + // old. + long expectedDelayMillis = calculateRefreshDelayMillisForTimeResult(timeResult, + normalPollingIntervalMillis); + verify(mockCallback).scheduleNextRefresh( + mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() + expectedDelayMillis); + + // Suggestions must be made every time if the cached time value is not too old in case it + // was refreshed by a different component. + NetworkTimeSuggestion expectedSuggestion = createExpectedSuggestion(timeResult); + verify(mockCallback, times(1)).submitSuggestion(expectedSuggestion); + } + + /** + * Confirms that if a refreshIfRequiredAndReschedule() call is made, e.g. for reasons besides + * scheduled alerts, and the latest time is not too old, then an NTP refresh won't be attempted. + * A suggestion will still be made. + */ + @Test + public void engineImpl_refreshIfRequiredAndReschedule_failureHandlingAfterLatestIsTooOld() { + mFakeElapsedRealtimeClock.setElapsedRealtimeMillis(ARBITRARY_ELAPSED_REALTIME_MILLIS); + + int normalPollingIntervalMillis = 7777777; + int maxTimeResultAgeMillis = normalPollingIntervalMillis; + int shortPollingIntervalMillis = 3333; + int tryAgainTimesMax = 5; + NetworkTimeUpdateService.Engine engine = new NetworkTimeUpdateService.EngineImpl( + mFakeElapsedRealtimeClock, + normalPollingIntervalMillis, shortPollingIntervalMillis, tryAgainTimesMax, + mMockNtpTrustedTime); + + // Simulated NTP client behavior: A cached time value is available, increment the clock, + // enough to consider the cached value too old. The refresh attempt will fail. + NtpTrustedTime.TimeResult timeResult = createNtpTimeResult( + mFakeElapsedRealtimeClock.getElapsedRealtimeMillis()); + when(mMockNtpTrustedTime.getCachedTimeResult()).thenReturn(timeResult); + mFakeElapsedRealtimeClock.incrementMillis(maxTimeResultAgeMillis); + when(mMockNtpTrustedTime.forceRefresh(mDummyNetwork)).thenReturn(false); + + RefreshCallbacks mockCallback = mock(RefreshCallbacks.class); + // Trigger the engine's logic. + engine.refreshIfRequiredAndReschedule(mDummyNetwork, "Test", mockCallback); + + // Expect a refresh attempt to have been made. + verify(mMockNtpTrustedTime, times(1)).forceRefresh(mDummyNetwork); + + // The next wake-up should be rescheduled using the short polling interval. + long expectedDelayMillis = shortPollingIntervalMillis; + verify(mockCallback).scheduleNextRefresh( + mFakeElapsedRealtimeClock.getElapsedRealtimeMillis() + expectedDelayMillis); + + // Suggestions should not be made if the cached time value is too old. + verify(mockCallback, never()).submitSuggestion(any()); + } + + private long calculateRefreshDelayMillisForTimeResult(NtpTrustedTime.TimeResult timeResult, + int normalPollingIntervalMillis) { + long currentElapsedRealtimeMillis = mFakeElapsedRealtimeClock.getElapsedRealtimeMillis(); + long timeResultAgeMillis = timeResult.getAgeMillis(currentElapsedRealtimeMillis); + return normalPollingIntervalMillis - timeResultAgeMillis; + } + + private static NetworkTimeSuggestion createExpectedSuggestion( + NtpTrustedTime.TimeResult timeResult) { + UnixEpochTime unixEpochTime = new UnixEpochTime( + timeResult.getElapsedRealtimeMillis(), timeResult.getTimeMillis()); + return new NetworkTimeSuggestion(unixEpochTime, timeResult.getUncertaintyMillis()); + } + + private static NtpTrustedTime.TimeResult createNtpTimeResult(long elapsedRealtimeMillis) { + return new NtpTrustedTime.TimeResult( + ARBITRARY_UNIX_EPOCH_TIME_MILLIS, + elapsedRealtimeMillis, + ARBITRARY_UNCERTAINTY_MILLIS, + FAKE_SERVER_ADDRESS); + } + + private static class FakeElapsedRealtimeClock implements Supplier<Long> { + + private long mElapsedRealtimeMillis; + + public void setElapsedRealtimeMillis(long elapsedRealtimeMillis) { + mElapsedRealtimeMillis = elapsedRealtimeMillis; + } + + public long getElapsedRealtimeMillis() { + return mElapsedRealtimeMillis; + } + + public long incrementMillis(int millis) { + mElapsedRealtimeMillis += millis; + return mElapsedRealtimeMillis; + } + + @Override + public Long get() { + return getElapsedRealtimeMillis(); + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorServiceTest.java b/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorServiceTest.java index a8572381208d..0b339ad52eda 100644 --- a/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorServiceTest.java @@ -380,6 +380,28 @@ public class TimeDetectorServiceTest { } @Test + public void testClearNetworkTime_withoutPermission() { + doThrow(new SecurityException("Mock")) + .when(mMockContext).enforceCallingPermission(anyString(), any()); + + assertThrows(SecurityException.class, + () -> mTimeDetectorService.clearNetworkTime()); + verify(mMockContext).enforceCallingPermission( + eq(android.Manifest.permission.SET_TIME), anyString()); + } + + @Test + public void testClearNetworkTime() throws Exception { + doNothing().when(mMockContext).enforceCallingPermission(anyString(), any()); + + mTimeDetectorService.clearNetworkTime(); + + verify(mMockContext).enforceCallingPermission( + eq(android.Manifest.permission.SET_TIME), anyString()); + verify(mFakeTimeDetectorStrategySpy).clearLatestNetworkSuggestion(); + } + + @Test public void testLatestNetworkTime() { NtpTrustedTime.TimeResult latestNetworkTime = new NtpTrustedTime.TimeResult( 1234L, 54321L, 999, InetSocketAddress.createUnresolved("test.timeserver", 123)); diff --git a/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorStrategyImplTest.java b/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorStrategyImplTest.java index caef4943118b..37da2a28a892 100644 --- a/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorStrategyImplTest.java +++ b/services/tests/servicestests/src/com/android/server/timedetector/TimeDetectorStrategyImplTest.java @@ -874,6 +874,7 @@ public class TimeDetectorStrategyImplTest { long expectedSystemClockMillis = script.calculateTimeInMillisForNow(timeSuggestion.getUnixEpochTime()); script.simulateNetworkTimeSuggestion(timeSuggestion) + .assertLatestNetworkSuggestion(timeSuggestion) .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH) .verifySystemClockWasSetAndResetCallTracking(expectedSystemClockMillis); } @@ -891,10 +892,55 @@ public class TimeDetectorStrategyImplTest { script.simulateTimePassing() .simulateNetworkTimeSuggestion(timeSuggestion) + .assertLatestNetworkSuggestion(timeSuggestion) .verifySystemClockWasNotSetAndResetCallTracking(); } @Test + public void testClearLatestNetworkSuggestion() { + ConfigurationInternal configInternal = + new ConfigurationInternal.Builder(CONFIG_AUTO_ENABLED) + .setOriginPriorities(ORIGIN_NETWORK, ORIGIN_EXTERNAL) + .build(); + Script script = new Script().simulateConfigurationInternalChange(configInternal); + + // Create two different time suggestions for the current elapsedRealtimeMillis. + ExternalTimeSuggestion externalTimeSuggestion = + script.generateExternalTimeSuggestion(ARBITRARY_TEST_TIME); + NetworkTimeSuggestion networkTimeSuggestion = + script.generateNetworkTimeSuggestion(ARBITRARY_TEST_TIME.plus(Duration.ofHours(5))); + script.simulateTimePassing(); + + // Suggest an external time: This should cause the device to change time. + { + long expectedSystemClockMillis = + script.calculateTimeInMillisForNow(externalTimeSuggestion.getUnixEpochTime()); + script.simulateExternalTimeSuggestion(externalTimeSuggestion) + .verifySystemClockWasSetAndResetCallTracking(expectedSystemClockMillis); + } + + // Suggest a network time: This should cause the device to change time because + // network > external. + { + long expectedSystemClockMillis = + script.calculateTimeInMillisForNow(networkTimeSuggestion.getUnixEpochTime()); + script.simulateNetworkTimeSuggestion(networkTimeSuggestion) + .assertLatestNetworkSuggestion(networkTimeSuggestion) + .verifySystemClockWasSetAndResetCallTracking(expectedSystemClockMillis); + } + + // Clear the network time. This should cause the device to change back to the external time, + // which is now the best time available. + { + long expectedSystemClockMillis = + script.calculateTimeInMillisForNow(externalTimeSuggestion.getUnixEpochTime()); + script.simulateClearLatestNetworkSuggestion() + .assertLatestNetworkSuggestion(null) + .verifySystemClockWasSetAndResetCallTracking(expectedSystemClockMillis); + } + } + + @Test public void testSuggestNetworkTime_rejectedBelowLowerBound() { ConfigurationInternal configInternal = new ConfigurationInternal.Builder(CONFIG_AUTO_ENABLED) @@ -908,6 +954,7 @@ public class TimeDetectorStrategyImplTest { NetworkTimeSuggestion timeSuggestion = script.generateNetworkTimeSuggestion(belowLowerBound); script.simulateNetworkTimeSuggestion(timeSuggestion) + .assertLatestNetworkSuggestion(null) .verifySystemClockConfidence(TIME_CONFIDENCE_LOW) .verifySystemClockWasNotSetAndResetCallTracking(); } @@ -926,6 +973,7 @@ public class TimeDetectorStrategyImplTest { NetworkTimeSuggestion timeSuggestion = script.generateNetworkTimeSuggestion(aboveLowerBound); script.simulateNetworkTimeSuggestion(timeSuggestion) + .assertLatestNetworkSuggestion(timeSuggestion) .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH) .verifySystemClockWasSetAndResetCallTracking(aboveLowerBound.toEpochMilli()); } @@ -944,6 +992,7 @@ public class TimeDetectorStrategyImplTest { NetworkTimeSuggestion timeSuggestion = script.generateNetworkTimeSuggestion(aboveUpperBound); script.simulateNetworkTimeSuggestion(timeSuggestion) + .assertLatestNetworkSuggestion(null) .verifySystemClockConfidence(TIME_CONFIDENCE_LOW) .verifySystemClockWasNotSetAndResetCallTracking(); } @@ -962,6 +1011,7 @@ public class TimeDetectorStrategyImplTest { NetworkTimeSuggestion timeSuggestion = script.generateNetworkTimeSuggestion(belowUpperBound); script.simulateNetworkTimeSuggestion(timeSuggestion) + .assertLatestNetworkSuggestion(timeSuggestion) .verifySystemClockConfidence(TIME_CONFIDENCE_HIGH) .verifySystemClockWasSetAndResetCallTracking(belowUpperBound.toEpochMilli()); } @@ -1745,7 +1795,7 @@ public class TimeDetectorStrategyImplTest { } @Test - public void suggestionsFromNetworkOriginNotInPriorityList_areIgnored() { + public void suggestionsFromNetworkOriginNotInPriorityList_areNotUsed() { ConfigurationInternal configInternal = new ConfigurationInternal.Builder(CONFIG_AUTO_ENABLED) .setOriginPriorities(ORIGIN_TELEPHONY) @@ -1757,11 +1807,12 @@ public class TimeDetectorStrategyImplTest { script.simulateNetworkTimeSuggestion(timeSuggestion) .assertLatestNetworkSuggestion(timeSuggestion) + .assertLatestNetworkSuggestion(timeSuggestion) .verifySystemClockWasNotSetAndResetCallTracking(); } @Test - public void suggestionsFromGnssOriginNotInPriorityList_areIgnored() { + public void suggestionsFromGnssOriginNotInPriorityList_areNotUsed() { ConfigurationInternal configInternal = new ConfigurationInternal.Builder(CONFIG_AUTO_ENABLED) .setOriginPriorities(ORIGIN_TELEPHONY) @@ -1777,7 +1828,7 @@ public class TimeDetectorStrategyImplTest { } @Test - public void suggestionsFromExternalOriginNotInPriorityList_areIgnored() { + public void suggestionsFromExternalOriginNotInPriorityList_areNotUsed() { ConfigurationInternal configInternal = new ConfigurationInternal.Builder(CONFIG_AUTO_ENABLED) .setOriginPriorities(ORIGIN_TELEPHONY) @@ -2015,6 +2066,11 @@ public class TimeDetectorStrategyImplTest { return this; } + Script simulateClearLatestNetworkSuggestion() { + mTimeDetectorStrategy.clearLatestNetworkSuggestion(); + return this; + } + Script simulateGnssTimeSuggestion(GnssTimeSuggestion timeSuggestion) { mTimeDetectorStrategy.suggestGnssTime(timeSuggestion); return this; @@ -2056,6 +2112,12 @@ public class TimeDetectorStrategyImplTest { return this; } + /** Calls {@link TimeDetectorStrategy#confirmTime(UnixEpochTime)}. */ + Script simulateConfirmTime(UnixEpochTime confirmationTime, boolean expectedReturnValue) { + assertEquals(expectedReturnValue, mTimeDetectorStrategy.confirmTime(confirmationTime)); + return this; + } + Script verifySystemClockWasNotSetAndResetCallTracking() { mFakeEnvironment.verifySystemClockNotSet(); mFakeEnvironment.resetCallTracking(); @@ -2218,11 +2280,6 @@ public class TimeDetectorStrategyImplTest { long calculateTimeInMillisForNow(UnixEpochTime unixEpochTime) { return unixEpochTime.at(peekElapsedRealtimeMillis()).getUnixEpochTimeMillis(); } - - Script simulateConfirmTime(UnixEpochTime confirmationTime, boolean expectedReturnValue) { - assertEquals(expectedReturnValue, mTimeDetectorStrategy.confirmTime(confirmationTime)); - return this; - } } private static TelephonyTimeSuggestion createTelephonyTimeSuggestion(int slotIndex, diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java index 4ee87d4b57b5..e02863e2c352 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -58,6 +58,7 @@ import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING; import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; +import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_OLD_ACTIVITY_OPEN; import static android.view.WindowManager.TRANSIT_PIP; @@ -3143,46 +3144,6 @@ public class ActivityRecordTests extends WindowTestsBase { assertFalse(activity.mDisplayContent.mClosingApps.contains(activity)); } - @Test - public void testImeInsetsFrozenFlag_resetWhenReparented() { - final ActivityRecord activity = createActivityWithTask(); - final WindowState app = createWindow(null, TYPE_APPLICATION, activity, "app"); - final WindowState imeWindow = createWindow(null, TYPE_APPLICATION, "imeWindow"); - final Task newTask = new TaskBuilder(mSupervisor).build(); - makeWindowVisible(app, imeWindow); - mDisplayContent.mInputMethodWindow = imeWindow; - mDisplayContent.setImeLayeringTarget(app); - mDisplayContent.setImeInputTarget(app); - - // Simulate app is closing and expect the last IME is shown and IME insets is frozen. - app.mActivityRecord.commitVisibility(false, false); - assertTrue(app.mActivityRecord.mLastImeShown); - assertTrue(app.mActivityRecord.mImeInsetsFrozenUntilStartInput); - - // Expect IME insets frozen state will reset when the activity is reparent to the new task. - activity.setState(RESUMED, "test"); - activity.reparent(newTask, 0 /* top */, "test"); - assertFalse(app.mActivityRecord.mImeInsetsFrozenUntilStartInput); - } - - @SetupWindows(addWindows = W_INPUT_METHOD) - @Test - public void testImeInsetsFrozenFlag_resetWhenResized() { - final WindowState app = createWindow(null, TYPE_APPLICATION, "app"); - makeWindowVisibleAndDrawn(app, mImeWindow); - mDisplayContent.setImeLayeringTarget(app); - mDisplayContent.setImeInputTarget(app); - - // Simulate app is closing and expect the last IME is shown and IME insets is frozen. - app.mActivityRecord.commitVisibility(false, false); - assertTrue(app.mActivityRecord.mLastImeShown); - assertTrue(app.mActivityRecord.mImeInsetsFrozenUntilStartInput); - - // Expect IME insets frozen state will reset when the activity is reparent to the new task. - app.mActivityRecord.onResize(); - assertFalse(app.mActivityRecord.mImeInsetsFrozenUntilStartInput); - } - @SetupWindows(addWindows = W_INPUT_METHOD) @Test public void testImeInsetsFrozenFlag_resetWhenNoImeFocusableInActivity() { @@ -3216,6 +3177,10 @@ public class ActivityRecordTests extends WindowTestsBase { public void testImeInsetsFrozenFlag_resetWhenReportedToBeImeInputTarget() { final WindowState app = createWindow(null, TYPE_APPLICATION, "app"); + mDisplayContent.getInsetsStateController().getSourceProvider(ITYPE_IME).setWindowContainer( + mImeWindow, null, null); + mImeWindow.getControllableInsetProvider().setServerVisible(true); + InsetsSource imeSource = new InsetsSource(ITYPE_IME, ime()); app.mAboveInsetsState.addSource(imeSource); mDisplayContent.setImeLayeringTarget(app); @@ -3233,8 +3198,10 @@ public class ActivityRecordTests extends WindowTestsBase { // Simulate app re-start input or turning screen off/on then unlocked by un-secure // keyguard to back to the app, expect IME insets is not frozen - mDisplayContent.updateImeInputAndControlTarget(app); app.mActivityRecord.commitVisibility(true, false); + mDisplayContent.updateImeInputAndControlTarget(app); + mDisplayContent.mWmService.mRoot.performSurfacePlacement(); + assertFalse(app.mActivityRecord.mImeInsetsFrozenUntilStartInput); imeSource.setVisible(true); @@ -3274,12 +3241,12 @@ public class ActivityRecordTests extends WindowTestsBase { assertTrue(app2.mActivityRecord.mImeInsetsFrozenUntilStartInput); // Simulate switching to app2 to make it visible to be IME targets. - makeWindowVisibleAndDrawn(app2); spyOn(app2); spyOn(app2.mClient); ArgumentCaptor<InsetsState> insetsStateCaptor = ArgumentCaptor.forClass(InsetsState.class); doReturn(true).when(app2).isReadyToDispatchInsetsState(); mDisplayContent.setImeLayeringTarget(app2); + app2.mActivityRecord.commitVisibility(true, false); mDisplayContent.updateImeInputAndControlTarget(app2); mDisplayContent.mWmService.mRoot.performSurfacePlacement(); @@ -3293,6 +3260,57 @@ public class ActivityRecordTests extends WindowTestsBase { } @Test + public void testImeInsetsFrozenFlag_multiWindowActivities() { + final WindowToken imeToken = createTestWindowToken(TYPE_INPUT_METHOD, mDisplayContent); + final WindowState ime = createWindow(null, TYPE_INPUT_METHOD, imeToken, "ime"); + makeWindowVisibleAndDrawn(ime); + + // Create a split-screen root task with activity1 and activity 2. + final Task task = new TaskBuilder(mSupervisor) + .setCreateParentTask(true).setCreateActivity(true).build(); + task.getRootTask().setWindowingMode(WINDOWING_MODE_MULTI_WINDOW); + final ActivityRecord activity1 = task.getTopNonFinishingActivity(); + activity1.getTask().setResumedActivity(activity1, "testApp1"); + + final ActivityRecord activity2 = new TaskBuilder(mSupervisor) + .setWindowingMode(WINDOWING_MODE_MULTI_WINDOW) + .setCreateActivity(true).build().getTopMostActivity(); + activity2.getTask().setResumedActivity(activity2, "testApp2"); + activity2.getTask().setParent(task.getRootTask()); + + // Simulate activity1 and activity2 both have set mImeInsetsFrozenUntilStartInput when + // invisible to user. + activity1.mImeInsetsFrozenUntilStartInput = true; + activity2.mImeInsetsFrozenUntilStartInput = true; + + final WindowState app1 = createWindow(null, TYPE_APPLICATION, activity1, "app1"); + final WindowState app2 = createWindow(null, TYPE_APPLICATION, activity2, "app2"); + makeWindowVisibleAndDrawn(app1, app2); + + final InsetsStateController controller = mDisplayContent.getInsetsStateController(); + controller.getSourceProvider(ITYPE_IME).setWindowContainer( + ime, null, null); + ime.getControllableInsetProvider().setServerVisible(true); + + // app1 starts input and expect IME insets for all activities in split-screen will be + // frozen until the input started. + mDisplayContent.setImeLayeringTarget(app1); + mDisplayContent.updateImeInputAndControlTarget(app1); + mDisplayContent.mWmService.mRoot.performSurfacePlacement(); + + assertEquals(app1, mDisplayContent.getImeInputTarget()); + assertFalse(activity1.mImeInsetsFrozenUntilStartInput); + assertFalse(activity2.mImeInsetsFrozenUntilStartInput); + + app1.setRequestedVisibleTypes(ime()); + controller.onInsetsModified(app1); + + // Expect all activities in split-screen will get IME insets visible state + assertTrue(app1.getInsetsState().peekSource(ITYPE_IME).isVisible()); + assertTrue(app2.getInsetsState().peekSource(ITYPE_IME).isVisible()); + } + + @Test public void testInClosingAnimation_visibilityNotCommitted_doNotHideSurface() { final WindowState app = createWindow(null, TYPE_APPLICATION, "app"); makeWindowVisibleAndDrawn(app); diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java index 8bb79e3f7ddc..45b30b204801 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java @@ -155,6 +155,18 @@ public final class DisplayRotationCompatPolicyTests extends WindowTestsBase { } @Test + public void testTreatmentDisabledPerApp_noForceRotationOrRefresh() + throws Exception { + configureActivity(SCREEN_ORIENTATION_PORTRAIT); + when(mActivity.mLetterboxUiController.shouldForceRotateForCameraCompat()) + .thenReturn(false); + + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + + assertNoForceRotationOrRefresh(); + } + + @Test public void testMultiWindowMode_returnUnspecified_noForceRotationOrRefresh() throws Exception { configureActivity(SCREEN_ORIENTATION_PORTRAIT); final TestSplitOrganizer organizer = new TestSplitOrganizer(mAtm, mDisplayContent); @@ -327,7 +339,21 @@ public final class DisplayRotationCompatPolicyTests extends WindowTestsBase { } @Test - public void testOnActivityConfigurationChanging_refreshDisabled_noRefresh() throws Exception { + public void testOnActivityConfigurationChanging_refreshDisabledViaFlag_noRefresh() + throws Exception { + configureActivity(SCREEN_ORIENTATION_PORTRAIT); + when(mActivity.mLetterboxUiController.shouldRefreshActivityForCameraCompat()) + .thenReturn(false); + + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + callOnActivityConfigurationChanging(mActivity, /* isDisplayRotationChanging */ true); + + assertActivityRefreshRequested(/* refreshRequested */ false); + } + + @Test + public void testOnActivityConfigurationChanging_refreshDisabledPerApp_noRefresh() + throws Exception { when(mLetterboxConfiguration.isCameraCompatRefreshEnabled()).thenReturn(false); configureActivity(SCREEN_ORIENTATION_PORTRAIT); @@ -362,6 +388,19 @@ public final class DisplayRotationCompatPolicyTests extends WindowTestsBase { assertActivityRefreshRequested(/* refreshRequested */ true, /* cycleThroughStop */ false); } + @Test + public void testOnActivityConfigurationChanging_cycleThroughStopDisabledForApp() + throws Exception { + configureActivity(SCREEN_ORIENTATION_PORTRAIT); + when(mActivity.mLetterboxUiController.shouldRefreshActivityViaPauseForCameraCompat()) + .thenReturn(true); + + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + callOnActivityConfigurationChanging(mActivity, /* isDisplayRotationChanging */ true); + + assertActivityRefreshRequested(/* refreshRequested */ true, /* cycleThroughStop */ false); + } + private void configureActivity(@ScreenOrientation int activityOrientation) { configureActivityAndDisplay(activityOrientation, ORIENTATION_PORTRAIT); } diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java index 6d778afee88c..5e087f06b36b 100644 --- a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java @@ -16,8 +16,14 @@ package com.android.server.wm; +import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION; +import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH; +import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE; import static android.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; +import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION; +import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH; +import static android.view.WindowManager.PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE; import static android.view.WindowManager.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; @@ -74,6 +80,8 @@ public class LetterboxUiControllerTest extends WindowTestsBase { mController = new LetterboxUiController(mWm, mActivity); } + // shouldIgnoreRequestedOrientation + @Test @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION}) public void testShouldIgnoreRequestedOrientation_activityRelaunching_returnsTrue() { @@ -134,7 +142,7 @@ public class LetterboxUiControllerTest extends WindowTestsBase { } @Test - @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION}) + @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH}) public void testShouldIgnoreRequestedOrientation_flagIsDisabled_returnsFalse() { prepareActivityThatShouldIgnoreRequestedOrientationDuringRelaunch(); doReturn(false).when(mLetterboxConfiguration) @@ -143,6 +151,163 @@ public class LetterboxUiControllerTest extends WindowTestsBase { assertFalse(mController.shouldIgnoreRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED)); } + // shouldRefreshActivityForCameraCompat + + @Test + public void testShouldRefreshActivityForCameraCompat_flagIsDisabled_returnsFalse() { + doReturn(false).when(mLetterboxConfiguration) + .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true); + + assertFalse(mController.shouldRefreshActivityForCameraCompat()); + } + + @Test + @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH}) + public void testShouldRefreshActivityForCameraCompat_overrideEnabled_returnsFalse() { + doReturn(true).when(mLetterboxConfiguration) + .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true); + + assertFalse(mController.shouldRefreshActivityForCameraCompat()); + } + + @Test + @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH}) + public void testShouldRefreshActivityForCameraCompat_propertyIsTrueAndOverride_returnsFalse() + throws Exception { + doReturn(true).when(mLetterboxConfiguration) + .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true); + mockThatProperty(PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH, /* value */ true); + + mController = new LetterboxUiController(mWm, mActivity); + + assertFalse(mController.shouldRefreshActivityForCameraCompat()); + } + + @Test + public void testShouldRefreshActivityForCameraCompat_propertyIsFalse_returnsFalse() + throws Exception { + doReturn(true).when(mLetterboxConfiguration) + .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true); + mockThatProperty(PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH, /* value */ false); + + mController = new LetterboxUiController(mWm, mActivity); + + assertFalse(mController.shouldRefreshActivityForCameraCompat()); + } + + @Test + public void testShouldRefreshActivityForCameraCompat_propertyIsTrue_returnsTrue() + throws Exception { + doReturn(true).when(mLetterboxConfiguration) + .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true); + mockThatProperty(PROPERTY_CAMERA_COMPAT_ALLOW_REFRESH, /* value */ true); + + mController = new LetterboxUiController(mWm, mActivity); + + assertTrue(mController.shouldRefreshActivityForCameraCompat()); + } + + // shouldRefreshActivityViaPauseForCameraCompat + + @Test + @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE}) + public void testShouldRefreshActivityViaPauseForCameraCompat_flagIsDisabled_returnsFalse() { + doReturn(false).when(mLetterboxConfiguration) + .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true); + + assertFalse(mController.shouldRefreshActivityViaPauseForCameraCompat()); + } + + @Test + @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE}) + public void testShouldRefreshActivityViaPauseForCameraCompat_overrideEnabled_returnsTrue() { + doReturn(true).when(mLetterboxConfiguration) + .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true); + + assertTrue(mController.shouldRefreshActivityViaPauseForCameraCompat()); + } + + @Test + @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE}) + public void testShouldRefreshActivityViaPauseForCameraCompat_propertyIsFalseAndOverride_returnFalse() + throws Exception { + doReturn(true).when(mLetterboxConfiguration) + .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true); + mockThatProperty(PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE, /* value */ false); + + mController = new LetterboxUiController(mWm, mActivity); + + assertFalse(mController.shouldRefreshActivityViaPauseForCameraCompat()); + } + + @Test + public void testShouldRefreshActivityViaPauseForCameraCompat_propertyIsTrue_returnsTrue() + throws Exception { + doReturn(true).when(mLetterboxConfiguration) + .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true); + mockThatProperty(PROPERTY_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE, /* value */ true); + + mController = new LetterboxUiController(mWm, mActivity); + + assertTrue(mController.shouldRefreshActivityViaPauseForCameraCompat()); + } + + // shouldForceRotateForCameraCompat + + @Test + public void testShouldForceRotateForCameraCompat_flagIsDisabled_returnsFalse() { + doReturn(false).when(mLetterboxConfiguration) + .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true); + + assertFalse(mController.shouldForceRotateForCameraCompat()); + } + + @Test + @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION}) + public void testShouldForceRotateForCameraCompat_overrideEnabled_returnsFalse() { + doReturn(true).when(mLetterboxConfiguration) + .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true); + + assertFalse(mController.shouldForceRotateForCameraCompat()); + } + + @Test + @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION}) + public void testShouldForceRotateForCameraCompat_propertyIsTrueAndOverride_returnsFalse() + throws Exception { + doReturn(true).when(mLetterboxConfiguration) + .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true); + mockThatProperty(PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION, /* value */ true); + + mController = new LetterboxUiController(mWm, mActivity); + + assertFalse(mController.shouldForceRotateForCameraCompat()); + } + + @Test + public void testShouldForceRotateForCameraCompat_propertyIsFalse_returnsFalse() + throws Exception { + doReturn(true).when(mLetterboxConfiguration) + .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true); + mockThatProperty(PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION, /* value */ false); + + mController = new LetterboxUiController(mWm, mActivity); + + assertFalse(mController.shouldForceRotateForCameraCompat()); + } + + @Test + public void testShouldForceRotateForCameraCompat_propertyIsTrue_returnsTrue() + throws Exception { + doReturn(true).when(mLetterboxConfiguration) + .isCameraCompatTreatmentEnabled(/* checkDeviceConfig */ true); + mockThatProperty(PROPERTY_CAMERA_COMPAT_ALLOW_FORCE_ROTATION, /* value */ true); + + mController = new LetterboxUiController(mWm, mActivity); + + assertTrue(mController.shouldForceRotateForCameraCompat()); + } + private void mockThatProperty(String propertyName, boolean value) throws Exception { Property property = new Property(propertyName, /* value */ value, /* packageName */ "", /* className */ ""); diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java index 656c48659383..a405e5a84360 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java @@ -23,7 +23,12 @@ import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_NONE; import static android.view.WindowManager.TRANSIT_OPEN; +import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_TASK_FRAGMENT; +import static android.window.TaskFragmentOperation.OP_TYPE_DELETE_TASK_FRAGMENT; +import static android.window.TaskFragmentOperation.OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT; +import static android.window.TaskFragmentOperation.OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS; import static android.window.TaskFragmentOperation.OP_TYPE_SET_ANIMATION_PARAMS; +import static android.window.TaskFragmentOperation.OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT; import static android.window.TaskFragmentOrganizer.KEY_ERROR_CALLBACK_OP_TYPE; import static android.window.TaskFragmentOrganizer.KEY_ERROR_CALLBACK_THROWABLE; import static android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_CHANGE; @@ -36,13 +41,6 @@ import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_ERROR; import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_INFO_CHANGED; import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED; import static android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_VANISHED; -import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT; -import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_DELETE_TASK_FRAGMENT; -import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT; -import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REPARENT_CHILDREN; -import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_ADJACENT_ROOTS; -import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS; -import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; @@ -339,11 +337,11 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { final Throwable exception = new IllegalArgumentException("Test exception"); mController.onTaskFragmentError(mTaskFragment.getTaskFragmentOrganizer(), - mErrorToken, null /* taskFragment */, HIERARCHY_OP_TYPE_SET_ADJACENT_ROOTS, + mErrorToken, null /* taskFragment */, OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS, exception); mController.dispatchPendingEvents(); - assertTaskFragmentErrorTransaction(HIERARCHY_OP_TYPE_SET_ADJACENT_ROOTS, + assertTaskFragmentErrorTransaction(OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS, exception.getClass()); } @@ -519,50 +517,20 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { @Test public void testApplyTransaction_enforceHierarchyChange_deleteTaskFragment() { doReturn(true).when(mTaskFragment).isAttached(); - - // Throw exception if the transaction is trying to change a window that is not organized by - // the organizer. - mTransaction.deleteTaskFragment(mFragmentWindowToken); - - assertApplyTransactionDisallowed(mTransaction); - - // Allow transaction to change a TaskFragment created by the organizer. - mTaskFragment.setTaskFragmentOrganizer(mOrganizerToken, 10 /* uid */, - "Test:TaskFragmentOrganizer" /* processName */); - clearInvocations(mAtm.mRootWindowContainer); - - assertApplyTransactionAllowed(mTransaction); - - // No lifecycle update when the TaskFragment is not recorded. - verify(mAtm.mRootWindowContainer, never()).resumeFocusedTasksTopActivities(); - mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment); - assertApplyTransactionAllowed(mTransaction); - - verify(mAtm.mRootWindowContainer).resumeFocusedTasksTopActivities(); - } - - @Test - public void testApplyTransaction_enforceHierarchyChange_setAdjacentRoots() { - final TaskFragment taskFragment2 = - new TaskFragment(mAtm, new Binder(), true /* createdByOrganizer */); - final WindowContainerToken token2 = taskFragment2.mRemoteToken.toWindowContainerToken(); // Throw exception if the transaction is trying to change a window that is not organized by // the organizer. - mTransaction.setAdjacentRoots(mFragmentWindowToken, token2); + mTransaction.deleteTaskFragment(mFragmentToken); assertApplyTransactionDisallowed(mTransaction); // Allow transaction to change a TaskFragment created by the organizer. mTaskFragment.setTaskFragmentOrganizer(mOrganizerToken, 10 /* uid */, "Test:TaskFragmentOrganizer" /* processName */); - taskFragment2.setTaskFragmentOrganizer(mOrganizerToken, 10 /* uid */, - "Test:TaskFragmentOrganizer" /* processName */); clearInvocations(mAtm.mRootWindowContainer); assertApplyTransactionAllowed(mTransaction); - verify(mAtm.mRootWindowContainer).resumeFocusedTasksTopActivities(); } @@ -693,7 +661,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { } @Test - public void testApplyTransaction_enforceTaskFragmentOrganized_setTaskFragmentOperation() { + public void testApplyTransaction_enforceTaskFragmentOrganized_addTaskFragmentOperation() { final Task task = createTask(mDisplayContent); mTaskFragment = new TaskFragmentBuilder(mAtm) .setParentTask(task) @@ -704,7 +672,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { OP_TYPE_SET_ANIMATION_PARAMS) .setAnimationParams(TaskFragmentAnimationParams.DEFAULT) .build(); - mTransaction.setTaskFragmentOperation(mFragmentToken, operation); + mTransaction.addTaskFragmentOperation(mFragmentToken, operation); mOrganizer.applyTransaction(mTransaction, TASK_FRAGMENT_TRANSIT_CHANGE, false /* shouldApplyIndependently */); @@ -718,7 +686,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { } @Test - public void testSetTaskFragmentOperation() { + public void testAddTaskFragmentOperation() { final Task task = createTask(mDisplayContent); mTaskFragment = new TaskFragmentBuilder(mAtm) .setParentTask(task) @@ -736,7 +704,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { OP_TYPE_SET_ANIMATION_PARAMS) .setAnimationParams(animationParams) .build(); - mTransaction.setTaskFragmentOperation(mFragmentToken, operation); + mTransaction.addTaskFragmentOperation(mFragmentToken, operation); mOrganizer.applyTransaction(mTransaction, TASK_FRAGMENT_TRANSIT_CHANGE, false /* shouldApplyIndependently */); assertApplyTransactionAllowed(mTransaction); @@ -845,26 +813,6 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { } @Test - public void testApplyTransaction_enforceHierarchyChange_reparentChildren() { - doReturn(true).when(mTaskFragment).isAttached(); - - // Throw exception if the transaction is trying to change a window that is not organized by - // the organizer. - mTransaction.reparentChildren(mFragmentWindowToken, null /* newParent */); - - assertApplyTransactionDisallowed(mTransaction); - - // Allow transaction to change a TaskFragment created by the organizer. - mTaskFragment.setTaskFragmentOrganizer(mOrganizerToken, 10 /* uid */, - "Test:TaskFragmentOrganizer" /* processName */); - clearInvocations(mAtm.mRootWindowContainer); - - assertApplyTransactionAllowed(mTransaction); - - verify(mAtm.mRootWindowContainer).resumeFocusedTasksTopActivities(); - } - - @Test public void testApplyTransaction_reparentActivityToTaskFragment_triggerLifecycleUpdate() { final Task task = createTask(mDisplayContent); final ActivityRecord activity = createActivityRecord(task); @@ -1040,7 +988,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { any(), any(), anyInt(), anyInt(), any()); verify(mWindowOrganizerController).sendTaskFragmentOperationFailure(eq(mIOrganizer), eq(mErrorToken), eq(mTaskFragment), - eq(HIERARCHY_OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT), + eq(OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT), any(IllegalArgumentException.class)); } @@ -1057,7 +1005,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { verify(mWindowOrganizerController).sendTaskFragmentOperationFailure(eq(mIOrganizer), eq(mErrorToken), eq(mTaskFragment), - eq(HIERARCHY_OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT), + eq(OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT), any(IllegalArgumentException.class)); assertNull(activity.getOrganizedTaskFragment()); } @@ -1074,8 +1022,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { assertApplyTransactionAllowed(mTransaction); verify(mWindowOrganizerController).sendTaskFragmentOperationFailure(eq(mIOrganizer), - eq(mErrorToken), eq(mTaskFragment), - eq(HIERARCHY_OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS), + eq(mErrorToken), eq(mTaskFragment), eq(OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS), any(IllegalArgumentException.class)); verify(mTaskFragment, never()).setAdjacentTaskFragment(any()); } @@ -1094,7 +1041,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { assertApplyTransactionAllowed(mTransaction); verify(mWindowOrganizerController).sendTaskFragmentOperationFailure(eq(mIOrganizer), - eq(mErrorToken), eq(null), eq(HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT), + eq(mErrorToken), eq(null), eq(OP_TYPE_CREATE_TASK_FRAGMENT), any(IllegalArgumentException.class)); assertNull(mWindowOrganizerController.getTaskFragment(fragmentToken)); } @@ -1105,12 +1052,12 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { spyOn(mWindowOrganizerController); // Not allow to delete a TaskFragment that is in a PIP Task. - mTransaction.deleteTaskFragment(mFragmentWindowToken) + mTransaction.deleteTaskFragment(mFragmentToken) .setErrorCallbackToken(mErrorToken); assertApplyTransactionAllowed(mTransaction); verify(mWindowOrganizerController).sendTaskFragmentOperationFailure(eq(mIOrganizer), - eq(mErrorToken), eq(mTaskFragment), eq(HIERARCHY_OP_TYPE_DELETE_TASK_FRAGMENT), + eq(mErrorToken), eq(mTaskFragment), eq(OP_TYPE_DELETE_TASK_FRAGMENT), any(IllegalArgumentException.class)); assertNotNull(mWindowOrganizerController.getTaskFragment(mFragmentToken)); @@ -1423,43 +1370,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { // The pending event will be dispatched on the handler (from requestTraversal). waitHandlerIdle(mWm.mAnimationHandler); - assertTaskFragmentErrorTransaction(HIERARCHY_OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT, - SecurityException.class); - } - - @Test - public void testMinDimensionViolation_ReparentChildren() { - final Task task = createTask(mDisplayContent); - final IBinder oldFragToken = new Binder(); - final TaskFragment oldTaskFrag = new TaskFragmentBuilder(mAtm) - .setParentTask(task) - .createActivityCount(1) - .setFragmentToken(oldFragToken) - .setOrganizer(mOrganizer) - .build(); - final ActivityRecord activity = oldTaskFrag.getTopMostActivity(); - // Make minWidth/minHeight exceeds mTaskFragment bounds. - activity.info.windowLayout = new ActivityInfo.WindowLayout( - 0, 0, 0, 0, 0, mTaskFragBounds.width() + 10, mTaskFragBounds.height() + 10); - mTaskFragment = new TaskFragmentBuilder(mAtm) - .setParentTask(task) - .setFragmentToken(mFragmentToken) - .setOrganizer(mOrganizer) - .setBounds(mTaskFragBounds) - .build(); - mWindowOrganizerController.mLaunchTaskFragments.put(oldFragToken, oldTaskFrag); - mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment); - - // Reparent oldTaskFrag's children to mTaskFragment, which is smaller than activity's - // minimum dimensions. - mTransaction.reparentChildren(oldTaskFrag.mRemoteToken.toWindowContainerToken(), - mTaskFragment.mRemoteToken.toWindowContainerToken()) - .setErrorCallbackToken(mErrorToken); - assertApplyTransactionAllowed(mTransaction); - // The pending event will be dispatched on the handler (from requestTraversal). - waitHandlerIdle(mWm.mAnimationHandler); - - assertTaskFragmentErrorTransaction(HIERARCHY_OP_TYPE_REPARENT_CHILDREN, + assertTaskFragmentErrorTransaction(OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT, SecurityException.class); } @@ -1634,7 +1545,8 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { } /** Asserts that there will be a transaction for TaskFragment error. */ - private void assertTaskFragmentErrorTransaction(int opType, @NonNull Class<?> exceptionClass) { + private void assertTaskFragmentErrorTransaction(@TaskFragmentOperation.OperationType int opType, + @NonNull Class<?> exceptionClass) { verify(mOrganizer).onTransactionReady(mTransactionCaptor.capture()); final TaskFragmentTransaction transaction = mTransactionCaptor.getValue(); final List<TaskFragmentTransaction.Change> changes = transaction.getChanges(); diff --git a/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java index a100b9aced21..ef2b691bac0c 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java @@ -393,7 +393,7 @@ public class WallpaperControllerTests extends WindowTestsBase { dc.updateOrientation(); dc.sendNewConfiguration(); spyOn(wallpaperWindow); - doReturn(new Rect(0, 0, width, height)).when(wallpaperWindow).getLastReportedBounds(); + doReturn(new Rect(0, 0, width, height)).when(wallpaperWindow).getParentFrame(); } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java index 5e0e2092f84f..6bce9594571f 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java @@ -45,6 +45,7 @@ import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL; import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD; +import static android.view.WindowManager.LayoutParams.TYPE_SECURE_SYSTEM_OVERLAY; import static android.view.WindowManager.LayoutParams.TYPE_TOAST; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; @@ -980,6 +981,19 @@ public class WindowStateTests extends WindowTestsBase { assertFalse(sameTokenWindow.needsRelativeLayeringToIme()); } + @UseTestDisplay(addWindows = {W_ACTIVITY, W_INPUT_METHOD}) + @Test + public void testNeedsRelativeLayeringToIme_systemDialog() { + WindowState systemDialogWindow = createWindow(null, TYPE_SECURE_SYSTEM_OVERLAY, + mDisplayContent, + "SystemDialog", true); + mDisplayContent.setImeLayeringTarget(mAppWindow); + mAppWindow.getRootTask().setWindowingMode(WINDOWING_MODE_MULTI_WINDOW); + makeWindowVisible(mImeWindow); + systemDialogWindow.mAttrs.flags |= FLAG_ALT_FOCUSABLE_IM; + assertTrue(systemDialogWindow.needsRelativeLayeringToIme()); + } + @Test public void testSetFreezeInsetsState() { final WindowState app = createWindow(null, TYPE_APPLICATION, "app"); diff --git a/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java b/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java index a95fa5a4e978..8c581580cf75 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java @@ -22,6 +22,7 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; +import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG; @@ -31,6 +32,7 @@ import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_PANEL; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL; import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL; +import static android.view.WindowManager.LayoutParams.TYPE_SECURE_SYSTEM_OVERLAY; import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR_ADDITIONAL; import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL; import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY; @@ -543,4 +545,28 @@ public class ZOrderingTests extends WindowTestsBase { assertZOrderGreaterThan(mTransaction, popupWindow.getSurfaceControl(), mDisplayContent.getImeContainer().getSurfaceControl()); } + + @Test + public void testSystemDialogWindow_expectHigherThanIme_inMultiWindow() { + // Simulate the app window is in multi windowing mode and being IME target + mAppWindow.getConfiguration().windowConfiguration.setWindowingMode( + WINDOWING_MODE_MULTI_WINDOW); + mDisplayContent.setImeLayeringTarget(mAppWindow); + mDisplayContent.setImeInputTarget(mAppWindow); + makeWindowVisible(mImeWindow); + + // Create a popupWindow + final WindowState systemDialogWindow = createWindow(null, TYPE_SECURE_SYSTEM_OVERLAY, + mDisplayContent, "SystemDialog", true); + systemDialogWindow.mAttrs.flags |= FLAG_ALT_FOCUSABLE_IM; + spyOn(systemDialogWindow); + + mDisplayContent.assignChildLayers(mTransaction); + + // Verify the surface layer of the popupWindow should higher than IME + verify(systemDialogWindow).needsRelativeLayeringToIme(); + assertThat(systemDialogWindow.needsRelativeLayeringToIme()).isTrue(); + assertZOrderGreaterThan(mTransaction, systemDialogWindow.getSurfaceControl(), + mDisplayContent.getImeContainer().getSurfaceControl()); + } } diff --git a/telephony/java/android/service/euicc/EuiccService.java b/telephony/java/android/service/euicc/EuiccService.java index e19117bc805f..2c0087eaabf4 100644 --- a/telephony/java/android/service/euicc/EuiccService.java +++ b/telephony/java/android/service/euicc/EuiccService.java @@ -490,6 +490,28 @@ public abstract class EuiccService extends Service { int slotId, DownloadableSubscription subscription, boolean forceDeactivateSim); /** + * Populate {@link DownloadableSubscription} metadata for the given downloadable subscription. + * + * @param slotId ID of the SIM slot to use for the operation. + * @param portIndex Index of the port from the slot. portIndex is used if the eUICC must + * be activated to perform the operation. + * @param subscription A subscription whose metadata needs to be populated. + * @param forceDeactivateSim If true, and if an active SIM must be deactivated to access the + * eUICC, perform this action automatically. Otherwise, {@link #RESULT_MUST_DEACTIVATE_SIM} + * should be returned to allow the user to consent to this operation first. + * @return The result of the operation. + * @see android.telephony.euicc.EuiccManager#getDownloadableSubscriptionMetadata + */ + @NonNull + public GetDownloadableSubscriptionMetadataResult onGetDownloadableSubscriptionMetadata( + int slotId, int portIndex, @NonNull DownloadableSubscription subscription, + boolean forceDeactivateSim) { + // stub implementation, LPA needs to implement this + throw new UnsupportedOperationException( + "LPA must override onGetDownloadableSubscriptionMetadata"); + } + + /** * Return metadata for subscriptions which are available for download for this device. * * @param slotId ID of the SIM slot to use for the operation. @@ -833,16 +855,31 @@ public abstract class EuiccService extends Service { } @Override - public void getDownloadableSubscriptionMetadata(int slotId, + public void getDownloadableSubscriptionMetadata(int slotId, int portIndex, DownloadableSubscription subscription, - boolean forceDeactivateSim, + boolean switchAfterDownload, boolean forceDeactivateSim, IGetDownloadableSubscriptionMetadataCallback callback) { mExecutor.execute(new Runnable() { @Override public void run() { - GetDownloadableSubscriptionMetadataResult result = - EuiccService.this.onGetDownloadableSubscriptionMetadata( + GetDownloadableSubscriptionMetadataResult result; + if (switchAfterDownload) { + try { + result = EuiccService.this.onGetDownloadableSubscriptionMetadata( + slotId, portIndex, subscription, forceDeactivateSim); + } catch (UnsupportedOperationException | AbstractMethodError e) { + Log.w(TAG, "The new onGetDownloadableSubscriptionMetadata(int, int, " + + "DownloadableSubscription, boolean) is not implemented." + + " Fall back to the old one.", e); + result = EuiccService.this.onGetDownloadableSubscriptionMetadata( slotId, subscription, forceDeactivateSim); + } + } else { + // When switchAfterDownload is false, this operation is port agnostic. + // Call API without portIndex. + result = EuiccService.this.onGetDownloadableSubscriptionMetadata( + slotId, subscription, forceDeactivateSim); + } try { callback.onComplete(result); } catch (RemoteException e) { diff --git a/telephony/java/android/service/euicc/IEuiccService.aidl b/telephony/java/android/service/euicc/IEuiccService.aidl index 6b0397d67015..f8d5ae9ca86d 100644 --- a/telephony/java/android/service/euicc/IEuiccService.aidl +++ b/telephony/java/android/service/euicc/IEuiccService.aidl @@ -38,8 +38,10 @@ oneway interface IEuiccService { void downloadSubscription(int slotId, int portIndex, in DownloadableSubscription subscription, boolean switchAfterDownload, boolean forceDeactivateSim, in Bundle resolvedBundle, in IDownloadSubscriptionCallback callback); - void getDownloadableSubscriptionMetadata(int slotId, in DownloadableSubscription subscription, - boolean forceDeactivateSim, in IGetDownloadableSubscriptionMetadataCallback callback); + void getDownloadableSubscriptionMetadata( + int slotId, int portIndex, in DownloadableSubscription subscription, + boolean switchAfterDownload, boolean forceDeactivateSim, + in IGetDownloadableSubscriptionMetadataCallback callback); void getEid(int slotId, in IGetEidCallback callback); void getOtaStatus(int slotId, in IGetOtaStatusCallback callback); void startOtaIfNecessary(int slotId, in IOtaStatusChangedCallback statusChangedCallback); diff --git a/tests/Input/src/com/android/test/input/InputDeviceTest.java b/tests/Input/src/com/android/test/input/InputDeviceTest.java index 797e818285f9..ddcc8112d6ed 100644 --- a/tests/Input/src/com/android/test/input/InputDeviceTest.java +++ b/tests/Input/src/com/android/test/input/InputDeviceTest.java @@ -69,7 +69,7 @@ public class InputDeviceTest { } private void assertInputDeviceParcelUnparcel(KeyCharacterMap keyCharacterMap) { - final InputDevice device = new InputDevice.Builder() + final InputDevice.Builder deviceBuilder = new InputDevice.Builder() .setId(DEVICE_ID) .setGeneration(42) .setControllerNumber(43) @@ -88,8 +88,20 @@ public class InputDeviceTest { .setHasBattery(true) .setKeyboardLanguageTag("en-US") .setKeyboardLayoutType("qwerty") - .setSupportsUsi(true) - .build(); + .setSupportsUsi(true); + + for (int i = 0; i < 30; i++) { + deviceBuilder.addMotionRange( + MotionEvent.AXIS_GENERIC_1, + InputDevice.SOURCE_UNKNOWN, + i, + i + 1, + i + 2, + i + 3, + i + 4); + } + + final InputDevice device = deviceBuilder.build(); Parcel parcel = Parcel.obtain(); device.writeToParcel(parcel, 0); diff --git a/tests/VectorDrawableTest/Android.bp b/tests/VectorDrawableTest/Android.bp index 9da7c5fdbb17..099d874375a1 100644 --- a/tests/VectorDrawableTest/Android.bp +++ b/tests/VectorDrawableTest/Android.bp @@ -26,5 +26,7 @@ package { android_test { name: "VectorDrawableTest", srcs: ["**/*.java"], + // certificate set as platform to allow testing of @hidden APIs + certificate: "platform", platform_apis: true, } diff --git a/tests/VectorDrawableTest/AndroidManifest.xml b/tests/VectorDrawableTest/AndroidManifest.xml index 5334dac57ca2..163e438e0677 100644 --- a/tests/VectorDrawableTest/AndroidManifest.xml +++ b/tests/VectorDrawableTest/AndroidManifest.xml @@ -158,6 +158,15 @@ <category android:name="com.android.test.dynamic.TEST"/> </intent-filter> </activity> + <activity android:name="LottieDrawableTest" + android:label="Lottie test bed" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + + <category android:name="com.android.test.dynamic.TEST" /> + </intent-filter> + </activity> </application> </manifest> diff --git a/tests/VectorDrawableTest/res/raw/lottie.json b/tests/VectorDrawableTest/res/raw/lottie.json new file mode 100644 index 000000000000..fea571c6bedb --- /dev/null +++ b/tests/VectorDrawableTest/res/raw/lottie.json @@ -0,0 +1,123 @@ +{ + "v":"4.6.9", + "fr":60, + "ip":0, + "op":200, + "w":800, + "h":600, + "nm":"Loader 1 JSON", + "ddd":0, + + + "layers":[ + { + "ddd":0, + "ind":1, + "ty":4, + "nm":"Custom Path 1", + "ao": 0, + "ip": 0, + "op": 300, + "st": 0, + "sr": 1, + "bm": 0, + "ks": { + "o": { "a":0, "k":100 }, + "r": { "a":1, "k": [ + { "s": [ 0 ], "e": [ 360], "i": { "x":0.5, "y":0.5 }, "o": { "x":0.5, "y":0.5 }, "t": 0 }, + { "t": 200 } + ] }, + "p": { "a":0, "k":[ 300, 300, 0 ] }, + "a": { "a":0, "k":[ 100, 100, 0 ] }, + "s": { "a":1, "k":[ + { "s": [ 100, 100 ], "e": [ 200, 200 ], "i": { "x":0.5, "y":0.5 }, "o": { "x":0.5, "y":0.5 }, "t": 0 }, + { "s": [ 200, 200 ], "e": [ 100, 100 ], "i": { "x":0.5, "y":0.5 }, "o": { "x":0.5, "y":0.5 }, "t": 100 }, + { "t": 200 } + ] } + }, + + "shapes":[ + { + "ty":"gr", + "it":[ + { + "ty" : "sh", + "nm" : "Path 1", + "ks" : { + "a" : 1, + "k" : [ + { + "s": [ { + "i": [ [ 0, 50 ], [ -50, 0 ], [ 0, -50 ], [ 50, 0 ] ], + "o": [ [ 0, -50 ], [ 50, 0 ], [ 0, 50 ], [ -50, 0 ] ], + "v": [ [ 0, 100 ], [ 100, 0 ], [ 200, 100 ], [ 100, 200 ] ], + "c": true + } ], + "e": [ { + "i": [ [ 50, 50 ], [ -50, 0 ], [ -50, -50 ], [ 50, 50 ] ], + "o": [ [ 50, -50 ], [ 50, 0 ], [ -50, 50 ], [ -50, 50 ] ], + "v": [ [ 0, 100 ], [ 100, 0 ], [ 200, 100 ], [ 100, 200 ] ], + "c": true + } ], + "i": { "x":0.5, "y":0.5 }, + "o": { "x":0.5, "y":0.5 }, + "t": 0 + }, + { + "s": [ { + "i": [ [ 50, 50 ], [ -50, 0 ], [ -50, -50 ], [ 50, 50 ] ], + "o": [ [ 50, -50 ], [ 50, 0 ], [ -50, 50 ], [ -50, 50 ] ], + "v": [ [ 0, 100 ], [ 100, 0 ], [ 200, 100 ], [ 100, 200 ] ], + "c": true + } ], + "e": [ { + "i": [ [ 0, 50 ], [ -50, 0 ], [ 0, -50 ], [ 50, 0 ] ], + "o": [ [ 0, -50 ], [ 50, 0 ], [ 0, 50 ], [ -50, 0 ] ], + "v": [ [ 0, 100 ], [ 100, 0 ], [ 200, 100 ], [ 100, 200 ] ], + "c": true + } ], + "i": { "x":0.5, "y":0.5 }, + "o": { "x":0.5, "y":0.5 }, + "t": 100 + }, + { + "t": 200 + } + ] + } + }, + + { + "ty": "st", + "nm": "Stroke 1", + "lc": 1, + "lj": 1, + "ml": 4, + "w" : { "a": 1, "k": [ + { "s": [ 30 ], "e": [ 50 ], "i": { "x":0.5, "y":0.5 }, "o": { "x":0.5, "y":0.5 }, "t": 0 }, + { "s": [ 50 ], "e": [ 30 ], "i": { "x":0.5, "y":0.5 }, "o": { "x":0.5, "y":0.5 }, "t": 100 }, + { "t": 200 } + ] }, + "o" : { "a": 0, "k": 100 }, + "c" : { "a": 1, "k": [ + { "s": [ 0, 1, 0 ], "e": [ 1, 0, 0 ], "i": { "x":0.5, "y":0.5 }, "o": { "x":0.5, "y":0.5 }, "t": 0 }, + { "s": [ 1, 0, 0 ], "e": [ 0, 1, 0 ], "i": { "x":0.5, "y":0.5 }, "o": { "x":0.5, "y":0.5 }, "t": 100 }, + { "t": 200 } + ] } + }, + + { + "ty":"tr", + "p" : { "a":0, "k":[ 0, 0 ] }, + "a" : { "a":0, "k":[ 0, 0 ] }, + "s" : { "a":0, "k":[ 100, 100 ] }, + "r" : { "a":0, "k": 0 }, + "o" : { "a":0, "k":100 }, + "nm": "Transform" + } + ] + } + ] + } + ] + } diff --git a/tests/VectorDrawableTest/src/com/android/test/dynamic/LottieDrawableTest.java b/tests/VectorDrawableTest/src/com/android/test/dynamic/LottieDrawableTest.java new file mode 100644 index 000000000000..05eae7b0e642 --- /dev/null +++ b/tests/VectorDrawableTest/src/com/android/test/dynamic/LottieDrawableTest.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.test.dynamic; + +import android.app.Activity; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.LottieDrawable; +import android.os.Bundle; +import android.view.View; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Scanner; + +@SuppressWarnings({"UnusedDeclaration"}) +public class LottieDrawableTest extends Activity { + private static final String TAG = "LottieDrawableTest"; + static final int BACKGROUND = 0xFFF44336; + + class LottieDrawableView extends View { + private Rect mLottieBounds; + + private LottieDrawable mLottie; + + LottieDrawableView(Context context, InputStream is) { + super(context); + Scanner s = new Scanner(is).useDelimiter("\\A"); + String json = s.hasNext() ? s.next() : ""; + try { + mLottie = LottieDrawable.makeLottieDrawable(json); + } catch (IOException e) { + throw new RuntimeException(TAG + ": error parsing test Lottie"); + } + mLottie.start(); + } + + @Override + protected void onDraw(Canvas canvas) { + canvas.drawColor(BACKGROUND); + + mLottie.setBounds(mLottieBounds); + mLottie.draw(canvas); + } + + public void setLottieSize(Rect bounds) { + mLottieBounds = bounds; + } + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + InputStream is = getResources().openRawResource(R.raw.lottie); + + LottieDrawableView view = new LottieDrawableView(this, is); + view.setLottieSize(new Rect(0, 0, 900, 900)); + setContentView(view); + } +} |