diff options
361 files changed, 9026 insertions, 2319 deletions
diff --git a/apct-tests/perftests/multiuser/src/android/multiuser/BroadcastWaiter.java b/apct-tests/perftests/multiuser/src/android/multiuser/BroadcastWaiter.java index dcabca476925..727c682b5202 100644 --- a/apct-tests/perftests/multiuser/src/android/multiuser/BroadcastWaiter.java +++ b/apct-tests/perftests/multiuser/src/android/multiuser/BroadcastWaiter.java @@ -45,7 +45,6 @@ public class BroadcastWaiter implements Closeable { private final int mTimeoutInSecond; private final Set<String> mActions; - private final Set<String> mActionReceivedForUser = new HashSet<>(); private final List<BroadcastReceiver> mBroadcastReceivers = new ArrayList<>(); private final Map<String, Semaphore> mSemaphoresMap = new ConcurrentHashMap<>(); @@ -80,7 +79,6 @@ public class BroadcastWaiter implements Closeable { final String data = intent.getDataString(); Log.d(mTag, "Received " + action + " for user " + userId + (!TextUtils.isEmpty(data) ? " with " + data : "")); - mActionReceivedForUser.add(action + userId); getSemaphore(action, userId).release(); } } @@ -95,10 +93,6 @@ public class BroadcastWaiter implements Closeable { mBroadcastReceivers.forEach(mContext::unregisterReceiver); } - public boolean hasActionBeenReceivedForUser(String action, int userId) { - return mActionReceivedForUser.contains(action + userId); - } - private boolean waitActionForUser(String action, int userId) { Log.d(mTag, "#waitActionForUser(action: " + action + ", userId: " + userId + ")"); @@ -129,7 +123,6 @@ public class BroadcastWaiter implements Closeable { public String runThenWaitForBroadcasts(int userId, FunctionalUtils.ThrowingRunnable runnable, String... actions) { for (String action : actions) { - mActionReceivedForUser.remove(action + userId); getSemaphore(action, userId).drainPermits(); } runnable.run(); @@ -140,9 +133,4 @@ public class BroadcastWaiter implements Closeable { } return null; } - - public boolean waitActionForUserIfNotReceivedYet(String action, int userId) { - return hasActionBeenReceivedForUser(action, userId) - || waitActionForUser(action, userId); - } } diff --git a/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java b/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java index b2bd8d7f5d5d..3f9b54cb8578 100644 --- a/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java +++ b/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java @@ -167,7 +167,7 @@ public class UserLifecycleTests { /** Tests creating a new user. */ @Test(timeout = TIMEOUT_MAX_TEST_TIME_MS) - public void createUser() { + public void createUser() throws RemoteException { while (mRunner.keepRunning()) { Log.i(TAG, "Starting timer"); final int userId = createUserNoFlags(); @@ -229,7 +229,7 @@ public class UserLifecycleTests { * Measures the time until unlock listener is triggered and user is unlocked. */ @Test(timeout = TIMEOUT_MAX_TEST_TIME_MS) - public void startAndUnlockUser() { + public void startAndUnlockUser() throws RemoteException { while (mRunner.keepRunning()) { mRunner.pauseTiming(); final int userId = createUserNoFlags(); @@ -451,7 +451,7 @@ public class UserLifecycleTests { /** Tests creating a new profile. */ @Test(timeout = TIMEOUT_MAX_TEST_TIME_MS) - public void managedProfileCreate() { + public void managedProfileCreate() throws RemoteException { assumeTrue(mHasManagedUserFeature); while (mRunner.keepRunning()) { @@ -468,7 +468,7 @@ public class UserLifecycleTests { /** Tests starting (unlocking) an uninitialized profile. */ @Test(timeout = TIMEOUT_MAX_TEST_TIME_MS) - public void managedProfileUnlock() { + public void managedProfileUnlock() throws RemoteException { assumeTrue(mHasManagedUserFeature); while (mRunner.keepRunning()) { @@ -639,7 +639,7 @@ public class UserLifecycleTests { // TODO: This is just a POC. Do this properly and add more. /** Tests starting (unlocking) a newly-created profile using the user-type-pkg-whitelist. */ @Test(timeout = TIMEOUT_MAX_TEST_TIME_MS) - public void managedProfileUnlock_usingWhitelist() { + public void managedProfileUnlock_usingWhitelist() throws RemoteException { assumeTrue(mHasManagedUserFeature); final int origMode = getUserTypePackageWhitelistMode(); setUserTypePackageWhitelistMode(USER_TYPE_PACKAGE_WHITELIST_MODE_ENFORCE @@ -665,7 +665,7 @@ public class UserLifecycleTests { } /** Tests starting (unlocking) a newly-created profile NOT using the user-type-pkg-whitelist. */ @Test(timeout = TIMEOUT_MAX_TEST_TIME_MS) - public void managedProfileUnlock_notUsingWhitelist() { + public void managedProfileUnlock_notUsingWhitelist() throws RemoteException { assumeTrue(mHasManagedUserFeature); final int origMode = getUserTypePackageWhitelistMode(); setUserTypePackageWhitelistMode(USER_TYPE_PACKAGE_WHITELIST_MODE_DISABLE); @@ -908,10 +908,8 @@ public class UserLifecycleTests { result != null && result.contains("Failed")); } - private void removeUser(int userId) { - if (mBroadcastWaiter.hasActionBeenReceivedForUser(Intent.ACTION_USER_STARTED, userId)) { - mBroadcastWaiter.waitActionForUserIfNotReceivedYet(Intent.ACTION_MEDIA_MOUNTED, userId); - } + private void removeUser(int userId) throws RemoteException { + stopUserAfterWaitingForBroadcastIdle(userId, true); try { mUm.removeUser(userId); final long startTime = System.currentTimeMillis(); diff --git a/apct-tests/perftests/windowmanager/src/android/wm/RelayoutPerfTest.java b/apct-tests/perftests/windowmanager/src/android/wm/RelayoutPerfTest.java index fb62920681de..b0da7d1e2870 100644 --- a/apct-tests/perftests/windowmanager/src/android/wm/RelayoutPerfTest.java +++ b/apct-tests/perftests/windowmanager/src/android/wm/RelayoutPerfTest.java @@ -127,7 +127,7 @@ public class RelayoutPerfTest extends WindowManagerPerfTestBase final ClientWindowFrames mOutFrames = new ClientWindowFrames(); final MergedConfiguration mOutMergedConfiguration = new MergedConfiguration(); final InsetsState mOutInsetsState = new InsetsState(); - final InsetsSourceControl[] mOutControls = new InsetsSourceControl[0]; + final InsetsSourceControl.Array mOutControls = new InsetsSourceControl.Array(); final IWindow mWindow; final View mView; final WindowManager.LayoutParams mParams; diff --git a/apct-tests/perftests/windowmanager/src/android/wm/WindowAddRemovePerfTest.java b/apct-tests/perftests/windowmanager/src/android/wm/WindowAddRemovePerfTest.java index cc74a5294f9d..b87e42e31da3 100644 --- a/apct-tests/perftests/windowmanager/src/android/wm/WindowAddRemovePerfTest.java +++ b/apct-tests/perftests/windowmanager/src/android/wm/WindowAddRemovePerfTest.java @@ -86,7 +86,7 @@ public class WindowAddRemovePerfTest extends WindowManagerPerfTestBase final WindowManager.LayoutParams mLayoutParams = new WindowManager.LayoutParams(); final int mRequestedVisibleTypes = WindowInsets.Type.defaultVisible(); final InsetsState mOutInsetsState = new InsetsState(); - final InsetsSourceControl[] mOutControls = new InsetsSourceControl[0]; + final InsetsSourceControl.Array mOutControls = new InsetsSourceControl.Array(); final Rect mOutAttachedFrame = new Rect(); final float[] mOutSizeCompatScale = { 1f }; diff --git a/apex/jobscheduler/framework/java/android/os/PowerExemptionManager.java b/apex/jobscheduler/framework/java/android/os/PowerExemptionManager.java index 78214dc27a9f..711caf7feb62 100644 --- a/apex/jobscheduler/framework/java/android/os/PowerExemptionManager.java +++ b/apex/jobscheduler/framework/java/android/os/PowerExemptionManager.java @@ -391,6 +391,12 @@ public class PowerExemptionManager { */ public static final int REASON_MEDIA_NOTIFICATION_TRANSFER = 325; + /** + * Package installer. + * @hide + */ + public static final int REASON_PACKAGE_INSTALLER = 326; + /** @hide The app requests out-out. */ public static final int REASON_OPT_OUT_REQUESTED = 1000; @@ -472,6 +478,7 @@ public class PowerExemptionManager { REASON_DISALLOW_APPS_CONTROL, REASON_ACTIVE_DEVICE_ADMIN, REASON_MEDIA_NOTIFICATION_TRANSFER, + REASON_PACKAGE_INSTALLER, }) @Retention(RetentionPolicy.SOURCE) public @interface ReasonCode {} @@ -839,6 +846,8 @@ public class PowerExemptionManager { return "REASON_OPT_OUT_REQUESTED"; case REASON_MEDIA_NOTIFICATION_TRANSFER: return "REASON_MEDIA_NOTIFICATION_TRANSFER"; + case REASON_PACKAGE_INSTALLER: + return "REASON_PACKAGE_INSTALLER"; default: return "(unknown:" + reasonCode + ")"; } diff --git a/core/api/current.txt b/core/api/current.txt index 326a8e781a60..5dd1b3938f40 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -122,6 +122,10 @@ package android { field public static final String LOADER_USAGE_STATS = "android.permission.LOADER_USAGE_STATS"; field public static final String LOCATION_HARDWARE = "android.permission.LOCATION_HARDWARE"; field public static final String MANAGE_DEVICE_LOCK_STATE = "android.permission.MANAGE_DEVICE_LOCK_STATE"; + field public static final String MANAGE_DEVICE_POLICY_ACROSS_USERS = "android.permission.MANAGE_DEVICE_POLICY_ACROSS_USERS"; + field public static final String MANAGE_DEVICE_POLICY_ACROSS_USERS_FULL = "android.permission.MANAGE_DEVICE_POLICY_ACROSS_USERS_FULL"; + field public static final String MANAGE_DEVICE_POLICY_ACROSS_USERS_SECURITY_CRITICAL = "android.permission.MANAGE_DEVICE_POLICY_ACROSS_USERS_SECURITY_CRITICAL"; + field public static final String MANAGE_DEVICE_POLICY_TIME = "android.permission.MANAGE_DEVICE_POLICY_TIME"; field public static final String MANAGE_DOCUMENTS = "android.permission.MANAGE_DOCUMENTS"; field public static final String MANAGE_EXTERNAL_STORAGE = "android.permission.MANAGE_EXTERNAL_STORAGE"; field public static final String MANAGE_MEDIA = "android.permission.MANAGE_MEDIA"; @@ -7499,9 +7503,9 @@ package android.app.admin { method @Nullable public String getAlwaysOnVpnPackage(@NonNull android.content.ComponentName); method @NonNull @WorkerThread public android.os.Bundle getApplicationRestrictions(@Nullable android.content.ComponentName, String); method @Deprecated @Nullable public String getApplicationRestrictionsManagingPackage(@NonNull android.content.ComponentName); - method public boolean getAutoTimeEnabled(@NonNull android.content.ComponentName); + method @RequiresPermission(anyOf={android.Manifest.permission.SET_TIME, "android.permission.QUERY_ADMIN_POLICY"}, conditional=true) public boolean getAutoTimeEnabled(@NonNull android.content.ComponentName); method @Deprecated public boolean getAutoTimeRequired(); - method public boolean getAutoTimeZoneEnabled(@NonNull android.content.ComponentName); + method @RequiresPermission(anyOf={android.Manifest.permission.SET_TIME_ZONE, "android.permission.QUERY_ADMIN_POLICY"}, conditional=true) public boolean getAutoTimeZoneEnabled(@NonNull android.content.ComponentName); method @NonNull public java.util.List<android.os.UserHandle> getBindDeviceAdminTargetUsers(@NonNull android.content.ComponentName); method public boolean getBluetoothContactSharingDisabled(@NonNull android.content.ComponentName); method public boolean getCameraDisabled(@Nullable android.content.ComponentName); @@ -7648,9 +7652,9 @@ package android.app.admin { method public boolean setApplicationHidden(@NonNull android.content.ComponentName, String, boolean); method @WorkerThread public void setApplicationRestrictions(@Nullable android.content.ComponentName, String, android.os.Bundle); method @Deprecated public void setApplicationRestrictionsManagingPackage(@NonNull android.content.ComponentName, @Nullable String) throws android.content.pm.PackageManager.NameNotFoundException; - method public void setAutoTimeEnabled(@NonNull android.content.ComponentName, boolean); + method @RequiresPermission(value=android.Manifest.permission.SET_TIME, conditional=true) public void setAutoTimeEnabled(@NonNull android.content.ComponentName, boolean); method @Deprecated public void setAutoTimeRequired(@NonNull android.content.ComponentName, boolean); - method public void setAutoTimeZoneEnabled(@NonNull android.content.ComponentName, boolean); + method @RequiresPermission(value=android.Manifest.permission.SET_TIME_ZONE, conditional=true) public void setAutoTimeZoneEnabled(@NonNull android.content.ComponentName, boolean); method public void setBackupServiceEnabled(@NonNull android.content.ComponentName, boolean); method public void setBluetoothContactSharingDisabled(@NonNull android.content.ComponentName, boolean); method public void setCameraDisabled(@NonNull android.content.ComponentName, boolean); @@ -7729,8 +7733,8 @@ package android.app.admin { method @Deprecated public int setStorageEncryption(@NonNull android.content.ComponentName, boolean); method public void setSystemSetting(@NonNull android.content.ComponentName, @NonNull String, String); method public void setSystemUpdatePolicy(@NonNull android.content.ComponentName, android.app.admin.SystemUpdatePolicy); - method public boolean setTime(@NonNull android.content.ComponentName, long); - method public boolean setTimeZone(@NonNull android.content.ComponentName, String); + method @RequiresPermission(value=android.Manifest.permission.SET_TIME, conditional=true) public boolean setTime(@NonNull android.content.ComponentName, long); + method @RequiresPermission(value=android.Manifest.permission.SET_TIME_ZONE, conditional=true) public boolean setTimeZone(@NonNull android.content.ComponentName, String); method public void setTrustAgentConfiguration(@NonNull android.content.ComponentName, @NonNull android.content.ComponentName, android.os.PersistableBundle); method public void setUninstallBlocked(@Nullable android.content.ComponentName, String, boolean); method public void setUsbDataSignalingEnabled(boolean); @@ -7803,7 +7807,7 @@ package android.app.admin { field @Deprecated public static final String EXTRA_PROVISIONING_EMAIL_ADDRESS = "android.app.extra.PROVISIONING_EMAIL_ADDRESS"; field public static final String EXTRA_PROVISIONING_IMEI = "android.app.extra.PROVISIONING_IMEI"; field public static final String EXTRA_PROVISIONING_KEEP_ACCOUNT_ON_MIGRATION = "android.app.extra.PROVISIONING_KEEP_ACCOUNT_ON_MIGRATION"; - field public static final String EXTRA_PROVISIONING_KEEP_SCREEN_ON = "android.app.extra.PROVISIONING_KEEP_SCREEN_ON"; + field @Deprecated public static final String EXTRA_PROVISIONING_KEEP_SCREEN_ON = "android.app.extra.PROVISIONING_KEEP_SCREEN_ON"; field public static final String EXTRA_PROVISIONING_LEAVE_ALL_SYSTEM_APPS_ENABLED = "android.app.extra.PROVISIONING_LEAVE_ALL_SYSTEM_APPS_ENABLED"; field public static final String EXTRA_PROVISIONING_LOCALE = "android.app.extra.PROVISIONING_LOCALE"; field public static final String EXTRA_PROVISIONING_LOCAL_TIME = "android.app.extra.PROVISIONING_LOCAL_TIME"; @@ -8004,6 +8008,26 @@ package android.app.admin { field public static final int PACKAGE_POLICY_BLOCKLIST = 1; // 0x1 } + public final class PolicyUpdateReason { + ctor public PolicyUpdateReason(int); + method public int getReasonCode(); + field public static final int REASON_CONFLICTING_ADMIN_POLICY = 0; // 0x0 + field public static final int REASON_UNKNOWN = -1; // 0xffffffff + } + + public abstract class PolicyUpdatesReceiver extends android.content.BroadcastReceiver { + ctor public PolicyUpdatesReceiver(); + method public void onPolicyChanged(@NonNull android.content.Context, @NonNull String, @NonNull android.os.Bundle, @NonNull android.app.admin.TargetUser, @NonNull android.app.admin.PolicyUpdateReason); + method public void onPolicySetResult(@NonNull android.content.Context, @NonNull String, @NonNull android.os.Bundle, @NonNull android.app.admin.TargetUser, int, @Nullable android.app.admin.PolicyUpdateReason); + method public final void onReceive(android.content.Context, android.content.Intent); + field public static final String ACTION_DEVICE_POLICY_CHANGED = "android.app.admin.action.DEVICE_POLICY_CHANGED"; + field public static final String ACTION_DEVICE_POLICY_SET_RESULT = "android.app.admin.action.DEVICE_POLICY_SET_RESULT"; + field public static final String EXTRA_PACKAGE_NAME = "android.app.admin.extra.PACKAGE_NAME"; + field public static final String EXTRA_PERMISSION_NAME = "android.app.admin.extra.PERMISSION_NAME"; + field public static final int POLICY_SET_RESULT_FAILURE = -1; // 0xffffffff + field public static final int POLICY_SET_RESULT_SUCCESS = 0; // 0x0 + } + public final class PreferentialNetworkServiceConfig implements android.os.Parcelable { method public int describeContents(); method @NonNull public int[] getExcludedUids(); @@ -8132,6 +8156,12 @@ package android.app.admin { field public static final int ERROR_UNKNOWN = 1; // 0x1 } + public final class TargetUser { + field @NonNull public static final android.app.admin.TargetUser GLOBAL; + field @NonNull public static final android.app.admin.TargetUser LOCAL_USER; + field @NonNull public static final android.app.admin.TargetUser PARENT_USER; + } + public final class UnsafeStateException extends java.lang.IllegalStateException implements android.os.Parcelable { method public int describeContents(); method @NonNull public java.util.List<java.lang.Integer> getReasons(); diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt index 2546294f9f5e..447b1136caa0 100644 --- a/core/api/module-lib-current.txt +++ b/core/api/module-lib-current.txt @@ -82,6 +82,7 @@ package android.content { } public abstract class Context { + method @NonNull public android.os.IBinder getIApplicationThreadBinder(); method @NonNull public android.os.UserHandle getUser(); field public static final String PAC_PROXY_SERVICE = "pac_proxy"; field public static final String TEST_NETWORK_SERVICE = "test_network"; diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 712ac94216d3..e94f5ac1fa40 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -204,6 +204,7 @@ package android { field public static final String MANAGE_WIFI_COUNTRY_CODE = "android.permission.MANAGE_WIFI_COUNTRY_CODE"; field public static final String MARK_DEVICE_ORGANIZATION_OWNED = "android.permission.MARK_DEVICE_ORGANIZATION_OWNED"; field public static final String MEDIA_RESOURCE_OVERRIDE_PID = "android.permission.MEDIA_RESOURCE_OVERRIDE_PID"; + field public static final String MIGRATE_HEALTH_CONNECT_DATA = "android.permission.MIGRATE_HEALTH_CONNECT_DATA"; field public static final String MODIFY_APPWIDGET_BIND_PERMISSIONS = "android.permission.MODIFY_APPWIDGET_BIND_PERMISSIONS"; field public static final String MODIFY_AUDIO_ROUTING = "android.permission.MODIFY_AUDIO_ROUTING"; field public static final String MODIFY_CELL_BROADCASTS = "android.permission.MODIFY_CELL_BROADCASTS"; @@ -6607,6 +6608,7 @@ package android.media { public class AudioManager { method @Deprecated public int abandonAudioFocus(android.media.AudioManager.OnAudioFocusChangeListener, android.media.AudioAttributes); method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void addAssistantServicesUids(@NonNull int[]); + method @RequiresPermission(anyOf={android.Manifest.permission.MODIFY_AUDIO_ROUTING, "android.permission.QUERY_AUDIO_STATE"}) public void addOnDevicesForAttributesChangedListener(@NonNull android.media.AudioAttributes, @NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.OnDevicesForAttributesChangedListener); method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void addOnNonDefaultDevicesForStrategyChangedListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.OnNonDefaultDevicesForStrategyChangedListener) throws java.lang.SecurityException; method @Deprecated @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void addOnPreferredDeviceForStrategyChangedListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.OnPreferredDeviceForStrategyChangedListener) throws java.lang.SecurityException; method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void addOnPreferredDevicesForCapturePresetChangedListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.OnPreferredDevicesForCapturePresetChangedListener) throws java.lang.SecurityException; @@ -6647,6 +6649,7 @@ package android.media { method public void registerVolumeGroupCallback(@NonNull java.util.concurrent.Executor, @NonNull android.media.AudioManager.VolumeGroupCallback); method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void removeAssistantServicesUids(@NonNull int[]); method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public boolean removeDeviceAsNonDefaultForStrategy(@NonNull android.media.audiopolicy.AudioProductStrategy, @NonNull android.media.AudioDeviceAttributes); + method @RequiresPermission(anyOf={android.Manifest.permission.MODIFY_AUDIO_ROUTING, "android.permission.QUERY_AUDIO_STATE"}) public void removeOnDevicesForAttributesChangedListener(@NonNull android.media.AudioManager.OnDevicesForAttributesChangedListener); method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void removeOnNonDefaultDevicesForStrategyChangedListener(@NonNull android.media.AudioManager.OnNonDefaultDevicesForStrategyChangedListener); method @Deprecated @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void removeOnPreferredDeviceForStrategyChangedListener(@NonNull android.media.AudioManager.OnPreferredDeviceForStrategyChangedListener); method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void removeOnPreferredDevicesForCapturePresetChangedListener(@NonNull android.media.AudioManager.OnPreferredDevicesForCapturePresetChangedListener); @@ -6704,6 +6707,10 @@ package android.media { field public static final int EVENT_TIMEOUT = 2; // 0x2 } + public static interface AudioManager.OnDevicesForAttributesChangedListener { + method public void onDevicesForAttributesChanged(@NonNull android.media.AudioAttributes, @NonNull java.util.List<android.media.AudioDeviceAttributes>); + } + public static interface AudioManager.OnNonDefaultDevicesForStrategyChangedListener { method public void onNonDefaultDevicesForStrategyChanged(@NonNull android.media.audiopolicy.AudioProductStrategy, @NonNull java.util.List<android.media.AudioDeviceAttributes>); } @@ -12539,6 +12546,19 @@ package android.service.voice { field public static final int INITIALIZATION_STATUS_UNKNOWN = 100; // 0x64 } + public abstract class VisualQueryDetectionService extends android.app.Service implements android.service.voice.SandboxedDetectionServiceBase { + ctor public VisualQueryDetectionService(); + method @Nullable public android.os.IBinder onBind(@NonNull android.content.Intent); + method public void onStartDetection(@NonNull android.service.voice.VisualQueryDetectionService.Callback); + method public void onStopDetection(); + method public void onUpdateState(@Nullable android.os.PersistableBundle, @Nullable android.os.SharedMemory, long, @Nullable java.util.function.IntConsumer); + field public static final String SERVICE_INTERFACE = "android.service.voice.VisualQueryDetectionService"; + } + + public static final class VisualQueryDetectionService.Callback { + ctor public VisualQueryDetectionService.Callback(); + } + public class VoiceInteractionService extends android.app.Service { method @NonNull public final android.service.voice.AlwaysOnHotwordDetector createAlwaysOnHotwordDetector(String, java.util.Locale, android.service.voice.AlwaysOnHotwordDetector.Callback); method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_HOTWORD_DETECTION) public final android.service.voice.AlwaysOnHotwordDetector createAlwaysOnHotwordDetector(String, java.util.Locale, @Nullable android.os.PersistableBundle, @Nullable android.os.SharedMemory, android.service.voice.AlwaysOnHotwordDetector.Callback); @@ -13212,10 +13232,10 @@ package android.telephony { } public final class CellBroadcastIdRange implements android.os.Parcelable { - ctor public CellBroadcastIdRange(int, int, int, boolean) throws java.lang.IllegalArgumentException; + ctor public CellBroadcastIdRange(@IntRange(from=0, to=65535) int, @IntRange(from=0, to=65535) int, int, boolean) throws java.lang.IllegalArgumentException; method public int describeContents(); - method public int getEndId(); - method public int getStartId(); + method @IntRange(from=0, to=65535) public int getEndId(); + method @IntRange(from=0, to=65535) public int getStartId(); method public int getType(); method public boolean isEnabled(); method public void writeToParcel(@NonNull android.os.Parcel, int); @@ -14157,11 +14177,11 @@ package android.telephony { field public static final int CDMA_SUBSCRIPTION_NV = 1; // 0x1 field public static final int CDMA_SUBSCRIPTION_RUIM_SIM = 0; // 0x0 field public static final int CDMA_SUBSCRIPTION_UNKNOWN = -1; // 0xffffffff - field public static final int CELLBROADCAST_RESULT_FAIL_ACTIVATION = 3; // 0x3 - field public static final int CELLBROADCAST_RESULT_FAIL_CONFIG = 2; // 0x2 - field public static final int CELLBROADCAST_RESULT_SUCCESS = 0; // 0x0 - field public static final int CELLBROADCAST_RESULT_UNKNOWN = -1; // 0xffffffff - field public static final int CELLBROADCAST_RESULT_UNSUPPORTED = 1; // 0x1 + field public static final int CELL_BROADCAST_RESULT_FAIL_ACTIVATION = 3; // 0x3 + field public static final int CELL_BROADCAST_RESULT_FAIL_CONFIG = 2; // 0x2 + field public static final int CELL_BROADCAST_RESULT_SUCCESS = 0; // 0x0 + field public static final int CELL_BROADCAST_RESULT_UNKNOWN = -1; // 0xffffffff + field public static final int CELL_BROADCAST_RESULT_UNSUPPORTED = 1; // 0x1 field public static final int ENABLE_NR_DUAL_CONNECTIVITY_INVALID_STATE = 4; // 0x4 field public static final int ENABLE_NR_DUAL_CONNECTIVITY_NOT_SUPPORTED = 1; // 0x1 field public static final int ENABLE_NR_DUAL_CONNECTIVITY_RADIO_ERROR = 3; // 0x3 diff --git a/core/java/android/app/ApplicationExitInfo.java b/core/java/android/app/ApplicationExitInfo.java index 871d15ec0b40..51ea04f397d2 100644 --- a/core/java/android/app/ApplicationExitInfo.java +++ b/core/java/android/app/ApplicationExitInfo.java @@ -416,6 +416,15 @@ public final class ApplicationExitInfo implements Parcelable { */ public static final int SUBREASON_UNDELIVERED_BROADCAST = 26; + /** + * The process was killed because its associated SDK sandbox process (where it had loaded SDKs) + * had died; this would be set only when the reason is {@link #REASON_DEPENDENCY_DIED}. + * + * For internal use only. + * @hide + */ + public static final int SUBREASON_SDK_SANDBOX_DIED = 27; + // If there is any OEM code which involves additional app kill reasons, it should // be categorized in {@link #REASON_OTHER}, with subreason code starting from 1000. diff --git a/core/java/android/app/ClientTransactionHandler.java b/core/java/android/app/ClientTransactionHandler.java index 419ffac230f7..e658cb7c302d 100644 --- a/core/java/android/app/ClientTransactionHandler.java +++ b/core/java/android/app/ClientTransactionHandler.java @@ -42,6 +42,8 @@ import java.util.Map; */ public abstract class ClientTransactionHandler { + private boolean mIsExecutingLocalTransaction; + // Schedule phase related logic and handlers. /** Prepare and schedule transaction for execution. */ @@ -56,9 +58,19 @@ public abstract class ClientTransactionHandler { */ @VisibleForTesting public void executeTransaction(ClientTransaction transaction) { - transaction.preExecute(this); - getTransactionExecutor().execute(transaction); - transaction.recycle(); + mIsExecutingLocalTransaction = true; + try { + transaction.preExecute(this); + getTransactionExecutor().execute(transaction); + } finally { + mIsExecutingLocalTransaction = false; + transaction.recycle(); + } + } + + /** Returns {@code true} if the current executing ClientTransaction is from local request. */ + public boolean isExecutingLocalTransaction() { + return mIsExecutingLocalTransaction; } /** diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index 1120257142dc..240dbe1eea24 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -2028,6 +2028,13 @@ class ContextImpl extends Context { } /** @hide */ + @NonNull + @Override + public IBinder getIApplicationThreadBinder() { + return getIApplicationThread().asBinder(); + } + + /** @hide */ @Override public Handler getMainThreadHandler() { return mMainThread.getHandler(); @@ -3039,7 +3046,8 @@ class ContextImpl extends Context { public void updateDeviceId(int updatedDeviceId) { if (!isValidDeviceId(updatedDeviceId)) { throw new IllegalArgumentException( - "Not a valid ID of the default device or any virtual device: " + mDeviceId); + "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/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index e729e7d8f9be..209b112ec9ed 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -16,6 +16,9 @@ package android.app.admin; +import static android.Manifest.permission.QUERY_ADMIN_POLICY; +import static android.Manifest.permission.SET_TIME; +import static android.Manifest.permission.SET_TIME_ZONE; import static android.content.Intent.LOCAL_FLAG_FROM_SYSTEM; import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_1; @@ -3311,13 +3314,16 @@ public class DevicePolicyManager { * A {@code boolean} flag that indicates whether the screen should be on throughout the * provisioning flow. * - * <p>The default value is {@code false}. - * * <p>This extra can either be passed as an extra to the {@link * #ACTION_PROVISION_MANAGED_PROFILE} intent, or it can be returned by the * admin app when performing the admin-integrated provisioning flow as a result of the * {@link #ACTION_GET_PROVISIONING_MODE} activity. + * + * @deprecated from {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, the flag wouldn't + * be functional. The screen is kept on throughout the provisioning flow. */ + + @Deprecated public static final String EXTRA_PROVISIONING_KEEP_SCREEN_ON = "android.app.extra.PROVISIONING_KEEP_SCREEN_ON"; @@ -8466,8 +8472,10 @@ public class DevicePolicyManager { } /** - * Called by a device owner, a profile owner for the primary user or a profile - * owner of an organization-owned managed profile to turn auto time on and off. + * Called by a device owner, a profile owner for the primary user, a profile + * owner of an organization-owned managed profile or, starting from Android + * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, holders of the permission + * {@link android.Manifest.permission#SET_TIME} to turn auto time on and off. * Callers are recommended to use {@link UserManager#DISALLOW_CONFIG_DATE_TIME} * to prevent the user from changing this setting. * <p> @@ -8478,8 +8486,10 @@ public class DevicePolicyManager { * @param admin Which {@link DeviceAdminReceiver} this request is associated with. * @param enabled Whether time should be obtained automatically from the network or not. * @throws SecurityException if caller is not a device owner, a profile owner for the - * primary user, or a profile owner of an organization-owned managed profile. + * primary user, or a profile owner of an organization-owned managed profile or a holder of the + * permission {@link android.Manifest.permission#SET_TIME}. */ + @RequiresPermission(value = SET_TIME, conditional = true) public void setAutoTimeEnabled(@NonNull ComponentName admin, boolean enabled) { if (mService != null) { try { @@ -8491,10 +8501,18 @@ public class DevicePolicyManager { } /** + * Returns true if auto time is enabled on the device. + * + * <p> Starting from Android {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, callers + * are also able to call this method if they hold the permission + *{@link android.Manifest.permission#SET_TIME}. + * * @return true if auto time is enabled on the device. - * @throws SecurityException if caller is not a device owner, a profile owner for the - * primary user, or a profile owner of an organization-owned managed profile. + * @throws SecurityException if the caller is not a device owner, a profile + * owner for the primary user, or a profile owner of an organization-owned managed profile or a + * holder of the permission {@link android.Manifest.permission#SET_TIME}. */ + @RequiresPermission(anyOf = {SET_TIME, QUERY_ADMIN_POLICY}, conditional = true) public boolean getAutoTimeEnabled(@NonNull ComponentName admin) { if (mService != null) { try { @@ -8507,8 +8525,10 @@ public class DevicePolicyManager { } /** - * Called by a device owner, a profile owner for the primary user or a profile - * owner of an organization-owned managed profile to turn auto time zone on and off. + * Called by a device owner, a profile owner for the primary user, a profile + * owner of an organization-owned managed profile or, starting from Android + * {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, holders of the permission + * {@link android.Manifest.permission#SET_TIME} to turn auto time zone on and off. * Callers are recommended to use {@link UserManager#DISALLOW_CONFIG_DATE_TIME} * to prevent the user from changing this setting. * <p> @@ -8519,8 +8539,10 @@ public class DevicePolicyManager { * @param admin Which {@link DeviceAdminReceiver} this request is associated with. * @param enabled Whether time zone should be obtained automatically from the network or not. * @throws SecurityException if caller is not a device owner, a profile owner for the - * primary user, or a profile owner of an organization-owned managed profile. + * primary user, or a profile owner of an organization-owned managed profile or a holder of the + * permission {@link android.Manifest.permission#SET_TIME_ZONE}. */ + @RequiresPermission(value = SET_TIME_ZONE, conditional = true) public void setAutoTimeZoneEnabled(@NonNull ComponentName admin, boolean enabled) { throwIfParentInstance("setAutoTimeZone"); if (mService != null) { @@ -8533,10 +8555,18 @@ public class DevicePolicyManager { } /** + * Returns true if auto time zone is enabled on the device. + * + * <p> Starting from Android {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, callers + * are also able to call this method if they hold the permission + *{@link android.Manifest.permission#SET_TIME}. + * * @return true if auto time zone is enabled on the device. - * @throws SecurityException if caller is not a device owner, a profile owner for the - * primary user, or a profile owner of an organization-owned managed profile. + * @throws SecurityException if the caller is not a device owner, a profile + * owner for the primary user, or a profile owner of an organization-owned managed profile or a + * holder of the permission {@link android.Manifest.permission#SET_TIME_ZONE}. */ + @RequiresPermission(anyOf = {SET_TIME_ZONE, QUERY_ADMIN_POLICY}, conditional = true) public boolean getAutoTimeZoneEnabled(@NonNull ComponentName admin) { throwIfParentInstance("getAutoTimeZone"); if (mService != null) { @@ -11875,17 +11905,21 @@ public class DevicePolicyManager { } /** - * Called by a device owner or a profile owner of an organization-owned managed - * profile to set the system wall clock time. This only takes effect if called when - * {@link android.provider.Settings.Global#AUTO_TIME} is 0, otherwise {@code false} - * will be returned. + * Called by a device owner, a profile owner of an organization-owned managed + * profile or, starting from Android {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, + * holders of the permission {@link android.Manifest.permission#SET_TIME} to set the system wall + * clock time. This only takes effect if called when + * {@link android.provider.Settings.Global#AUTO_TIME} is 0, otherwise {@code false} will be + * returned. * * @param admin Which {@link DeviceAdminReceiver} this request is associated with * @param millis time in milliseconds since the Epoch * @return {@code true} if set time succeeded, {@code false} otherwise. * @throws SecurityException if {@code admin} is not a device owner or a profile owner - * of an organization-owned managed profile. + * of an organization-owned managed profile or a holder of the permission + * {@link android.Manifest.permission#SET_TIME}. */ + @RequiresPermission(value = SET_TIME, conditional = true) public boolean setTime(@NonNull ComponentName admin, long millis) { throwIfParentInstance("setTime"); if (mService != null) { @@ -11899,10 +11933,12 @@ public class DevicePolicyManager { } /** - * Called by a device owner or a profile owner of an organization-owned managed - * profile to set the system's persistent default time zone. This only takes - * effect if called when {@link android.provider.Settings.Global#AUTO_TIME_ZONE} - * is 0, otherwise {@code false} will be returned. + * Called by a device owner, a profile owner of an organization-owned managed + * profile or, starting from Android {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, + * holders of the permission {@link android.Manifest.permission#SET_TIME_ZONE} to set the + * system's persistent default time zone. This only take effect if called when + * {@link android.provider.Settings.Global#AUTO_TIME_ZONE} is 0, otherwise {@code false} will be + * returned. * * @see android.app.AlarmManager#setTimeZone(String) * @param admin Which {@link DeviceAdminReceiver} this request is associated with @@ -11910,8 +11946,10 @@ public class DevicePolicyManager { * {@link java.util.TimeZone#getAvailableIDs} * @return {@code true} if set timezone succeeded, {@code false} otherwise. * @throws SecurityException if {@code admin} is not a device owner or a profile owner - * of an organization-owned managed profile. + * of an organization-owned managed profile or a holder of the permissions + * {@link android.Manifest.permission#SET_TIME_ZONE}. */ + @RequiresPermission(value = SET_TIME_ZONE, conditional = true) public boolean setTimeZone(@NonNull ComponentName admin, String timeZone) { throwIfParentInstance("setTimeZone"); if (mService != null) { diff --git a/core/java/android/app/admin/DevicePolicyManagerInternal.java b/core/java/android/app/admin/DevicePolicyManagerInternal.java index 840f3a3c6bb8..3a61ca1fbf93 100644 --- a/core/java/android/app/admin/DevicePolicyManagerInternal.java +++ b/core/java/android/app/admin/DevicePolicyManagerInternal.java @@ -271,6 +271,31 @@ public abstract class DevicePolicyManagerInternal { public abstract void resetOp(int op, String packageName, @UserIdInt int userId); /** + * Checks if the calling process has been granted permission to apply a device policy on a + * specific user. + * + * The given permission will be checked along with its associated cross-user permission, if it + * exists and the target user is different to the calling user. + * + * @param permission The name of the permission being checked. + * @param targetUserId The userId of the user which the caller needs permission to act on. + * @throws SecurityException If the calling process has not been granted the permission. + */ + public abstract void enforcePermission(String permission, int targetUserId); + + /** + * Return whether the calling process has been granted permission to apply a device policy on + * a specific user. + * + * The given permission will be checked along with its associated cross-user + * permission, if it exists and the target user is different to the calling user. + * + * @param permission The name of the permission being checked. + * @param targetUserId The userId of the user which the caller needs permission to act on. + */ + public abstract boolean hasPermission(String permission, int targetUserId); + + /** * Returns whether new "turn off work" behavior is enabled via feature flag. */ public abstract boolean isKeepProfilesRunningEnabled(); diff --git a/core/java/android/app/admin/PolicyUpdateReason.java b/core/java/android/app/admin/PolicyUpdateReason.java new file mode 100644 index 000000000000..97d282dbc8d7 --- /dev/null +++ b/core/java/android/app/admin/PolicyUpdateReason.java @@ -0,0 +1,74 @@ +/* + * 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.app.admin; + +import android.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Class containing the reason a policy (set from {@link DevicePolicyManager}) hasn't been enforced + * (passed in to {@link PolicyUpdatesReceiver#onPolicySetResult}) or has changed (passed in to + * {@link PolicyUpdatesReceiver#onPolicyChanged}). + */ +public final class PolicyUpdateReason { + + /** + * Reason code to indicate that the policy has not been enforced or has changed for an unknown + * reason. + */ + public static final int REASON_UNKNOWN = -1; + + /** + * Reason code to indicate that the policy has not been enforced or has changed because another + * admin has set a conflicting policy on the device. + */ + public static final int REASON_CONFLICTING_ADMIN_POLICY = 0; + + /** + * Reason codes for {@link #getReasonCode()}. + * + * @hide + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = true, prefix = { "REASON_" }, value = { + REASON_UNKNOWN, + REASON_CONFLICTING_ADMIN_POLICY, + }) + public @interface ReasonCode {} + + private final int mReasonCode; + + /** + * Constructor for {@code PolicyUpdateReason} that takes in a reason code describing why the + * policy has changed. + * + * @param reasonCode Describes why the policy has changed. + */ + public PolicyUpdateReason(@ReasonCode int reasonCode) { + this.mReasonCode = reasonCode; + } + + /** + * Returns reason code for why a policy hasn't been applied or has changed. + */ + @ReasonCode + public int getReasonCode() { + return mReasonCode; + } +} diff --git a/core/java/android/app/admin/PolicyUpdatesReceiver.java b/core/java/android/app/admin/PolicyUpdatesReceiver.java new file mode 100644 index 000000000000..ff30a5f8a037 --- /dev/null +++ b/core/java/android/app/admin/PolicyUpdatesReceiver.java @@ -0,0 +1,315 @@ +/* + * 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.app.admin; + +import android.annotation.BroadcastBehavior; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SdkConstant; +import android.annotation.SuppressLint; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Objects; + +// TODO(b/261432333): Add more detailed javadocs on using DeviceAdminService. +/** + * Base class for implementing a policy update receiver. This class provides a convenience for + * interpreting the raw intent actions ({@link #ACTION_DEVICE_POLICY_SET_RESULT} and + * {@link #ACTION_DEVICE_POLICY_CHANGED}) that are sent by the system. + * + * <p>The callback methods happen on the main thread of the process. Thus, long-running + * operations must be done on another thread. + * + * <p>When publishing your {@code PolicyUpdatesReceiver} subclass as a receiver, it must + * require the {@link android.Manifest.permission#BIND_DEVICE_ADMIN} permission. + */ +public abstract class PolicyUpdatesReceiver extends BroadcastReceiver { + private static String TAG = "PolicyUpdatesReceiver"; + + /** + * Result code passed in to {@link #onPolicySetResult} to indicate that the policy has been + * set successfully. + */ + public static final int POLICY_SET_RESULT_SUCCESS = 0; + + /** + * Result code passed in to {@link #onPolicySetResult} to indicate that the policy has NOT been + * set, a {@link PolicyUpdateReason} will be passed in to {@link #onPolicySetResult} to indicate + * the reason. + */ + public static final int POLICY_SET_RESULT_FAILURE = -1; + + /** + * Result codes passed in to {@link #onPolicySetResult}. + * + * @hide + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = true, prefix = { "POLICY_SET_RESULT_" }, value = { + POLICY_SET_RESULT_SUCCESS, + POLICY_SET_RESULT_FAILURE, + }) + public @interface ResultCode {} + + /** + * Action for a broadcast sent to admins to communicate back the result of setting a policy in + * {@link DevicePolicyManager}. + * + * <p>Admins wishing to receive these updates (via {@link #onPolicySetResult}) should include + * this action in the intent filter for their receiver in the manifest, the receiver + * must be protected by {@link android.Manifest.permission#BIND_DEVICE_ADMIN} to ensure that + * only the system can send updates. + * + * <p>Admins shouldn't implement {@link #onReceive} and should instead implement + * {@link #onPolicySetResult}. + */ + @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION) + @BroadcastBehavior(explicitOnly = true) + public static final String ACTION_DEVICE_POLICY_SET_RESULT = + "android.app.admin.action.DEVICE_POLICY_SET_RESULT"; + + /** + * Action for a broadcast sent to admins to communicate back a change in a policy they have + * previously set. + * + * <p>Admins wishing to receive these updates should include this action in the intent filter + * for their receiver in the manifest, the receiver must be protected by + * {@link android.Manifest.permission#BIND_DEVICE_ADMIN} to ensure that only the system can + * send updates. + * + * <p>Admins shouldn't implement {@link #onReceive} and should instead implement + * {@link #onPolicyChanged}. + */ + @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION) + @BroadcastBehavior(explicitOnly = true) + public static final String ACTION_DEVICE_POLICY_CHANGED = + "android.app.admin.action.DEVICE_POLICY_CHANGED"; + + // TODO(b/264510719): Remove once API linter is fixed + @SuppressLint("ActionValue") + /** + * A string extra holding the package name the policy applies to, (see + * {@link PolicyUpdatesReceiver#onPolicyChanged} and + * {@link PolicyUpdatesReceiver#onPolicySetResult}) + */ + public static final String EXTRA_PACKAGE_NAME = + "android.app.admin.extra.PACKAGE_NAME"; + + // TODO(b/264510719): Remove once API linter is fixed + @SuppressLint("ActionValue") + /** + * A string extra holding the permission name the policy applies to, (see + * {@link PolicyUpdatesReceiver#onPolicyChanged} and + * {@link PolicyUpdatesReceiver#onPolicySetResult}) + */ + public static final String EXTRA_PERMISSION_NAME = + "android.app.admin.extra.PERMISSION_NAME"; + + /** + * @hide + */ + public static final String EXTRA_POLICY_CHANGED_KEY = + "android.app.admin.extra.POLICY_CHANGED_KEY"; + + /** + * @hide + */ + public static final String EXTRA_POLICY_KEY = "android.app.admin.extra.POLICY_KEY"; + + /** + * @hide + */ + public static final String EXTRA_POLICY_BUNDLE_KEY = + "android.app.admin.extra.POLICY_BUNDLE_KEY"; + + /** + * @hide + */ + public static final String EXTRA_POLICY_SET_RESULT_KEY = + "android.app.admin.extra.POLICY_SET_RESULT_KEY"; + + /** + * @hide + */ + public static final String EXTRA_POLICY_UPDATE_REASON_KEY = + "android.app.admin.extra.POLICY_UPDATE_REASON_KEY"; + + /** + * @hide + */ + public static final String EXTRA_POLICY_TARGET_USER_ID = + "android.app.admin.extra.POLICY_TARGET_USER_ID"; + + /** + * Intercept standard policy update broadcasts. Implementations should not override this + * method and rely on the callbacks instead. + * + * @hide + */ + @Override + public final void onReceive(Context context, Intent intent) { + Objects.requireNonNull(intent.getAction()); + switch (intent.getAction()) { + case ACTION_DEVICE_POLICY_SET_RESULT: + Log.i(TAG, "Received ACTION_DEVICE_POLICY_SET_RESULT"); + onPolicySetResult(context, getPolicyKey(intent), getPolicyExtraBundle(intent), + getTargetUser(intent), getPolicyResult(intent), getFailureReason(intent)); + break; + case ACTION_DEVICE_POLICY_CHANGED: + Log.i(TAG, "Received ACTION_DEVICE_POLICY_CHANGED"); + onPolicyChanged(context, getPolicyKey(intent), getPolicyExtraBundle(intent), + getTargetUser(intent), getPolicyChangedReason(intent)); + break; + default: + Log.e(TAG, "Unknown action received: " + intent.getAction()); + } + } + + /** + * @hide + */ + static String getPolicyKey(Intent intent) { + if (!intent.hasExtra(EXTRA_POLICY_KEY)) { + throw new IllegalArgumentException("PolicyKey has to be provided."); + } + return intent.getStringExtra(EXTRA_POLICY_KEY); + } + + /** + * @hide + */ + @ResultCode + static int getPolicyResult(Intent intent) { + if (!intent.hasExtra(EXTRA_POLICY_SET_RESULT_KEY)) { + throw new IllegalArgumentException("Result has to be provided."); + } + return intent.getIntExtra(EXTRA_POLICY_SET_RESULT_KEY, POLICY_SET_RESULT_FAILURE); + } + + /** + * @hide + */ + @NonNull + static Bundle getPolicyExtraBundle(Intent intent) { + Bundle bundle = intent.getBundleExtra(EXTRA_POLICY_BUNDLE_KEY); + return bundle == null ? new Bundle() : bundle; + } + + /** + * @hide + */ + @Nullable + static PolicyUpdateReason getFailureReason(Intent intent) { + if (getPolicyResult(intent) != POLICY_SET_RESULT_FAILURE) { + return null; + } + return getPolicyChangedReason(intent); + } + + /** + * @hide + */ + @NonNull + static PolicyUpdateReason getPolicyChangedReason(Intent intent) { + int reasonCode = intent.getIntExtra( + EXTRA_POLICY_UPDATE_REASON_KEY, PolicyUpdateReason.REASON_UNKNOWN); + return new PolicyUpdateReason(reasonCode); + } + + /** + * @hide + */ + @NonNull + static TargetUser getTargetUser(Intent intent) { + if (!intent.hasExtra(EXTRA_POLICY_TARGET_USER_ID)) { + throw new IllegalArgumentException("TargetUser has to be provided."); + } + int targetUserId = intent.getIntExtra( + EXTRA_POLICY_TARGET_USER_ID, TargetUser.LOCAL_USER_ID); + return new TargetUser(targetUserId); + } + + // TODO(b/260847505): Add javadocs to explain which DPM APIs are supported + /** + * Callback triggered after an admin has set a policy using one of the APIs in + * {@link DevicePolicyManager} to notify the admin whether it has been successful or not. + * + * <p>Admins wishing to receive this callback should include + * {@link PolicyUpdatesReceiver#ACTION_DEVICE_POLICY_SET_RESULT} in the intent filter for their + * receiver in the manifest, the receiver must be protected by + * {@link android.Manifest.permission#BIND_DEVICE_ADMIN} to ensure that only the system can + * send updates. + * + * @param context the running context as per {@link #onReceive} + * @param policyKey Key to identify which policy this callback relates to. + * @param additionalPolicyParams Bundle containing additional params that may be required to + * identify some of the policy + * (e.g. {@link PolicyUpdatesReceiver#EXTRA_PACKAGE_NAME} + * and {@link PolicyUpdatesReceiver#EXTRA_PERMISSION_NAME}). + * Each policy will document the required additional params if + * needed. + * @param targetUser The {@link TargetUser} which this policy relates to. + * @param result Indicates whether the policy has been set successfully, + * (see {@link PolicyUpdatesReceiver#POLICY_SET_RESULT_SUCCESS} and + * {@link PolicyUpdatesReceiver#POLICY_SET_RESULT_FAILURE}). + * @param reason Indicates the reason the policy failed to apply, {@code null} if the policy was + * applied successfully. + */ + public void onPolicySetResult( + @NonNull Context context, + @NonNull String policyKey, + @NonNull Bundle additionalPolicyParams, + @NonNull TargetUser targetUser, + @ResultCode int result, + @Nullable PolicyUpdateReason reason) {} + + // TODO(b/260847505): Add javadocs to explain which DPM APIs are supported + // TODO(b/261430877): Add javadocs to explain when will this get triggered + /** + * Callback triggered when a policy previously set by the admin has changed. + * + * <p>Admins wishing to receive this callback should include + * {@link PolicyUpdatesReceiver#ACTION_DEVICE_POLICY_CHANGED} in the intent filter for their + * receiver in the manifest, the receiver must be protected by + * {@link android.Manifest.permission#BIND_DEVICE_ADMIN} to ensure that only the system can + * send updates. + * + * @param context the running context as per {@link #onReceive} + * @param policyKey Key to identify which policy this callback relates to. + * @param additionalPolicyParams Bundle containing additional params that may be required to + * identify some of the policy + * (e.g. {@link PolicyUpdatesReceiver#EXTRA_PACKAGE_NAME} + * and {@link PolicyUpdatesReceiver#EXTRA_PERMISSION_NAME}). + * Each policy will document the required additional params if + * needed. + * @param targetUser The {@link TargetUser} which this policy relates to. + * @param reason Indicates the reason the policy value has changed. + */ + public void onPolicyChanged( + @NonNull Context context, + @NonNull String policyKey, + @NonNull Bundle additionalPolicyParams, + @NonNull TargetUser targetUser, + @NonNull PolicyUpdateReason reason) {} +} diff --git a/core/java/android/app/admin/TargetUser.java b/core/java/android/app/admin/TargetUser.java new file mode 100644 index 000000000000..acbac29dabe6 --- /dev/null +++ b/core/java/android/app/admin/TargetUser.java @@ -0,0 +1,85 @@ +/* + * 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.app.admin; + +import android.annotation.NonNull; +import android.annotation.Nullable; + +import java.util.Objects; + +/** + * Class representing the target user of a policy set by an admin + * (set from {@link DevicePolicyManager}), this is passed in to + * {@link PolicyUpdatesReceiver#onPolicySetResult} and + * {@link PolicyUpdatesReceiver#onPolicyChanged}. + */ +public final class TargetUser { + /** + * @hide + */ + public static final int LOCAL_USER_ID = -1; + + /** + * @hide + */ + public static final int PARENT_USER_ID = -2; + + /** + * @hide + */ + public static final int GLOBAL_USER_ID = -3; + + /** + * Indicates that the policy relates to the user the admin is installed on. + */ + @NonNull + public static final TargetUser LOCAL_USER = new TargetUser(LOCAL_USER_ID); + + /** + * For admins of profiles, this indicates that the policy relates to the parent profile. + */ + @NonNull + public static final TargetUser PARENT_USER = new TargetUser(PARENT_USER_ID); + + /** + * This indicates the policy is a global policy. + */ + @NonNull + public static final TargetUser GLOBAL = new TargetUser(GLOBAL_USER_ID); + + private final int mUserId; + + /** + * @hide + */ + public TargetUser(int userId) { + mUserId = userId; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TargetUser other = (TargetUser) o; + return mUserId == other.mUserId; + } + + @Override + public int hashCode() { + return Objects.hash(mUserId); + } +} diff --git a/core/java/android/app/servertransaction/ActivityRelaunchItem.java b/core/java/android/app/servertransaction/ActivityRelaunchItem.java index c09461c1fb52..b26dac712609 100644 --- a/core/java/android/app/servertransaction/ActivityRelaunchItem.java +++ b/core/java/android/app/servertransaction/ActivityRelaunchItem.java @@ -57,7 +57,10 @@ public class ActivityRelaunchItem extends ActivityTransactionItem { @Override public void preExecute(ClientTransactionHandler client, IBinder token) { - CompatibilityInfo.applyOverrideScaleIfNeeded(mConfig); + // The local config is already scaled so only apply if this item is from server side. + if (!client.isExecutingLocalTransaction()) { + CompatibilityInfo.applyOverrideScaleIfNeeded(mConfig); + } mActivityClientRecord = client.prepareRelaunchActivity(token, mPendingResults, mPendingNewIntents, mConfigChanges, mConfig, mPreserveWindow); } diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 7d7232e36d72..12a2cae4c5c8 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -2346,7 +2346,8 @@ public abstract class Context { @SystemApi public void sendBroadcastMultiplePermissions(@NonNull Intent intent, @NonNull String[] receiverPermissions, @Nullable BroadcastOptions options) { - sendBroadcastMultiplePermissions(intent, receiverPermissions, options.toBundle()); + sendBroadcastMultiplePermissions(intent, receiverPermissions, + (options == null ? null : options.toBundle())); } /** @@ -7491,6 +7492,18 @@ public abstract class Context { } /** + * Get the binder object associated with the IApplicationThread of this Context. + * + * This can be used by a mainline module to uniquely identify a specific app process. + * @hide + */ + @NonNull + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + public IBinder getIApplicationThreadBinder() { + throw new RuntimeException("Not implemented. Must override in a subclass."); + } + + /** * @hide */ public Handler getMainThreadHandler() { diff --git a/core/java/android/content/ContextWrapper.java b/core/java/android/content/ContextWrapper.java index 0a32dd78092f..0dc4adc79f30 100644 --- a/core/java/android/content/ContextWrapper.java +++ b/core/java/android/content/ContextWrapper.java @@ -1277,6 +1277,14 @@ public class ContextWrapper extends Context { * @hide */ @Override + public IBinder getIApplicationThreadBinder() { + return mBase.getIApplicationThreadBinder(); + } + + /** + * @hide + */ + @Override public Handler getMainThreadHandler() { return mBase.getMainThreadHandler(); } diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java index c5ebf34c40ca..a9f55bc1ea4c 100644 --- a/core/java/android/content/pm/ActivityInfo.java +++ b/core/java/android/content/pm/ActivityInfo.java @@ -1066,6 +1066,19 @@ public class ActivityInfo extends ComponentInfo implements Parcelable { public @interface SizeChangesSupportMode {} /** + * This change id enables compat policy that ignores app requested orientation in + * response to an app calling {@link android.app.Activity#setRequestedOrientation}. See + * com.android.server.wm.LetterboxUiController#shouldIgnoreRequestedOrientation for + * details. + * @hide + */ + @ChangeId + @Overridable + @Disabled + public static final long OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION = + 254631730L; // buganizer id + + /** * This change id forces the packages it is applied to never have Display API sandboxing * applied for a letterbox or SCM activity. The Display APIs will continue to provide * DisplayArea bounds. diff --git a/core/java/android/hardware/Camera.java b/core/java/android/hardware/Camera.java index 5291d2b73891..c716f319103a 100644 --- a/core/java/android/hardware/Camera.java +++ b/core/java/android/hardware/Camera.java @@ -17,12 +17,7 @@ package android.hardware; import static android.system.OsConstants.EACCES; -import static android.system.OsConstants.EBUSY; -import static android.system.OsConstants.EINVAL; import static android.system.OsConstants.ENODEV; -import static android.system.OsConstants.ENOSYS; -import static android.system.OsConstants.EOPNOTSUPP; -import static android.system.OsConstants.EUSERS; import android.annotation.Nullable; import android.annotation.SdkConstant; diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java index ed1e9e5f6228..5b6e288d9da9 100644 --- a/core/java/android/hardware/camera2/CameraManager.java +++ b/core/java/android/hardware/camera2/CameraManager.java @@ -59,7 +59,6 @@ import android.util.Log; import android.util.Size; import android.view.Display; -import com.android.internal.annotations.GuardedBy; import com.android.internal.util.ArrayUtils; import java.lang.ref.WeakReference; diff --git a/core/java/android/hardware/camera2/params/MandatoryStreamCombination.java b/core/java/android/hardware/camera2/params/MandatoryStreamCombination.java index 6f77d12cc463..3fc44f8f84ac 100644 --- a/core/java/android/hardware/camera2/params/MandatoryStreamCombination.java +++ b/core/java/android/hardware/camera2/params/MandatoryStreamCombination.java @@ -2429,7 +2429,7 @@ public final class MandatoryStreamCombination { long minFrameDuration = mStreamConfigMap.getOutputMinFrameDuration( android.media.MediaRecorder.class, sz); // Give some margin for rounding error - if (minFrameDuration > (1e9 / 30.1)) { + if (minFrameDuration < (1e9 / 29.9)) { Log.i(TAG, "External camera " + mCameraId + " has max video size:" + sz); return sz; } diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl index 6314cabd298d..222cf080379c 100644 --- a/core/java/android/hardware/input/IInputManager.aidl +++ b/core/java/android/hardware/input/IInputManager.aidl @@ -22,6 +22,8 @@ import android.hardware.input.KeyboardLayout; import android.hardware.input.IInputDevicesChangedListener; import android.hardware.input.IInputDeviceBatteryListener; import android.hardware.input.IInputDeviceBatteryState; +import android.hardware.input.IKeyboardBacklightListener; +import android.hardware.input.IKeyboardBacklightState; import android.hardware.input.ITabletModeChangedListener; import android.hardware.input.TouchCalibration; import android.os.CombinedVibration; @@ -221,4 +223,14 @@ interface IInputManager { @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " + "android.Manifest.permission.MONITOR_INPUT)") void pilferPointers(IBinder inputChannelToken); + + @EnforcePermission("MONITOR_KEYBOARD_BACKLIGHT") + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " + + "android.Manifest.permission.MONITOR_KEYBOARD_BACKLIGHT)") + void registerKeyboardBacklightListener(IKeyboardBacklightListener listener); + + @EnforcePermission("MONITOR_KEYBOARD_BACKLIGHT") + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " + + "android.Manifest.permission.MONITOR_KEYBOARD_BACKLIGHT)") + void unregisterKeyboardBacklightListener(IKeyboardBacklightListener listener); } diff --git a/core/java/android/hardware/input/IKeyboardBacklightListener.aidl b/core/java/android/hardware/input/IKeyboardBacklightListener.aidl new file mode 100644 index 000000000000..2c1f2520350d --- /dev/null +++ b/core/java/android/hardware/input/IKeyboardBacklightListener.aidl @@ -0,0 +1,28 @@ +/* + * 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.hardware.input; + +import android.hardware.input.IKeyboardBacklightState; + +/** @hide */ +oneway interface IKeyboardBacklightListener { + + /** + * Called when the keyboard backlight brightness is changed. + */ + void onBrightnessChanged(int deviceId, in IKeyboardBacklightState state, boolean isTriggeredByKeyPress); +} diff --git a/core/java/android/hardware/input/IKeyboardBacklightState.aidl b/core/java/android/hardware/input/IKeyboardBacklightState.aidl new file mode 100644 index 000000000000..b04aa66d3e81 --- /dev/null +++ b/core/java/android/hardware/input/IKeyboardBacklightState.aidl @@ -0,0 +1,27 @@ +/* + * 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.hardware.input; + +/** @hide */ +@JavaDerive(equals=true) +parcelable IKeyboardBacklightState { + /** Current brightness level of the keyboard backlight in the range [0, maxBrightnessLevel]*/ + int brightnessLevel; + + /** Maximum brightness level of keyboard backlight */ + int maxBrightnessLevel; +}
\ No newline at end of file diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java index 655e5981971f..fb201cfbbe38 100644 --- a/core/java/android/hardware/input/InputManager.java +++ b/core/java/android/hardware/input/InputManager.java @@ -119,6 +119,12 @@ public final class InputManager { @GuardedBy("mBatteryListenersLock") private IInputDeviceBatteryListener mInputDeviceBatteryListener; + private final Object mKeyboardBacklightListenerLock = new Object(); + @GuardedBy("mKeyboardBacklightListenerLock") + private List<KeyboardBacklightListenerDelegate> mKeyboardBacklightListeners; + @GuardedBy("mKeyboardBacklightListenerLock") + private IKeyboardBacklightListener mKeyboardBacklightListener; + private InputDeviceSensorManager mInputDeviceSensorManager; /** * Broadcast Action: Query available keyboard layouts. @@ -2281,6 +2287,74 @@ public final class InputManager { } /** + * Registers a Keyboard backlight change listener to be notified about {@link + * KeyboardBacklightState} changes for connected keyboard devices. + * + * @param executor an executor on which the callback will be called + * @param listener the {@link KeyboardBacklightListener} + * @hide + * @see #unregisterKeyboardBacklightListener(KeyboardBacklightListener) + * @throws IllegalArgumentException if {@code listener} has already been registered previously. + * @throws NullPointerException if {@code listener} or {@code executor} is null. + */ + @RequiresPermission(Manifest.permission.MONITOR_KEYBOARD_BACKLIGHT) + public void registerKeyboardBacklightListener(@NonNull Executor executor, + @NonNull KeyboardBacklightListener listener) throws IllegalArgumentException { + Objects.requireNonNull(executor, "executor should not be null"); + Objects.requireNonNull(listener, "listener should not be null"); + + synchronized (mKeyboardBacklightListenerLock) { + if (mKeyboardBacklightListener == null) { + mKeyboardBacklightListeners = new ArrayList<>(); + mKeyboardBacklightListener = new LocalKeyboardBacklightListener(); + + try { + mIm.registerKeyboardBacklightListener(mKeyboardBacklightListener); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + for (KeyboardBacklightListenerDelegate delegate : mKeyboardBacklightListeners) { + if (delegate.mListener == listener) { + throw new IllegalArgumentException("Listener has already been registered!"); + } + } + KeyboardBacklightListenerDelegate delegate = + new KeyboardBacklightListenerDelegate(listener, executor); + mKeyboardBacklightListeners.add(delegate); + } + } + + /** + * Unregisters a previously added Keyboard backlight change listener. + * + * @param listener the {@link KeyboardBacklightListener} + * @see #registerKeyboardBacklightListener(Executor, KeyboardBacklightListener) + * @hide + */ + @RequiresPermission(Manifest.permission.MONITOR_KEYBOARD_BACKLIGHT) + public void unregisterKeyboardBacklightListener( + @NonNull KeyboardBacklightListener listener) { + Objects.requireNonNull(listener, "listener should not be null"); + + synchronized (mKeyboardBacklightListenerLock) { + if (mKeyboardBacklightListeners == null) { + return; + } + mKeyboardBacklightListeners.removeIf((delegate) -> delegate.mListener == listener); + if (mKeyboardBacklightListeners.isEmpty()) { + try { + mIm.unregisterKeyboardBacklightListener(mKeyboardBacklightListener); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + mKeyboardBacklightListeners = null; + mKeyboardBacklightListener = null; + } + } + } + + /** * A callback used to be notified about battery state changes for an input device. The * {@link #onBatteryStateChanged(int, long, BatteryState)} method will be called once after the * listener is successfully registered to provide the initial battery state of the device. @@ -2373,6 +2447,27 @@ public final class InputManager { void onTabletModeChanged(long whenNanos, boolean inTabletMode); } + /** + * A callback used to be notified about keyboard backlight state changes for keyboard device. + * The {@link #onKeyboardBacklightChanged(int, KeyboardBacklightState, boolean)} method + * will be called once after the listener is successfully registered to provide the initial + * keyboard backlight state of the device. + * @see #registerKeyboardBacklightListener(Executor, KeyboardBacklightListener) + * @see #unregisterKeyboardBacklightListener(KeyboardBacklightListener) + * @hide + */ + public interface KeyboardBacklightListener { + /** + * Called when the keyboard backlight brightness level changes. + * @param deviceId the keyboard for which the backlight brightness changed. + * @param state the new keyboard backlight state, never null. + * @param isTriggeredByKeyPress whether brightness change was triggered by the user + * pressing up/down key on the keyboard. + */ + void onKeyboardBacklightChanged( + int deviceId, @NonNull KeyboardBacklightState state, boolean isTriggeredByKeyPress); + } + private final class TabletModeChangedListener extends ITabletModeChangedListener.Stub { @Override public void onTabletModeChanged(long whenNanos, boolean inTabletMode) { @@ -2481,4 +2576,59 @@ public final class InputManager { } } } + + // Implementation of the android.hardware.input.KeyboardBacklightState interface used to report + // the keyboard backlight state via the KeyboardBacklightListener interfaces. + private static final class LocalKeyboardBacklightState extends KeyboardBacklightState { + + private final int mBrightnessLevel; + private final int mMaxBrightnessLevel; + + LocalKeyboardBacklightState(int brightnesslevel, int maxBrightnessLevel) { + mBrightnessLevel = brightnesslevel; + mMaxBrightnessLevel = maxBrightnessLevel; + } + + @Override + public int getBrightnessLevel() { + return mBrightnessLevel; + } + + @Override + public int getMaxBrightnessLevel() { + return mMaxBrightnessLevel; + } + } + + private static final class KeyboardBacklightListenerDelegate { + final KeyboardBacklightListener mListener; + final Executor mExecutor; + + KeyboardBacklightListenerDelegate(KeyboardBacklightListener listener, Executor executor) { + mListener = listener; + mExecutor = executor; + } + + void notifyKeyboardBacklightChange(int deviceId, IKeyboardBacklightState state, + boolean isTriggeredByKeyPress) { + mExecutor.execute(() -> + mListener.onKeyboardBacklightChanged(deviceId, + new LocalKeyboardBacklightState(state.brightnessLevel, + state.maxBrightnessLevel), isTriggeredByKeyPress)); + } + } + + private class LocalKeyboardBacklightListener extends IKeyboardBacklightListener.Stub { + + @Override + public void onBrightnessChanged(int deviceId, IKeyboardBacklightState state, + boolean isTriggeredByKeyPress) { + synchronized (mKeyboardBacklightListenerLock) { + if (mKeyboardBacklightListeners == null) return; + for (KeyboardBacklightListenerDelegate delegate : mKeyboardBacklightListeners) { + delegate.notifyKeyboardBacklightChange(deviceId, state, isTriggeredByKeyPress); + } + } + } + } } diff --git a/core/java/android/hardware/input/KeyboardBacklightState.java b/core/java/android/hardware/input/KeyboardBacklightState.java new file mode 100644 index 000000000000..4beaf6d36d7f --- /dev/null +++ b/core/java/android/hardware/input/KeyboardBacklightState.java @@ -0,0 +1,41 @@ +/* + * 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.hardware.input; + +/** + * The KeyboardBacklightState class is a representation of a keyboard backlight which is a + * single-colored backlight that illuminates all the keys on the keyboard. + * + * @hide + */ +public abstract class KeyboardBacklightState { + + /** + * Get the backlight brightness level in range [0, {@link #getMaxBrightnessLevel()}]. + * + * @return backlight brightness level + */ + public abstract int getBrightnessLevel(); + + /** + * Get the max backlight brightness level. + * + * @return max backlight brightness level + */ + public abstract int getMaxBrightnessLevel(); +} + diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java index 99152347106f..92ee393247a2 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -2095,6 +2095,15 @@ public class UserManager { } /** + * Returns whether multiple admins are enabled on the device + * @hide + */ + public static boolean isMultipleAdminEnabled() { + return Resources.getSystem() + .getBoolean(com.android.internal.R.bool.config_enableMultipleAdmins); + } + + /** * Checks whether the device is running in a headless system user mode. * * <p>Headless system user mode means the {@link #isSystemUser() system user} runs system diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 718e212b61b7..2bdd360c58a6 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -15174,6 +15174,15 @@ public final class Settings { "low_power_mode_suggestion_params"; /** + * Whether low power mode reminder is enabled. If this value is 0, the device will not + * receive low power notification. + * + * @hide + */ + public static final String LOW_POWER_MODE_REMINDER_ENABLED = + "low_power_mode_reminder_enabled"; + + /** * If not 0, the activity manager will aggressively finish activities and * processes as soon as they are no longer needed. If 0, the normal * extended lifetime is used. diff --git a/core/java/android/service/voice/AbstractHotwordDetector.java b/core/java/android/service/voice/AbstractDetector.java index c90ab6777515..db0ede5a9e70 100644 --- a/core/java/android/service/voice/AbstractHotwordDetector.java +++ b/core/java/android/service/voice/AbstractDetector.java @@ -41,9 +41,17 @@ import com.android.internal.app.IVoiceInteractionManagerService; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; -/** Base implementation of {@link HotwordDetector}. */ -abstract class AbstractHotwordDetector implements HotwordDetector { - private static final String TAG = AbstractHotwordDetector.class.getSimpleName(); +/** Base implementation of {@link HotwordDetector}. + * + * This class provides methods to manage the detector lifecycle for both + * {@link HotwordDetectionService} and {@link VisualQueryDetectionService}. We keep the name of the + * interface {@link HotwordDetector} since {@link VisualQueryDetectionService} can be logically + * treated as a visual activation hotword detection and also because of the existing public + * interface. To avoid confusion on the naming between the trusted hotword framework and the actual + * isolated {@link HotwordDetectionService}, the hotword from the names is removed. + */ +abstract class AbstractDetector implements HotwordDetector { + private static final String TAG = AbstractDetector.class.getSimpleName(); private static final boolean DEBUG = false; protected final Object mLock = new Object(); @@ -51,14 +59,14 @@ abstract class AbstractHotwordDetector implements HotwordDetector { private final IVoiceInteractionManagerService mManagerService; private final Handler mHandler; private final HotwordDetector.Callback mCallback; - private Consumer<AbstractHotwordDetector> mOnDestroyListener; + private Consumer<AbstractDetector> mOnDestroyListener; private final AtomicBoolean mIsDetectorActive; /** * A token which is used by voice interaction system service to identify different detectors. */ private final IBinder mToken = new Binder(); - AbstractHotwordDetector( + AbstractDetector( IVoiceInteractionManagerService managerService, HotwordDetector.Callback callback) { mManagerService = managerService; @@ -139,7 +147,7 @@ abstract class AbstractHotwordDetector implements HotwordDetector { } } - void registerOnDestroyListener(Consumer<AbstractHotwordDetector> onDestroyListener) { + void registerOnDestroyListener(Consumer<AbstractDetector> onDestroyListener) { synchronized (mLock) { if (mOnDestroyListener != null) { throw new IllegalStateException("only one destroy listener can be registered"); diff --git a/core/java/android/service/voice/AlwaysOnHotwordDetector.java b/core/java/android/service/voice/AlwaysOnHotwordDetector.java index 9008bf7d48ec..e8e8f1ae5947 100644 --- a/core/java/android/service/voice/AlwaysOnHotwordDetector.java +++ b/core/java/android/service/voice/AlwaysOnHotwordDetector.java @@ -81,7 +81,7 @@ import java.util.Set; * mark and track it as such. */ @SystemApi -public class AlwaysOnHotwordDetector extends AbstractHotwordDetector { +public class AlwaysOnHotwordDetector extends AbstractDetector { //---- States of Keyphrase availability. Return codes for onAvailabilityChanged() ----// /** * Indicates that this hotword detector is no longer valid for any recognition diff --git a/core/java/android/service/voice/SoftwareHotwordDetector.java b/core/java/android/service/voice/SoftwareHotwordDetector.java index f1b774591394..ffc662163e8e 100644 --- a/core/java/android/service/voice/SoftwareHotwordDetector.java +++ b/core/java/android/service/voice/SoftwareHotwordDetector.java @@ -46,7 +46,7 @@ import java.io.PrintWriter; * * @hide **/ -class SoftwareHotwordDetector extends AbstractHotwordDetector { +class SoftwareHotwordDetector extends AbstractDetector { private static final String TAG = SoftwareHotwordDetector.class.getSimpleName(); private static final boolean DEBUG = false; diff --git a/core/java/android/service/voice/VisualQueryDetectionService.java b/core/java/android/service/voice/VisualQueryDetectionService.java new file mode 100644 index 000000000000..d8266f3f7a0f --- /dev/null +++ b/core/java/android/service/voice/VisualQueryDetectionService.java @@ -0,0 +1,198 @@ +/* + * 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.service.voice; + +import android.annotation.DurationMillisLong; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SdkConstant; +import android.annotation.SystemApi; +import android.app.Service; +import android.content.ContentCaptureOptions; +import android.content.Intent; +import android.hardware.soundtrigger.SoundTrigger; +import android.media.AudioFormat; +import android.os.IBinder; +import android.os.IRemoteCallback; +import android.os.ParcelFileDescriptor; +import android.os.PersistableBundle; +import android.os.RemoteException; +import android.os.SharedMemory; +import android.speech.IRecognitionServiceManager; +import android.util.Log; +import android.view.contentcapture.IContentCaptureManager; + +import java.util.function.IntConsumer; + +/** + * Implemented by an application that wants to offer query detection with visual signals. + * + * This service leverages visual signals such as camera frames to detect and stream queries from the + * device microphone to the {@link VoiceInteractionService}, without the support of hotword. The + * system will bind an application's {@link VoiceInteractionService} first. When + * {@link VoiceInteractionService#createVisualQueryDetector(PersistableBundle, SharedMemory, + * Executor, VisualQueryDetector.Callback)} is called, the system will bind the application's + * {@link VisualQueryDetectionService}. When requested from {@link VoiceInteractionService}, the + * system calls into the {@link VisualQueryDetectionService#onStartDetection(Callback)} to enable + * detection. This method MUST be implemented to support visual query detection service. + * + * Note: Methods in this class may be called concurrently. + * + * @hide + */ +@SystemApi +public abstract class VisualQueryDetectionService extends Service + implements SandboxedDetectionServiceBase { + + private static final String TAG = VisualQueryDetectionService.class.getSimpleName(); + + private static final long UPDATE_TIMEOUT_MILLIS = 20000; + + /** + * The {@link Intent} that must be declared as handled by the service. + * To be supported, the service must also require the + * {@link android.Manifest.permission#BIND_VISUAL_QUERY_DETECTION_SERVICE} permission + * so that other applications can not abuse it. + */ + @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION) + public static final String SERVICE_INTERFACE = + "android.service.voice.VisualQueryDetectionService"; + + + /** @hide */ + public static final String KEY_INITIALIZATION_STATUS = "initialization_status"; + + + private final ISandboxedDetectionService mInterface = new ISandboxedDetectionService.Stub() { + + @Override + public void updateState(PersistableBundle options, SharedMemory sharedMemory, + IRemoteCallback callback) throws RemoteException { + Log.v(TAG, "#updateState" + (callback != null ? " with callback" : "")); + VisualQueryDetectionService.this.onUpdateStateInternal( + options, + sharedMemory, + callback); + } + + @Override + public void ping(IRemoteCallback callback) throws RemoteException { + callback.sendResult(null); + } + + @Override + public void detectFromDspSource( + SoundTrigger.KeyphraseRecognitionEvent event, + AudioFormat audioFormat, + long timeoutMillis, + IDspHotwordDetectionCallback callback) { + throw new UnsupportedOperationException("Not supported by VisualQueryDetectionService"); + } + + @Override + public void detectFromMicrophoneSource( + ParcelFileDescriptor audioStream, + @HotwordDetectionService.AudioSource int audioSource, + AudioFormat audioFormat, + PersistableBundle options, + IDspHotwordDetectionCallback callback) { + throw new UnsupportedOperationException("Not supported by VisualQueryDetectionService"); + } + + @Override + public void updateAudioFlinger(IBinder audioFlinger) { + Log.v(TAG, "Ignore #updateAudioFlinger"); + } + + @Override + public void updateContentCaptureManager(IContentCaptureManager manager, + ContentCaptureOptions options) { + Log.v(TAG, "Ignore #updateContentCaptureManager"); + } + + @Override + public void updateRecognitionServiceManager(IRecognitionServiceManager manager) { + Log.v(TAG, "Ignore #updateRecognitionServiceManager"); + } + + @Override + public void stopDetection() { + throw new UnsupportedOperationException("Not supported by VisualQueryDetectionService"); + } + }; + + /** + * {@inheritDoc} + * @hide + */ + @Override + @SystemApi + public void onUpdateState( + @Nullable PersistableBundle options, + @Nullable SharedMemory sharedMemory, + @DurationMillisLong long callbackTimeoutMillis, + @Nullable IntConsumer statusCallback) { + } + + @Override + @Nullable + public IBinder onBind(@NonNull Intent intent) { + if (SERVICE_INTERFACE.equals(intent.getAction())) { + return mInterface.asBinder(); + } + Log.w(TAG, "Tried to bind to wrong intent (should be " + SERVICE_INTERFACE + ": " + + intent); + return null; + } + + private void onUpdateStateInternal(@Nullable PersistableBundle options, + @Nullable SharedMemory sharedMemory, IRemoteCallback callback) { + IntConsumer intConsumer = + SandboxedDetectionServiceBase.createInitializationStatusConsumer(callback); + onUpdateState(options, sharedMemory, UPDATE_TIMEOUT_MILLIS, intConsumer); + } + + /** + * This is called after the service is set up and the client should open the camera and the + * microphone to start recognition. + * + * Called when the {@link VoiceInteractionService} requests that this service + * {@link HotwordDetector#startRecognition()} start recognition on audio coming directly + * from the device microphone. + * + * @param callback The callback to use for responding to the detection request. + * + */ + public void onStartDetection(@NonNull Callback callback) { + throw new UnsupportedOperationException(); + } + + /** + * Called when the {@link VoiceInteractionService} + * {@link HotwordDetector#stopRecognition()} requests that recognition be stopped. + */ + public void onStopDetection() { + } + + /** + * Callback for sending out signals and returning query results. + */ + public static final class Callback { + //TODO: Add callback to send signals to VIS and SysUI. + } + +} diff --git a/core/java/android/service/voice/VoiceInteractionService.java b/core/java/android/service/voice/VoiceInteractionService.java index a59578ee8d9d..9e1518d899e0 100644 --- a/core/java/android/service/voice/VoiceInteractionService.java +++ b/core/java/android/service/voice/VoiceInteractionService.java @@ -168,7 +168,7 @@ public class VoiceInteractionService extends Service { private KeyphraseEnrollmentInfo mKeyphraseEnrollmentInfo; - private final Set<HotwordDetector> mActiveHotwordDetectors = new ArraySet<>(); + private final Set<HotwordDetector> mActiveDetectors = new ArraySet<>(); /** * Called when a user has activated an affordance to launch voice assist from the Keyguard. @@ -319,7 +319,7 @@ public class VoiceInteractionService extends Service { private void onSoundModelsChangedInternal() { synchronized (this) { // TODO: Stop recognition if a sound model that was being recognized gets deleted. - mActiveHotwordDetectors.forEach(detector -> { + mActiveDetectors.forEach(detector -> { if (detector instanceof AlwaysOnHotwordDetector) { ((AlwaysOnHotwordDetector) detector).onSoundModelsChanged(); } @@ -429,7 +429,7 @@ public class VoiceInteractionService extends Service { // Allow only one concurrent recognition via the APIs. safelyShutdownAllHotwordDetectors(); } else { - for (HotwordDetector detector : mActiveHotwordDetectors) { + for (HotwordDetector detector : mActiveDetectors) { if (detector.isUsingSandboxedDetectionService() != supportHotwordDetectionService) { throw new IllegalStateException( @@ -447,13 +447,13 @@ public class VoiceInteractionService extends Service { callback, mKeyphraseEnrollmentInfo, mSystemService, getApplicationContext().getApplicationInfo().targetSdkVersion, supportHotwordDetectionService); - mActiveHotwordDetectors.add(dspDetector); + mActiveDetectors.add(dspDetector); try { dspDetector.registerOnDestroyListener(this::onHotwordDetectorDestroyed); dspDetector.initialize(options, sharedMemory); } catch (Exception e) { - mActiveHotwordDetectors.remove(dspDetector); + mActiveDetectors.remove(dspDetector); dspDetector.destroy(); throw e; } @@ -512,7 +512,7 @@ public class VoiceInteractionService extends Service { // Allow only one concurrent recognition via the APIs. safelyShutdownAllHotwordDetectors(); } else { - for (HotwordDetector detector : mActiveHotwordDetectors) { + for (HotwordDetector detector : mActiveDetectors) { if (!detector.isUsingSandboxedDetectionService()) { throw new IllegalStateException( "It disallows to create trusted and non-trusted detectors " @@ -528,14 +528,14 @@ public class VoiceInteractionService extends Service { SoftwareHotwordDetector softwareHotwordDetector = new SoftwareHotwordDetector( mSystemService, null, callback); - mActiveHotwordDetectors.add(softwareHotwordDetector); + mActiveDetectors.add(softwareHotwordDetector); try { softwareHotwordDetector.registerOnDestroyListener( this::onHotwordDetectorDestroyed); softwareHotwordDetector.initialize(options, sharedMemory); } catch (Exception e) { - mActiveHotwordDetectors.remove(softwareHotwordDetector); + mActiveDetectors.remove(softwareHotwordDetector); softwareHotwordDetector.destroy(); throw e; } @@ -586,7 +586,7 @@ public class VoiceInteractionService extends Service { private void safelyShutdownAllHotwordDetectors() { synchronized (mLock) { - mActiveHotwordDetectors.forEach(detector -> { + mActiveDetectors.forEach(detector -> { try { detector.destroy(); } catch (Exception ex) { @@ -598,13 +598,13 @@ public class VoiceInteractionService extends Service { private void onHotwordDetectorDestroyed(@NonNull HotwordDetector detector) { synchronized (mLock) { - mActiveHotwordDetectors.remove(detector); + mActiveDetectors.remove(detector); shutdownHotwordDetectionServiceIfRequiredLocked(); } } private void shutdownHotwordDetectionServiceIfRequiredLocked() { - for (HotwordDetector detector : mActiveHotwordDetectors) { + for (HotwordDetector detector : mActiveDetectors) { if (detector.isUsingSandboxedDetectionService()) { return; } @@ -638,11 +638,11 @@ public class VoiceInteractionService extends Service { protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { pw.println("VOICE INTERACTION"); synchronized (mLock) { - pw.println(" HotwordDetector(s)"); - if (mActiveHotwordDetectors.size() == 0) { + pw.println(" Sandboxed Detector(s)"); + if (mActiveDetectors.size() == 0) { pw.println(" NULL"); } else { - mActiveHotwordDetectors.forEach(detector -> { + mActiveDetectors.forEach(detector -> { detector.dump(" ", pw); pw.println(); }); diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java index 0eab81c03259..44afb89e1ccc 100644 --- a/core/java/android/service/wallpaper/WallpaperService.java +++ b/core/java/android/service/wallpaper/WallpaperService.java @@ -252,7 +252,7 @@ public abstract class WallpaperService extends Service { final Rect mDispatchedStableInsets = new Rect(); DisplayCutout mDispatchedDisplayCutout = DisplayCutout.NO_CUTOUT; final InsetsState mInsetsState = new InsetsState(); - final InsetsSourceControl[] mTempControls = new InsetsSourceControl[0]; + final InsetsSourceControl.Array mTempControls = new InsetsSourceControl.Array(); final MergedConfiguration mMergedConfiguration = new MergedConfiguration(); final Bundle mSyncSeqIdBundle = new Bundle(); private final Point mSurfaceSize = new Point(); diff --git a/core/java/android/view/EventLogTags.logtags b/core/java/android/view/EventLogTags.logtags index 1ad3472e4c75..cc0b18a652d5 100644 --- a/core/java/android/view/EventLogTags.logtags +++ b/core/java/android/view/EventLogTags.logtags @@ -44,6 +44,12 @@ option java_package android.view 32007 imf_ime_anim_finish (token|3),(animation type|1),(alpha|5),(shown|1),(insets|3) # IME animation is canceled. 32008 imf_ime_anim_cancel (token|3),(animation type|1),(pending insets|3) +# IME remote animation is started. +32009 imf_ime_remote_anim_start (token|3),(displayId|1),(direction|1),(alpha|5),(startY|5),(endY|5),(leash|3),(insets|3),(surface position|3),(ime frame|3) +# IME remote animation is end. +32010 imf_ime_remote_anim_end (token|3),(displayId|1),(direction|1),(endY|5),(leash|3),(insets|3),(surface position|3),(ime frame|3) +# IME remote animation is canceled. +32011 imf_ime_remote_anim_cancel (token|3),(displayId|1),(insets|3) # 62000 - 62199 reserved for inputflinger diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl index 0aba80db5378..e38e5370ec45 100644 --- a/core/java/android/view/IWindowManager.aidl +++ b/core/java/android/view/IWindowManager.aidl @@ -721,8 +721,7 @@ interface IWindowManager * * @return {@code true} if system bars are always consumed. */ - boolean getWindowInsets(in WindowManager.LayoutParams attrs, int displayId, - out InsetsState outInsetsState); + boolean getWindowInsets(int displayId, in IBinder token, out InsetsState outInsetsState); /** * Returns a list of {@link android.view.DisplayInfo} for the logical display. This is not diff --git a/core/java/android/view/IWindowSession.aidl b/core/java/android/view/IWindowSession.aidl index e4d878a59c11..943d64ab986c 100644 --- a/core/java/android/view/IWindowSession.aidl +++ b/core/java/android/view/IWindowSession.aidl @@ -49,12 +49,12 @@ interface IWindowSession { int addToDisplay(IWindow window, in WindowManager.LayoutParams attrs, in int viewVisibility, in int layerStackId, int requestedVisibleTypes, out InputChannel outInputChannel, out InsetsState insetsState, - out InsetsSourceControl[] activeControls, out Rect attachedFrame, + out InsetsSourceControl.Array activeControls, out Rect attachedFrame, out float[] sizeCompatScale); int addToDisplayAsUser(IWindow window, in WindowManager.LayoutParams attrs, in int viewVisibility, in int layerStackId, in int userId, int requestedVisibleTypes, out InputChannel outInputChannel, out InsetsState insetsState, - out InsetsSourceControl[] activeControls, out Rect attachedFrame, + out InsetsSourceControl.Array activeControls, out Rect attachedFrame, out float[] sizeCompatScale); int addToDisplayWithoutInputChannel(IWindow window, in WindowManager.LayoutParams attrs, in int viewVisibility, in int layerStackId, out InsetsState insetsState, @@ -91,7 +91,7 @@ interface IWindowSession { int requestedWidth, int requestedHeight, int viewVisibility, int flags, int seq, int lastSyncSeqId, out ClientWindowFrames outFrames, out MergedConfiguration outMergedConfiguration, out SurfaceControl outSurfaceControl, - out InsetsState insetsState, out InsetsSourceControl[] activeControls, + out InsetsState insetsState, out InsetsSourceControl.Array activeControls, out Bundle bundle); /** diff --git a/core/java/android/view/InputDevice.java b/core/java/android/view/InputDevice.java index 89c7a360a91c..5a9a2520d839 100644 --- a/core/java/android/view/InputDevice.java +++ b/core/java/android/view/InputDevice.java @@ -26,7 +26,6 @@ import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; import android.hardware.BatteryState; import android.hardware.SensorManager; -import android.hardware.input.InputDeviceCountryCode; import android.hardware.input.InputDeviceIdentifier; import android.hardware.input.InputManager; import android.hardware.lights.LightsManager; @@ -75,8 +74,6 @@ public final class InputDevice implements Parcelable { private final int mSources; private final int mKeyboardType; private final KeyCharacterMap mKeyCharacterMap; - @InputDeviceCountryCode - private final int mCountryCode; @Nullable private final String mKeyboardLanguageTag; @Nullable @@ -468,10 +465,9 @@ public final class InputDevice implements Parcelable { */ private InputDevice(int id, int generation, int controllerNumber, String name, int vendorId, int productId, String descriptor, boolean isExternal, int sources, int keyboardType, - KeyCharacterMap keyCharacterMap, @InputDeviceCountryCode int countryCode, - @Nullable String keyboardLanguageTag, @Nullable String keyboardLayoutType, - boolean hasVibrator, boolean hasMicrophone, boolean hasButtonUnderPad, - boolean hasSensor, boolean hasBattery, boolean supportsUsi) { + KeyCharacterMap keyCharacterMap, @Nullable String keyboardLanguageTag, + @Nullable String keyboardLayoutType, boolean hasVibrator, boolean hasMicrophone, + boolean hasButtonUnderPad, boolean hasSensor, boolean hasBattery, boolean supportsUsi) { mId = id; mGeneration = generation; mControllerNumber = controllerNumber; @@ -483,7 +479,6 @@ public final class InputDevice implements Parcelable { mSources = sources; mKeyboardType = keyboardType; mKeyCharacterMap = keyCharacterMap; - mCountryCode = countryCode; if (keyboardLanguageTag != null) { mKeyboardLanguageTag = ULocale .createCanonical(ULocale.forLanguageTag(keyboardLanguageTag)) @@ -513,7 +508,6 @@ public final class InputDevice implements Parcelable { mIsExternal = in.readInt() != 0; mSources = in.readInt(); mKeyboardType = in.readInt(); - mCountryCode = in.readInt(); mKeyboardLanguageTag = in.readString8(); mKeyboardLayoutType = in.readString8(); mHasVibrator = in.readInt() != 0; @@ -558,8 +552,6 @@ public final class InputDevice implements Parcelable { private boolean mHasButtonUnderPad = false; private boolean mHasSensor = false; private boolean mHasBattery = false; - @InputDeviceCountryCode - private int mCountryCode = InputDeviceCountryCode.INVALID; private String mKeyboardLanguageTag = null; private String mKeyboardLayoutType = null; private boolean mSupportsUsi = false; @@ -660,12 +652,6 @@ public final class InputDevice implements Parcelable { return this; } - /** @see InputDevice#getCountryCode() */ - public Builder setCountryCode(@InputDeviceCountryCode int countryCode) { - mCountryCode = countryCode; - return this; - } - /** @see InputDevice#getKeyboardLanguageTag() */ public Builder setKeyboardLanguageTag(String keyboardLanguageTag) { mKeyboardLanguageTag = keyboardLanguageTag; @@ -688,8 +674,8 @@ public final class InputDevice implements Parcelable { public InputDevice build() { return new InputDevice(mId, mGeneration, mControllerNumber, mName, mVendorId, mProductId, mDescriptor, mIsExternal, mSources, mKeyboardType, mKeyCharacterMap, - mCountryCode, mKeyboardLanguageTag, mKeyboardLayoutType, mHasVibrator, - mHasMicrophone, mHasButtonUnderPad, mHasSensor, mHasBattery, mSupportsUsi); + mKeyboardLanguageTag, mKeyboardLayoutType, mHasVibrator, mHasMicrophone, + mHasButtonUnderPad, mHasSensor, mHasBattery, mSupportsUsi); } } @@ -909,16 +895,6 @@ public final class InputDevice implements Parcelable { } /** - * Gets Country code associated with the device - * - * @hide - */ - @InputDeviceCountryCode - public int getCountryCode() { - return mCountryCode; - } - - /** * Returns the keyboard language as an IETF * <a href="https://tools.ietf.org/html/bcp47">BCP-47</a> * conformant tag if available. @@ -1393,7 +1369,6 @@ public final class InputDevice implements Parcelable { out.writeInt(mIsExternal ? 1 : 0); out.writeInt(mSources); out.writeInt(mKeyboardType); - out.writeInt(mCountryCode); out.writeString8(mKeyboardLanguageTag); out.writeString8(mKeyboardLayoutType); out.writeInt(mHasVibrator ? 1 : 0); @@ -1445,8 +1420,6 @@ public final class InputDevice implements Parcelable { } description.append("\n"); - description.append(" Country Code: ").append(mCountryCode).append("\n"); - description.append(" Has Vibrator: ").append(mHasVibrator).append("\n"); description.append(" Has Sensor: ").append(mHasSensor).append("\n"); @@ -1457,6 +1430,15 @@ public final class InputDevice implements Parcelable { description.append(" Supports USI: ").append(mSupportsUsi).append("\n"); + if (mKeyboardLanguageTag != null) { + description.append(" Keyboard language tag: ").append(mKeyboardLanguageTag).append( + "\n"); + } + + if (mKeyboardLayoutType != null) { + description.append(" Keyboard layout type: ").append(mKeyboardLayoutType).append("\n"); + } + description.append(" Sources: 0x").append(Integer.toHexString(mSources)).append(" ("); appendSourceDescriptionIfApplicable(description, SOURCE_KEYBOARD, "keyboard"); appendSourceDescriptionIfApplicable(description, SOURCE_DPAD, "dpad"); diff --git a/core/java/android/view/InsetsSourceControl.aidl b/core/java/android/view/InsetsSourceControl.aidl index 755bf456658d..7301ee761522 100644 --- a/core/java/android/view/InsetsSourceControl.aidl +++ b/core/java/android/view/InsetsSourceControl.aidl @@ -17,3 +17,4 @@ package android.view; parcelable InsetsSourceControl; +parcelable InsetsSourceControl.Array; diff --git a/core/java/android/view/InsetsSourceControl.java b/core/java/android/view/InsetsSourceControl.java index 610cfe40ebce..c849cb5bfe2b 100644 --- a/core/java/android/view/InsetsSourceControl.java +++ b/core/java/android/view/InsetsSourceControl.java @@ -257,4 +257,52 @@ public class InsetsSourceControl implements Parcelable { } proto.end(token); } + + /** + * Used to obtain the array from the argument of a binder call. In this way, the length of the + * array can be dynamic. + */ + public static class Array implements Parcelable { + + private @Nullable InsetsSourceControl[] mControls; + + public Array() { + } + + public Array(Parcel in) { + readFromParcel(in); + } + + public void set(@Nullable InsetsSourceControl[] controls) { + mControls = controls; + } + + public @Nullable InsetsSourceControl[] get() { + return mControls; + } + + @Override + public int describeContents() { + return 0; + } + + public void readFromParcel(Parcel in) { + mControls = in.createTypedArray(InsetsSourceControl.CREATOR); + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeTypedArray(mControls, flags); + } + + public static final @NonNull Creator<Array> CREATOR = new Creator<>() { + public Array createFromParcel(Parcel in) { + return new Array(in); + } + + public Array[] newArray(int size) { + return new Array[size]; + } + }; + } } diff --git a/core/java/android/view/MotionEvent.java b/core/java/android/view/MotionEvent.java index 4fbb249c507f..1ff7ae662da0 100644 --- a/core/java/android/view/MotionEvent.java +++ b/core/java/android/view/MotionEvent.java @@ -1491,11 +1491,23 @@ public final class MotionEvent extends InputEvent implements Parcelable { */ public static final int CLASSIFICATION_TWO_FINGER_SWIPE = 3; + /** + * Classification constant: multi-finger swipe. + * + * The current event stream represents the user swiping with three or more fingers on a + * touchpad. Unlike two-finger swipes, these are only to be handled by the system UI, which is + * why they have a separate constant from two-finger swipes. + * + * @see #getClassification + * @hide + */ + public static final int CLASSIFICATION_MULTI_FINGER_SWIPE = 4; + /** @hide */ @Retention(SOURCE) @IntDef(prefix = { "CLASSIFICATION" }, value = { CLASSIFICATION_NONE, CLASSIFICATION_AMBIGUOUS_GESTURE, CLASSIFICATION_DEEP_PRESS, - CLASSIFICATION_TWO_FINGER_SWIPE}) + CLASSIFICATION_TWO_FINGER_SWIPE, CLASSIFICATION_MULTI_FINGER_SWIPE}) public @interface Classification {}; /** @@ -3941,7 +3953,8 @@ public final class MotionEvent extends InputEvent implements Parcelable { return "DEEP_PRESS"; case CLASSIFICATION_TWO_FINGER_SWIPE: return "TWO_FINGER_SWIPE"; - + case CLASSIFICATION_MULTI_FINGER_SWIPE: + return "MULTI_FINGER_SWIPE"; } return "UNKNOWN"; } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index c0f4731aeaf4..c4da009efaf3 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -23,7 +23,6 @@ import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.INVALID_DISPLAY; import static android.view.InputDevice.SOURCE_CLASS_NONE; import static android.view.InsetsState.ITYPE_IME; -import static android.view.InsetsState.SIZE; import static android.view.View.PFLAG_DRAW_ANIMATION; import static android.view.View.SYSTEM_UI_FLAG_FULLSCREEN; import static android.view.View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; @@ -705,7 +704,7 @@ public final class ViewRootImpl implements ViewParent, private int mRelayoutSeq; private final Rect mWinFrameInScreen = new Rect(); private final InsetsState mTempInsets = new InsetsState(); - private final InsetsSourceControl[] mTempControls = new InsetsSourceControl[SIZE]; + private final InsetsSourceControl.Array mTempControls = new InsetsSourceControl.Array(); private final WindowConfiguration mTempWinConfig = new WindowConfiguration(); private float mInvCompatScale = 1f; final ViewTreeObserver.InternalInsetsInfo mLastGivenInsets @@ -1264,7 +1263,7 @@ public final class ViewRootImpl implements ViewParent, } if (mTranslator != null) { mTranslator.translateInsetsStateInScreenToAppWindow(mTempInsets); - mTranslator.translateSourceControlsInScreenToAppWindow(mTempControls); + mTranslator.translateSourceControlsInScreenToAppWindow(mTempControls.get()); mTranslator.translateRectInScreenToAppWindow(attachedFrame); } mTmpFrames.attachedFrame = attachedFrame; @@ -1288,7 +1287,7 @@ public final class ViewRootImpl implements ViewParent, (res & WindowManagerGlobal.ADD_FLAG_ALWAYS_CONSUME_SYSTEM_BARS) != 0; mPendingAlwaysConsumeSystemBars = mAttachInfo.mAlwaysConsumeSystemBars; mInsetsController.onStateChanged(mTempInsets); - mInsetsController.onControlsChanged(mTempControls); + mInsetsController.onControlsChanged(mTempControls.get()); final InsetsState state = mInsetsController.getState(); final Rect displayCutoutSafe = mTempRect; state.getDisplayCutoutSafe(displayCutoutSafe); @@ -8334,12 +8333,12 @@ public final class ViewRootImpl implements ViewParent, mTranslator.translateRectInScreenToAppWindow(mTmpFrames.displayFrame); mTranslator.translateRectInScreenToAppWindow(mTmpFrames.attachedFrame); mTranslator.translateInsetsStateInScreenToAppWindow(mTempInsets); - mTranslator.translateSourceControlsInScreenToAppWindow(mTempControls); + mTranslator.translateSourceControlsInScreenToAppWindow(mTempControls.get()); } mInvCompatScale = 1f / mTmpFrames.compatScale; CompatibilityInfo.applyOverrideScaleIfNeeded(mPendingMergedConfiguration); mInsetsController.onStateChanged(mTempInsets); - mInsetsController.onControlsChanged(mTempControls); + mInsetsController.onControlsChanged(mTempControls.get()); mPendingAlwaysConsumeSystemBars = (relayoutResult & RELAYOUT_RES_CONSUME_ALWAYS_SYSTEM_BARS) != 0; diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index f375ccb7e207..43cf75859ce9 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -814,6 +814,45 @@ public interface WindowManager extends ViewManager { } /** + * Activity level {@link android.content.pm.PackageManager.Property PackageManager + * .Property} for an app to inform the system that the activity can be opted-in or opted-out + * from the compatibility treatment that avoids {@link + * android.app.Activity#setRequestedOrientation} loops. The loop can be trigerred by + * ignoreRequestedOrientation display setting enabled on the device or by the landscape natural + * orientation of the device. + * + * <p>The treatment is disabled by default but device manufacturers can enable the treatment + * using their discretion to improve display compatibility. + * + * <p>With this property set to {@code true}, the system could ignore {@link + * android.app.Activity#setRequestedOrientation} call from an app if one of the following + * conditions are true: + * <ul> + * <li>Activity is relaunching due to the previous {@link + * android.app.Activity#setRequestedOrientation} call. + * <li>Camera compatibility force rotation treatment is active for the package. + * </ul> + * + * <p>Setting this property to {@code false} informs the system that the activity must be + * opted-out from the compatibility treatment even if the device manufacturer has opted the app + * into the treatment. + * + * <p><b>Syntax:</b> + * <pre> + * <activity> + * <property + * android:name="android.window.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION" + * android:value="true|false"/> + * </activity> + * </pre> + * + * @hide + */ + // TODO(b/263984287): Make this public API. + String PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION = + "android.window.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION"; + + /** * @hide */ public static final String PARCEL_KEY_SHORTCUTS_ARRAY = "shortcuts_array"; diff --git a/core/java/android/view/WindowlessWindowManager.java b/core/java/android/view/WindowlessWindowManager.java index 4c95728548c5..edf33f15a9ca 100644 --- a/core/java/android/view/WindowlessWindowManager.java +++ b/core/java/android/view/WindowlessWindowManager.java @@ -148,7 +148,7 @@ public class WindowlessWindowManager implements IWindowSession { public int addToDisplay(IWindow window, WindowManager.LayoutParams attrs, int viewVisibility, int displayId, @InsetsType int requestedVisibleTypes, InputChannel outInputChannel, InsetsState outInsetsState, - InsetsSourceControl[] outActiveControls, Rect outAttachedFrame, + InsetsSourceControl.Array outActiveControls, Rect outAttachedFrame, float[] outSizeCompatScale) { final SurfaceControl.Builder b = new SurfaceControl.Builder(mSurfaceSession) .setFormat(attrs.format) @@ -200,7 +200,7 @@ public class WindowlessWindowManager implements IWindowSession { public int addToDisplayAsUser(IWindow window, WindowManager.LayoutParams attrs, int viewVisibility, int displayId, int userId, @InsetsType int requestedVisibleTypes, InputChannel outInputChannel, InsetsState outInsetsState, - InsetsSourceControl[] outActiveControls, Rect outAttachedFrame, + InsetsSourceControl.Array outActiveControls, Rect outAttachedFrame, float[] outSizeCompatScale) { return addToDisplay(window, attrs, viewVisibility, displayId, requestedVisibleTypes, outInputChannel, outInsetsState, outActiveControls, outAttachedFrame, @@ -290,7 +290,7 @@ public class WindowlessWindowManager implements IWindowSession { int requestedWidth, int requestedHeight, int viewFlags, int flags, int seq, int lastSyncSeqId, ClientWindowFrames outFrames, MergedConfiguration outMergedConfiguration, SurfaceControl outSurfaceControl, - InsetsState outInsetsState, InsetsSourceControl[] outActiveControls, + InsetsState outInsetsState, InsetsSourceControl.Array outActiveControls, Bundle outSyncSeqIdBundle) { final State state; synchronized (this) { diff --git a/core/java/android/view/inputmethod/ImeTracker.java b/core/java/android/view/inputmethod/ImeTracker.java index 9ed5c29089b2..3b6ec800836a 100644 --- a/core/java/android/view/inputmethod/ImeTracker.java +++ b/core/java/android/view/inputmethod/ImeTracker.java @@ -44,8 +44,7 @@ public interface ImeTracker { String TAG = "ImeTracker"; /** The debug flag for IME visibility event log. */ - // TODO(b/239501597) : Have a system property to control this flag. - boolean DEBUG_IME_VISIBILITY = false; + boolean DEBUG_IME_VISIBILITY = SystemProperties.getBoolean("persist.debug.imf_event", false); /** The message to indicate if there is no valid {@link Token}. */ String TOKEN_NONE = "TOKEN_NONE"; diff --git a/core/java/android/window/TaskFragmentAnimationParams.aidl b/core/java/android/window/TaskFragmentAnimationParams.aidl new file mode 100644 index 000000000000..04dee58089d4 --- /dev/null +++ b/core/java/android/window/TaskFragmentAnimationParams.aidl @@ -0,0 +1,23 @@ +/* + * 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.window; + +/** + * Data object for animation related override of TaskFragment. + * @hide + */ +parcelable TaskFragmentAnimationParams; diff --git a/core/java/android/window/TaskFragmentAnimationParams.java b/core/java/android/window/TaskFragmentAnimationParams.java new file mode 100644 index 000000000000..a600a4db42b8 --- /dev/null +++ b/core/java/android/window/TaskFragmentAnimationParams.java @@ -0,0 +1,129 @@ +/* + * 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.window; + +import android.annotation.ColorInt; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Data object for animation related override of TaskFragment. + * @hide + */ +// TODO(b/206557124): Add more animation customization options. +public final class TaskFragmentAnimationParams implements Parcelable { + + /** The default {@link TaskFragmentAnimationParams} to use when there is no app override. */ + public static final TaskFragmentAnimationParams DEFAULT = + new TaskFragmentAnimationParams.Builder().build(); + + @ColorInt + private final int mAnimationBackgroundColor; + + private TaskFragmentAnimationParams(@ColorInt int animationBackgroundColor) { + mAnimationBackgroundColor = animationBackgroundColor; + } + + /** + * The {@link ColorInt} to use for the background during the animation with this TaskFragment if + * the animation requires a background. + * + * The default value is {@code 0}, which is to use the theme window background. + */ + @ColorInt + public int getAnimationBackgroundColor() { + return mAnimationBackgroundColor; + } + + private TaskFragmentAnimationParams(Parcel in) { + mAnimationBackgroundColor = in.readInt(); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mAnimationBackgroundColor); + } + + @NonNull + public static final Creator<TaskFragmentAnimationParams> CREATOR = + new Creator<TaskFragmentAnimationParams>() { + @Override + public TaskFragmentAnimationParams createFromParcel(Parcel in) { + return new TaskFragmentAnimationParams(in); + } + + @Override + public TaskFragmentAnimationParams[] newArray(int size) { + return new TaskFragmentAnimationParams[size]; + } + }; + + @Override + public String toString() { + return "TaskFragmentAnimationParams{" + + " animationBgColor=" + Integer.toHexString(mAnimationBackgroundColor) + + "}"; + } + + @Override + public int hashCode() { + return mAnimationBackgroundColor; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (!(obj instanceof TaskFragmentAnimationParams)) { + return false; + } + final TaskFragmentAnimationParams other = (TaskFragmentAnimationParams) obj; + return mAnimationBackgroundColor == other.mAnimationBackgroundColor; + } + + @Override + public int describeContents() { + return 0; + } + + /** Builder to construct the {@link TaskFragmentAnimationParams}. */ + public static final class Builder { + + @ColorInt + private int mAnimationBackgroundColor = 0; + + /** + * Sets the {@link ColorInt} to use for the background during the animation with this + * TaskFragment if the animation requires a background. The default value is + * {@code 0}, which is to use the theme window background. + * + * @param color a packed color int, {@code AARRGGBB}, for the animation background color. + * @return this {@link Builder}. + */ + @NonNull + public Builder setAnimationBackgroundColor(@ColorInt int color) { + mAnimationBackgroundColor = color; + return this; + } + + /** Constructs the {@link TaskFragmentAnimationParams}. */ + @NonNull + public TaskFragmentAnimationParams build() { + return new TaskFragmentAnimationParams(mAnimationBackgroundColor); + } + } +} diff --git a/core/java/android/window/TaskFragmentOperation.aidl b/core/java/android/window/TaskFragmentOperation.aidl new file mode 100644 index 000000000000..c21700c6634b --- /dev/null +++ b/core/java/android/window/TaskFragmentOperation.aidl @@ -0,0 +1,23 @@ +/* + * 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.window; + +/** + * Data object of params for TaskFragment related {@link WindowContainerTransaction} operation. + * @hide + */ +parcelable TaskFragmentOperation; diff --git a/core/java/android/window/TaskFragmentOperation.java b/core/java/android/window/TaskFragmentOperation.java new file mode 100644 index 000000000000..bec6c58e4c8a --- /dev/null +++ b/core/java/android/window/TaskFragmentOperation.java @@ -0,0 +1,168 @@ +/* + * 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.window; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.IBinder; +import android.os.Parcel; +import android.os.Parcelable; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Objects; + +/** + * Data object of params for TaskFragment related {@link WindowContainerTransaction} operation. + * + * @see WindowContainerTransaction#setTaskFragmentOperation(IBinder, TaskFragmentOperation). + * @hide + */ +// TODO(b/263436063): move other TaskFragment related operation here. +public final class TaskFragmentOperation implements Parcelable { + + /** Sets the {@link TaskFragmentAnimationParams} for the given TaskFragment. */ + public static final int OP_TYPE_SET_ANIMATION_PARAMS = 0; + + @IntDef(prefix = { "OP_TYPE_" }, value = { + OP_TYPE_SET_ANIMATION_PARAMS + }) + @Retention(RetentionPolicy.SOURCE) + @interface OperationType {} + + @OperationType + private final int mOpType; + + @Nullable + private final TaskFragmentAnimationParams mAnimationParams; + + private TaskFragmentOperation(@OperationType int opType, + @Nullable TaskFragmentAnimationParams animationParams) { + mOpType = opType; + mAnimationParams = animationParams; + } + + private TaskFragmentOperation(Parcel in) { + mOpType = in.readInt(); + mAnimationParams = in.readTypedObject(TaskFragmentAnimationParams.CREATOR); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mOpType); + dest.writeTypedObject(mAnimationParams, flags); + } + + @NonNull + public static final Creator<TaskFragmentOperation> CREATOR = + new Creator<TaskFragmentOperation>() { + @Override + public TaskFragmentOperation createFromParcel(Parcel in) { + return new TaskFragmentOperation(in); + } + + @Override + public TaskFragmentOperation[] newArray(int size) { + return new TaskFragmentOperation[size]; + } + }; + + /** + * Gets the {@link OperationType} of this {@link TaskFragmentOperation}. + */ + @OperationType + public int getOpType() { + return mOpType; + } + + /** + * Gets the animation related override of TaskFragment. + */ + @Nullable + public TaskFragmentAnimationParams getAnimationParams() { + return mAnimationParams; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append("TaskFragmentOperation{ opType=").append(mOpType); + if (mAnimationParams != null) { + sb.append(", animationParams=").append(mAnimationParams); + } + + sb.append('}'); + return sb.toString(); + } + + @Override + public int hashCode() { + int result = mOpType; + result = result * 31 + mAnimationParams.hashCode(); + return result; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (!(obj instanceof TaskFragmentOperation)) { + return false; + } + final TaskFragmentOperation other = (TaskFragmentOperation) obj; + return mOpType == other.mOpType + && Objects.equals(mAnimationParams, other.mAnimationParams); + } + + @Override + public int describeContents() { + return 0; + } + + /** Builder to construct the {@link TaskFragmentOperation}. */ + public static final class Builder { + + @OperationType + private final int mOpType; + + @Nullable + private TaskFragmentAnimationParams mAnimationParams; + + /** + * @param opType the {@link OperationType} of this {@link TaskFragmentOperation}. + */ + public Builder(@OperationType int opType) { + mOpType = opType; + } + + /** + * Sets the {@link TaskFragmentAnimationParams} for the given TaskFragment. + */ + @NonNull + public Builder setAnimationParams(@Nullable TaskFragmentAnimationParams animationParams) { + mAnimationParams = animationParams; + return this; + } + + /** + * Constructs the {@link TaskFragmentOperation}. + */ + @NonNull + public TaskFragmentOperation build() { + return new TaskFragmentOperation(mOpType, mAnimationParams); + } + } +} diff --git a/core/java/android/window/WindowContainerTransaction.java b/core/java/android/window/WindowContainerTransaction.java index 5793674caaa6..647ccf51b5ef 100644 --- a/core/java/android/window/WindowContainerTransaction.java +++ b/core/java/android/window/WindowContainerTransaction.java @@ -751,6 +751,30 @@ public final class WindowContainerTransaction implements Parcelable { } /** + * Sets the {@link TaskFragmentOperation} to apply to the given TaskFragment. + * + * @param fragmentToken client assigned unique token to create TaskFragment with specified in + * {@link TaskFragmentCreationParams#getFragmentToken()}. + * @param taskFragmentOperation the {@link TaskFragmentOperation} to apply to the given + * TaskFramgent. + * @hide + */ + @NonNull + public WindowContainerTransaction setTaskFragmentOperation(@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) + .setContainer(fragmentToken) + .setTaskFragmentOperation(taskFragmentOperation) + .build(); + mHierarchyOps.add(hierarchyOp); + return this; + } + + /** * Sets/removes the always on top flag for this {@code windowContainer}. See * {@link com.android.server.wm.ConfigurationContainer#setAlwaysOnTop(boolean)}. * Please note that this method is only intended to be used for a @@ -1261,6 +1285,7 @@ public final class WindowContainerTransaction implements Parcelable { 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; // The following key(s) are for use with mLaunchOptions: // When launching a task (eg. from recents), this is the taskId to be launched. @@ -1301,10 +1326,14 @@ public final class WindowContainerTransaction implements Parcelable { @Nullable private Intent mActivityIntent; - // Used as options for WindowContainerTransaction#createTaskFragment(). + /** Used as options for {@link #createTaskFragment}. */ @Nullable private TaskFragmentCreationParams mTaskFragmentCreationOptions; + /** Used as options for {@link #setTaskFragmentOperation}. */ + @Nullable + private TaskFragmentOperation mTaskFragmentOperation; + @Nullable private PendingIntent mPendingIntent; @@ -1424,6 +1453,7 @@ public final class WindowContainerTransaction implements Parcelable { mLaunchOptions = copy.mLaunchOptions; mActivityIntent = copy.mActivityIntent; mTaskFragmentCreationOptions = copy.mTaskFragmentCreationOptions; + mTaskFragmentOperation = copy.mTaskFragmentOperation; mPendingIntent = copy.mPendingIntent; mShortcutInfo = copy.mShortcutInfo; mAlwaysOnTop = copy.mAlwaysOnTop; @@ -1447,6 +1477,7 @@ public final class WindowContainerTransaction implements Parcelable { 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); mAlwaysOnTop = in.readBoolean(); @@ -1535,6 +1566,11 @@ public final class WindowContainerTransaction implements Parcelable { } @Nullable + public TaskFragmentOperation getTaskFragmentOperation() { + return mTaskFragmentOperation; + } + + @Nullable public PendingIntent getPendingIntent() { return mPendingIntent; } @@ -1612,6 +1648,9 @@ public final class WindowContainerTransaction implements Parcelable { 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 + + " operation= " + mTaskFragmentOperation + "}"; default: return "{mType=" + mType + " container=" + mContainer + " reparent=" + mReparent + " mToTop=" + mToTop @@ -1639,6 +1678,7 @@ public final class WindowContainerTransaction implements Parcelable { dest.writeBundle(mLaunchOptions); dest.writeTypedObject(mActivityIntent, flags); dest.writeTypedObject(mTaskFragmentCreationOptions, flags); + dest.writeTypedObject(mTaskFragmentOperation, flags); dest.writeTypedObject(mPendingIntent, flags); dest.writeTypedObject(mShortcutInfo, flags); dest.writeBoolean(mAlwaysOnTop); @@ -1696,6 +1736,9 @@ public final class WindowContainerTransaction implements Parcelable { private TaskFragmentCreationParams mTaskFragmentCreationOptions; @Nullable + private TaskFragmentOperation mTaskFragmentOperation; + + @Nullable private PendingIntent mPendingIntent; @Nullable @@ -1775,6 +1818,12 @@ public final class WindowContainerTransaction implements Parcelable { return this; } + Builder setTaskFragmentOperation( + @Nullable TaskFragmentOperation taskFragmentOperation) { + mTaskFragmentOperation = taskFragmentOperation; + return this; + } + Builder setReparentLeafTaskIfRelaunch(boolean reparentLeafTaskIfRelaunch) { mReparentLeafTaskIfRelaunch = reparentLeafTaskIfRelaunch; return this; @@ -1804,6 +1853,7 @@ public final class WindowContainerTransaction implements Parcelable { hierarchyOp.mPendingIntent = mPendingIntent; hierarchyOp.mAlwaysOnTop = mAlwaysOnTop; hierarchyOp.mTaskFragmentCreationOptions = mTaskFragmentCreationOptions; + hierarchyOp.mTaskFragmentOperation = mTaskFragmentOperation; hierarchyOp.mShortcutInfo = mShortcutInfo; hierarchyOp.mReparentLeafTaskIfRelaunch = mReparentLeafTaskIfRelaunch; diff --git a/core/java/android/window/WindowMetricsController.java b/core/java/android/window/WindowMetricsController.java index 5b08879ae266..06449d519f68 100644 --- a/core/java/android/window/WindowMetricsController.java +++ b/core/java/android/window/WindowMetricsController.java @@ -24,8 +24,10 @@ import android.annotation.NonNull; import android.app.ResourcesManager; import android.app.WindowConfiguration; import android.content.Context; +import android.content.res.CompatibilityInfo; import android.content.res.Configuration; import android.graphics.Rect; +import android.os.IBinder; import android.os.RemoteException; import android.util.DisplayMetrics; import android.view.Display; @@ -89,23 +91,16 @@ public final class WindowMetricsController { isScreenRound = config.isScreenRound(); windowingMode = winConfig.getWindowingMode(); } - final WindowInsets windowInsets = computeWindowInsets(bounds, isScreenRound, windowingMode); + final IBinder token = Context.getToken(mContext); + final WindowInsets windowInsets = getWindowInsetsFromServerForCurrentDisplay(token, + bounds, isScreenRound, windowingMode); return new WindowMetrics(bounds, windowInsets, density); } - private WindowInsets computeWindowInsets(Rect bounds, boolean isScreenRound, - @WindowConfiguration.WindowingMode int windowingMode) { - // Initialize params which used for obtaining all system insets. - final WindowManager.LayoutParams params = new WindowManager.LayoutParams(); - params.token = Context.getToken(mContext); - return getWindowInsetsFromServerForCurrentDisplay(params, bounds, isScreenRound, - windowingMode); - } - private WindowInsets getWindowInsetsFromServerForCurrentDisplay( - WindowManager.LayoutParams attrs, Rect bounds, boolean isScreenRound, + IBinder token, Rect bounds, boolean isScreenRound, @WindowConfiguration.WindowingMode int windowingMode) { - return getWindowInsetsFromServerForDisplay(mContext.getDisplayId(), attrs, bounds, + return getWindowInsetsFromServerForDisplay(mContext.getDisplayId(), token, bounds, isScreenRound, windowingMode); } @@ -113,22 +108,26 @@ public final class WindowMetricsController { * Retrieves WindowInsets for the given context and display, given the window bounds. * * @param displayId the ID of the logical display to calculate insets for - * @param attrs the LayoutParams for the calling app + * @param token the token of Activity or WindowContext * @param bounds the window bounds to calculate insets for * @param isScreenRound if the display identified by displayId is round * @param windowingMode the windowing mode of the window to calculate insets for * @return WindowInsets calculated for the given window bounds, on the given display */ - private static WindowInsets getWindowInsetsFromServerForDisplay(int displayId, - WindowManager.LayoutParams attrs, Rect bounds, boolean isScreenRound, - int windowingMode) { + private static WindowInsets getWindowInsetsFromServerForDisplay(int displayId, IBinder token, + Rect bounds, boolean isScreenRound, int windowingMode) { try { final InsetsState insetsState = new InsetsState(); final boolean alwaysConsumeSystemBars = WindowManagerGlobal.getWindowManagerService() - .getWindowInsets(attrs, displayId, insetsState); - return insetsState.calculateInsets(bounds, null /* ignoringVisibilityState*/, - isScreenRound, alwaysConsumeSystemBars, SOFT_INPUT_ADJUST_NOTHING, attrs.flags, - SYSTEM_UI_FLAG_VISIBLE, attrs.type, windowingMode, + .getWindowInsets(displayId, token, insetsState); + final float overrideInvScale = CompatibilityInfo.getOverrideInvertedScale(); + if (overrideInvScale != 1f) { + insetsState.scale(overrideInvScale); + } + return insetsState.calculateInsets(bounds, null /* ignoringVisibilityState */, + isScreenRound, alwaysConsumeSystemBars, SOFT_INPUT_ADJUST_NOTHING, + 0 /* flags */, SYSTEM_UI_FLAG_VISIBLE, + WindowManager.LayoutParams.INVALID_WINDOW_TYPE, windowingMode, null /* typeSideMap */); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); @@ -149,7 +148,6 @@ public final class WindowMetricsController { Set<WindowMetrics> maxMetrics = new HashSet<>(); WindowInsets windowInsets; DisplayInfo currentDisplayInfo; - final WindowManager.LayoutParams params = new WindowManager.LayoutParams(); for (int i = 0; i < possibleDisplayInfos.size(); i++) { currentDisplayInfo = possibleDisplayInfos.get(i); @@ -162,7 +160,7 @@ public final class WindowMetricsController { // Initialize insets based upon display rotation. Note any window-provided insets // will not be set. windowInsets = getWindowInsetsFromServerForDisplay( - currentDisplayInfo.displayId, params, + currentDisplayInfo.displayId, null /* token */, new Rect(0, 0, currentDisplayInfo.getNaturalWidth(), currentDisplayInfo.getNaturalHeight()), isScreenRound, WINDOWING_MODE_FULLSCREEN); diff --git a/core/jni/android_view_InputDevice.cpp b/core/jni/android_view_InputDevice.cpp index 02f6a77828c6..7002d9b4c489 100644 --- a/core/jni/android_view_InputDevice.cpp +++ b/core/jni/android_view_InputDevice.cpp @@ -78,11 +78,10 @@ jobject android_view_InputDevice_create(JNIEnv* env, const InputDeviceInfo& devi static_cast<int32_t>(ident.product), descriptorObj.get(), deviceInfo.isExternal(), deviceInfo.getSources(), deviceInfo.getKeyboardType(), kcmObj.get(), - deviceInfo.getCountryCode(), keyboardLanguageTagObj.get(), - keyboardLayoutTypeObj.get(), deviceInfo.hasVibrator(), - deviceInfo.hasMic(), deviceInfo.hasButtonUnderPad(), - deviceInfo.hasSensor(), deviceInfo.hasBattery(), - deviceInfo.supportsUsi())); + keyboardLanguageTagObj.get(), keyboardLayoutTypeObj.get(), + deviceInfo.hasVibrator(), deviceInfo.hasMic(), + deviceInfo.hasButtonUnderPad(), deviceInfo.hasSensor(), + deviceInfo.hasBattery(), deviceInfo.supportsUsi())); // Note: We do not populate the Bluetooth address into the InputDevice object to avoid leaking // it to apps that do not have the Bluetooth permission. @@ -106,7 +105,7 @@ int register_android_view_InputDevice(JNIEnv* env) gInputDeviceClassInfo.ctor = GetMethodIDOrDie(env, gInputDeviceClassInfo.clazz, "<init>", "(IIILjava/lang/String;IILjava/lang/" - "String;ZIILandroid/view/KeyCharacterMap;ILjava/" + "String;ZIILandroid/view/KeyCharacterMap;Ljava/" "lang/String;Ljava/lang/String;ZZZZZZ)V"); gInputDeviceClassInfo.addMotionRange = GetMethodIDOrDie(env, gInputDeviceClassInfo.clazz, diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index a7c48f3a4e8f..bfa530143380 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -3121,6 +3121,34 @@ <permission android:name="android.permission.MANAGE_DEVICE_POLICY_APP_EXEMPTIONS" android:protectionLevel="signature|role" /> + <!-- Allows an application to manage date and time device policy. --> + <permission android:name="android.permission.MANAGE_DEVICE_POLICY_TIME" + android:protectionLevel="internal|role" /> + + <!-- Allows an application to set device policies outside the current user + that are critical for securing data within the current user. + <p>Holding this permission allows the use of other held MANAGE_DEVICE_POLICY_* + permissions across all users on the device provided they are required for securing data + within the current user.--> + <permission android:name="android.permission.MANAGE_DEVICE_POLICY_ACROSS_USERS_SECURITY_CRITICAL" + android:protectionLevel="internal|role" /> + + <!-- Allows an application to set device policies outside the current user + that are required for securing device ownership without accessing user data. + <p>Holding this permission allows the use of other held MANAGE_DEVICE_POLICY_* + permissions across all users on the device provided they do not grant access to user + data. --> + <permission android:name="android.permission.MANAGE_DEVICE_POLICY_ACROSS_USERS" + android:protectionLevel="internal|role" /> + + <!-- Allows an application to set device policies outside the current user. + <p>Fuller form of {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_ACROSS_USERS} + that removes the restriction on accessing user data. + <p>Holding this permission allows the use of any other held MANAGE_DEVICE_POLICY_* + permissions across all users on the device.--> + <permission android:name="android.permission.MANAGE_DEVICE_POLICY_ACROSS_USERS_FULL" + android:protectionLevel="internal|role" /> + <!-- @SystemApi @hide Allows an application to set a device owner on retail demo devices.--> <permission android:name="android.permission.PROVISION_DEMO_DEVICE" android:protectionLevel="signature|setup" /> @@ -6827,6 +6855,12 @@ <permission android:name="android.permission.REMAP_MODIFIER_KEYS" android:protectionLevel="signature" /> + <!-- Allows low-level access to monitor keyboard backlight changes. + <p>Not for use by third-party applications. + @hide --> + <permission android:name="android.permission.MONITOR_KEYBOARD_BACKLIGHT" + android:protectionLevel="signature" /> + <uses-permission android:name="android.permission.HANDLE_QUERY_PACKAGE_RESTART" /> <!-- Allows financed device kiosk apps to perform actions on the Device Lock service @@ -6865,6 +6899,12 @@ <permission android:name="android.permission.GET_APP_METADATA" android:protectionLevel="signature" /> + <!-- @SystemApi Allows the holder to call health connect migration APIs. + @hide --> + <permission android:name="android.permission.MIGRATE_HEALTH_CONNECT_DATA" + android:protectionLevel="signature|knownSigner" + android:knownCerts="@array/config_healthConnectMigrationKnownSigners" /> + <!-- Attribution for Geofencing service. --> <attribution android:tag="GeofencingService" android:label="@string/geofencing_service"/> <!-- Attribution for Country Detector. --> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 23103679f440..a4d6fdd28054 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -5245,6 +5245,14 @@ having a separating hinge. --> <bool name="config_isDisplayHingeAlwaysSeparating">false</bool> + <!-- Whether enabling rotation compat policy for immersive apps that prevents auto rotation + into non-optimal screen orientation while in fullscreen. This is needed because immersive + apps, such as games, are often not optimized for all orientations and can have a poor UX + when rotated. Additionally, some games rely on sensors for the gameplay so users can + trigger such rotations accidentally when auto rotation is on. + Applicable only if ignoreOrientationRequest is enabled. --> + <bool name="config_letterboxIsDisplayRotationImmersiveAppCompatPolicyEnabled">false</bool> + <!-- Aspect ratio of letterboxing for fixed orientation. Values <= 1.0 will be ignored. Note: Activity min/max aspect ratio restrictions will still be respected. Therefore this override can control the maximum screen area that can be occupied by @@ -5360,6 +5368,11 @@ If given value is outside of this range, the option 0 (top) is assummed. --> <integer name="config_letterboxDefaultPositionForTabletopModeReachability">0</integer> + <!-- Whether should ignore app requested orientation in response to an app + calling Activity#setRequestedOrientation. See + LetterboxUiController#shouldIgnoreRequestedOrientation for details. --> + <bool name="config_letterboxIsPolicyForIgnoringRequestedOrientationEnabled">false</bool> + <!-- Whether displaying letterbox education is enabled for letterboxed fullscreen apps. --> <bool name="config_letterboxIsEducationEnabled">false</bool> @@ -6113,4 +6126,9 @@ <item>@string/config_mainDisplayShape</item> <item>@string/config_secondaryDisplayShape</item> </string-array> + <!-- Certificate digests for trusted apps that will be allowed to obtain the knownSigner Health + Connect Migration permissions. The digest should be computed over the DER encoding of the + trusted certificate using the SHA-256 digest algorithm. --> + <string-array name="config_healthConnectMigrationKnownSigners"> + </string-array> </resources> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index af29b233ba83..cd39e590310b 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -945,7 +945,6 @@ <java-symbol type="string" name="serviceErased" /> <java-symbol type="string" name="serviceNotProvisioned" /> <java-symbol type="string" name="serviceRegistered" /> - <java-symbol type="string" name="setup_autofill" /> <java-symbol type="string" name="share" /> <java-symbol type="string" name="shareactionprovider_share_with" /> <java-symbol type="string" name="shareactionprovider_share_with_application" /> @@ -4414,6 +4413,7 @@ <java-symbol type="dimen" name="controls_thumbnail_image_max_height" /> <java-symbol type="dimen" name="controls_thumbnail_image_max_width" /> + <java-symbol type="bool" name="config_letterboxIsDisplayRotationImmersiveAppCompatPolicyEnabled" /> <java-symbol type="dimen" name="config_fixedOrientationLetterboxAspectRatio" /> <java-symbol type="dimen" name="config_letterboxBackgroundWallpaperBlurRadius" /> <java-symbol type="integer" name="config_letterboxActivityCornersRadius" /> @@ -4430,6 +4430,7 @@ <java-symbol type="integer" name="config_letterboxDefaultPositionForVerticalReachability" /> <java-symbol type="integer" name="config_letterboxDefaultPositionForBookModeReachability" /> <java-symbol type="integer" name="config_letterboxDefaultPositionForTabletopModeReachability" /> + <java-symbol type="bool" name="config_letterboxIsPolicyForIgnoringRequestedOrientationEnabled" /> <java-symbol type="bool" name="config_letterboxIsEducationEnabled" /> <java-symbol type="dimen" name="config_letterboxDefaultMinAspectRatioForUnresizableApps" /> <java-symbol type="bool" name="config_letterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled" /> diff --git a/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java b/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java index abbbb2f516b2..e164e08e66e6 100644 --- a/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java +++ b/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java @@ -228,6 +228,20 @@ public class ActivityThreadTest { InstrumentationRegistry.getInstrumentation().waitForIdleSync(); assertScreenScale(scale, activity, originalActivityConfig, originalActivityMetrics); + + // Execute a local relaunch item with current scaled config (e.g. simulate recreate), + // the config should not be scaled again. + final Configuration currentConfig = activity.getResources().getConfiguration(); + final ClientTransaction localTransaction = + newTransaction(activityThread, activity.getActivityToken()); + localTransaction.addCallback(ActivityRelaunchItem.obtain( + null /* pendingResults */, null /* pendingIntents */, 0 /* configChanges */, + new MergedConfiguration(currentConfig, currentConfig), + true /* preserveWindow */)); + InstrumentationRegistry.getInstrumentation().runOnMainSync( + () -> activityThread.executeTransaction(localTransaction)); + + assertScreenScale(scale, activity, originalActivityConfig, originalActivityMetrics); } finally { CompatibilityInfo.setOverrideInvertedScale(originalScale); InstrumentationRegistry.getInstrumentation().runOnMainSync( diff --git a/core/tests/coretests/src/android/companion/virtual/OWNERS b/core/tests/coretests/src/android/companion/virtual/OWNERS index 1a3e927a106f..2e475a9a2742 100644 --- a/core/tests/coretests/src/android/companion/virtual/OWNERS +++ b/core/tests/coretests/src/android/companion/virtual/OWNERS @@ -1,3 +1 @@ -set noparent - include /services/companion/java/com/android/server/companion/virtual/OWNERS
\ No newline at end of file diff --git a/core/tests/coretests/src/android/hardware/input/KeyboardBacklightListenerTest.kt b/core/tests/coretests/src/android/hardware/input/KeyboardBacklightListenerTest.kt new file mode 100644 index 000000000000..91d19a19379d --- /dev/null +++ b/core/tests/coretests/src/android/hardware/input/KeyboardBacklightListenerTest.kt @@ -0,0 +1,163 @@ +/* + * 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.hardware.input + +import android.os.Handler +import android.os.HandlerExecutor +import android.os.test.TestLooper +import android.platform.test.annotations.Presubmit +import com.android.server.testutils.any +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.doAnswer +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoJUnitRunner +import java.util.concurrent.Executor +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.fail + +/** + * Tests for [InputManager.KeyboardBacklightListener]. + * + * Build/Install/Run: + * atest FrameworksCoreTests:KeyboardBacklightListenerTest + */ +@Presubmit +@RunWith(MockitoJUnitRunner::class) +class KeyboardBacklightListenerTest { + @get:Rule + val rule = MockitoJUnit.rule()!! + + private lateinit var testLooper: TestLooper + private var registeredListener: IKeyboardBacklightListener? = null + private lateinit var executor: Executor + private lateinit var inputManager: InputManager + + @Mock + private lateinit var iInputManagerMock: IInputManager + + @Before + fun setUp() { + testLooper = TestLooper() + executor = HandlerExecutor(Handler(testLooper.looper)) + registeredListener = null + inputManager = InputManager.resetInstance(iInputManagerMock) + + // Handle keyboard backlight listener registration. + doAnswer { + val listener = it.getArgument(0) as IKeyboardBacklightListener + if (registeredListener != null && + registeredListener!!.asBinder() != listener.asBinder()) { + // There can only be one registered keyboard backlight listener per process. + fail("Trying to register a new listener when one already exists") + } + registeredListener = listener + null + }.`when`(iInputManagerMock).registerKeyboardBacklightListener(any()) + + // Handle keyboard backlight listener being unregistered. + doAnswer { + val listener = it.getArgument(0) as IKeyboardBacklightListener + if (registeredListener == null || + registeredListener!!.asBinder() != listener.asBinder()) { + fail("Trying to unregister a listener that is not registered") + } + registeredListener = null + null + }.`when`(iInputManagerMock).unregisterKeyboardBacklightListener(any()) + } + + @After + fun tearDown() { + InputManager.clearInstance() + } + + private fun notifyKeyboardBacklightChanged( + deviceId: Int, + brightnessLevel: Int, + maxBrightnessLevel: Int = 10, + isTriggeredByKeyPress: Boolean = true + ) { + registeredListener!!.onBrightnessChanged(deviceId, IKeyboardBacklightState().apply { + this.brightnessLevel = brightnessLevel + this.maxBrightnessLevel = maxBrightnessLevel + }, isTriggeredByKeyPress) + } + + @Test + fun testListenerIsNotifiedCorrectly() { + var callbackCount = 0 + + // Add a keyboard backlight listener + inputManager.registerKeyboardBacklightListener(executor) { + deviceId: Int, + keyboardBacklightState: KeyboardBacklightState, + isTriggeredByKeyPress: Boolean -> + callbackCount++ + assertEquals(1, deviceId) + assertEquals(2, keyboardBacklightState.brightnessLevel) + assertEquals(10, keyboardBacklightState.maxBrightnessLevel) + assertEquals(true, isTriggeredByKeyPress) + } + + // Adding the listener should register the callback with InputManagerService. + assertNotNull(registeredListener) + + // Notifying keyboard backlight change will notify the listener. + notifyKeyboardBacklightChanged(1 /*deviceId*/, 2 /* brightnessLevel */) + testLooper.dispatchNext() + assertEquals(1, callbackCount) + } + + @Test + fun testMultipleListeners() { + // Set up two callbacks. + var callbackCount1 = 0 + var callbackCount2 = 0 + val callback1 = InputManager.KeyboardBacklightListener { _, _, _ -> callbackCount1++ } + val callback2 = InputManager.KeyboardBacklightListener { _, _, _ -> callbackCount2++ } + + // Add both keyboard backlight listeners + inputManager.registerKeyboardBacklightListener(executor, callback1) + inputManager.registerKeyboardBacklightListener(executor, callback2) + + // Adding the listeners should register the callback with InputManagerService. + assertNotNull(registeredListener) + + // Notifying keyboard backlight change trigger the both callbacks. + notifyKeyboardBacklightChanged(1 /*deviceId*/, 1 /* brightnessLevel */) + testLooper.dispatchAll() + assertEquals(1, callbackCount1) + assertEquals(1, callbackCount2) + + inputManager.unregisterKeyboardBacklightListener(callback2) + // Notifying keyboard backlight change should still trigger callback1. + notifyKeyboardBacklightChanged(1 /*deviceId*/, 2 /* brightnessLevel */) + testLooper.dispatchAll() + assertEquals(2, callbackCount1) + + // Unregister all listeners, should remove registered listener from InputManagerService + inputManager.unregisterKeyboardBacklightListener(callback1) + assertNull(registeredListener) + } +} 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 b910287aa535..87fa63d7fe14 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java @@ -17,6 +17,7 @@ package androidx.window.extensions.embedding; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; +import static android.window.TaskFragmentOperation.OP_TYPE_SET_ANIMATION_PARAMS; import static androidx.window.extensions.embedding.SplitContainer.getFinishPrimaryWithSecondaryBehavior; import static androidx.window.extensions.embedding.SplitContainer.getFinishSecondaryWithPrimaryBehavior; @@ -31,8 +32,10 @@ import android.graphics.Rect; import android.os.Bundle; import android.os.IBinder; import android.util.ArrayMap; +import android.window.TaskFragmentAnimationParams; import android.window.TaskFragmentCreationParams; import android.window.TaskFragmentInfo; +import android.window.TaskFragmentOperation; import android.window.TaskFragmentOrganizer; import android.window.TaskFragmentTransaction; import android.window.WindowContainerTransaction; @@ -114,13 +117,14 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { * @param activityIntent Intent to start the secondary Activity with. * @param activityOptions ActivityOptions to start the secondary Activity with. * @param windowingMode the windowing mode to set for the TaskFragments. + * @param splitAttributes the {@link SplitAttributes} to represent the split. */ void startActivityToSide(@NonNull WindowContainerTransaction wct, @NonNull IBinder launchingFragmentToken, @NonNull Rect launchingFragmentBounds, @NonNull Activity launchingActivity, @NonNull IBinder secondaryFragmentToken, @NonNull Rect secondaryFragmentBounds, @NonNull Intent activityIntent, @Nullable Bundle activityOptions, @NonNull SplitRule rule, - @WindowingMode int windowingMode) { + @WindowingMode int windowingMode, @NonNull SplitAttributes splitAttributes) { final IBinder ownerToken = launchingActivity.getActivityToken(); // Create or resize the launching TaskFragment. @@ -131,6 +135,7 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { createTaskFragmentAndReparentActivity(wct, launchingFragmentToken, ownerToken, launchingFragmentBounds, windowingMode, launchingActivity); } + updateAnimationParams(wct, launchingFragmentToken, splitAttributes); // Create a TaskFragment for the secondary activity. final TaskFragmentCreationParams fragmentOptions = new TaskFragmentCreationParams.Builder( @@ -144,6 +149,7 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { .setPairedPrimaryFragmentToken(launchingFragmentToken) .build(); createTaskFragment(wct, fragmentOptions); + updateAnimationParams(wct, secondaryFragmentToken, splitAttributes); wct.startActivityInTaskFragment(secondaryFragmentToken, ownerToken, activityIntent, activityOptions); @@ -163,6 +169,7 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { resizeTaskFragment(wct, fragmentToken, new Rect()); setAdjacentTaskFragments(wct, fragmentToken, null /* secondary */, null /* splitRule */); updateWindowingMode(wct, fragmentToken, WINDOWING_MODE_UNDEFINED); + updateAnimationParams(wct, fragmentToken, TaskFragmentAnimationParams.DEFAULT); } /** @@ -175,6 +182,7 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { createTaskFragmentAndReparentActivity( wct, fragmentToken, activity.getActivityToken(), new Rect(), WINDOWING_MODE_UNDEFINED, activity); + updateAnimationParams(wct, fragmentToken, TaskFragmentAnimationParams.DEFAULT); } /** @@ -270,6 +278,24 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { wct.setWindowingMode(mFragmentInfos.get(fragmentToken).getToken(), windowingMode); } + /** + * Updates the {@link TaskFragmentAnimationParams} for the given TaskFragment based on + * {@link SplitAttributes}. + */ + void updateAnimationParams(@NonNull WindowContainerTransaction wct, + @NonNull IBinder fragmentToken, @NonNull SplitAttributes splitAttributes) { + updateAnimationParams(wct, fragmentToken, createAnimationParamsOrDefault(splitAttributes)); + } + + void updateAnimationParams(@NonNull WindowContainerTransaction wct, + @NonNull IBinder fragmentToken, @NonNull TaskFragmentAnimationParams animationParams) { + final TaskFragmentOperation operation = new TaskFragmentOperation.Builder( + OP_TYPE_SET_ANIMATION_PARAMS) + .setAnimationParams(animationParams) + .build(); + wct.setTaskFragmentOperation(fragmentToken, operation); + } + void deleteTaskFragment(@NonNull WindowContainerTransaction wct, @NonNull IBinder fragmentToken) { if (!mFragmentInfos.containsKey(fragmentToken)) { @@ -291,4 +317,14 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { public void onTransactionReady(@NonNull TaskFragmentTransaction transaction) { mCallback.onTransactionReady(transaction); } + + private static TaskFragmentAnimationParams createAnimationParamsOrDefault( + @Nullable SplitAttributes splitAttributes) { + if (splitAttributes == null) { + return TaskFragmentAnimationParams.DEFAULT; + } + return new TaskFragmentAnimationParams.Builder() + .setAnimationBackgroundColor(splitAttributes.getAnimationBackgroundColor()) + .build(); + } } 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 ce7d695beb2a..1e004a722cef 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -65,6 +65,7 @@ import android.util.Pair; import android.util.Size; import android.util.SparseArray; import android.view.WindowMetrics; +import android.window.TaskFragmentAnimationParams; import android.window.TaskFragmentInfo; import android.window.TaskFragmentParentInfo; import android.window.TaskFragmentTransaction; @@ -1157,6 +1158,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen taskId); mPresenter.createTaskFragment(wct, expandedContainer.getTaskFragmentToken(), activityInTask.getActivityToken(), new Rect(), WINDOWING_MODE_UNDEFINED); + mPresenter.updateAnimationParams(wct, expandedContainer.getTaskFragmentToken(), + TaskFragmentAnimationParams.DEFAULT); return expandedContainer; } 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 9db9f8788190..7b2af4933e66 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -36,6 +36,7 @@ import android.util.Size; import android.view.View; import android.view.WindowInsets; import android.view.WindowMetrics; +import android.window.TaskFragmentAnimationParams; import android.window.TaskFragmentCreationParams; import android.window.WindowContainerTransaction; @@ -176,7 +177,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, taskProperties, splitAttributes); final TaskFragmentContainer primaryContainer = prepareContainerForActivity(wct, - primaryActivity, primaryRectBounds, null); + primaryActivity, primaryRectBounds, splitAttributes, null /* containerToAvoid */); // Create new empty task fragment final int taskId = primaryContainer.getTaskId(); @@ -189,6 +190,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { createTaskFragment(wct, secondaryContainer.getTaskFragmentToken(), primaryActivity.getActivityToken(), secondaryRectBounds, windowingMode); + updateAnimationParams(wct, secondaryContainer.getTaskFragmentToken(), splitAttributes); // Set adjacent to each other so that the containers below will be invisible. setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule, @@ -222,7 +224,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { final Rect primaryRectBounds = getBoundsForPosition(POSITION_START, taskProperties, splitAttributes); final TaskFragmentContainer primaryContainer = prepareContainerForActivity(wct, - primaryActivity, primaryRectBounds, null); + primaryActivity, primaryRectBounds, splitAttributes, null /* containerToAvoid */); final Rect secondaryRectBounds = getBoundsForPosition(POSITION_END, taskProperties, splitAttributes); @@ -236,7 +238,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { containerToAvoid = curSecondaryContainer; } final TaskFragmentContainer secondaryContainer = prepareContainerForActivity(wct, - secondaryActivity, secondaryRectBounds, containerToAvoid); + secondaryActivity, secondaryRectBounds, splitAttributes, containerToAvoid); // Set adjacent to each other so that the containers below will be invisible. setAdjacentTaskFragments(wct, primaryContainer, secondaryContainer, rule, @@ -253,7 +255,8 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { */ private TaskFragmentContainer prepareContainerForActivity( @NonNull WindowContainerTransaction wct, @NonNull Activity activity, - @NonNull Rect bounds, @Nullable TaskFragmentContainer containerToAvoid) { + @NonNull Rect bounds, @NonNull SplitAttributes splitAttributes, + @Nullable TaskFragmentContainer containerToAvoid) { TaskFragmentContainer container = mController.getContainerWithActivity(activity); final int taskId = container != null ? container.getTaskId() : activity.getTaskId(); if (container == null || container == containerToAvoid) { @@ -270,6 +273,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { .getWindowingModeForSplitTaskFragment(bounds); updateTaskFragmentWindowingModeIfRegistered(wct, container, windowingMode); } + updateAnimationParams(wct, container.getTaskFragmentToken(), splitAttributes); return container; } @@ -314,7 +318,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { rule, splitAttributes); startActivityToSide(wct, primaryContainer.getTaskFragmentToken(), primaryRectBounds, launchingActivity, secondaryContainer.getTaskFragmentToken(), secondaryRectBounds, - activityIntent, activityOptions, rule, windowingMode); + activityIntent, activityOptions, rule, windowingMode, splitAttributes); if (isPlaceholder) { // When placeholder is launched in split, we should keep the focus on the primary. wct.requestFocusOnTaskFragment(primaryContainer.getTaskFragmentToken()); @@ -365,6 +369,8 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { primaryRectBounds); updateTaskFragmentWindowingModeIfRegistered(wct, primaryContainer, windowingMode); updateTaskFragmentWindowingModeIfRegistered(wct, secondaryContainer, windowingMode); + updateAnimationParams(wct, primaryContainer.getTaskFragmentToken(), splitAttributes); + updateAnimationParams(wct, secondaryContainer.getTaskFragmentToken(), splitAttributes); } private void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct, @@ -459,6 +465,24 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { super.updateWindowingMode(wct, fragmentToken, windowingMode); } + @Override + void updateAnimationParams(@NonNull WindowContainerTransaction wct, + @NonNull IBinder fragmentToken, @NonNull TaskFragmentAnimationParams animationParams) { + final TaskFragmentContainer container = mController.getContainer(fragmentToken); + if (container == null) { + throw new IllegalStateException("Setting animation params for a task fragment that is" + + " not registered with controller."); + } + + if (container.areLastRequestedAnimationParamsEqual(animationParams)) { + // Return early if the animation params were already requested + return; + } + + container.setLastRequestAnimationParams(animationParams); + super.updateAnimationParams(wct, fragmentToken, animationParams); + } + /** * Expands the split container if the current split bounds are smaller than the Activity or * Intent that is added to the container. diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java index 6bfdfe7593b8..076856c373d6 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java @@ -26,6 +26,7 @@ import android.graphics.Rect; import android.os.Binder; import android.os.IBinder; import android.util.Size; +import android.window.TaskFragmentAnimationParams; import android.window.TaskFragmentInfo; import android.window.WindowContainerTransaction; @@ -108,6 +109,13 @@ class TaskFragmentContainer { private int mLastRequestedWindowingMode = WINDOWING_MODE_UNDEFINED; /** + * TaskFragmentAnimationParams that was requested last via + * {@link android.window.WindowContainerTransaction}. + */ + @NonNull + private TaskFragmentAnimationParams mLastAnimationParams = TaskFragmentAnimationParams.DEFAULT; + + /** * When the TaskFragment has appeared in server, but is empty, we should remove the TaskFragment * if it is still empty after the timeout. */ @@ -560,6 +568,21 @@ class TaskFragmentContainer { mLastRequestedWindowingMode = windowingModes; } + /** + * Checks if last requested {@link TaskFragmentAnimationParams} are equal to the provided value. + */ + boolean areLastRequestedAnimationParamsEqual( + @NonNull TaskFragmentAnimationParams animationParams) { + return mLastAnimationParams.equals(animationParams); + } + + /** + * Updates the last requested {@link TaskFragmentAnimationParams}. + */ + void setLastRequestAnimationParams(@NonNull TaskFragmentAnimationParams animationParams) { + mLastAnimationParams = animationParams; + } + /** Gets the parent leaf Task id. */ int getTaskId() { return mTaskContainer.getTaskId(); diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java index 13a2c78d463e..d189ae2cf72e 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/WindowExtensionsTest.java @@ -22,6 +22,7 @@ import android.platform.test.annotations.Presubmit; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import androidx.window.extensions.embedding.SplitAttributes; import org.junit.Before; import org.junit.Test; @@ -53,4 +54,15 @@ public class WindowExtensionsTest { public void testGetActivityEmbeddingComponent() { assertThat(mExtensions.getActivityEmbeddingComponent()).isNotNull(); } + + @Test + public void testSplitAttributes_default() { + // Make sure the default value in the extensions aar. + final SplitAttributes splitAttributes = new SplitAttributes.Builder().build(); + assertThat(splitAttributes.getLayoutDirection()) + .isEqualTo(SplitAttributes.LayoutDirection.LOCALE); + assertThat(splitAttributes.getSplitType()) + .isEqualTo(new SplitAttributes.SplitType.RatioSplitType(0.5f)); + assertThat(splitAttributes.getAnimationBackgroundColor()).isEqualTo(0); + } } 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 6dae0a1086b3..fcd4d621e753 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 @@ -19,6 +19,7 @@ package androidx.window.extensions.embedding; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.view.Display.DEFAULT_DISPLAY; +import static android.window.TaskFragmentOperation.OP_TYPE_SET_ANIMATION_PARAMS; import static androidx.window.extensions.embedding.EmbeddingTestUtils.DEFAULT_FINISH_PRIMARY_WITH_SECONDARY; import static androidx.window.extensions.embedding.EmbeddingTestUtils.DEFAULT_FINISH_SECONDARY_WITH_PRIMARY; @@ -60,12 +61,15 @@ import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.content.res.Resources; +import android.graphics.Color; import android.graphics.Rect; import android.os.IBinder; import android.platform.test.annotations.Presubmit; import android.util.Pair; import android.util.Size; +import android.window.TaskFragmentAnimationParams; import android.window.TaskFragmentInfo; +import android.window.TaskFragmentOperation; import android.window.WindowContainerTransaction; import androidx.test.core.app.ApplicationProvider; @@ -163,7 +167,38 @@ public class SplitPresenterTest { WINDOWING_MODE_MULTI_WINDOW); verify(mTransaction, never()).setWindowingMode(any(), anyInt()); + } + + @Test + public void testUpdateAnimationParams() { + final TaskFragmentContainer container = mController.newContainer(mActivity, TASK_ID); + + // Verify the default. + assertTrue(container.areLastRequestedAnimationParamsEqual( + TaskFragmentAnimationParams.DEFAULT)); + + final int bgColor = Color.GREEN; + final TaskFragmentAnimationParams animationParams = + new TaskFragmentAnimationParams.Builder() + .setAnimationBackgroundColor(bgColor) + .build(); + mPresenter.updateAnimationParams(mTransaction, container.getTaskFragmentToken(), + animationParams); + + final TaskFragmentOperation expectedOperation = new TaskFragmentOperation.Builder( + OP_TYPE_SET_ANIMATION_PARAMS) + .setAnimationParams(animationParams) + .build(); + verify(mTransaction).setTaskFragmentOperation(container.getTaskFragmentToken(), + expectedOperation); + assertTrue(container.areLastRequestedAnimationParamsEqual(animationParams)); + + // No request to set the same animation params. + clearInvocations(mTransaction); + mPresenter.updateAnimationParams(mTransaction, container.getTaskFragmentToken(), + animationParams); + verify(mTransaction, never()).setTaskFragmentOperation(any(), any()); } @Test diff --git a/libs/WindowManager/Jetpack/window-extensions-release.aar b/libs/WindowManager/Jetpack/window-extensions-release.aar Binary files differindex 4978e04e0115..84ab4487feee 100644 --- a/libs/WindowManager/Jetpack/window-extensions-release.aar +++ b/libs/WindowManager/Jetpack/window-extensions-release.aar diff --git a/libs/WindowManager/Shell/res/color/decor_caption_title_color.xml b/libs/WindowManager/Shell/res/color/decor_title_color.xml index 1ecc13e4da38..1ecc13e4da38 100644 --- a/libs/WindowManager/Shell/res/color/decor_caption_title_color.xml +++ b/libs/WindowManager/Shell/res/color/decor_title_color.xml diff --git a/libs/WindowManager/Shell/res/drawable/decor_caption_menu_background.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_decor_menu_background.xml index 416287d2cbb3..416287d2cbb3 100644 --- a/libs/WindowManager/Shell/res/drawable/decor_caption_menu_background.xml +++ b/libs/WindowManager/Shell/res/drawable/desktop_mode_decor_menu_background.xml diff --git a/libs/WindowManager/Shell/res/drawable/decor_caption_title.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_decor_title.xml index 416287d2cbb3..416287d2cbb3 100644 --- a/libs/WindowManager/Shell/res/drawable/decor_caption_title.xml +++ b/libs/WindowManager/Shell/res/drawable/desktop_mode_decor_title.xml diff --git a/libs/WindowManager/Shell/res/layout/caption_handle_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_decor_handle_menu.xml index 582a11cfdb8e..8b4792acba3e 100644 --- a/libs/WindowManager/Shell/res/layout/caption_handle_menu.xml +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_decor_handle_menu.xml @@ -20,7 +20,7 @@ android:id="@+id/handle_menu" android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center_horizontal" -android:background="@drawable/decor_caption_menu_background"> +android:background="@drawable/desktop_mode_decor_menu_background"> <Button style="@style/CaptionButtonStyle" android:id="@+id/fullscreen_button" diff --git a/libs/WindowManager/Shell/res/layout/caption_window_decoration.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor.xml index 51e634c17532..2a4cc02f0925 100644 --- a/libs/WindowManager/Shell/res/layout/caption_window_decoration.xml +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor.xml @@ -16,10 +16,10 @@ --> <com.android.wm.shell.windowdecor.WindowDecorLinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/caption" + android:id="@+id/desktop_mode_caption" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:background="@drawable/decor_caption_title"> + android:background="@drawable/desktop_mode_decor_title"> <Button style="@style/CaptionButtonStyle" android:id="@+id/back_button" diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java index bd2ea9c1f822..94e01e96730c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java @@ -223,16 +223,6 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, mObscuredTouchRegion = obscuredRegion; } - private void onLocationChanged(WindowContainerTransaction wct) { - // Update based on the screen bounds - getBoundsOnScreen(mTmpRect); - getRootView().getBoundsOnScreen(mTmpRootRect); - if (!mTmpRootRect.contains(mTmpRect)) { - mTmpRect.offsetTo(0, 0); - } - wct.setBounds(mTaskToken, mTmpRect); - } - /** * Call when view position or size has changed. Do not call when animating. */ @@ -245,10 +235,15 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, if (isUsingShellTransitions() && mTaskViewTransitions.hasPending()) return; WindowContainerTransaction wct = new WindowContainerTransaction(); - onLocationChanged(wct); + updateWindowBounds(wct); mSyncQueue.queue(wct); } + private void updateWindowBounds(WindowContainerTransaction wct) { + getBoundsOnScreen(mTmpRect); + wct.setBounds(mTaskToken, mTmpRect); + } + /** * Release this container if it is initialized. */ @@ -572,7 +567,7 @@ public class TaskView extends SurfaceView implements SurfaceHolder.Callback, .apply(); // TODO: determine if this is really necessary or not - onLocationChanged(wct); + updateWindowBounds(wct); } else { // The surface has already been destroyed before the task has appeared, // so go ahead and hide the task entirely diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java index 7aae6335398a..d0aef2023048 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java @@ -16,6 +16,12 @@ package com.android.wm.shell.common; +import static android.view.EventLogTags.IMF_IME_REMOTE_ANIM_CANCEL; +import static android.view.EventLogTags.IMF_IME_REMOTE_ANIM_END; +import static android.view.EventLogTags.IMF_IME_REMOTE_ANIM_START; +import static android.view.inputmethod.ImeTracker.DEBUG_IME_VISIBILITY; +import static android.view.inputmethod.ImeTracker.TOKEN_NONE; + import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; @@ -26,6 +32,7 @@ import android.content.res.Configuration; import android.graphics.Point; import android.graphics.Rect; import android.os.RemoteException; +import android.util.EventLog; import android.util.Slog; import android.util.SparseArray; import android.view.IDisplayWindowInsetsController; @@ -47,6 +54,7 @@ import androidx.annotation.VisibleForTesting; import com.android.wm.shell.sysui.ShellInit; import java.util.ArrayList; +import java.util.Objects; import java.util.concurrent.Executor; /** @@ -274,29 +282,30 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged } if (hasImeSourceControl) { - final Point lastSurfacePosition = mImeSourceControl != null - ? mImeSourceControl.getSurfacePosition() : null; - final boolean positionChanged = - !imeSourceControl.getSurfacePosition().equals(lastSurfacePosition); - final boolean leashChanged = - !haveSameLeash(mImeSourceControl, imeSourceControl); if (mAnimation != null) { + final Point lastSurfacePosition = hadImeSourceControl + ? mImeSourceControl.getSurfacePosition() : null; + final boolean positionChanged = + !imeSourceControl.getSurfacePosition().equals(lastSurfacePosition); if (positionChanged) { startAnimation(mImeShowing, true /* forceRestart */, null /* statsToken */); } } else { - if (leashChanged) { + if (!haveSameLeash(mImeSourceControl, imeSourceControl)) { applyVisibilityToLeash(imeSourceControl); } if (!mImeShowing) { removeImeSurface(); } - if (mImeSourceControl != null) { - mImeSourceControl.release(SurfaceControl::release); - } } - mImeSourceControl = imeSourceControl; + } else if (mAnimation != null) { + mAnimation.cancel(); + } + + if (hadImeSourceControl && mImeSourceControl != imeSourceControl) { + mImeSourceControl.release(SurfaceControl::release); } + mImeSourceControl = imeSourceControl; } private void applyVisibilityToLeash(InsetsSourceControl imeSourceControl) { @@ -469,6 +478,15 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged ImeTracker.PHASE_WM_ANIMATION_RUNNING); t.show(mImeSourceControl.getLeash()); } + if (DEBUG_IME_VISIBILITY) { + EventLog.writeEvent(IMF_IME_REMOTE_ANIM_START, + statsToken != null ? statsToken.getTag() : TOKEN_NONE, + mDisplayId, mAnimationDirection, alpha, startY , endY, + Objects.toString(mImeSourceControl.getLeash()), + Objects.toString(mImeSourceControl.getInsetsHint()), + Objects.toString(mImeSourceControl.getSurfacePosition()), + Objects.toString(mImeFrame)); + } t.apply(); mTransactionPool.release(t); } @@ -476,6 +494,11 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged @Override public void onAnimationCancel(Animator animation) { mCancelled = true; + if (DEBUG_IME_VISIBILITY) { + EventLog.writeEvent(IMF_IME_REMOTE_ANIM_CANCEL, + statsToken != null ? statsToken.getTag() : TOKEN_NONE, mDisplayId, + Objects.toString(mImeSourceControl.getInsetsHint())); + } } @Override @@ -499,6 +522,15 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged ImeTracker.get().onCancelled(mStatsToken, ImeTracker.PHASE_WM_ANIMATION_RUNNING); } + if (DEBUG_IME_VISIBILITY) { + EventLog.writeEvent(IMF_IME_REMOTE_ANIM_END, + statsToken != null ? statsToken.getTag() : TOKEN_NONE, + mDisplayId, mAnimationDirection, endY, + Objects.toString(mImeSourceControl.getLeash()), + Objects.toString(mImeSourceControl.getInsetsHint()), + Objects.toString(mImeSourceControl.getSurfacePosition()), + Objects.toString(mImeFrame)); + } t.apply(); mTransactionPool.release(t); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java index 65da757b1396..a05ed4f24a08 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java @@ -111,7 +111,7 @@ public class TaskSnapshotWindow { final SurfaceControl surfaceControl = new SurfaceControl(); final ClientWindowFrames tmpFrames = new ClientWindowFrames(); - final InsetsSourceControl[] tmpControls = new InsetsSourceControl[0]; + final InsetsSourceControl.Array tmpControls = new InsetsSourceControl.Array(); final MergedConfiguration tmpMergedConfiguration = new MergedConfiguration(); final TaskDescription taskDescription; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java index 039f0e3b5917..fc2a828fb263 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java @@ -374,8 +374,6 @@ public class Transitions implements RemoteCallable<Transitions> { // If this is a transferred starting window, we want it immediately visible. && (change.getFlags() & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) == 0) { t.setAlpha(leash, 0.f); - // fix alpha in finish transaction in case the animator itself no-ops. - finishT.setAlpha(leash, 1.f); } } else if (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK) { finishT.hide(leash); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index b500f5fb0155..b4301577ac10 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -282,7 +282,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { public boolean onTouch(View v, MotionEvent e) { boolean isDrag = false; int id = v.getId(); - if (id != R.id.caption_handle && id != R.id.caption) { + if (id != R.id.caption_handle && id != R.id.desktop_mode_caption) { return false; } if (id == R.id.caption_handle) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java index 467f374f2110..9c2beb9c4b2b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java @@ -132,7 +132,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mRelayoutParams.reset(); mRelayoutParams.mRunningTaskInfo = taskInfo; - mRelayoutParams.mLayoutResId = R.layout.caption_window_decoration; + mRelayoutParams.mLayoutResId = R.layout.desktop_mode_window_decor; mRelayoutParams.mCaptionHeightId = R.dimen.freeform_decor_caption_height; mRelayoutParams.mCaptionWidthId = R.dimen.freeform_decor_caption_width; mRelayoutParams.mShadowRadiusId = shadowRadiusID; @@ -212,7 +212,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin * Sets up listeners when a new root view is created. */ private void setupRootView() { - View caption = mResult.mRootView.findViewById(R.id.caption); + View caption = mResult.mRootView.findViewById(R.id.desktop_mode_caption); caption.setOnTouchListener(mOnCaptionTouchListener); View close = caption.findViewById(R.id.close_window); close.setOnClickListener(mOnCaptionButtonClickListener); @@ -243,7 +243,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin */ private void setCaptionVisibility(boolean visible) { int v = visible ? View.VISIBLE : View.GONE; - View captionView = mResult.mRootView.findViewById(R.id.caption); + View captionView = mResult.mRootView.findViewById(R.id.desktop_mode_caption); captionView.setVisibility(v); if (!visible) closeHandleMenu(); } @@ -265,7 +265,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin */ void setButtonVisibility(boolean visible) { int visibility = visible ? View.VISIBLE : View.GONE; - View caption = mResult.mRootView.findViewById(R.id.caption); + View caption = mResult.mRootView.findViewById(R.id.desktop_mode_caption); View back = caption.findViewById(R.id.back_button); View close = caption.findViewById(R.id.close_window); back.setVisibility(visibility); @@ -304,7 +304,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin int width = loadDimensionPixelSize(resources, mRelayoutParams.mCaptionWidthId); int height = loadDimensionPixelSize(resources, mRelayoutParams.mCaptionHeightId); String namePrefix = "Caption Menu"; - mHandleMenu = addWindow(R.layout.caption_handle_menu, namePrefix, t, + mHandleMenu = addWindow(R.layout.desktop_mode_decor_handle_menu, namePrefix, t, x - mResult.mDecorContainerOffsetX, y - mResult.mDecorContainerOffsetY, width, height); mSyncQueue.runInSync(transaction -> { @@ -336,7 +336,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin */ void closeHandleMenuIfNeeded(MotionEvent ev) { if (isHandleMenuActive()) { - if (!checkEventInCaptionView(ev, R.id.caption)) { + if (!checkEventInCaptionView(ev, R.id.desktop_mode_caption)) { closeHandleMenu(); } } @@ -389,7 +389,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin */ void checkClickEvent(MotionEvent ev) { if (mResult.mRootView == null) return; - View caption = mResult.mRootView.findViewById(R.id.caption); + View caption = mResult.mRootView.findViewById(R.id.desktop_mode_caption); PointF inputPoint = offsetCaptionLocation(ev); if (!isHandleMenuActive()) { View handle = caption.findViewById(R.id.caption_handle); 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 04b1bdd3fd46..08ed91b3cab1 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 @@ -26,6 +26,9 @@ import androidx.test.uiautomator.Until import com.android.server.wm.flicker.FlickerBuilder import com.android.server.wm.flicker.FlickerTest import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory +import com.android.server.wm.flicker.navBarLayerIsVisibleAtEnd +import com.android.server.wm.flicker.navBarLayerPositionAtEnd +import org.junit.Assume import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized @@ -91,6 +94,10 @@ class LaunchBubbleFromLockScreen(flicker: FlickerTest) : BaseBubbleScreen(flicke flicker.assertLayersEnd { this.isVisible(testApp) } } + @Postsubmit @Test fun navBarLayerIsVisibleAtEnd() = flicker.navBarLayerIsVisibleAtEnd() + + @Postsubmit @Test fun navBarLayerPositionAtEnd() = flicker.navBarLayerPositionAtEnd() + /** {@inheritDoc} */ @FlakyTest @Test @@ -98,19 +105,28 @@ class LaunchBubbleFromLockScreen(flicker: FlickerTest) : BaseBubbleScreen(flicke super.visibleLayersShownMoreThanOneConsecutiveEntry() /** {@inheritDoc} */ - @FlakyTest(bugId = 206753786) + @Postsubmit @Test - override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd() + override fun navBarLayerIsVisibleAtStartAndEnd() { + Assume.assumeTrue(flicker.scenario.isGesturalNavigation) + super.navBarLayerIsVisibleAtStartAndEnd() + } /** {@inheritDoc} */ - @FlakyTest(bugId = 206753786) + @Postsubmit @Test - override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd() + override fun navBarLayerPositionAtStartAndEnd() { + Assume.assumeTrue(flicker.scenario.isGesturalNavigation) + super.navBarLayerPositionAtStartAndEnd() + } /** {@inheritDoc} */ - @FlakyTest(bugId = 206753786) + @Postsubmit @Test - override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible() + override fun navBarWindowIsAlwaysVisible() { + Assume.assumeTrue(flicker.scenario.isGesturalNavigation) + super.navBarWindowIsAlwaysVisible() + } /** {@inheritDoc} */ @FlakyTest(bugId = 242088970) diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleScreen.kt index 9b4e39c42d11..b69ff6451d1c 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleScreen.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/LaunchBubbleScreen.kt @@ -16,7 +16,7 @@ package com.android.wm.shell.flicker.bubble -import android.platform.test.annotations.RequiresDevice +import androidx.test.filters.RequiresDevice import androidx.test.uiautomator.By import androidx.test.uiautomator.Until import com.android.server.wm.flicker.FlickerBuilder diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownShelfHeightChangeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownShelfHeightChangeTest.kt index e9847fae00fe..16acc11f5729 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownShelfHeightChangeTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownShelfHeightChangeTest.kt @@ -17,7 +17,7 @@ package com.android.wm.shell.flicker.pip import android.platform.test.annotations.Presubmit -import android.platform.test.annotations.RequiresDevice +import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerBuilder import com.android.server.wm.flicker.FlickerTest import com.android.server.wm.flicker.FlickerTestFactory diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/PipTestBase.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/PipTestBase.kt index a16f5f6f1620..2cb18f948f0e 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/PipTestBase.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/PipTestBase.kt @@ -24,10 +24,7 @@ import androidx.test.uiautomator.UiDevice import org.junit.Before import org.junit.runners.Parameterized -abstract class PipTestBase( - protected val rotationName: String, - protected val rotation: Int -) { +abstract class PipTestBase(protected val rotationName: String, protected val rotation: Int) { val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() val uiDevice = UiDevice.getInstance(instrumentation) val packageManager: PackageManager = instrumentation.context.packageManager diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt index 73671dbb3c6f..fcdad960107f 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt @@ -93,7 +93,8 @@ class DragDividerToResize(flicker: FlickerTest) : SplitScreenBase(flicker) { } @FlakyTest(bugId = 263213649) - @Test fun primaryAppLayerKeepVisible_ShellTransit() { + @Test + fun primaryAppLayerKeepVisible_ShellTransit() { Assume.assumeTrue(isShellTransitionsEnabled) flicker.layerKeepVisible(primaryApp) } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java index 22df362a6ed3..ffb1a4d66f1e 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java @@ -22,6 +22,8 @@ import static android.view.Surface.ROTATION_0; import static android.view.WindowInsets.Type.ime; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; @@ -124,6 +126,15 @@ public class DisplayImeControllerTest extends ShellTestCase { verify(mT).show(any()); } + @Test + public void insetsControlChanged_updateImeSourceControl() { + mPerDisplay.insetsControlChanged(insetsStateWithIme(false), insetsSourceControl()); + assertNotNull(mPerDisplay.mImeSourceControl); + + mPerDisplay.insetsControlChanged(new InsetsState(), new InsetsSourceControl[]{}); + assertNull(mPerDisplay.mImeSourceControl); + } + private InsetsSourceControl[] insetsSourceControl() { return new InsetsSourceControl[]{ new InsetsSourceControl( diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java index d06fb55a5769..7ec4e21bcfcc 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java @@ -30,10 +30,13 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static java.lang.Integer.MAX_VALUE; + import android.content.ComponentName; import android.content.Context; import android.content.pm.PackageManager; import android.content.res.Configuration; +import android.graphics.Point; import android.graphics.Rect; import android.os.RemoteException; import android.test.suitebuilder.annotation.SmallTest; @@ -135,12 +138,12 @@ public class PipControllerTest extends ShellTestCase { @Test public void instantiatePipController_addInitCallback() { - verify(mShellInit, times(1)).addInitCallback(any(), any()); + verify(mShellInit, times(1)).addInitCallback(any(), eq(mPipController)); } @Test public void instantiateController_registerDumpCallback() { - verify(mMockShellCommandHandler, times(1)).addDumpCallback(any(), any()); + verify(mMockShellCommandHandler, times(1)).addDumpCallback(any(), eq(mPipController)); } @Test @@ -156,7 +159,7 @@ public class PipControllerTest extends ShellTestCase { @Test public void instantiatePipController_registerExternalInterface() { verify(mShellController, times(1)).addExternalInterface( - eq(ShellSharedConstants.KEY_EXTRA_SHELL_PIP), any(), any()); + eq(ShellSharedConstants.KEY_EXTRA_SHELL_PIP), any(), eq(mPipController)); } @Test @@ -252,6 +255,10 @@ public class PipControllerTest extends ShellTestCase { final int displayId = 1; final Rect bounds = new Rect(0, 0, 10, 10); when(mMockPipBoundsAlgorithm.getDefaultBounds()).thenReturn(bounds); + when(mMockPipBoundsState.getBounds()).thenReturn(bounds); + when(mMockPipBoundsState.getMinSize()).thenReturn(new Point(1, 1)); + when(mMockPipBoundsState.getMaxSize()).thenReturn(new Point(MAX_VALUE, MAX_VALUE)); + when(mMockPipBoundsState.getBounds()).thenReturn(bounds); when(mMockPipBoundsState.getDisplayId()).thenReturn(displayId); when(mMockPipBoundsState.getDisplayLayout()).thenReturn(mMockDisplayLayout1); when(mMockDisplayController.getDisplayLayout(displayId)).thenReturn(mMockDisplayLayout2); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java index fbc50c68eff9..8d92d0864338 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java @@ -34,6 +34,7 @@ import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.TestShellExecutor; import com.android.wm.shell.common.ExternalInterfaceBinder; import com.android.wm.shell.common.ShellExecutor; @@ -61,10 +62,9 @@ public class ShellControllerTest extends ShellTestCase { @Mock private ShellCommandHandler mShellCommandHandler; @Mock - private ShellExecutor mExecutor; - @Mock private Context mTestUserContext; + private TestShellExecutor mExecutor; private ShellController mController; private TestConfigurationChangeListener mConfigChangeListener; private TestKeyguardChangeListener mKeyguardChangeListener; @@ -77,6 +77,7 @@ public class ShellControllerTest extends ShellTestCase { mKeyguardChangeListener = new TestKeyguardChangeListener(); mConfigChangeListener = new TestConfigurationChangeListener(); mUserChangeListener = new TestUserChangeListener(); + mExecutor = new TestShellExecutor(); mController = new ShellController(mShellInit, mShellCommandHandler, mExecutor); mController.onConfigurationChanged(getConfigurationCopy()); } @@ -104,6 +105,7 @@ public class ShellControllerTest extends ShellTestCase { Bundle b = new Bundle(); mController.asShell().createExternalInterfaces(b); + mExecutor.flushAll(); assertTrue(b.getIBinder(EXTRA_TEST_BINDER) == callback); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java index c764741d4cd6..595c3b4880df 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java @@ -936,7 +936,7 @@ public class ShellTransitionTests extends ShellTestCase { TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode, RunningTaskInfo taskInfo) { final TransitionInfo.Change change = - new TransitionInfo.Change(null /* token */, null /* leash */); + new TransitionInfo.Change(null /* token */, createMockSurface(true)); change.setMode(mode); change.setTaskInfo(taskInfo); mInfo.addChange(change); @@ -961,7 +961,7 @@ public class ShellTransitionTests extends ShellTestCase { final TransitionInfo.Change mChange; ChangeBuilder(@WindowManager.TransitionType int mode) { - mChange = new TransitionInfo.Change(null /* token */, null /* leash */); + mChange = new TransitionInfo.Change(null /* token */, createMockSurface(true)); mChange.setMode(mode); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.java index a5e3a2e76ce5..355072116cb1 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.java @@ -205,28 +205,32 @@ public class DesktopModeWindowDecorViewModelTests extends ShellTestCase { "testEventReceiversOnMultipleDisplays", /*width=*/ 400, /*height=*/ 400, /*densityDpi=*/ 320, surfaceView.getHolder().getSurface(), DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY); - int secondaryDisplayId = secondaryDisplay.getDisplay().getDisplayId(); - - final int taskId = 1; - final ActivityManager.RunningTaskInfo taskInfo = - createTaskInfo(taskId, Display.DEFAULT_DISPLAY, WINDOWING_MODE_FREEFORM); - final ActivityManager.RunningTaskInfo secondTaskInfo = - createTaskInfo(taskId + 1, secondaryDisplayId, WINDOWING_MODE_FREEFORM); - final ActivityManager.RunningTaskInfo thirdTaskInfo = - createTaskInfo(taskId + 2, secondaryDisplayId, WINDOWING_MODE_FREEFORM); - - SurfaceControl surfaceControl = mock(SurfaceControl.class); - final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class); - final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); - - mDesktopModeWindowDecorViewModel.onTaskOpening(taskInfo, surfaceControl, startT, - finishT); - mDesktopModeWindowDecorViewModel.onTaskOpening(secondTaskInfo, surfaceControl, - startT, finishT); - mDesktopModeWindowDecorViewModel.onTaskOpening(thirdTaskInfo, surfaceControl, - startT, finishT); - mDesktopModeWindowDecorViewModel.destroyWindowDecoration(thirdTaskInfo); - mDesktopModeWindowDecorViewModel.destroyWindowDecoration(taskInfo); + try { + int secondaryDisplayId = secondaryDisplay.getDisplay().getDisplayId(); + + final int taskId = 1; + final ActivityManager.RunningTaskInfo taskInfo = + createTaskInfo(taskId, Display.DEFAULT_DISPLAY, WINDOWING_MODE_FREEFORM); + final ActivityManager.RunningTaskInfo secondTaskInfo = + createTaskInfo(taskId + 1, secondaryDisplayId, WINDOWING_MODE_FREEFORM); + final ActivityManager.RunningTaskInfo thirdTaskInfo = + createTaskInfo(taskId + 2, secondaryDisplayId, WINDOWING_MODE_FREEFORM); + + SurfaceControl surfaceControl = mock(SurfaceControl.class); + final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class); + final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); + + mDesktopModeWindowDecorViewModel.onTaskOpening(taskInfo, surfaceControl, startT, + finishT); + mDesktopModeWindowDecorViewModel.onTaskOpening(secondTaskInfo, surfaceControl, + startT, finishT); + mDesktopModeWindowDecorViewModel.onTaskOpening(thirdTaskInfo, surfaceControl, + startT, finishT); + mDesktopModeWindowDecorViewModel.destroyWindowDecoration(thirdTaskInfo); + mDesktopModeWindowDecorViewModel.destroyWindowDecoration(taskInfo); + } finally { + secondaryDisplay.release(); + } }); verify(mMockInputMonitorFactory, times(2)).create(any(), any()); verify(mInputMonitor, times(1)).dispose(); @@ -239,7 +243,7 @@ public class DesktopModeWindowDecorViewModelTests extends ShellTestCase { r.run(); latch.countDown(); }); - latch.await(20, TimeUnit.MILLISECONDS); + latch.await(1, TimeUnit.SECONDS); } private static ActivityManager.RunningTaskInfo createTaskInfo(int taskId, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java index dd9ab9899e13..ec4f17fd072b 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java @@ -48,6 +48,7 @@ import android.view.SurfaceControlViewHost; import android.view.View; import android.view.ViewRootImpl; import android.view.WindowManager.LayoutParams; +import android.window.TaskConstants; import android.window.WindowContainerTransaction; import androidx.test.filters.SmallTest; @@ -232,7 +233,8 @@ public class WindowDecorationTests extends ShellTestCase { verify(mMockSurfaceControlStartT) .setColor(taskBackgroundSurface, new float[] {1.f, 1.f, 0.f}); verify(mMockSurfaceControlStartT).setShadowRadius(taskBackgroundSurface, 10); - verify(mMockSurfaceControlStartT).setLayer(taskBackgroundSurface, -1); + verify(mMockSurfaceControlStartT).setLayer(taskBackgroundSurface, + TaskConstants.TASK_CHILD_LAYER_TASK_BACKGROUND); verify(mMockSurfaceControlStartT).show(taskBackgroundSurface); verify(captionContainerSurfaceBuilder).setParent(decorContainerSurface); @@ -560,7 +562,8 @@ public class WindowDecorationTests extends ShellTestCase { int height = loadDimensionPixelSize(resources, mRelayoutParams.mCaptionHeightId); String name = "Test Window"; WindowDecoration.AdditionalWindow additionalWindow = - addWindow(R.layout.caption_handle_menu, name, mMockSurfaceControlAddWindowT, + addWindow(R.layout.desktop_mode_decor_handle_menu, name, + mMockSurfaceControlAddWindowT, x - mRelayoutResult.mDecorContainerOffsetX, y - mRelayoutResult.mDecorContainerOffsetY, width, height); diff --git a/libs/hwui/tests/unit/AutoBackendTextureReleaseTests.cpp b/libs/hwui/tests/unit/AutoBackendTextureReleaseTests.cpp index 2ec78a429481..138b3efd10ed 100644 --- a/libs/hwui/tests/unit/AutoBackendTextureReleaseTests.cpp +++ b/libs/hwui/tests/unit/AutoBackendTextureReleaseTests.cpp @@ -29,7 +29,7 @@ AHardwareBuffer* allocHardwareBuffer() { .height = 16, .layers = 1, .format = AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM, - .usage = AHARDWAREBUFFER_USAGE_CPU_READ_RARELY | AHARDWAREBUFFER_USAGE_CPU_WRITE_RARELY, + .usage = AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE, }; constexpr int kSucceeded = 0; int status = AHardwareBuffer_allocate(&desc, &buffer); diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index a84c42af93c2..761edf6c6170 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -5855,6 +5855,117 @@ public class AudioManager { } } + // Each listener corresponds to a unique callback stub because each listener can subscribe to + // different AudioAttributes. + private final ConcurrentHashMap<OnDevicesForAttributesChangedListener, + IDevicesForAttributesCallbackStub> mDevicesForAttributesListenerToStub = + new ConcurrentHashMap<>(); + + private static final class IDevicesForAttributesCallbackStub + extends IDevicesForAttributesCallback.Stub { + ListenerInfo<OnDevicesForAttributesChangedListener> mInfo; + + IDevicesForAttributesCallbackStub(@NonNull OnDevicesForAttributesChangedListener listener, + @NonNull Executor executor) { + mInfo = new ListenerInfo<>(listener, executor); + } + + public void register(boolean register, AudioAttributes attributes) { + try { + if (register) { + getService().addOnDevicesForAttributesChangedListener(attributes, this); + } else { + getService().removeOnDevicesForAttributesChangedListener(this); + } + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + @Override + public void onDevicesForAttributesChanged(AudioAttributes attributes, boolean forVolume, + List<AudioDeviceAttributes> devices) { + // forVolume is ignored. The case where it is `true` is not handled. + mInfo.mExecutor.execute(() -> + mInfo.mListener.onDevicesForAttributesChanged( + attributes, devices)); + } + } + + /** + * @hide + * Interface to be notified of when routing changes for the registered audio attributes. + */ + @SystemApi + public interface OnDevicesForAttributesChangedListener { + /** + * Called on the listener to indicate that the audio devices for the given audio + * attributes have changed. + * @param attributes the {@link AudioAttributes} whose routing changed + * @param devices a list of newly routed audio devices + */ + void onDevicesForAttributesChanged(@NonNull AudioAttributes attributes, + @NonNull List<AudioDeviceAttributes> devices); + } + + /** + * @hide + * Adds a listener for being notified of routing changes for the given {@link AudioAttributes}. + * @param attributes the {@link AudioAttributes} to listen for routing changes + * @param executor + * @param listener + */ + @SystemApi + @RequiresPermission(anyOf = { + android.Manifest.permission.MODIFY_AUDIO_ROUTING, + android.Manifest.permission.QUERY_AUDIO_STATE + }) + public void addOnDevicesForAttributesChangedListener(@NonNull AudioAttributes attributes, + @NonNull @CallbackExecutor Executor executor, + @NonNull OnDevicesForAttributesChangedListener listener) { + Objects.requireNonNull(attributes); + Objects.requireNonNull(executor); + Objects.requireNonNull(listener); + + synchronized (mDevicesForAttributesListenerToStub) { + IDevicesForAttributesCallbackStub callbackStub = + mDevicesForAttributesListenerToStub.get(listener); + + if (callbackStub == null) { + callbackStub = new IDevicesForAttributesCallbackStub(listener, executor); + mDevicesForAttributesListenerToStub.put(listener, callbackStub); + } + + callbackStub.register(true, attributes); + } + } + + /** + * @hide + * Removes a previously registered listener for being notified of routing changes for the given + * {@link AudioAttributes}. + * @param listener + */ + @SystemApi + @RequiresPermission(anyOf = { + android.Manifest.permission.MODIFY_AUDIO_ROUTING, + android.Manifest.permission.QUERY_AUDIO_STATE + }) + public void removeOnDevicesForAttributesChangedListener( + @NonNull OnDevicesForAttributesChangedListener listener) { + Objects.requireNonNull(listener); + + synchronized (mDevicesForAttributesListenerToStub) { + IDevicesForAttributesCallbackStub callbackStub = + mDevicesForAttributesListenerToStub.get(listener); + if (callbackStub != null) { + callbackStub.register(false, null /* attributes */); + } + + mDevicesForAttributesListenerToStub.remove(listener); + } + } + /** * Get the audio devices that would be used for the routing of the given audio attributes. * These are the devices anticipated to play sound from an {@link AudioTrack} created with diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl index 0de367d3bc7e..5ee32d61e1c1 100644 --- a/media/java/android/media/IAudioService.aidl +++ b/media/java/android/media/IAudioService.aidl @@ -36,6 +36,7 @@ import android.media.IAudioServerStateDispatcher; import android.media.ICapturePresetDevicesRoleDispatcher; import android.media.ICommunicationDeviceDispatcher; import android.media.IDeviceVolumeBehaviorDispatcher; +import android.media.IDevicesForAttributesCallback; import android.media.IMuteAwaitConnectionCallback; import android.media.IPlaybackConfigDispatcher; import android.media.IPreferredMixerAttributesDispatcher; @@ -356,6 +357,12 @@ interface IAudioService { List<AudioDeviceAttributes> getDevicesForAttributesUnprotected(in AudioAttributes attributes); + void addOnDevicesForAttributesChangedListener(in AudioAttributes attributes, + in IDevicesForAttributesCallback callback); + + oneway void removeOnDevicesForAttributesChangedListener( + in IDevicesForAttributesCallback callback); + int setAllowedCapturePolicy(in int capturePolicy); int getAllowedCapturePolicy(); diff --git a/media/java/android/media/IDevicesForAttributesCallback.aidl b/media/java/android/media/IDevicesForAttributesCallback.aidl new file mode 100644 index 000000000000..489ecf6081e2 --- /dev/null +++ b/media/java/android/media/IDevicesForAttributesCallback.aidl @@ -0,0 +1,33 @@ +/* + * 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.media; + +import android.media.AudioAttributes; +import android.media.AudioDeviceAttributes; + +/** + * AIDL for AudioService to signal updates of audio devices routing for attributes. + * + * {@hide} + */ +oneway interface IDevicesForAttributesCallback { + + void onDevicesForAttributesChanged(in AudioAttributes attributes, boolean forVolume, + in List<AudioDeviceAttributes> devices); + +} + diff --git a/media/java/android/media/IMediaRouterService.aidl b/media/java/android/media/IMediaRouterService.aidl index f6a9162cda39..aa7e4df18186 100644 --- a/media/java/android/media/IMediaRouterService.aidl +++ b/media/java/android/media/IMediaRouterService.aidl @@ -50,8 +50,7 @@ interface IMediaRouterService { // MediaRouterService.java for readability. // Methods for MediaRouter2 - boolean verifyPackageName(String clientPackageName); - void enforceMediaContentControlPermission(); + boolean verifyPackageExists(String clientPackageName); List<MediaRoute2Info> getSystemRoutes(); RoutingSessionInfo getSystemSessionInfo(); diff --git a/media/java/android/media/ImageWriter.java b/media/java/android/media/ImageWriter.java index 32a2ad3fc5d7..93259992d339 100644 --- a/media/java/android/media/ImageWriter.java +++ b/media/java/android/media/ImageWriter.java @@ -431,17 +431,15 @@ public class ImageWriter implements AutoCloseable { * @see Image#close */ public Image dequeueInputImage() { - synchronized (mCloseLock) { - if (mDequeuedImages.size() >= mMaxImages) { - throw new IllegalStateException( - "Already dequeued max number of Images " + mMaxImages); - } - WriterSurfaceImage newImage = new WriterSurfaceImage(this); - nativeDequeueInputImage(mNativeContext, newImage); - mDequeuedImages.add(newImage); - newImage.mIsImageValid = true; - return newImage; + if (mDequeuedImages.size() >= mMaxImages) { + throw new IllegalStateException( + "Already dequeued max number of Images " + mMaxImages); } + WriterSurfaceImage newImage = new WriterSurfaceImage(this); + nativeDequeueInputImage(mNativeContext, newImage); + mDequeuedImages.add(newImage); + newImage.mIsImageValid = true; + return newImage; } /** @@ -500,52 +498,50 @@ public class ImageWriter implements AutoCloseable { throw new IllegalArgumentException("image shouldn't be null"); } - synchronized (mCloseLock) { - boolean ownedByMe = isImageOwnedByMe(image); - if (ownedByMe && !(((WriterSurfaceImage) image).mIsImageValid)) { - throw new IllegalStateException("Image from ImageWriter is invalid"); - } - - // For images from other components that have non-null owner, need to detach first, - // then attach. Images without owners must already be attachable. - if (!ownedByMe) { - if ((image.getOwner() instanceof ImageReader)) { - ImageReader prevOwner = (ImageReader) image.getOwner(); + boolean ownedByMe = isImageOwnedByMe(image); + if (ownedByMe && !(((WriterSurfaceImage) image).mIsImageValid)) { + throw new IllegalStateException("Image from ImageWriter is invalid"); + } - prevOwner.detachImage(image); - } else if (image.getOwner() != null) { - throw new IllegalArgumentException( - "Only images from ImageReader can be queued to" - + " ImageWriter, other image source is not supported yet!"); - } + // For images from other components that have non-null owner, need to detach first, + // then attach. Images without owners must already be attachable. + if (!ownedByMe) { + if ((image.getOwner() instanceof ImageReader)) { + ImageReader prevOwner = (ImageReader) image.getOwner(); - attachAndQueueInputImage(image); - // This clears the native reference held by the original owner. - // When this Image is detached later by this ImageWriter, the - // native memory won't be leaked. - image.close(); - return; + prevOwner.detachImage(image); + } else if (image.getOwner() != null) { + throw new IllegalArgumentException( + "Only images from ImageReader can be queued to" + + " ImageWriter, other image source is not supported yet!"); } - Rect crop = image.getCropRect(); - nativeQueueInputImage(mNativeContext, image, image.getTimestamp(), image.getDataSpace(), - crop.left, crop.top, crop.right, crop.bottom, image.getTransform(), - image.getScalingMode()); + attachAndQueueInputImage(image); + // This clears the native reference held by the original owner. + // When this Image is detached later by this ImageWriter, the + // native memory won't be leaked. + image.close(); + return; + } - /** - * Only remove and cleanup the Images that are owned by this - * ImageWriter. Images detached from other owners are only temporarily - * owned by this ImageWriter and will be detached immediately after they - * are released by downstream consumers, so there is no need to keep - * track of them in mDequeuedImages. - */ - if (ownedByMe) { - mDequeuedImages.remove(image); - // Do not call close here, as close is essentially cancel image. - WriterSurfaceImage wi = (WriterSurfaceImage) image; - wi.clearSurfacePlanes(); - wi.mIsImageValid = false; - } + Rect crop = image.getCropRect(); + nativeQueueInputImage(mNativeContext, image, image.getTimestamp(), image.getDataSpace(), + crop.left, crop.top, crop.right, crop.bottom, image.getTransform(), + image.getScalingMode()); + + /** + * Only remove and cleanup the Images that are owned by this + * ImageWriter. Images detached from other owners are only temporarily + * owned by this ImageWriter and will be detached immediately after they + * are released by downstream consumers, so there is no need to keep + * track of them in mDequeuedImages. + */ + if (ownedByMe) { + mDequeuedImages.remove(image); + // Do not call close here, as close is essentially cancel image. + WriterSurfaceImage wi = (WriterSurfaceImage) image; + wi.clearSurfacePlanes(); + wi.mIsImageValid = false; } } @@ -681,11 +677,11 @@ public class ImageWriter implements AutoCloseable { */ @Override public void close() { + setOnImageReleasedListener(null, null); synchronized (mCloseLock) { if (!mIsWriterValid) { return; } - setOnImageReleasedListener(null, null); for (Image image : mDequeuedImages) { image.close(); } @@ -817,14 +813,12 @@ public class ImageWriter implements AutoCloseable { } final Handler handler; - final boolean isWriterValid; synchronized (iw.mListenerLock) { handler = iw.mListenerHandler; } - synchronized (iw.mCloseLock) { - isWriterValid = iw.mIsWriterValid; - } - if (handler != null && isWriterValid) { + + if (handler != null) { + // The ListenerHandler will take care of ensuring that the parent ImageWriter is valid handler.sendEmptyMessage(0); } } diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java index 5faa794151b1..fa74a9f12d7c 100644 --- a/media/java/android/media/MediaRouter2.java +++ b/media/java/android/media/MediaRouter2.java @@ -208,9 +208,9 @@ public final class MediaRouter2 { IMediaRouterService.Stub.asInterface( ServiceManager.getService(Context.MEDIA_ROUTER_SERVICE)); try { - // SecurityException will be thrown if there's no permission. - serviceBinder.enforceMediaContentControlPermission(); - if (!serviceBinder.verifyPackageName(clientPackageName)) { + // verifyPackageExists throws SecurityException if the caller doesn't hold + // MEDIA_CONTENT_CONTROL permission. + if (!serviceBinder.verifyPackageExists(clientPackageName)) { Log.e(TAG, "Package " + clientPackageName + " not found. Ignoring."); return null; } diff --git a/native/android/input.cpp b/native/android/input.cpp index 812db0f1c507..5e5ebed78e61 100644 --- a/native/android/input.cpp +++ b/native/android/input.cpp @@ -297,6 +297,8 @@ int32_t AMotionEvent_getClassification(const AInputEvent* motion_event) { return AMOTION_EVENT_CLASSIFICATION_DEEP_PRESS; case android::MotionClassification::TWO_FINGER_SWIPE: return AMOTION_EVENT_CLASSIFICATION_TWO_FINGER_SWIPE; + case android::MotionClassification::MULTI_FINGER_SWIPE: + return AMOTION_EVENT_CLASSIFICATION_MULTI_FINGER_SWIPE; } } diff --git a/packages/DynamicSystemInstallationService/AndroidManifest.xml b/packages/DynamicSystemInstallationService/AndroidManifest.xml index 176534829222..b194738c67b6 100644 --- a/packages/DynamicSystemInstallationService/AndroidManifest.xml +++ b/packages/DynamicSystemInstallationService/AndroidManifest.xml @@ -3,6 +3,7 @@ android:sharedUserId="android.uid.system"> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED"/> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.MANAGE_DYNAMIC_SYSTEM" /> <uses-permission android:name="android.permission.REBOOT" /> @@ -19,6 +20,7 @@ android:enabled="true" android:exported="true" android:permission="android.permission.INSTALL_DYNAMIC_SYSTEM" + android:foregroundServiceType="systemExempted" android:process=":dynsystem"> <intent-filter> <action android:name="android.os.image.action.NOTIFY_IF_IN_USE" /> diff --git a/packages/PackageInstaller/AndroidManifest.xml b/packages/PackageInstaller/AndroidManifest.xml index 696ea4a2c164..9e249c4c7974 100644 --- a/packages/PackageInstaller/AndroidManifest.xml +++ b/packages/PackageInstaller/AndroidManifest.xml @@ -18,6 +18,8 @@ <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" /> <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" /> <uses-permission android:name="com.google.android.permission.INSTALL_WEARABLE_PACKAGES" /> @@ -140,6 +142,7 @@ <!-- Wearable Components --> <service android:name=".wear.WearPackageInstallerService" android:permission="com.google.android.permission.INSTALL_WEARABLE_PACKAGES" + android:foregroundServiceType="systemExempted" android:exported="true"/> <provider android:name=".wear.WearPackageIconProvider" diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/AndroidManifest.xml b/packages/SettingsLib/CollapsingToolbarBaseActivity/AndroidManifest.xml index 244b367423a4..51fc7ed64660 100644 --- a/packages/SettingsLib/CollapsingToolbarBaseActivity/AndroidManifest.xml +++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/AndroidManifest.xml @@ -18,6 +18,6 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.settingslib.widget"> - <uses-sdk android:minSdkVersion="29" /> + <uses-sdk android:minSdkVersion="21" /> </manifest> diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout/collapsing_toolbar_base_layout.xml b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout/collapsing_toolbar_base_layout.xml index c799b9962828..02f69f679a46 100644 --- a/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout/collapsing_toolbar_base_layout.xml +++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout/collapsing_toolbar_base_layout.xml @@ -29,6 +29,12 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:theme="?android:attr/actionBarTheme" /> + <androidx.appcompat.widget.Toolbar + android:id="@+id/support_action_bar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:theme="?android:attr/actionBarTheme" + android:visibility="gone" /> <FrameLayout android:id="@+id/content_frame" android:layout_width="match_parent" diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarAppCompatActivity.java b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarAppCompatActivity.java new file mode 100644 index 000000000000..dcc6e5a37246 --- /dev/null +++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarAppCompatActivity.java @@ -0,0 +1,179 @@ +/* + * 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.collapsingtoolbar; + +import android.app.ActionBar; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toolbar; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; + +import com.android.settingslib.utils.BuildCompatUtils; +import com.android.settingslib.widget.R; + +import com.google.android.material.appbar.AppBarLayout; +import com.google.android.material.appbar.CollapsingToolbarLayout; +import com.google.android.material.color.DynamicColors; + +/** + * A base Activity that has a collapsing toolbar layout is used for the activities intending to + * enable the collapsing toolbar function. + */ +public class CollapsingToolbarAppCompatActivity extends AppCompatActivity { + + private class DelegateCallback implements CollapsingToolbarDelegate.HostCallback { + @Nullable + @Override + public ActionBar setActionBar(Toolbar toolbar) { + return null; + } + + @Nullable + @Override + public androidx.appcompat.app.ActionBar setActionBar( + androidx.appcompat.widget.Toolbar toolbar) { + CollapsingToolbarAppCompatActivity.super.setSupportActionBar(toolbar); + return CollapsingToolbarAppCompatActivity.super.getSupportActionBar(); + } + + @Override + public void setOuterTitle(CharSequence title) { + CollapsingToolbarAppCompatActivity.super.setTitle(title); + } + } + + private CollapsingToolbarDelegate mToolbardelegate; + + private int mCustomizeLayoutResId = 0; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (BuildCompatUtils.isAtLeastS()) { + DynamicColors.applyToActivityIfAvailable(this); + } + setTheme(R.style.Theme_SubSettingsBase); + + if (mCustomizeLayoutResId > 0 && !BuildCompatUtils.isAtLeastS()) { + super.setContentView(mCustomizeLayoutResId); + return; + } + + View view = getToolbarDelegate().onCreateView(getLayoutInflater(), null, this); + super.setContentView(view); + } + + @Override + public void setContentView(int layoutResID) { + final ViewGroup parent = (mToolbardelegate == null) ? findViewById(R.id.content_frame) + : mToolbardelegate.getContentFrameLayout(); + if (parent != null) { + parent.removeAllViews(); + } + LayoutInflater.from(this).inflate(layoutResID, parent); + } + + @Override + public void setContentView(View view) { + final ViewGroup parent = (mToolbardelegate == null) ? findViewById(R.id.content_frame) + : mToolbardelegate.getContentFrameLayout(); + if (parent != null) { + parent.addView(view); + } + } + + @Override + public void setContentView(View view, ViewGroup.LayoutParams params) { + final ViewGroup parent = (mToolbardelegate == null) ? findViewById(R.id.content_frame) + : mToolbardelegate.getContentFrameLayout(); + if (parent != null) { + parent.addView(view, params); + } + } + + /** + * This method allows an activity to replace the default layout with a customize layout. Notice + * that it will no longer apply the features being provided by this class when this method + * gets called. + */ + protected void setCustomizeContentView(int layoutResId) { + mCustomizeLayoutResId = layoutResId; + } + + @Override + public void setTitle(CharSequence title) { + getToolbarDelegate().setTitle(title); + } + + @Override + public void setTitle(int titleId) { + setTitle(getText(titleId)); + } + + @Override + public boolean onSupportNavigateUp() { + if (getSupportFragmentManager().getBackStackEntryCount() > 0) { + getSupportFragmentManager().popBackStackImmediate(); + } + + // Closes the activity if there is no fragment inside the stack. Otherwise the activity will + // has a blank screen since there is no any fragment. + if (getSupportFragmentManager().getBackStackEntryCount() == 0) { + finishAfterTransition(); + } + return true; + } + + @Override + public void onBackPressed() { + super.onBackPressed(); + + // Closes the activity if there is no fragment inside the stack. Otherwise the activity will + // has a blank screen since there is no any fragment. onBackPressed() in Activity.java only + // handles popBackStackImmediate(). This will close activity to avoid a blank screen. + if (getSupportFragmentManager().getBackStackEntryCount() == 0) { + finishAfterTransition(); + } + } + + /** + * Returns an instance of collapsing toolbar. + */ + @Nullable + public CollapsingToolbarLayout getCollapsingToolbarLayout() { + return getToolbarDelegate().getCollapsingToolbarLayout(); + } + + /** + * Return an instance of app bar. + */ + @Nullable + public AppBarLayout getAppBarLayout() { + return getToolbarDelegate().getAppBarLayout(); + } + + private CollapsingToolbarDelegate getToolbarDelegate() { + if (mToolbardelegate == null) { + mToolbardelegate = new CollapsingToolbarDelegate(new DelegateCallback()); + } + return mToolbardelegate; + } +} diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarBaseActivity.java b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarBaseActivity.java index 8c8b47875bec..01f92c4fa7b1 100644 --- a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarBaseActivity.java +++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarBaseActivity.java @@ -117,12 +117,30 @@ public class CollapsingToolbarBaseActivity extends FragmentActivity { @Override public boolean onNavigateUp() { - if (!super.onNavigateUp()) { + if (getSupportFragmentManager().getBackStackEntryCount() > 0) { + getSupportFragmentManager().popBackStackImmediate(); + } + + // Closes the activity if there is no fragment inside the stack. Otherwise the activity will + // has a blank screen since there is no any fragment. + if (getSupportFragmentManager().getBackStackEntryCount() == 0) { finishAfterTransition(); } return true; } + @Override + public void onBackPressed() { + super.onBackPressed(); + + // Closes the activity if there is no fragment inside the stack. Otherwise the activity will + // has a blank screen since there is no any fragment. onBackPressed() in Activity.java only + // handles popBackStackImmediate(). This will close activity to avoid a blank screen. + if (getSupportFragmentManager().getBackStackEntryCount() == 0) { + finishAfterTransition(); + } + } + /** * Returns an instance of collapsing toolbar. */ diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarDelegate.java b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarDelegate.java index 01698b7937aa..1c2288acd358 100644 --- a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarDelegate.java +++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarDelegate.java @@ -19,9 +19,11 @@ package com.android.settingslib.collapsingtoolbar; import static android.text.Layout.HYPHENATION_FREQUENCY_NORMAL_FAST; import android.app.ActionBar; +import android.app.Activity; import android.content.res.Configuration; import android.graphics.text.LineBreakConfig; import android.os.Build; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -30,6 +32,7 @@ import android.widget.Toolbar; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; import androidx.coordinatorlayout.widget.CoordinatorLayout; import com.android.settingslib.widget.R; @@ -42,7 +45,7 @@ import com.google.android.material.appbar.CollapsingToolbarLayout; * extend from {@link CollapsingToolbarBaseActivity} or from {@link CollapsingToolbarBaseFragment}. */ public class CollapsingToolbarDelegate { - + private static final String TAG = "CTBdelegate"; /** Interface to be implemented by the host of the Collapsing Toolbar. */ public interface HostCallback { /** @@ -53,6 +56,13 @@ public class CollapsingToolbarDelegate { @Nullable ActionBar setActionBar(Toolbar toolbar); + /** Sets support tool bar and return support action bar, this is for AppCompatActivity. */ + @Nullable + default androidx.appcompat.app.ActionBar setActionBar( + androidx.appcompat.widget.Toolbar toolbar) { + return null; + } + /** Sets a title on the host. */ void setOuterTitle(CharSequence title); } @@ -79,6 +89,13 @@ public class CollapsingToolbarDelegate { /** Method to call that creates the root view of the collapsing toolbar. */ @SuppressWarnings("RestrictTo") public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container) { + return onCreateView(inflater, container, null); + } + + /** Method to call that creates the root view of the collapsing toolbar. */ + @SuppressWarnings("RestrictTo") + View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + Activity activity) { final View view = inflater.inflate(R.layout.collapsing_toolbar_base_layout, container, false); if (view instanceof CoordinatorLayout) { @@ -99,17 +116,57 @@ public class CollapsingToolbarDelegate { } } autoSetCollapsingToolbarLayoutScrolling(); - mToolbar = view.findViewById(R.id.action_bar); mContentFrameLayout = view.findViewById(R.id.content_frame); - final ActionBar actionBar = mHostCallback.setActionBar(mToolbar); + if (activity instanceof AppCompatActivity) { + Log.d(TAG, "onCreateView: from AppCompatActivity and sub-class."); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + initSupportActionBar(inflater); + } else { + initRSupportActionBar(view); + } + } else { + Log.d(TAG, "onCreateView: from NonAppCompatActivity."); + mToolbar = view.findViewById(R.id.action_bar); + final ActionBar actionBar = mHostCallback.setActionBar(mToolbar); + // Enable title and home button by default + if (actionBar != null) { + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.setHomeButtonEnabled(true); + actionBar.setDisplayShowTitleEnabled(true); + } + } + return view; + } - // Enable title and home button by default + private void initSupportActionBar(@NonNull LayoutInflater inflater) { + if (mCollapsingToolbarLayout == null) { + return; + } + mCollapsingToolbarLayout.removeAllViews(); + inflater.inflate(R.layout.support_toolbar, mCollapsingToolbarLayout); + final androidx.appcompat.widget.Toolbar supportToolbar = + mCollapsingToolbarLayout.findViewById(R.id.support_action_bar); + final androidx.appcompat.app.ActionBar actionBar = + mHostCallback.setActionBar(supportToolbar); + if (actionBar != null) { + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.setHomeButtonEnabled(true); + actionBar.setDisplayShowTitleEnabled(true); + } + } + + private void initRSupportActionBar(View view) { + view.findViewById(R.id.action_bar).setVisibility(View.GONE); + final androidx.appcompat.widget.Toolbar supportToolbar = + view.findViewById(R.id.support_action_bar); + supportToolbar.setVisibility(View.VISIBLE); + final androidx.appcompat.app.ActionBar actionBar = + mHostCallback.setActionBar(supportToolbar); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setHomeButtonEnabled(true); actionBar.setDisplayShowTitleEnabled(true); } - return view; } /** Return an instance of CoordinatorLayout. */ @@ -160,9 +217,13 @@ public class CollapsingToolbarDelegate { new AppBarLayout.Behavior.DragCallback() { @Override public boolean canDrag(@NonNull AppBarLayout appBarLayout) { - // Header can be scrolling while device in landscape mode. - return appBarLayout.getResources().getConfiguration().orientation - == Configuration.ORIENTATION_LANDSCAPE; + // Header can be scrolling while device in landscape mode and SDK > 33 + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.TIRAMISU) { + return false; + } else { + return appBarLayout.getResources().getConfiguration().orientation + == Configuration.ORIENTATION_LANDSCAPE; + } } }); params.setBehavior(behavior); diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/widget/CollapsingCoordinatorLayout.java b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/widget/CollapsingCoordinatorLayout.java index d67ac3b86050..e4e34f8cae1e 100644 --- a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/widget/CollapsingCoordinatorLayout.java +++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/widget/CollapsingCoordinatorLayout.java @@ -49,7 +49,7 @@ import com.google.android.material.appbar.CollapsingToolbarLayout; */ @RequiresApi(Build.VERSION_CODES.S) public class CollapsingCoordinatorLayout extends CoordinatorLayout { - private static final String TAG = "CollapsingCoordinatorLayout"; + private static final String TAG = "CollapsingCoordinator"; private static final float TOOLBAR_LINE_SPACING_MULTIPLIER = 1.1f; private CharSequence mToolbarTitle; @@ -255,9 +255,13 @@ public class CollapsingCoordinatorLayout extends CoordinatorLayout { new AppBarLayout.Behavior.DragCallback() { @Override public boolean canDrag(@NonNull AppBarLayout appBarLayout) { - // Header can be scrolling while device in landscape mode. - return appBarLayout.getResources().getConfiguration().orientation - == Configuration.ORIENTATION_LANDSCAPE; + // Header can be scrolling while device in landscape mode and SDK > 33 + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.TIRAMISU) { + return false; + } else { + return appBarLayout.getResources().getConfiguration().orientation + == Configuration.ORIENTATION_LANDSCAPE; + } } }); params.setBehavior(behavior); diff --git a/packages/SettingsLib/MainSwitchPreference/res/layout/settingslib_main_switch_bar.xml b/packages/SettingsLib/MainSwitchPreference/res/layout/settingslib_main_switch_bar.xml index 59ae1221ddd3..b39d09f38b62 100644 --- a/packages/SettingsLib/MainSwitchPreference/res/layout/settingslib_main_switch_bar.xml +++ b/packages/SettingsLib/MainSwitchPreference/res/layout/settingslib_main_switch_bar.xml @@ -18,7 +18,11 @@ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_height="wrap_content" - android:layout_width="match_parent"> + android:layout_width="match_parent" + android:paddingLeft="?android:attr/listPreferredItemPaddingLeft" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingRight="?android:attr/listPreferredItemPaddingRight" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"> <TextView android:id="@+id/switch_text" @@ -50,7 +54,6 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" - android:layout_marginEnd="@dimen/settingslib_switchbar_subsettings_margin_end" android:focusable="false" android:clickable="false" android:theme="@style/SwitchBar.Switch.Settingslib"/> diff --git a/packages/SettingsLib/MainSwitchPreference/res/values-sw600dp/dmiens.xml b/packages/SettingsLib/MainSwitchPreference/res/values-sw600dp/dmiens.xml deleted file mode 100644 index 55a2589102d3..000000000000 --- a/packages/SettingsLib/MainSwitchPreference/res/values-sw600dp/dmiens.xml +++ /dev/null @@ -1,22 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - Copyright (C) 2021 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. - --> - -<resources> - - <!-- SwitchBar sub settings margin start / end --> - <dimen name="settingslib_switchbar_subsettings_margin_start">80dp</dimen> -</resources> diff --git a/packages/SettingsLib/MainSwitchPreference/res/values-sw720dp-land/dmiens.xml b/packages/SettingsLib/MainSwitchPreference/res/values-sw720dp-land/dmiens.xml deleted file mode 100644 index 53995bcf055b..000000000000 --- a/packages/SettingsLib/MainSwitchPreference/res/values-sw720dp-land/dmiens.xml +++ /dev/null @@ -1,23 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - Copyright (C) 2021 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. - --> - -<resources> - - <!-- SwitchBar sub settings margin start / end --> - <dimen name="settingslib_switchbar_subsettings_margin_start">128dp</dimen> - <dimen name="settingslib_switchbar_subsettings_margin_end">128dp</dimen> -</resources> diff --git a/packages/SettingsLib/MainSwitchPreference/res/values-sw720dp/dmiens.xml b/packages/SettingsLib/MainSwitchPreference/res/values-sw720dp/dmiens.xml deleted file mode 100644 index 9015c581eff5..000000000000 --- a/packages/SettingsLib/MainSwitchPreference/res/values-sw720dp/dmiens.xml +++ /dev/null @@ -1,23 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - Copyright (C) 2021 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. - --> - -<resources> - - <!-- SwitchBar sub settings margin start / end --> - <dimen name="settingslib_switchbar_subsettings_margin_start">80dp</dimen> - <dimen name="settingslib_switchbar_subsettings_margin_end">80dp</dimen> -</resources> diff --git a/packages/SettingsLib/MainSwitchPreference/res/values/dimens.xml b/packages/SettingsLib/MainSwitchPreference/res/values/dimens.xml index 157a54e3573d..88b2c8728495 100644 --- a/packages/SettingsLib/MainSwitchPreference/res/values/dimens.xml +++ b/packages/SettingsLib/MainSwitchPreference/res/values/dimens.xml @@ -27,6 +27,6 @@ <dimen name="settingslib_switch_title_margin">24dp</dimen> <!-- SwitchBar sub settings margin start / end --> - <dimen name="settingslib_switchbar_subsettings_margin_start">72dp</dimen> + <dimen name="settingslib_switchbar_subsettings_margin_start">56dp</dimen> <dimen name="settingslib_switchbar_subsettings_margin_end">16dp</dimen> </resources> diff --git a/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchBar.java b/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchBar.java index 86fec50d7c21..864a8bb17058 100644 --- a/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchBar.java +++ b/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchBar.java @@ -161,6 +161,19 @@ public class MainSwitchBar extends LinearLayout implements CompoundButton.OnChec } /** + * Set icon space reserved for title + */ + public void setIconSpaceReserved(boolean iconSpaceReserved) { + if (mTextView != null && !BuildCompatUtils.isAtLeastS()) { + LayoutParams params = (LayoutParams) mTextView.getLayoutParams(); + int iconSpace = getContext().getResources().getDimensionPixelSize( + R.dimen.settingslib_switchbar_subsettings_margin_start); + params.setMarginStart(iconSpaceReserved ? iconSpace : 0); + mTextView.setLayoutParams(params); + } + } + + /** * Show the MainSwitchBar */ public void show() { diff --git a/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchPreference.java b/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchPreference.java index fc0e05f7fb46..53cc268851e7 100644 --- a/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchPreference.java +++ b/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchPreference.java @@ -37,7 +37,6 @@ public class MainSwitchPreference extends TwoStatePreference implements OnMainSw private final List<OnMainSwitchChangeListener> mSwitchChangeListeners = new ArrayList<>(); private MainSwitchBar mMainSwitchBar; - private CharSequence mTitle; public MainSwitchPreference(Context context) { super(context); @@ -68,6 +67,10 @@ public class MainSwitchPreference extends TwoStatePreference implements OnMainSw holder.setDividerAllowedBelow(false); mMainSwitchBar = (MainSwitchBar) holder.findViewById(R.id.settingslib_main_switch_bar); + // To support onPreferenceChange callback, it needs to call callChangeListener() when + // MainSwitchBar is clicked. + mMainSwitchBar.setOnClickListener((view) -> callChangeListener(isChecked())); + setIconSpaceReserved(isIconSpaceReserved()); updateStatus(isChecked()); registerListenerToSwitchBar(); } @@ -82,6 +85,10 @@ public class MainSwitchPreference extends TwoStatePreference implements OnMainSw final CharSequence title = a.getText( androidx.preference.R.styleable.Preference_android_title); setTitle(title); + + final boolean bIconSpaceReserved = a.getBoolean( + androidx.preference.R.styleable.Preference_android_iconSpaceReserved, true); + setIconSpaceReserved(bIconSpaceReserved); a.recycle(); } } @@ -96,9 +103,17 @@ public class MainSwitchPreference extends TwoStatePreference implements OnMainSw @Override public void setTitle(CharSequence title) { - mTitle = title; + super.setTitle(title); if (mMainSwitchBar != null) { - mMainSwitchBar.setTitle(mTitle); + mMainSwitchBar.setTitle(title); + } + } + + @Override + public void setIconSpaceReserved(boolean iconSpaceReserved) { + super.setIconSpaceReserved(iconSpaceReserved); + if (mMainSwitchBar != null) { + mMainSwitchBar.setIconSpaceReserved(iconSpaceReserved); } } @@ -113,7 +128,7 @@ public class MainSwitchPreference extends TwoStatePreference implements OnMainSw public void updateStatus(boolean checked) { setChecked(checked); if (mMainSwitchBar != null) { - mMainSwitchBar.setTitle(mTitle); + mMainSwitchBar.setTitle(getTitle()); mMainSwitchBar.show(); } } @@ -125,6 +140,7 @@ public class MainSwitchPreference extends TwoStatePreference implements OnMainSw if (!mSwitchChangeListeners.contains(listener)) { mSwitchChangeListeners.add(listener); } + if (mMainSwitchBar != null) { mMainSwitchBar.addOnSwitchChangeListener(listener); } diff --git a/packages/SettingsLib/SettingsTheme/res/values-v31/style_preference.xml b/packages/SettingsLib/SettingsTheme/res/values-v31/style_preference.xml index bda478e6e4fe..f1e028b405db 100644 --- a/packages/SettingsLib/SettingsTheme/res/values-v31/style_preference.xml +++ b/packages/SettingsLib/SettingsTheme/res/values-v31/style_preference.xml @@ -17,6 +17,7 @@ <resources> <style name="PreferenceTheme.SettingsLib" parent="@style/PreferenceThemeOverlay"> <item name="preferenceCategoryTitleTextAppearance">@style/TextAppearance.CategoryTitle.SettingsLib</item> + <item name="preferenceScreenStyle">@style/SettingsPreferenceScreen.SettingsLib</item> <item name="preferenceCategoryStyle">@style/SettingsCategoryPreference.SettingsLib</item> <item name="preferenceStyle">@style/SettingsPreference.SettingsLib</item> <item name="checkBoxPreferenceStyle">@style/SettingsCheckBoxPreference.SettingsLib</item> @@ -28,6 +29,11 @@ <item name="footerPreferenceStyle">@style/Preference.Material</item> </style> + <style name="SettingsPreferenceScreen.SettingsLib" parent="@style/Preference.PreferenceScreen.Material"> + <item name="layout">@layout/settingslib_preference</item> + <item name="iconSpaceReserved">@bool/settingslib_config_icon_space_reserved</item> + </style> + <style name="SettingsCategoryPreference.SettingsLib" parent="@style/Preference.Category.Material"> <item name="iconSpaceReserved">@bool/settingslib_config_icon_space_reserved</item> <item name="allowDividerAbove">@bool/settingslib_config_allow_divider</item> diff --git a/packages/SettingsLib/SettingsTheme/res/values/styles.xml b/packages/SettingsLib/SettingsTheme/res/values/styles.xml index 328ab46ed2f9..af3fc4817c45 100644 --- a/packages/SettingsLib/SettingsTheme/res/values/styles.xml +++ b/packages/SettingsLib/SettingsTheme/res/values/styles.xml @@ -28,19 +28,19 @@ </style> <style name="TextAppearance.TopIntroText" - parent="@android:style/TextAppearance.DeviceDefault"> + parent="@android:style/TextAppearance.DeviceDefault"> <item name="android:textSize">14sp</item> <item name="android:textColor">?android:attr/textColorSecondary</item> </style> <style name="TextAppearance.EntityHeaderTitle" - parent="@android:style/TextAppearance.DeviceDefault.WindowTitle"> + parent="@android:style/TextAppearance.DeviceDefault.WindowTitle"> <item name="android:textColor">?android:attr/textColorPrimary</item> <item name="android:textSize">20sp</item> </style> <style name="TextAppearance.EntityHeaderSummary" - parent="@android:style/TextAppearance.DeviceDefault"> + parent="@android:style/TextAppearance.DeviceDefault"> <item name="android:textAlignment">viewStart</item> <item name="android:textColor">?android:attr/textColorSecondary</item> <item name="android:singleLine">true</item> diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/SpinnerPage.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/SpinnerPage.kt index ba769d206fd5..8fdc22f7bb3c 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/SpinnerPage.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/SpinnerPage.kt @@ -19,9 +19,11 @@ package com.android.settingslib.spa.gallery.ui import android.os.Bundle import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.tooling.preview.Preview import com.android.settingslib.spa.framework.common.SettingsEntryBuilder import com.android.settingslib.spa.framework.common.SettingsPage @@ -32,6 +34,7 @@ import com.android.settingslib.spa.widget.preference.Preference import com.android.settingslib.spa.widget.preference.PreferenceModel import com.android.settingslib.spa.widget.scaffold.RegularScaffold import com.android.settingslib.spa.widget.ui.Spinner +import com.android.settingslib.spa.widget.ui.SpinnerOption private const val TITLE = "Sample Spinner" @@ -55,16 +58,16 @@ object SpinnerPageProvider : SettingsPageProvider { @Composable override fun Page(arguments: Bundle?) { RegularScaffold(title = getTitle(arguments)) { - val selectedIndex = rememberSaveable { mutableStateOf(0) } + var selectedId by rememberSaveable { mutableStateOf(1) } Spinner( - options = (1..3).map { "Option $it" }, - selectedIndex = selectedIndex.value, - setIndex = { selectedIndex.value = it }, + options = (1..3).map { SpinnerOption(id = it, text = "Option $it") }, + selectedId = selectedId, + setId = { selectedId = it }, ) Preference(object : PreferenceModel { - override val title = "Selected index" + override val title = "Selected id" override val summary = remember { - derivedStateOf { selectedIndex.value.toString() } + derivedStateOf { selectedId.toString() } } }) } diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/ui/SpinnerScreenshotTest.kt b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/ui/SpinnerScreenshotTest.kt index e9b5b306fc0a..0b4d5e4ad629 100644 --- a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/ui/SpinnerScreenshotTest.kt +++ b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/ui/SpinnerScreenshotTest.kt @@ -17,6 +17,7 @@ package com.android.settingslib.spa.screenshot import com.android.settingslib.spa.widget.ui.Spinner +import com.android.settingslib.spa.widget.ui.SpinnerOption import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -43,9 +44,9 @@ class SpinnerScreenshotTest(emulationSpec: DeviceEmulationSpec) { fun test() { screenshotRule.screenshotTest("spinner") { Spinner( - options = (1..3).map { "Option $it" }, - selectedIndex = 0, - setIndex = {}, + options = (1..3).map { SpinnerOption(id = it, text = "Option $it") }, + selectedId = 1, + setId = {}, ) } } diff --git a/packages/SettingsLib/Spa/spa/Android.bp b/packages/SettingsLib/Spa/spa/Android.bp index 40613ceaaf5f..139f3e13d5bc 100644 --- a/packages/SettingsLib/Spa/spa/Android.bp +++ b/packages/SettingsLib/Spa/spa/Android.bp @@ -27,6 +27,7 @@ android_library { "androidx.slice_slice-builders", "androidx.slice_slice-core", "androidx.slice_slice-view", + "androidx.compose.animation_animation", "androidx.compose.material3_material3", "androidx.compose.material_material-icons-extended", "androidx.compose.runtime_runtime", diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt index aa10cc82a14e..a81e2e330b0f 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/BrowseActivity.kt @@ -14,6 +14,8 @@ * limitations under the License. */ +@file:OptIn(ExperimentalAnimationApi::class) + package com.android.settingslib.spa.framework import android.content.Intent @@ -21,25 +23,31 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.annotation.VisibleForTesting +import androidx.compose.animation.AnimatedContentScope +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.unit.IntOffset import androidx.core.view.WindowCompat import androidx.navigation.NavGraph.Companion.findStartDestination -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController import com.android.settingslib.spa.R import com.android.settingslib.spa.framework.common.LogCategory import com.android.settingslib.spa.framework.common.SettingsPage import com.android.settingslib.spa.framework.common.SettingsPageProvider import com.android.settingslib.spa.framework.common.SettingsPageProviderRepository import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory +import com.android.settingslib.spa.framework.compose.AnimatedNavHost import com.android.settingslib.spa.framework.compose.LocalNavController import com.android.settingslib.spa.framework.compose.NavControllerWrapperImpl +import com.android.settingslib.spa.framework.compose.composable import com.android.settingslib.spa.framework.compose.localNavController +import com.android.settingslib.spa.framework.compose.rememberAnimatedNavController import com.android.settingslib.spa.framework.theme.SettingsTheme import com.android.settingslib.spa.framework.util.PageEvent import com.android.settingslib.spa.framework.util.getDestination @@ -86,7 +94,7 @@ open class BrowseActivity : ComponentActivity() { @VisibleForTesting @Composable fun BrowseContent(sppRepository: SettingsPageProviderRepository, initialIntent: Intent? = null) { - val navController = rememberNavController() + val navController = rememberAnimatedNavController() CompositionLocalProvider(navController.localNavController()) { val controller = LocalNavController.current as NavControllerWrapperImpl controller.NavContent(sppRepository.getAllProviders()) @@ -97,15 +105,41 @@ fun BrowseContent(sppRepository: SettingsPageProviderRepository, initialIntent: @Composable private fun NavControllerWrapperImpl.NavContent(allProvider: Collection<SettingsPageProvider>) { val nullPage = SettingsPage.createNull() - NavHost( + AnimatedNavHost( navController = navController, startDestination = nullPage.sppName, ) { + val slideEffect = tween<IntOffset>(durationMillis = 300) + val fadeEffect = tween<Float>(durationMillis = 300) composable(nullPage.sppName) {} for (spp in allProvider) { composable( route = spp.name + spp.parameter.navRoute(), arguments = spp.parameter, + enterTransition = { + slideIntoContainer( + AnimatedContentScope.SlideDirection.Left, + animationSpec = slideEffect + ) + fadeIn(animationSpec = fadeEffect) + }, + exitTransition = { + slideOutOfContainer( + AnimatedContentScope.SlideDirection.Left, + animationSpec = slideEffect + ) + fadeOut(animationSpec = fadeEffect) + }, + popEnterTransition = { + slideIntoContainer( + AnimatedContentScope.SlideDirection.Right, + animationSpec = slideEffect + ) + fadeIn(animationSpec = fadeEffect) + }, + popExitTransition = { + slideOutOfContainer( + AnimatedContentScope.SlideDirection.Right, + animationSpec = slideEffect + ) + fadeOut(animationSpec = fadeEffect) + }, ) { navBackStackEntry -> spp.PageEvent(navBackStackEntry.arguments) spp.Page(navBackStackEntry.arguments) diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/AnimatedComposeNavigator.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/AnimatedComposeNavigator.kt new file mode 100644 index 000000000000..930a83f76e3f --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/AnimatedComposeNavigator.kt @@ -0,0 +1,79 @@ +/* + * 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.settingslib.spa.framework.compose + +import androidx.compose.animation.AnimatedVisibilityScope +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavDestination +import androidx.navigation.NavOptions +import androidx.navigation.Navigator + +/** + * Navigator that navigates through [Composable]s. Every destination using this Navigator must + * set a valid [Composable] by setting it directly on an instantiated [Destination] or calling + * [composable]. + */ +@ExperimentalAnimationApi +@Navigator.Name("animatedComposable") +public class AnimatedComposeNavigator : Navigator<AnimatedComposeNavigator.Destination>() { + internal val transitionsInProgress get() = state.transitionsInProgress + + internal val backStack get() = state.backStack + + internal val isPop = mutableStateOf(false) + + override fun navigate( + entries: List<NavBackStackEntry>, + navOptions: NavOptions?, + navigatorExtras: Extras? + ) { + entries.forEach { entry -> + state.pushWithTransition(entry) + } + isPop.value = false + } + + override fun createDestination(): Destination { + return Destination(this, content = { }) + } + + override fun popBackStack(popUpTo: NavBackStackEntry, savedState: Boolean) { + state.popWithTransition(popUpTo, savedState) + isPop.value = true + } + + internal fun markTransitionComplete(entry: NavBackStackEntry) { + state.markTransitionComplete(entry) + } + + /** + * NavDestination specific to [AnimatedComposeNavigator] + */ + @ExperimentalAnimationApi + @NavDestination.ClassType(Composable::class) + public class Destination( + navigator: AnimatedComposeNavigator, + internal val content: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit + ) : NavDestination(navigator) + + internal companion object { + internal const val NAME = "animatedComposable" + } +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/AnimatedNavHost.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/AnimatedNavHost.kt new file mode 100644 index 000000000000..013757282427 --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/AnimatedNavHost.kt @@ -0,0 +1,267 @@ +/* + * 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.settingslib.spa.framework.compose + +import androidx.activity.compose.LocalOnBackPressedDispatcherOwner +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedContentScope +import androidx.compose.animation.ContentTransform +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.core.tween +import androidx.compose.animation.core.updateTransition +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.with +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveableStateHolder +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavDestination +import androidx.navigation.NavDestination.Companion.hierarchy +import androidx.navigation.NavGraph +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavHostController +import androidx.navigation.Navigator +import androidx.navigation.compose.DialogHost +import androidx.navigation.compose.DialogNavigator +import androidx.navigation.compose.LocalOwnersProvider +import androidx.navigation.createGraph +import androidx.navigation.get +import kotlinx.coroutines.flow.map + +/** + * Provides in place in the Compose hierarchy for self contained navigation to occur. + * + * Once this is called, any Composable within the given [NavGraphBuilder] can be navigated to from + * the provided [navController]. + * + * The builder passed into this method is [remember]ed. This means that for this NavHost, the + * contents of the builder cannot be changed. + * + * @param navController the navController for this host + * @param startDestination the route for the start destination + * @param modifier The modifier to be applied to the layout. + * @param route the route for the graph + * @param enterTransition callback to define enter transitions for destination in this host + * @param exitTransition callback to define exit transitions for destination in this host + * @param popEnterTransition callback to define popEnter transitions for destination in this host + * @param popExitTransition callback to define popExit transitions for destination in this host + * @param builder the builder used to construct the graph + */ +@Composable +@ExperimentalAnimationApi +public fun AnimatedNavHost( + navController: NavHostController, + startDestination: String, + modifier: Modifier = Modifier, + contentAlignment: Alignment = Alignment.Center, + route: String? = null, + enterTransition: (AnimatedContentScope<NavBackStackEntry>.() -> EnterTransition) = + { fadeIn(animationSpec = tween(700)) }, + exitTransition: (AnimatedContentScope<NavBackStackEntry>.() -> ExitTransition) = + { fadeOut(animationSpec = tween(700)) }, + popEnterTransition: (AnimatedContentScope<NavBackStackEntry>.() -> EnterTransition) = + enterTransition, + popExitTransition: (AnimatedContentScope<NavBackStackEntry>.() -> ExitTransition) = + exitTransition, + builder: NavGraphBuilder.() -> Unit +) { + AnimatedNavHost( + navController, + remember(route, startDestination, builder) { + navController.createGraph(startDestination, route, builder) + }, + modifier, + contentAlignment, + enterTransition, + exitTransition, + popEnterTransition, + popExitTransition + ) +} + +/** + * Provides in place in the Compose hierarchy for self contained navigation to occur. + * + * Once this is called, any Composable within the given [NavGraphBuilder] can be navigated to from + * the provided [navController]. + * + * @param navController the navController for this host + * @param graph the graph for this host + * @param modifier The modifier to be applied to the layout. + * @param enterTransition callback to define enter transitions for destination in this host + * @param exitTransition callback to define exit transitions for destination in this host + * @param popEnterTransition callback to define popEnter transitions for destination in this host + * @param popExitTransition callback to define popExit transitions for destination in this host + */ +@ExperimentalAnimationApi +@Composable +public fun AnimatedNavHost( + navController: NavHostController, + graph: NavGraph, + modifier: Modifier = Modifier, + contentAlignment: Alignment = Alignment.Center, + enterTransition: (AnimatedContentScope<NavBackStackEntry>.() -> EnterTransition) = + { fadeIn(animationSpec = tween(700)) }, + exitTransition: (AnimatedContentScope<NavBackStackEntry>.() -> ExitTransition) = + { fadeOut(animationSpec = tween(700)) }, + popEnterTransition: (AnimatedContentScope<NavBackStackEntry>.() -> EnterTransition) = + enterTransition, + popExitTransition: (AnimatedContentScope<NavBackStackEntry>.() -> ExitTransition) = + exitTransition, +) { + + val lifecycleOwner = LocalLifecycleOwner.current + val viewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) { + "NavHost requires a ViewModelStoreOwner to be provided via LocalViewModelStoreOwner" + } + val onBackPressedDispatcherOwner = LocalOnBackPressedDispatcherOwner.current + val onBackPressedDispatcher = onBackPressedDispatcherOwner?.onBackPressedDispatcher + + // on successful recompose we setup the navController with proper inputs + // after the first time, this will only happen again if one of the inputs changes + navController.setLifecycleOwner(lifecycleOwner) + navController.setViewModelStore(viewModelStoreOwner.viewModelStore) + if (onBackPressedDispatcher != null) { + navController.setOnBackPressedDispatcher(onBackPressedDispatcher) + } + + navController.graph = graph + + val saveableStateHolder = rememberSaveableStateHolder() + + // Find the ComposeNavigator, returning early if it isn't found + // (such as is the case when using TestNavHostController) + val composeNavigator = navController.navigatorProvider.get<Navigator<out NavDestination>>( + AnimatedComposeNavigator.NAME + ) as? AnimatedComposeNavigator ?: return + val visibleEntries by remember(navController.visibleEntries) { + navController.visibleEntries.map { + it.filter { entry -> + entry.destination.navigatorName == AnimatedComposeNavigator.NAME + } + } + }.collectAsState(emptyList()) + + val backStackEntry = visibleEntries.lastOrNull() + + if (backStackEntry != null) { + val finalEnter: AnimatedContentScope<NavBackStackEntry>.() -> EnterTransition = { + val targetDestination = targetState.destination as AnimatedComposeNavigator.Destination + + if (composeNavigator.isPop.value) { + targetDestination.hierarchy.firstNotNullOfOrNull { destination -> + popEnterTransitions[destination.route]?.invoke(this) + } ?: popEnterTransition.invoke(this) + } else { + targetDestination.hierarchy.firstNotNullOfOrNull { destination -> + enterTransitions[destination.route]?.invoke(this) + } ?: enterTransition.invoke(this) + } + } + + val finalExit: AnimatedContentScope<NavBackStackEntry>.() -> ExitTransition = { + val initialDestination = + initialState.destination as AnimatedComposeNavigator.Destination + + if (composeNavigator.isPop.value) { + initialDestination.hierarchy.firstNotNullOfOrNull { destination -> + popExitTransitions[destination.route]?.invoke(this) + } ?: popExitTransition.invoke(this) + } else { + initialDestination.hierarchy.firstNotNullOfOrNull { destination -> + exitTransitions[destination.route]?.invoke(this) + } ?: exitTransition.invoke(this) + } + } + + val transition = updateTransition(backStackEntry, label = "entry") + transition.AnimatedContent( + modifier, + transitionSpec = { + val zIndex = if (composeNavigator.isPop.value) { + visibleEntries.indexOf(initialState).toFloat() + } else { + visibleEntries.indexOf(targetState).toFloat() + } + // If the initialState of the AnimatedContent is not in visibleEntries, we are in + // a case where visible has cleared the old state for some reason, so instead of + // attempting to animate away from the initialState, we skip the animation. + if (initialState in visibleEntries) { + ContentTransform(finalEnter(this), finalExit(this), zIndex) + } else { + EnterTransition.None with ExitTransition.None + } + }, + contentAlignment, + contentKey = { it.id } + ) { + // In some specific cases, such as clearing your back stack by changing your + // start destination, AnimatedContent can contain an entry that is no longer + // part of visible entries since it was cleared from the back stack and is not + // animating. In these cases the currentEntry will be null, and in those cases, + // AnimatedContent will just skip attempting to transition the old entry. + // See https://issuetracker.google.com/238686802 + val currentEntry = visibleEntries.lastOrNull { entry -> + it == entry + } + // while in the scope of the composable, we provide the navBackStackEntry as the + // ViewModelStoreOwner and LifecycleOwner + currentEntry?.LocalOwnersProvider(saveableStateHolder) { + (currentEntry.destination as AnimatedComposeNavigator.Destination) + .content(this, currentEntry) + } + } + if (transition.currentState == transition.targetState) { + visibleEntries.forEach { entry -> + composeNavigator.markTransitionComplete(entry) + } + } + } + + val dialogNavigator = navController.navigatorProvider.get<Navigator<out NavDestination>>( + "dialog" + ) as? DialogNavigator ?: return + + // Show any dialog destinations + DialogHost(dialogNavigator) +} + +@ExperimentalAnimationApi +internal val enterTransitions = + mutableMapOf<String?, + (AnimatedContentScope<NavBackStackEntry>.() -> EnterTransition?)?>() + +@ExperimentalAnimationApi +internal val exitTransitions = + mutableMapOf<String?, (AnimatedContentScope<NavBackStackEntry>.() -> ExitTransition?)?>() + +@ExperimentalAnimationApi +internal val popEnterTransitions = + mutableMapOf<String?, (AnimatedContentScope<NavBackStackEntry>.() -> EnterTransition?)?>() + +@ExperimentalAnimationApi +internal val popExitTransitions = + mutableMapOf<String?, (AnimatedContentScope<NavBackStackEntry>.() -> ExitTransition?)?>() diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/NavGraphBuilder.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/NavGraphBuilder.kt new file mode 100644 index 000000000000..9e58603bbaff --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/NavGraphBuilder.kt @@ -0,0 +1,118 @@ +/* + * 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.settingslib.spa.framework.compose + +import androidx.compose.animation.AnimatedContentScope +import androidx.compose.animation.AnimatedVisibilityScope +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.runtime.Composable +import androidx.navigation.NamedNavArgument +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavDeepLink +import androidx.navigation.NavGraph +import androidx.navigation.NavGraphBuilder +import androidx.navigation.compose.navigation +import androidx.navigation.get + +/** + * Add the [Composable] to the [NavGraphBuilder] + * + * @param route route for the destination + * @param arguments list of arguments to associate with destination + * @param deepLinks list of deep links to associate with the destinations + * @param enterTransition callback to determine the destination's enter transition + * @param exitTransition callback to determine the destination's exit transition + * @param popEnterTransition callback to determine the destination's popEnter transition + * @param popExitTransition callback to determine the destination's popExit transition + * @param content composable for the destination + */ +@ExperimentalAnimationApi +public fun NavGraphBuilder.composable( + route: String, + arguments: List<NamedNavArgument> = emptyList(), + deepLinks: List<NavDeepLink> = emptyList(), + enterTransition: (AnimatedContentScope<NavBackStackEntry>.() -> EnterTransition?)? = null, + exitTransition: (AnimatedContentScope<NavBackStackEntry>.() -> ExitTransition?)? = null, + popEnterTransition: ( + AnimatedContentScope<NavBackStackEntry>.() -> EnterTransition? + )? = enterTransition, + popExitTransition: ( + AnimatedContentScope<NavBackStackEntry>.() -> ExitTransition? + )? = exitTransition, + content: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit +) { + addDestination( + AnimatedComposeNavigator.Destination( + provider[AnimatedComposeNavigator::class], + content + ).apply { + this.route = route + arguments.forEach { (argumentName, argument) -> + addArgument(argumentName, argument) + } + deepLinks.forEach { deepLink -> + addDeepLink(deepLink) + } + enterTransition?.let { enterTransitions[route] = enterTransition } + exitTransition?.let { exitTransitions[route] = exitTransition } + popEnterTransition?.let { popEnterTransitions[route] = popEnterTransition } + popExitTransition?.let { popExitTransitions[route] = popExitTransition } + } + ) +} + +/** + * Construct a nested [NavGraph] + * + * @param startDestination the starting destination's route for this NavGraph + * @param route the destination's unique route + * @param arguments list of arguments to associate with destination + * @param deepLinks list of deep links to associate with the destinations + * @param enterTransition callback to define enter transitions for destination in this NavGraph + * @param exitTransition callback to define exit transitions for destination in this NavGraph + * @param popEnterTransition callback to define pop enter transitions for destination in this + * NavGraph + * @param popExitTransition callback to define pop exit transitions for destination in this NavGraph + * @param builder the builder used to construct the graph + * + * @return the newly constructed nested NavGraph + */ +@ExperimentalAnimationApi +public fun NavGraphBuilder.navigation( + startDestination: String, + route: String, + arguments: List<NamedNavArgument> = emptyList(), + deepLinks: List<NavDeepLink> = emptyList(), + enterTransition: (AnimatedContentScope<NavBackStackEntry>.() -> EnterTransition?)? = null, + exitTransition: (AnimatedContentScope<NavBackStackEntry>.() -> ExitTransition?)? = null, + popEnterTransition: ( + AnimatedContentScope<NavBackStackEntry>.() -> EnterTransition? + )? = enterTransition, + popExitTransition: ( + AnimatedContentScope<NavBackStackEntry>.() -> ExitTransition? + )? = exitTransition, + builder: NavGraphBuilder.() -> Unit +) { + navigation(startDestination, route, arguments, deepLinks, builder).apply { + enterTransition?.let { enterTransitions[route] = enterTransition } + exitTransition?.let { exitTransitions[route] = exitTransition } + popEnterTransition?.let { popEnterTransitions[route] = popEnterTransition } + popExitTransition?.let { popExitTransitions[route] = popExitTransition } + } +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/NavHostController.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/NavHostController.kt new file mode 100644 index 000000000000..a8ac86c2fb15 --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/NavHostController.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spa.framework.compose + +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.navigation.NavDestination +import androidx.navigation.NavHostController +import androidx.navigation.Navigator +import androidx.navigation.compose.rememberNavController + +/** + * Creates a NavHostController that handles the adding of the [ComposeNavigator], [DialogNavigator] + * and [AnimatedComposeNavigator]. Additional [androidx.navigation.Navigator] instances should be + * added in a [androidx.compose.runtime.SideEffect] block. + * + * @see AnimatedNavHost + */ +@ExperimentalAnimationApi +@Composable +fun rememberAnimatedNavController( + vararg navigators: Navigator<out NavDestination> +): NavHostController { + val animatedNavigator = remember { AnimatedComposeNavigator() } + return rememberNavController(animatedNavigator, *navigators) +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Spinner.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Spinner.kt index 429b81a04659..64a9c7353f07 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Spinner.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Spinner.kt @@ -45,8 +45,13 @@ import androidx.compose.ui.unit.dp import com.android.settingslib.spa.framework.theme.SettingsDimension import com.android.settingslib.spa.framework.theme.SettingsTheme +data class SpinnerOption( + val id: Int, + val text: String, +) + @Composable -fun Spinner(options: List<String>, selectedIndex: Int, setIndex: (index: Int) -> Unit) { +fun Spinner(options: List<SpinnerOption>, selectedId: Int?, setId: (id: Int) -> Unit) { if (options.isEmpty()) { return } @@ -68,7 +73,7 @@ fun Spinner(options: List<String>, selectedIndex: Int, setIndex: (index: Int) -> ), contentPadding = contentPadding, ) { - SpinnerText(options[selectedIndex]) + SpinnerText(options.find { it.id == selectedId }) Icon( imageVector = when { expanded -> Icons.Outlined.ArrowDropUp @@ -83,18 +88,18 @@ fun Spinner(options: List<String>, selectedIndex: Int, setIndex: (index: Int) -> modifier = Modifier.background(SettingsTheme.colorScheme.spinnerItemContainer), offset = DpOffset(x = 0.dp, y = 4.dp), ) { - options.forEachIndexed { index, option -> + for (option in options) { DropdownMenuItem( text = { SpinnerText( - text = option, + option = option, modifier = Modifier.padding(end = 24.dp), color = SettingsTheme.colorScheme.onSpinnerItemContainer, ) }, onClick = { expanded = false - setIndex(index) + setId(option.id) }, contentPadding = contentPadding, ) @@ -105,12 +110,12 @@ fun Spinner(options: List<String>, selectedIndex: Int, setIndex: (index: Int) -> @Composable private fun SpinnerText( - text: String, + option: SpinnerOption?, modifier: Modifier = Modifier, color: Color = Color.Unspecified, ) { Text( - text = text, + text = option?.text ?: "", modifier = modifier.padding(end = SettingsDimension.itemPaddingEnd), color = color, style = MaterialTheme.typography.labelLarge, @@ -121,11 +126,11 @@ private fun SpinnerText( @Composable private fun SpinnerPreview() { SettingsTheme { - var selectedIndex by rememberSaveable { mutableStateOf(0) } + var selectedId by rememberSaveable { mutableStateOf(1) } Spinner( - options = (1..3).map { "Option $it" }, - selectedIndex = selectedIndex, - setIndex = { selectedIndex = it }, + options = (1..3).map { SpinnerOption(id = it, text = "Option $it") }, + selectedId = selectedId, + setId = { selectedId = it }, ) } } diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/ui/SpinnerTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/ui/SpinnerTest.kt index 6c56d63c18c7..33a4080376fb 100644 --- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/ui/SpinnerTest.kt +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/ui/SpinnerTest.kt @@ -36,28 +36,28 @@ class SpinnerTest { @Test fun spinner_initialState() { - var selectedIndex by mutableStateOf(0) + var selectedId by mutableStateOf(1) composeTestRule.setContent { Spinner( - options = (1..3).map { "Option $it" }, - selectedIndex = selectedIndex, - setIndex = { selectedIndex = it }, + options = (1..3).map { SpinnerOption(id = it, text = "Option $it") }, + selectedId = selectedId, + setId = { selectedId = it }, ) } composeTestRule.onNodeWithText("Option 1").assertIsDisplayed() composeTestRule.onNodeWithText("Option 2").assertDoesNotExist() - assertThat(selectedIndex).isEqualTo(0) + assertThat(selectedId).isEqualTo(1) } @Test fun spinner_canChangeState() { - var selectedIndex by mutableStateOf(0) + var selectedId by mutableStateOf(1) composeTestRule.setContent { Spinner( - options = (1..3).map { "Option $it" }, - selectedIndex = selectedIndex, - setIndex = { selectedIndex = it }, + options = (1..3).map { SpinnerOption(id = it, text = "Option $it") }, + selectedId = selectedId, + setId = { selectedId = it }, ) } @@ -66,6 +66,6 @@ class SpinnerTest { composeTestRule.onNodeWithText("Option 1").assertDoesNotExist() composeTestRule.onNodeWithText("Option 2").assertIsDisplayed() - assertThat(selectedIndex).isEqualTo(1) + assertThat(selectedId).isEqualTo(2) } } diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListModel.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListModel.kt index af5c5dcd37d5..791b4e0a7f28 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListModel.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListModel.kt @@ -4,6 +4,7 @@ import android.content.pm.ApplicationInfo import android.icu.text.CollationKey import androidx.compose.runtime.Composable import androidx.compose.runtime.State +import com.android.settingslib.spa.widget.ui.SpinnerOption import com.android.settingslib.spaprivileged.template.app.AppListItem import com.android.settingslib.spaprivileged.template.app.AppListItemModel import kotlinx.coroutines.flow.Flow @@ -23,7 +24,7 @@ interface AppListModel<T : AppRecord> { * * Default no spinner will be shown. */ - fun getSpinnerOptions(): List<String> = emptyList() + fun getSpinnerOptions(recordList: List<T>): List<SpinnerOption> = emptyList() /** * Loads the extra info for the App List, and generates the [AppRecord] List. @@ -42,8 +43,10 @@ interface AppListModel<T : AppRecord> { * This function is called when the App List's loading is finished and displayed to the user. * * Could do some pre-cache here. + * + * @return true to enable pre-fetching app labels. */ - suspend fun onFirstLoaded(recordList: List<T>) {} + suspend fun onFirstLoaded(recordList: List<T>) = false /** * Gets the comparator to sort the App List. diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt index 4c144b294fa5..cbb4fbe32713 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt @@ -21,6 +21,7 @@ import android.content.Intent import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import android.content.pm.ResolveInfo +import com.android.internal.R import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow @@ -49,7 +50,7 @@ internal interface AppListRepository { } -internal class AppListRepositoryImpl(context: Context) : AppListRepository { +internal class AppListRepositoryImpl(private val context: Context) : AppListRepository { private val packageManager = context.packageManager override suspend fun loadApps(config: AppListConfig): List<ApplicationInfo> = coroutineScope { @@ -59,6 +60,9 @@ internal class AppListRepositoryImpl(context: Context) : AppListRepository { .map { it.packageName } .toSet() } + val hideWhenDisabledPackagesDeferred = async { + context.resources.getStringArray(R.array.config_hideWhenDisabled_packageNames) + } val flags = PackageManager.ApplicationInfoFlags.of( (PackageManager.MATCH_DISABLED_COMPONENTS or PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS).toLong() @@ -67,8 +71,9 @@ internal class AppListRepositoryImpl(context: Context) : AppListRepository { packageManager.getInstalledApplicationsAsUser(flags, config.userId) val hiddenSystemModules = hiddenSystemModulesDeferred.await() + val hideWhenDisabledPackages = hideWhenDisabledPackagesDeferred.await() installedApplicationsAsUser.filter { app -> - app.isInAppList(config.showInstantApps, hiddenSystemModules) + app.isInAppList(config.showInstantApps, hiddenSystemModules, hideWhenDisabledPackages) } } @@ -116,12 +121,13 @@ internal class AppListRepositoryImpl(context: Context) : AppListRepository { private fun ApplicationInfo.isInAppList( showInstantApps: Boolean, hiddenSystemModules: Set<String>, + hideWhenDisabledPackages: Array<String>, ) = when { !showInstantApps && isInstantApp -> false packageName in hiddenSystemModules -> false + packageName in hideWhenDisabledPackages -> enabled && !isDisabledUntilUsed enabled -> true - isDisabledUntilUsed -> true - else -> false + else -> enabledSetting == PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER } } } diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListViewModel.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListViewModel.kt index 650b27845bd2..df828f2c0fa2 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListViewModel.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListViewModel.kt @@ -25,9 +25,11 @@ import androidx.lifecycle.viewModelScope import com.android.settingslib.spa.framework.util.StateFlowBridge import com.android.settingslib.spa.framework.util.asyncMapItem import com.android.settingslib.spa.framework.util.waitFirst +import com.android.settingslib.spa.widget.ui.SpinnerOption import java.util.concurrent.ConcurrentHashMap import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine @@ -48,6 +50,12 @@ internal data class AppListData<T : AppRecord>( AppListData(appEntries.filter(predicate), option) } +internal interface IAppListViewModel<T : AppRecord> { + val option: StateFlowBridge<Int?> + val spinnerOptionsFlow: Flow<List<SpinnerOption>> + val appListDataFlow: Flow<AppListData<T>> +} + internal class AppListViewModel<T : AppRecord>( application: Application, ) : AppListViewModelImpl<T>(application) @@ -57,11 +65,11 @@ internal open class AppListViewModelImpl<T : AppRecord>( application: Application, appListRepositoryFactory: (Context) -> AppListRepository = ::AppListRepositoryImpl, appRepositoryFactory: (Context) -> AppRepository = ::AppRepositoryImpl, -) : AndroidViewModel(application) { +) : AndroidViewModel(application), IAppListViewModel<T> { val appListConfig = StateFlowBridge<AppListConfig>() val listModel = StateFlowBridge<AppListModel<T>>() val showSystem = StateFlowBridge<Boolean>() - val option = StateFlowBridge<Int>() + final override val option = StateFlowBridge<Int?>() val searchQuery = StateFlowBridge<String>() private val appListRepository = appListRepositoryFactory(application) @@ -84,7 +92,12 @@ internal open class AppListViewModelImpl<T : AppRecord>( recordList.filter { showAppPredicate(it.app) } } - val appListDataFlow = option.flow.flatMapLatest(::filterAndSort) + override val spinnerOptionsFlow = + recordListFlow.combine(listModel.flow) { recordList, listModel -> + listModel.getSpinnerOptions(recordList) + } + + override val appListDataFlow = option.flow.flatMapLatest(::filterAndSort) .combine(searchQuery.flow) { appListData, searchQuery -> appListData.filter { it.label.contains(other = searchQuery, ignoreCase = true) @@ -97,7 +110,7 @@ internal open class AppListViewModelImpl<T : AppRecord>( } fun reloadApps() { - viewModelScope.launch { + scope.launch { appsStateFlow.value = appListRepository.loadApps(appListConfig.flow.first()) } } @@ -124,17 +137,16 @@ internal open class AppListViewModelImpl<T : AppRecord>( recordListFlow .waitFirst(appListDataFlow) .combine(listModel.flow) { recordList, listModel -> - listModel.maybePreFetchLabels(recordList) - listModel.onFirstLoaded(recordList) + if (listModel.onFirstLoaded(recordList)) { + preFetchLabels(recordList) + } } .launchIn(scope) } - private fun AppListModel<T>.maybePreFetchLabels(recordList: List<T>) { - if (getSpinnerOptions().isNotEmpty()) { - for (record in recordList) { - getLabel(record.app) - } + private fun preFetchLabels(recordList: List<T>) { + for (record in recordList) { + getLabel(record.app) } } diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppOpsController.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppOpsController.kt index e8788048776e..6cd6e951ef02 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppOpsController.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppOpsController.kt @@ -17,7 +17,6 @@ package com.android.settingslib.spaprivileged.model.app import android.app.AppOpsManager.MODE_ALLOWED -import android.app.AppOpsManager.MODE_ERRORED import android.app.AppOpsManager.Mode import android.content.Context import android.content.pm.ApplicationInfo @@ -33,14 +32,14 @@ interface IAppOpsController { fun setAllowed(allowed: Boolean) - @Mode - fun getMode(): Int + @Mode fun getMode(): Int } class AppOpsController( context: Context, private val app: ApplicationInfo, private val op: Int, + private val modeForNotAllowed: Int, private val setModeByUid: Boolean = false, ) : IAppOpsController { private val appOpsManager = context.appOpsManager @@ -49,7 +48,7 @@ class AppOpsController( get() = _mode override fun setAllowed(allowed: Boolean) { - val mode = if (allowed) MODE_ALLOWED else MODE_ERRORED + val mode = if (allowed) MODE_ALLOWED else modeForNotAllowed if (setModeByUid) { appOpsManager.setUidMode(op, app.uid, mode) } else { @@ -58,12 +57,12 @@ class AppOpsController( _mode.postValue(mode) } - @Mode - override fun getMode(): Int = appOpsManager.checkOpNoThrow(op, app.uid, app.packageName) + @Mode override fun getMode(): Int = appOpsManager.checkOpNoThrow(op, app.uid, app.packageName) - private val _mode = object : MutableLiveData<Int>() { - override fun onActive() { - postValue(getMode()) + private val _mode = + object : MutableLiveData<Int>() { + override fun onActive() { + postValue(getMode()) + } } - } } diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt index 3ff1d897ad6f..34c3ee0e2c0c 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt @@ -19,13 +19,16 @@ package com.android.settingslib.spaprivileged.template.app import android.content.Intent import android.content.IntentFilter import android.os.UserHandle +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp @@ -34,8 +37,11 @@ import com.android.settingslib.spa.framework.compose.LogCompositions import com.android.settingslib.spa.framework.compose.TimeMeasurer.Companion.rememberTimeMeasurer import com.android.settingslib.spa.framework.compose.rememberLazyListStateAndHideKeyboardWhenStartScroll import com.android.settingslib.spa.framework.compose.toState +import com.android.settingslib.spa.framework.util.StateFlowBridge import com.android.settingslib.spa.widget.ui.CategoryTitle import com.android.settingslib.spa.widget.ui.PlaceholderTitle +import com.android.settingslib.spa.widget.ui.Spinner +import com.android.settingslib.spa.widget.ui.SpinnerOption import com.android.settingslib.spaprivileged.R import com.android.settingslib.spaprivileged.framework.compose.DisposableBroadcastReceiverAsUser import com.android.settingslib.spaprivileged.model.app.AppEntry @@ -44,6 +50,7 @@ import com.android.settingslib.spaprivileged.model.app.AppListData import com.android.settingslib.spaprivileged.model.app.AppListModel import com.android.settingslib.spaprivileged.model.app.AppListViewModel import com.android.settingslib.spaprivileged.model.app.AppRecord +import com.android.settingslib.spaprivileged.model.app.IAppListViewModel import kotlinx.coroutines.Dispatchers private const val TAG = "AppList" @@ -51,7 +58,6 @@ private const val CONTENT_TYPE_HEADER = "header" data class AppListState( val showSystem: State<Boolean>, - val option: State<Int>, val searchQuery: State<String>, ) @@ -71,16 +77,36 @@ data class AppListInput<T : AppRecord>( */ @Composable fun <T : AppRecord> AppListInput<T>.AppList() { - AppListImpl { loadAppListData(config, listModel, state) } + AppListImpl { rememberViewModel(config, listModel, state) } } @Composable internal fun <T : AppRecord> AppListInput<T>.AppListImpl( - appListDataSupplier: @Composable () -> State<AppListData<T>?>, + viewModelSupplier: @Composable () -> IAppListViewModel<T>, ) { LogCompositions(TAG, config.userId.toString()) - val appListData = appListDataSupplier() - listModel.AppListWidget(appListData, header, bottomPadding, noItemMessage) + val viewModel = viewModelSupplier() + Column(Modifier.fillMaxSize()) { + val optionsState = viewModel.spinnerOptionsFlow.collectAsState(null, Dispatchers.IO) + SpinnerOptions(optionsState, viewModel.option) + val appListData = viewModel.appListDataFlow.collectAsState(null, Dispatchers.IO) + listModel.AppListWidget(appListData, header, bottomPadding, noItemMessage) + } +} + +@Composable +private fun SpinnerOptions( + optionsState: State<List<SpinnerOption>?>, + optionBridge: StateFlowBridge<Int?>, +) { + val options = optionsState.value + val selectedOption = rememberSaveable(options) { + mutableStateOf(options?.let { it.firstOrNull()?.id ?: -1 }) + } + optionBridge.Sync(selectedOption) + if (options != null) { + Spinner(options, selectedOption.value) { selectedOption.value = it } + } } @Composable @@ -131,16 +157,15 @@ private fun <T : AppRecord> AppListModel<T>.getGroupTitleIfFirst( } @Composable -private fun <T : AppRecord> loadAppListData( +private fun <T : AppRecord> rememberViewModel( config: AppListConfig, listModel: AppListModel<T>, state: AppListState, -): State<AppListData<T>?> { +): AppListViewModel<T> { val viewModel: AppListViewModel<T> = viewModel(key = config.userId.toString()) viewModel.appListConfig.setIfAbsent(config) viewModel.listModel.setIfAbsent(listModel) viewModel.showSystem.Sync(state.showSystem) - viewModel.option.Sync(state.option) viewModel.searchQuery.Sync(state.searchQuery) DisposableBroadcastReceiverAsUser( @@ -153,5 +178,5 @@ private fun <T : AppRecord> loadAppListData( onStart = { viewModel.reloadApps() }, ) { viewModel.reloadApps() } - return viewModel.appListDataFlow.collectAsState(null, Dispatchers.IO) + return viewModel }
\ No newline at end of file diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListPage.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListPage.kt index 7d21d98820d3..404e27ccb79a 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListPage.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppListPage.kt @@ -16,18 +16,13 @@ package com.android.settingslib.spaprivileged.template.app -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import com.android.settingslib.spa.widget.scaffold.MoreOptionsAction import com.android.settingslib.spa.widget.scaffold.MoreOptionsScope import com.android.settingslib.spa.widget.scaffold.SearchScaffold -import com.android.settingslib.spa.widget.ui.Spinner import com.android.settingslib.spaprivileged.R import com.android.settingslib.spaprivileged.model.app.AppListConfig import com.android.settingslib.spaprivileged.model.app.AppListModel @@ -61,27 +56,21 @@ fun <T : AppRecord> AppListPage( }, ) { bottomPadding, searchQuery -> WorkProfilePager(primaryUserOnly) { userInfo -> - Column(Modifier.fillMaxSize()) { - val options = remember { listModel.getSpinnerOptions() } - val selectedOption = rememberSaveable { mutableStateOf(0) } - Spinner(options, selectedOption.value) { selectedOption.value = it } - val appListInput = AppListInput( - config = AppListConfig( - userId = userInfo.id, - showInstantApps = showInstantApps, - ), - listModel = listModel, - state = AppListState( - showSystem = showSystem, - option = selectedOption, - searchQuery = searchQuery, - ), - header = header, - bottomPadding = bottomPadding, - noItemMessage = noItemMessage, - ) - appList(appListInput) - } + val appListInput = AppListInput( + config = AppListConfig( + userId = userInfo.id, + showInstantApps = showInstantApps, + ), + listModel = listModel, + state = AppListState( + showSystem = showSystem, + searchQuery = searchQuery, + ), + header = header, + bottomPadding = bottomPadding, + noItemMessage = noItemMessage, + ) + appList(appListInput) } } } diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppList.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppList.kt index ee21b81fe92a..53af25b81580 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppList.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppList.kt @@ -18,6 +18,7 @@ package com.android.settingslib.spaprivileged.template.app import android.app.AppOpsManager.MODE_ALLOWED import android.app.AppOpsManager.MODE_DEFAULT +import android.app.AppOpsManager.MODE_ERRORED import android.content.Context import android.content.pm.ApplicationInfo import androidx.compose.runtime.Composable @@ -25,6 +26,8 @@ import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.remember +import com.android.settingslib.spa.framework.compose.stateOf +import com.android.settingslib.spa.framework.util.asyncMapItem import com.android.settingslib.spa.framework.util.filterItem import com.android.settingslib.spaprivileged.model.app.AppOpsController import com.android.settingslib.spaprivileged.model.app.AppRecord @@ -37,6 +40,7 @@ import kotlinx.coroutines.flow.map data class AppOpPermissionRecord( override val app: ApplicationInfo, + val hasRequestBroaderPermission: Boolean, val hasRequestPermission: Boolean, var appOpsController: IAppOpsController, ) : AppRecord @@ -50,9 +54,26 @@ abstract class AppOpPermissionListModel( abstract val permission: String /** + * When set, specifies the broader permission who trumps the [permission]. + * + * When trumped, the [permission] is not changeable and model shows the [permission] as allowed. + */ + open val broaderPermission: String? = null + + /** + * Indicates whether [permission] has protection level appop flag. + * + * If true, it uses getAppOpPermissionPackages() to fetch bits to decide whether the permission + * is requested. + */ + open val permissionHasAppopFlag: Boolean = true + + open val modeForNotAllowed: Int = MODE_ERRORED + + /** * Use AppOpsManager#setUidMode() instead of AppOpsManager#setMode() when set allowed. * - * Security related app-ops should be set with setUidMode() instead of setMode(). + * Security or privacy related app-ops should be set with setUidMode() instead of setMode(). */ open val setModeByUid = false @@ -60,31 +81,54 @@ abstract class AppOpPermissionListModel( private val notChangeablePackages = setOf("android", "com.android.systemui", context.packageName) + private fun createAppOpsController(app: ApplicationInfo) = + AppOpsController( + context = context, + app = app, + op = appOp, + setModeByUid = setModeByUid, + modeForNotAllowed = modeForNotAllowed, + ) + + private fun createRecord( + app: ApplicationInfo, + hasRequestPermission: Boolean + ): AppOpPermissionRecord = + with(packageManagers) { + AppOpPermissionRecord( + app = app, + hasRequestBroaderPermission = + broaderPermission?.let { app.hasRequestPermission(it) } ?: false, + hasRequestPermission = hasRequestPermission, + appOpsController = createAppOpsController(app), + ) + } + override fun transform(userIdFlow: Flow<Int>, appListFlow: Flow<List<ApplicationInfo>>) = - userIdFlow.map { userId -> - packageManagers.getAppOpPermissionPackages(userId, permission) - }.combine(appListFlow) { packageNames, appList -> - appList.map { app -> - AppOpPermissionRecord( - app = app, - hasRequestPermission = app.packageName in packageNames, - appOpsController = createAppOpsController(app), - ) + if (permissionHasAppopFlag) { + userIdFlow + .map { userId -> packageManagers.getAppOpPermissionPackages(userId, permission) } + .combine(appListFlow) { packageNames, appList -> + appList.map { app -> + createRecord( + app = app, + hasRequestPermission = app.packageName in packageNames, + ) + } + } + } else { + appListFlow.asyncMapItem { app -> + with(packageManagers) { createRecord(app, app.hasRequestPermission(permission)) } } } - override fun transformItem(app: ApplicationInfo) = AppOpPermissionRecord( - app = app, - hasRequestPermission = with(packageManagers) { app.hasRequestPermission(permission) }, - appOpsController = createAppOpsController(app), - ) - - private fun createAppOpsController(app: ApplicationInfo) = AppOpsController( - context = context, - app = app, - op = appOp, - setModeByUid = setModeByUid, - ) + override fun transformItem(app: ApplicationInfo) = + with(packageManagers) { + createRecord( + app = app, + hasRequestPermission = app.hasRequestPermission(permission), + ) + } override fun filter(userIdFlow: Flow<Int>, recordListFlow: Flow<List<AppOpPermissionRecord>>) = recordListFlow.filterItem(::isChangeable) @@ -95,15 +139,19 @@ abstract class AppOpPermissionListModel( */ @Composable override fun isAllowed(record: AppOpPermissionRecord): State<Boolean?> { + if (record.hasRequestBroaderPermission) { + // Broader permission trumps the specific permission. + return stateOf(true) + } + val mode = record.appOpsController.mode.observeAsState() return remember { derivedStateOf { when (mode.value) { null -> null MODE_ALLOWED -> true - MODE_DEFAULT -> with(packageManagers) { - record.app.hasGrantPermission(permission) - } + MODE_DEFAULT -> + with(packageManagers) { record.app.hasGrantPermission(permission) } else -> false } } @@ -111,7 +159,9 @@ abstract class AppOpPermissionListModel( } override fun isChangeable(record: AppOpPermissionRecord) = - record.hasRequestPermission && record.app.packageName !in notChangeablePackages + record.hasRequestPermission && + !record.hasRequestBroaderPermission && + record.app.packageName !in notChangeablePackages override fun setAllowed(record: AppOpPermissionRecord, newAllowed: Boolean) { record.appOpsController.setAllowed(newAllowed) diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt index 76cff0bba875..e9fcbd2cfa68 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt @@ -81,6 +81,13 @@ internal class TogglePermissionAppInfoPageProvider( navArgument(USER_ID) { type = NavType.IntType }, ) + /** + * Gets the route prefix to this page. + * + * Expose route prefix to enable enter from non-SPA pages. + */ + fun getRoutePrefix(permissionType: String) = "$PAGE_NAME/$permissionType" + @Composable fun navigator(permissionType: String, app: ApplicationInfo) = navigator(route = "$PAGE_NAME/$permissionType/${app.toRoute()}") diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppList.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppList.kt index ce8fc9df7c38..1ab623076f0a 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppList.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppList.kt @@ -93,6 +93,14 @@ interface TogglePermissionAppListProvider { fun getAppListRoute(): String = TogglePermissionAppListPageProvider.getRoute(permissionType) + /** + * Gets the route prefix to the toggle permission App Info page. + * + * Expose route prefix to enable enter from non-SPA pages. + */ + fun getAppInfoRoutePrefix(): String = + TogglePermissionAppInfoPageProvider.getRoutePrefix(permissionType) + @Composable fun InfoPageEntryItem(app: ApplicationInfo) { val listModel = rememberContext(::createModel) diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt index 26174c2e03ca..2d8f0098480c 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt @@ -23,7 +23,9 @@ import android.content.pm.PackageManager import android.content.pm.PackageManager.ApplicationInfoFlags import android.content.pm.PackageManager.ResolveInfoFlags import android.content.pm.ResolveInfo +import android.content.res.Resources import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.internal.R import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.first @@ -51,12 +53,18 @@ class AppListRepositoryTest { private lateinit var context: Context @Mock + private lateinit var resources: Resources + + @Mock private lateinit var packageManager: PackageManager private lateinit var repository: AppListRepository @Before fun setUp() { + whenever(context.resources).thenReturn(resources) + whenever(resources.getStringArray(R.array.config_hideWhenDisabled_packageNames)) + .thenReturn(emptyArray()) whenever(context.packageManager).thenReturn(packageManager) whenever(packageManager.getInstalledModules(anyInt())).thenReturn(emptyList()) whenever( @@ -93,12 +101,61 @@ class AppListRepositoryTest { } @Test - fun loadApps_isDisabledUntilUsed() = runTest { + fun loadApps_isHideWhenDisabledPackageAndDisabled() = runTest { val app = ApplicationInfo().apply { - packageName = "is.disabled.until.used" + packageName = "is.hide.when.disabled" enabled = false + } + whenever(resources.getStringArray(R.array.config_hideWhenDisabled_packageNames)) + .thenReturn(arrayOf(app.packageName)) + mockInstalledApplications(listOf(app)) + val appListConfig = AppListConfig(userId = USER_ID, showInstantApps = false) + + val appListFlow = repository.loadApps(appListConfig) + + assertThat(appListFlow).isEmpty() + } + + @Test + fun loadApps_isHideWhenDisabledPackageAndDisabledUntilUsed() = runTest { + val app = ApplicationInfo().apply { + packageName = "is.hide.when.disabled" + enabled = true enabledSetting = PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED } + whenever(resources.getStringArray(R.array.config_hideWhenDisabled_packageNames)) + .thenReturn(arrayOf(app.packageName)) + mockInstalledApplications(listOf(app)) + val appListConfig = AppListConfig(userId = USER_ID, showInstantApps = false) + + val appListFlow = repository.loadApps(appListConfig) + + assertThat(appListFlow).isEmpty() + } + + @Test + fun loadApps_isHideWhenDisabledPackageAndEnabled() = runTest { + val app = ApplicationInfo().apply { + packageName = "is.hide.when.disabled" + enabled = true + } + whenever(resources.getStringArray(R.array.config_hideWhenDisabled_packageNames)) + .thenReturn(arrayOf(app.packageName)) + mockInstalledApplications(listOf(app)) + val appListConfig = AppListConfig(userId = USER_ID, showInstantApps = false) + + val appListFlow = repository.loadApps(appListConfig) + + assertThat(appListFlow).containsExactly(app) + } + + @Test + fun loadApps_disabledByUser() = runTest { + val app = ApplicationInfo().apply { + packageName = "disabled.by.user" + enabled = false + enabledSetting = PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER + } mockInstalledApplications(listOf(app)) val appListConfig = AppListConfig(userId = USER_ID, showInstantApps = false) @@ -108,7 +165,7 @@ class AppListRepositoryTest { } @Test - fun loadApps_disabled() = runTest { + fun loadApps_disabledButNotByUser() = runTest { val app = ApplicationInfo().apply { packageName = "disabled" enabled = false diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListViewModelTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListViewModelTest.kt index b9c875ba803a..f51448744c56 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListViewModelTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListViewModelTest.kt @@ -118,7 +118,8 @@ private class TestAppListModel : AppListModel<TestAppRecord> { override fun transform(userIdFlow: Flow<Int>, appListFlow: Flow<List<ApplicationInfo>>) = appListFlow.mapItem(::TestAppRecord) - override suspend fun onFirstLoaded(recordList: List<TestAppRecord>) { + override suspend fun onFirstLoaded(recordList: List<TestAppRecord>): Boolean { onFirstLoadedCalled = true + return false } } diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppOpsControllerTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppOpsControllerTest.kt index 668bfdfd7e32..53e52d0e02e8 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppOpsControllerTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppOpsControllerTest.kt @@ -31,21 +31,18 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever import org.mockito.Spy import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoRule -import org.mockito.Mockito.`when` as whenever @RunWith(AndroidJUnit4::class) class AppOpsControllerTest { - @get:Rule - val mockito: MockitoRule = MockitoJUnit.rule() + @get:Rule val mockito: MockitoRule = MockitoJUnit.rule() - @Spy - private val context: Context = ApplicationProvider.getApplicationContext() + @Spy private val context: Context = ApplicationProvider.getApplicationContext() - @Mock - private lateinit var appOpsManager: AppOpsManager + @Mock private lateinit var appOpsManager: AppOpsManager @Before fun setUp() { @@ -54,7 +51,13 @@ class AppOpsControllerTest { @Test fun setAllowed_setToTrue() { - val controller = AppOpsController(context = context, app = APP, op = OP) + val controller = + AppOpsController( + context = context, + app = APP, + op = OP, + modeForNotAllowed = MODE_ERRORED + ) controller.setAllowed(true) @@ -63,7 +66,13 @@ class AppOpsControllerTest { @Test fun setAllowed_setToFalse() { - val controller = AppOpsController(context = context, app = APP, op = OP) + val controller = + AppOpsController( + context = context, + app = APP, + op = OP, + modeForNotAllowed = MODE_ERRORED + ) controller.setAllowed(false) @@ -73,7 +82,13 @@ class AppOpsControllerTest { @Test fun setAllowed_setToTrueByUid() { val controller = - AppOpsController(context = context, app = APP, op = OP, setModeByUid = true) + AppOpsController( + context = context, + app = APP, + op = OP, + modeForNotAllowed = MODE_ERRORED, + setModeByUid = true + ) controller.setAllowed(true) @@ -83,7 +98,13 @@ class AppOpsControllerTest { @Test fun setAllowed_setToFalseByUid() { val controller = - AppOpsController(context = context, app = APP, op = OP, setModeByUid = true) + AppOpsController( + context = context, + app = APP, + op = OP, + modeForNotAllowed = MODE_ERRORED, + setModeByUid = true + ) controller.setAllowed(false) @@ -92,10 +113,15 @@ class AppOpsControllerTest { @Test fun getMode() { - whenever( - appOpsManager.checkOpNoThrow(OP, APP.uid, APP.packageName) - ).thenReturn(MODE_ALLOWED) - val controller = AppOpsController(context = context, app = APP, op = OP) + whenever(appOpsManager.checkOpNoThrow(OP, APP.uid, APP.packageName)) + .thenReturn(MODE_ALLOWED) + val controller = + AppOpsController( + context = context, + app = APP, + op = OP, + modeForNotAllowed = MODE_ERRORED + ) val mode = controller.getMode() @@ -104,9 +130,10 @@ class AppOpsControllerTest { private companion object { const val OP = 1 - val APP = ApplicationInfo().apply { - packageName = "package.name" - uid = 123 - } + val APP = + ApplicationInfo().apply { + packageName = "package.name" + uid = 123 + } } -}
\ No newline at end of file +} diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListPageTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListPageTest.kt index 62413864c7df..06003c0cb8f9 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListPageTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListPageTest.kt @@ -56,7 +56,6 @@ class AppListPageTest { val state = inputState!!.state assertThat(state.showSystem.value).isFalse() - assertThat(state.option.value).isEqualTo(0) assertThat(state.searchQuery.value).isEqualTo("") } @@ -89,38 +88,14 @@ class AppListPageTest { .assertIsDisplayed() } - @Test - fun whenHasOptions_firstOptionDisplayed() { - val inputState by setContent(options = listOf(OPTION_0, OPTION_1)) - - composeTestRule.onNodeWithText(OPTION_0).assertIsDisplayed() - composeTestRule.onNodeWithText(OPTION_1).assertDoesNotExist() - val state = inputState!!.state - assertThat(state.option.value).isEqualTo(0) - } - - @Test - fun whenHasOptions_couldSwitchOption() { - val inputState by setContent(options = listOf(OPTION_0, OPTION_1)) - - composeTestRule.onNodeWithText(OPTION_0).performClick() - composeTestRule.onNodeWithText(OPTION_1).performClick() - - composeTestRule.onNodeWithText(OPTION_1).assertIsDisplayed() - composeTestRule.onNodeWithText(OPTION_0).assertDoesNotExist() - val state = inputState!!.state - assertThat(state.option.value).isEqualTo(1) - } - private fun setContent( - options: List<String> = emptyList(), header: @Composable () -> Unit = {}, ): State<AppListInput<TestAppRecord>?> { val appListState = mutableStateOf<AppListInput<TestAppRecord>?>(null) composeTestRule.setContent { AppListPage( title = TITLE, - listModel = TestAppListModel(options), + listModel = TestAppListModel(), header = header, appList = { appListState.value = this }, ) @@ -130,7 +105,5 @@ class AppListPageTest { private companion object { const val TITLE = "Title" - const val OPTION_0 = "Option 1" - const val OPTION_1 = "Option 2" } } diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListTest.kt index 0154aa194426..2a1f7a4ad908 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppListTest.kt @@ -24,17 +24,21 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick import androidx.compose.ui.unit.dp import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.android.settingslib.spa.framework.compose.stateOf import com.android.settingslib.spa.framework.compose.toState +import com.android.settingslib.spa.framework.util.StateFlowBridge +import com.android.settingslib.spa.widget.ui.SpinnerOption import com.android.settingslib.spaprivileged.R import com.android.settingslib.spaprivileged.model.app.AppEntry import com.android.settingslib.spaprivileged.model.app.AppListConfig import com.android.settingslib.spaprivileged.model.app.AppListData +import com.android.settingslib.spaprivileged.model.app.IAppListViewModel import com.android.settingslib.spaprivileged.tests.testutils.TestAppListModel import com.android.settingslib.spaprivileged.tests.testutils.TestAppRecord +import kotlinx.coroutines.flow.flowOf import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -47,6 +51,25 @@ class AppListTest { private val context: Context = ApplicationProvider.getApplicationContext() @Test + fun whenHasOptions_firstOptionDisplayed() { + setContent(options = listOf(OPTION_0, OPTION_1)) + + composeTestRule.onNodeWithText(OPTION_0).assertIsDisplayed() + composeTestRule.onNodeWithText(OPTION_1).assertDoesNotExist() + } + + @Test + fun whenHasOptions_couldSwitchOption() { + setContent(options = listOf(OPTION_0, OPTION_1)) + + composeTestRule.onNodeWithText(OPTION_0).performClick() + composeTestRule.onNodeWithText(OPTION_1).performClick() + + composeTestRule.onNodeWithText(OPTION_1).assertIsDisplayed() + composeTestRule.onNodeWithText(OPTION_0).assertDoesNotExist() + } + + @Test fun whenNoApps() { setContent(appEntries = emptyList()) @@ -85,28 +108,37 @@ class AppListTest { } private fun setContent( - appEntries: List<AppEntry<TestAppRecord>>, + options: List<String> = emptyList(), + appEntries: List<AppEntry<TestAppRecord>> = emptyList(), header: @Composable () -> Unit = {}, enableGrouping: Boolean = false, ) { composeTestRule.setContent { - val appListInput = AppListInput( + AppListInput( config = AppListConfig(userId = USER_ID, showInstantApps = false), listModel = TestAppListModel(enableGrouping = enableGrouping), state = AppListState( showSystem = false.toState(), - option = 0.toState(), searchQuery = "".toState(), ), header = header, bottomPadding = 0.dp, - ) - appListInput.AppListImpl { stateOf(AppListData(appEntries, option = 0)) } + ).AppListImpl { + object : IAppListViewModel<TestAppRecord> { + override val option: StateFlowBridge<Int?> = StateFlowBridge() + override val spinnerOptionsFlow = flowOf(options.mapIndexed { index, option -> + SpinnerOption(id = index, text = option) + }) + override val appListDataFlow = flowOf(AppListData(appEntries, option = 0)) + } + } } } private companion object { const val USER_ID = 0 + const val OPTION_0 = "Option 1" + const val OPTION_1 = "Option 2" const val HEADER = "Header" const val GROUP_A = "Group A" const val GROUP_B = "Group B" diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppListTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppListTest.kt index 966b86927e55..da765ba87e46 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppListTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppOpPermissionAppListTest.kt @@ -39,28 +39,23 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever import org.mockito.Spy import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoRule -import org.mockito.Mockito.`when` as whenever @OptIn(ExperimentalCoroutinesApi::class) @RunWith(AndroidJUnit4::class) class AppOpPermissionAppListTest { - @get:Rule - val mockito: MockitoRule = MockitoJUnit.rule() + @get:Rule val mockito: MockitoRule = MockitoJUnit.rule() - @get:Rule - val composeTestRule = createComposeRule() + @get:Rule val composeTestRule = createComposeRule() - @Spy - private val context: Context = ApplicationProvider.getApplicationContext() + @Spy private val context: Context = ApplicationProvider.getApplicationContext() - @Mock - private lateinit var packageManagers: IPackageManagers + @Mock private lateinit var packageManagers: IPackageManagers - @Mock - private lateinit var appOpsManager: AppOpsManager + @Mock private lateinit var appOpsManager: AppOpsManager private lateinit var listModel: TestAppOpPermissionAppListModel @@ -79,9 +74,7 @@ class AppOpPermissionAppListTest { @Test fun transformItem_hasRequestPermission() = runTest { - with(packageManagers) { - whenever(APP.hasRequestPermission(PERMISSION)).thenReturn(true) - } + with(packageManagers) { whenever(APP.hasRequestPermission(PERMISSION)).thenReturn(true) } val record = listModel.transformItem(APP) @@ -90,25 +83,47 @@ class AppOpPermissionAppListTest { @Test fun transformItem_notRequestPermission() = runTest { + with(packageManagers) { whenever(APP.hasRequestPermission(PERMISSION)).thenReturn(false) } + + val record = listModel.transformItem(APP) + + assertThat(record.hasRequestPermission).isFalse() + } + + @Test + fun transformItem_hasRequestBroaderPermission() = runTest { + listModel.broaderPermission = BROADER_PERMISSION with(packageManagers) { - whenever(APP.hasRequestPermission(PERMISSION)).thenReturn(false) + whenever(APP.hasRequestPermission(BROADER_PERMISSION)).thenReturn(true) } val record = listModel.transformItem(APP) - assertThat(record.hasRequestPermission).isFalse() + assertThat(record.hasRequestBroaderPermission).isTrue() } @Test - fun filter() = runTest { + fun transformItem_notRequestBroaderPermission() = runTest { + listModel.broaderPermission = BROADER_PERMISSION with(packageManagers) { - whenever(APP.hasRequestPermission(PERMISSION)).thenReturn(false) + whenever(APP.hasRequestPermission(BROADER_PERMISSION)).thenReturn(false) } - val record = AppOpPermissionRecord( - app = APP, - hasRequestPermission = false, - appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT), - ) + + val record = listModel.transformItem(APP) + + assertThat(record.hasRequestPermission).isFalse() + } + + @Test + fun filter() = runTest { + with(packageManagers) { whenever(APP.hasRequestPermission(PERMISSION)).thenReturn(false) } + val record = + AppOpPermissionRecord( + app = APP, + hasRequestBroaderPermission = false, + hasRequestPermission = false, + appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT), + ) val recordListFlow = listModel.filter(flowOf(USER_ID), flowOf(listOf(record))) @@ -118,11 +133,13 @@ class AppOpPermissionAppListTest { @Test fun isAllowed_allowed() { - val record = AppOpPermissionRecord( - app = APP, - hasRequestPermission = true, - appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_ALLOWED), - ) + val record = + AppOpPermissionRecord( + app = APP, + hasRequestBroaderPermission = false, + hasRequestPermission = true, + appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_ALLOWED), + ) val isAllowed = getIsAllowed(record) @@ -131,14 +148,14 @@ class AppOpPermissionAppListTest { @Test fun isAllowed_defaultAndHasGrantPermission() { - with(packageManagers) { - whenever(APP.hasGrantPermission(PERMISSION)).thenReturn(true) - } - val record = AppOpPermissionRecord( - app = APP, - hasRequestPermission = true, - appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT), - ) + with(packageManagers) { whenever(APP.hasGrantPermission(PERMISSION)).thenReturn(true) } + val record = + AppOpPermissionRecord( + app = APP, + hasRequestBroaderPermission = false, + hasRequestPermission = true, + appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT), + ) val isAllowed = getIsAllowed(record) @@ -147,27 +164,49 @@ class AppOpPermissionAppListTest { @Test fun isAllowed_defaultAndNotGrantPermission() { + with(packageManagers) { whenever(APP.hasGrantPermission(PERMISSION)).thenReturn(false) } + val record = + AppOpPermissionRecord( + app = APP, + hasRequestBroaderPermission = false, + hasRequestPermission = true, + appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT), + ) + + val isAllowed = getIsAllowed(record) + + assertThat(isAllowed).isFalse() + } + + @Test + fun isAllowed_broaderPermissionTrumps() { + listModel.broaderPermission = BROADER_PERMISSION with(packageManagers) { whenever(APP.hasGrantPermission(PERMISSION)).thenReturn(false) + whenever(APP.hasGrantPermission(BROADER_PERMISSION)).thenReturn(true) } - val record = AppOpPermissionRecord( - app = APP, - hasRequestPermission = true, - appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT), - ) + val record = + AppOpPermissionRecord( + app = APP, + hasRequestBroaderPermission = true, + hasRequestPermission = false, + appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_ERRORED), + ) val isAllowed = getIsAllowed(record) - assertThat(isAllowed).isFalse() + assertThat(isAllowed).isTrue() } @Test fun isAllowed_notAllowed() { - val record = AppOpPermissionRecord( - app = APP, - hasRequestPermission = true, - appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_ERRORED), - ) + val record = + AppOpPermissionRecord( + app = APP, + hasRequestBroaderPermission = false, + hasRequestPermission = true, + appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_ERRORED), + ) val isAllowed = getIsAllowed(record) @@ -176,11 +215,13 @@ class AppOpPermissionAppListTest { @Test fun isChangeable_notRequestPermission() { - val record = AppOpPermissionRecord( - app = APP, - hasRequestPermission = false, - appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT), - ) + val record = + AppOpPermissionRecord( + app = APP, + hasRequestBroaderPermission = false, + hasRequestPermission = false, + appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT), + ) val isChangeable = listModel.isChangeable(record) @@ -189,11 +230,13 @@ class AppOpPermissionAppListTest { @Test fun isChangeable_notChangeablePackages() { - val record = AppOpPermissionRecord( - app = NOT_CHANGEABLE_APP, - hasRequestPermission = true, - appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT), - ) + val record = + AppOpPermissionRecord( + app = NOT_CHANGEABLE_APP, + hasRequestBroaderPermission = false, + hasRequestPermission = true, + appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT), + ) val isChangeable = listModel.isChangeable(record) @@ -202,11 +245,13 @@ class AppOpPermissionAppListTest { @Test fun isChangeable_hasRequestPermissionAndChangeable() { - val record = AppOpPermissionRecord( - app = APP, - hasRequestPermission = true, - appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT), - ) + val record = + AppOpPermissionRecord( + app = APP, + hasRequestBroaderPermission = false, + hasRequestPermission = true, + appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT), + ) val isChangeable = listModel.isChangeable(record) @@ -214,13 +259,31 @@ class AppOpPermissionAppListTest { } @Test + fun isChangeable_broaderPermissionTrumps() { + listModel.broaderPermission = BROADER_PERMISSION + val record = + AppOpPermissionRecord( + app = APP, + hasRequestBroaderPermission = true, + hasRequestPermission = true, + appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT), + ) + + val isChangeable = listModel.isChangeable(record) + + assertThat(isChangeable).isFalse() + } + + @Test fun setAllowed() { val appOpsController = FakeAppOpsController(fakeMode = AppOpsManager.MODE_DEFAULT) - val record = AppOpPermissionRecord( - app = APP, - hasRequestPermission = true, - appOpsController = appOpsController, - ) + val record = + AppOpPermissionRecord( + app = APP, + hasRequestBroaderPermission = false, + hasRequestPermission = true, + appOpsController = appOpsController, + ) listModel.setAllowed(record = record, newAllowed = true) @@ -239,9 +302,7 @@ class AppOpPermissionAppListTest { private fun getIsAllowed(record: AppOpPermissionRecord): Boolean? { lateinit var isAllowedState: State<Boolean?> - composeTestRule.setContent { - isAllowedState = listModel.isAllowed(record) - } + composeTestRule.setContent { isAllowedState = listModel.isAllowed(record) } return isAllowedState.value } @@ -250,8 +311,12 @@ class AppOpPermissionAppListTest { override val pageTitleResId = R.string.test_app_op_permission_title override val switchTitleResId = R.string.test_app_op_permission_switch_title override val footerResId = R.string.test_app_op_permission_footer + override val appOp = AppOpsManager.OP_MANAGE_MEDIA override val permission = PERMISSION + override val permissionHasAppopFlag = true + override var broaderPermission: String? = null + override var setModeByUid = false } @@ -259,12 +324,9 @@ class AppOpPermissionAppListTest { const val USER_ID = 0 const val PACKAGE_NAME = "package.name" const val PERMISSION = "PERMISSION" - val APP = ApplicationInfo().apply { - packageName = PACKAGE_NAME - } - val NOT_CHANGEABLE_APP = ApplicationInfo().apply { - packageName = "android" - } + const val BROADER_PERMISSION = "BROADER_PERMISSION" + val APP = ApplicationInfo().apply { packageName = PACKAGE_NAME } + val NOT_CHANGEABLE_APP = ApplicationInfo().apply { packageName = "android" } } } diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/tests/testutils/TestAppListModel.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/tests/testutils/TestAppListModel.kt index ada4016bea13..a7a153ba479c 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/tests/testutils/TestAppListModel.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/tests/testutils/TestAppListModel.kt @@ -28,11 +28,8 @@ data class TestAppRecord( ) : AppRecord class TestAppListModel( - private val options: List<String> = emptyList(), private val enableGrouping: Boolean = false, ) : AppListModel<TestAppRecord> { - override fun getSpinnerOptions() = options - override fun transform(userIdFlow: Flow<Int>, appListFlow: Flow<List<ApplicationInfo>>) = appListFlow.mapItem(::TestAppRecord) diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java index 4a1a4e61f887..810545c40738 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java @@ -291,6 +291,18 @@ public class BluetoothUtils { return false; } + /** + * Check if a device class matches with a defined BluetoothClass device. + * + * @param device Must be one of the public constants in {@link BluetoothClass.Device} + * @return true if device class matches, false otherwise. + */ + public static boolean isDeviceClassMatched(@NonNull BluetoothDevice bluetoothDevice, + int device) { + return bluetoothDevice.getBluetoothClass() != null + && bluetoothDevice.getBluetoothClass().getDeviceClass() == device; + } + private static boolean isAdvancedHeaderEnabled() { if (!DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SETTINGS_UI, BT_ADVANCED_HEADER_ENABLED, true)) { diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java index 2951001f5368..c2c1b551470b 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java @@ -1028,12 +1028,11 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> if (BluetoothUuid.containsAnyUuid(uuids, PbapServerProfile.PBAB_CLIENT_UUIDS)) { // The pairing dialog now warns of phone-book access for paired devices. // No separate prompt is displayed after pairing. - final BluetoothClass bluetoothClass = mDevice.getBluetoothClass(); if (mDevice.getPhonebookAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) { - if (bluetoothClass != null && (bluetoothClass.getDeviceClass() - == BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE - || bluetoothClass.getDeviceClass() - == BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET)) { + if (BluetoothUtils.isDeviceClassMatched(mDevice, + BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE) + || BluetoothUtils.isDeviceClassMatched(mDevice, + BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET)) { EventLog.writeEvent(0x534e4554, "138529441", -1, ""); } } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java index 221836b02347..f741f655bf7c 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java @@ -390,7 +390,10 @@ public class CachedBluetoothDeviceManager { Log.d(TAG, "Bond " + device.getAnonymizedAddress() + " by CSIP"); mOngoingSetMemberPair = device; syncConfigFromMainDevice(device, groupId); - device.createBond(BluetoothDevice.TRANSPORT_LE); + if (!device.createBond(BluetoothDevice.TRANSPORT_LE)) { + Log.d(TAG, "Bonding could not be started"); + mOngoingSetMemberPair = null; + } } private void syncConfigFromMainDevice(BluetoothDevice device, int groupId) { diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidStatsLogUtils.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidStatsLogUtils.java index feb5e0bf6e69..1401a4f923b6 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidStatsLogUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidStatsLogUtils.java @@ -41,8 +41,8 @@ public final class HearingAidStatsLogUtils { } /** - * Logs hearing aid device information to westworld, including device mode, device side, and - * entry page id where the binding(connecting) process starts. + * Logs hearing aid device information to statsd, including device mode, device side, and entry + * page id where the binding(connecting) process starts. * * Only logs the info once after hearing aid is bonded(connected). Clears the map entry of this * device when logging is completed. diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java index 562d4800cc45..e781f1307072 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java @@ -275,6 +275,10 @@ public class LeAudioProfile implements LocalBluetoothProfile { } public int getDrawableResource(BluetoothClass btClass) { + if (btClass == null) { + Log.e(TAG, "No btClass."); + return R.drawable.ic_bt_le_audio_speakers; + } switch (btClass.getDeviceClass()) { case BluetoothClass.Device.AUDIO_VIDEO_UNCATEGORIZED: case BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET: diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java index 28286817ea7e..aa6aaaf352cb 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java @@ -59,6 +59,7 @@ public class GlobalSettings { Settings.Global.ENABLE_AUTOMATIC_SYSTEM_SERVER_HEAP_DUMPS, Settings.Global.ENCODED_SURROUND_OUTPUT, Settings.Global.ENCODED_SURROUND_OUTPUT_ENABLED_FORMATS, + Settings.Global.LOW_POWER_MODE_REMINDER_ENABLED, Settings.Global.LOW_POWER_MODE_TRIGGER_LEVEL, Settings.Global.LOW_POWER_MODE_STICKY_AUTO_DISABLE_ENABLED, Settings.Global.LOW_POWER_MODE_STICKY_AUTO_DISABLE_LEVEL, diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java index 46876b457b54..2b0d8370bb01 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java @@ -132,6 +132,7 @@ public class GlobalSettingsValidators { new DiscreteValueValidator(new String[] {"0", "1"})); VALIDATORS.put(Global.LOW_POWER_MODE_TRIGGER_LEVEL, PERCENTAGE_INTEGER_VALIDATOR); VALIDATORS.put(Global.LOW_POWER_MODE_TRIGGER_LEVEL_MAX, PERCENTAGE_INTEGER_VALIDATOR); + VALIDATORS.put(Global.LOW_POWER_MODE_REMINDER_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put( Global.AUTOMATIC_POWER_SAVE_MODE, new DiscreteValueValidator(new String[] {"0", "1"})); diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index af4d7d46f320..a418c204f6b5 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -24,6 +24,7 @@ <!-- Standard permissions granted to the shell. --> <uses-permission android:name="android.permission.MANAGE_HEALTH_DATA" /> + <uses-permission android:name="android.permission.MIGRATE_HEALTH_CONNECT_DATA" /> <uses-permission android:name="android.permission.LAUNCH_DEVICE_MANAGER_SETUP" /> <uses-permission android:name="android.permission.GET_RUNTIME_PERMISSIONS" /> <uses-permission android:name="android.permission.SEND_SMS" /> @@ -794,6 +795,9 @@ <!-- Permission required for CTS test - CtsPackageInstallTestCases--> <uses-permission android:name="android.permission.GET_APP_METADATA" /> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED"/> + <application android:label="@string/app_label" android:theme="@android:style/Theme.DeviceDefault.DayNight" android:defaultToDeviceProtectedStorage="true" @@ -871,6 +875,7 @@ <service android:name=".BugreportProgressService" + android:foregroundServiceType="systemExempted" android:exported="false"/> </application> </manifest> diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index cefcf066d641..2c9dad96b076 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -170,6 +170,7 @@ <!-- Screen Recording --> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" /> <uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.CAPTURE_AUDIO_OUTPUT"/> @@ -414,7 +415,8 @@ android:process=":screenshot_cross_profile" android:exported="false" /> - <service android:name=".screenrecord.RecordingService" /> + <service android:name=".screenrecord.RecordingService" + android:foregroundServiceType="systemExempted"/> <receiver android:name=".SysuiRestartReceiver" android:exported="false"> diff --git a/packages/SystemUI/TEST_MAPPING b/packages/SystemUI/TEST_MAPPING index f92625bd9a73..b22195a99eb2 100644 --- a/packages/SystemUI/TEST_MAPPING +++ b/packages/SystemUI/TEST_MAPPING @@ -32,18 +32,19 @@ } ] }, - { - // TODO(b/251476085): Consider merging with SystemUIGoogleScreenshotTests (in U+) - "name": "SystemUIGoogleBiometricsScreenshotTests", - "options": [ - { - "exclude-annotation": "org.junit.Ignore" - }, - { - "exclude-annotation": "androidx.test.filters.FlakyTest" - } - ] - }, +// Disable until can pass: b/259124654 +// { +// // TODO(b/251476085): Consider merging with SystemUIGoogleScreenshotTests (in U+) +// "name": "SystemUIGoogleBiometricsScreenshotTests", +// "options": [ +// { +// "exclude-annotation": "org.junit.Ignore" +// }, +// { +// "exclude-annotation": "androidx.test.filters.FlakyTest" +// } +// ] +// }, { // Permission indicators "name": "CtsPermission4TestCases", diff --git a/packages/SystemUI/accessibility/accessibilitymenu/Android.bp b/packages/SystemUI/accessibility/accessibilitymenu/Android.bp new file mode 100644 index 000000000000..a494f5e086ae --- /dev/null +++ b/packages/SystemUI/accessibility/accessibilitymenu/Android.bp @@ -0,0 +1,32 @@ +// +// Copyright (C) 2023 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +android_app { + name: "AccessibilityMenu", + srcs: [ + "src/**/*.java", + ], + system_ext_specific: true, + platform_apis: true, + resource_dirs: ["res"], + certificate: "platform", + // This app uses allowlisted privileged permissions. + privileged: true, +} diff --git a/packages/SystemUI/accessibility/accessibilitymenu/AndroidManifest.xml b/packages/SystemUI/accessibility/accessibilitymenu/AndroidManifest.xml new file mode 100644 index 000000000000..26748a92c4ed --- /dev/null +++ b/packages/SystemUI/accessibility/accessibilitymenu/AndroidManifest.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2023 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + package="com.android.systemui.accessibility.accessibilitymenu"> + <application> + <service + android:name="com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService" + android:exported="false" + android:label="Accessibility Menu (System)" + android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"> + <intent-filter> + <action android:name="android.accessibilityservice.AccessibilityService"/> + </intent-filter> + <meta-data + android:name="android.accessibilityservice" + android:resource="@xml/accessibilitymenu_service"/> + </service> + </application> +</manifest>
\ No newline at end of file diff --git a/packages/SystemUI/accessibility/accessibilitymenu/OWNERS b/packages/SystemUI/accessibility/accessibilitymenu/OWNERS new file mode 100644 index 000000000000..b74281edbf52 --- /dev/null +++ b/packages/SystemUI/accessibility/accessibilitymenu/OWNERS @@ -0,0 +1 @@ +include /core/java/android/view/accessibility/OWNERS diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/xml/accessibilitymenu_service.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/xml/accessibilitymenu_service.xml new file mode 100644 index 000000000000..96882d335d4b --- /dev/null +++ b/packages/SystemUI/accessibility/accessibilitymenu/res/xml/accessibilitymenu_service.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2023 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"/>
\ No newline at end of file diff --git a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/AccessibilityMenuService.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/AccessibilityMenuService.java new file mode 100644 index 000000000000..8b759004f657 --- /dev/null +++ b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/AccessibilityMenuService.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.accessibility.accessibilitymenu; + +import android.accessibilityservice.AccessibilityService; +import android.view.accessibility.AccessibilityEvent; + +/** @hide */ +public class AccessibilityMenuService extends AccessibilityService { + + @Override + public void onAccessibilityEvent(AccessibilityEvent event) { + } + + @Override + public void onInterrupt() { + } +} diff --git a/packages/SystemUI/docs/demo_mode.md b/packages/SystemUI/docs/demo_mode.md index 6cf7060c794c..b2424f42bf43 100644 --- a/packages/SystemUI/docs/demo_mode.md +++ b/packages/SystemUI/docs/demo_mode.md @@ -22,42 +22,42 @@ intent. <br/> Commands are sent as string extras with key ```command``` (required). Possible values are: -Command | Subcommand | Argument | Description ---- | --- | --- | --- -```enter``` | | | Enters demo mode, bar state allowed to be modified (for convenience, any of the other non-exit commands will automatically flip demo mode on, no need to call this explicitly in practice) -```exit``` | | | Exits demo mode, bars back to their system-driven state -```battery``` | | | Control the battery display - | ```level``` | | Sets the battery level (0 - 100) - | ```plugged``` | | Sets charging state (```true```, ```false```) - | ```powersave``` | | Sets power save mode (```true```, ```anything else```) -```network``` | | | Control the RSSI display - | ```airplane``` | | ```show``` to show icon, any other value to hide - | ```fully``` | | Sets MCS state to fully connected (```true```, ```false```) - | ```wifi``` | | ```show``` to show icon, any other value to hide - | | ```level``` | Sets wifi level (null or 0-4) - | ```mobile``` | | ```show``` to show icon, any other value to hide - | | ```datatype``` | Values: ```1x```, ```3g```, ```4g```, ```e```, ```g```, ```h```, ```lte```, ```roam```, any other value to hide - | | ```level``` | Sets mobile signal strength level (null or 0-4) - | ```carriernetworkchange``` | | Sets mobile signal icon to carrier network change UX when disconnected (```show``` to show icon, any other value to hide) - | ```sims``` | | Sets the number of sims (1-8) - | ```nosim``` | | ```show``` to show icon, any other value to hide -```bars``` | | | Control the visual style of the bars (opaque, translucent, etc) - | ```mode``` | | Sets the bars visual style (opaque, translucent, semi-transparent) -```status``` | | | Control the system status icons - | ```volume``` | | Sets the icon in the volume slot (```silent```, ```vibrate```, any other value to hide) - | ```bluetooth``` | | Sets the icon in the bluetooth slot (```connected```, ```disconnected```, any other value to hide) - | ```location``` | | Sets the icon in the location slot (```show```, any other value to hide) - | ```alarm``` | | Sets the icon in the alarm_clock slot (```show```, any other value to hide) - | ```sync``` | | Sets the icon in the sync_active slot (```show```, any other value to hide) - | ```tty``` | | Sets the icon in the tty slot (```show```, any other value to hide) - | ```eri``` | | Sets the icon in the cdma_eri slot (```show```, any other value to hide) - | ```mute``` | | Sets the icon in the mute slot (```show```, any other value to hide) - | ```speakerphone``` | | Sets the icon in the speakerphone slot (```show```, any other value to hide) -```notifications``` | | | Control the notification icons - | ```visible``` | | ```false``` to hide the notification icons, any other value to show -```clock``` | | | Control the clock display - | ```millis``` | | Sets the time in millis - | ```hhmm``` | | Sets the time in hh:mm +| Command | Subcommand | Argument | Description +| --- | --- | --- | --- +| ```enter``` | | | Enters demo mode, bar state allowed to be modified (for convenience, any of the other non-exit commands will automatically flip demo mode on, no need to call this explicitly in practice) +| ```exit``` | | | Exits demo mode, bars back to their system-driven state +| ```battery``` | | | Control the battery display +| | ```level``` | | Sets the battery level (0 - 100) +| | ```plugged``` | | Sets charging state (```true```, ```false```) +| | ```powersave``` | | Sets power save mode (```true```, ```anything else```) +| ```network``` | | | Control the RSSI display +| | ```airplane``` | | ```show``` to show icon, any other value to hide +| | ```fully``` | | Sets MCS state to fully connected (```true```, ```false```) +| | ```wifi``` | | ```show``` to show icon, any other value to hide +| | | ```level``` | Sets wifi level (null or 0-4) +| | ```mobile``` | | ```show``` to show icon, any other value to hide +| | | ```datatype``` | Values: ```1x```, ```3g```, ```4g```, ```e```, ```g```, ```h```, ```lte```, ```roam```, any other value to hide +| | | ```level``` | Sets mobile signal strength level (null or 0-4) +| | ```carriernetworkchange``` | | Sets mobile signal icon to carrier network change UX when disconnected (```show``` to show icon, any other value to hide) +| | ```sims``` | | Sets the number of sims (1-8) +| | ```nosim``` | | ```show``` to show icon, any other value to hide +| ```bars``` | | | Control the visual style of the bars (opaque, translucent, etc) +| | ```mode``` | | Sets the bars visual style (opaque, translucent, semi-transparent) +| ```status``` | | | Control the system status icons +| | ```volume``` | | Sets the icon in the volume slot (```silent```, ```vibrate```, any other value to hide) +| | ```bluetooth``` | | Sets the icon in the bluetooth slot (```connected```, ```disconnected```, any other value to hide) +| | ```location``` | | Sets the icon in the location slot (```show```, any other value to hide) +| | ```alarm``` | | Sets the icon in the alarm_clock slot (```show```, any other value to hide) +| | ```sync``` | | Sets the icon in the sync_active slot (```show```, any other value to hide) +| | ```tty``` | | Sets the icon in the tty slot (```show```, any other value to hide) +| | ```eri``` | | Sets the icon in the cdma_eri slot (```show```, any other value to hide) +| | ```mute``` | | Sets the icon in the mute slot (```show```, any other value to hide) +| | ```speakerphone``` | | Sets the icon in the speakerphone slot (```show```, any other value to hide) +| ```notifications``` | | | Control the notification icons +| | ```visible``` | | ```false``` to hide the notification icons, any other value to show +| ```clock``` | | | Control the clock display +| | ```millis``` | | Sets the time in millis +| | ```hhmm``` | | Sets the time in hh:mm ## Examples Enter demo mode diff --git a/packages/SystemUI/docs/modern-architecture.png b/packages/SystemUI/docs/modern-architecture.png Binary files differnew file mode 100644 index 000000000000..2636362dd2ec --- /dev/null +++ b/packages/SystemUI/docs/modern-architecture.png diff --git a/packages/SystemUI/docs/status-bar-data-pipeline.md b/packages/SystemUI/docs/status-bar-data-pipeline.md new file mode 100644 index 000000000000..9fcdce1ef717 --- /dev/null +++ b/packages/SystemUI/docs/status-bar-data-pipeline.md @@ -0,0 +1,261 @@ +# Status Bar Data Pipeline + +## Background + +The status bar is the UI shown at the top of the user's screen that gives them +information about the time, notifications, and system status like mobile +conectivity and battery level. This document is about the implementation of the +wifi and mobile system icons on the right side: + + + +In Android U, the data pipeline that determines what mobile and wifi icons to +show in the status bar has been re-written with a new architecture. This format +generally follows Android best practices to +[app architecture](https://developer.android.com/topic/architecture#recommended-app-arch). +This document serves as a guide for the new architecture, and as a guide for how +OEMs can add customizations to the new architecture. + +## Architecture + +In the new architecture, there is a separate pipeline for each type of icon. For +Android U, **only the wifi icon and mobile icons have been implemented in the +new architecture**. + +As shown in the Android best practices guide, each new pipeline has a data +layer, a domain layer, and a UI layer: + + + +The classes in the data layer are `repository` instances. The classes in the +domain layer are `interactor` instances. The classes in the UI layer are +`viewmodel` instances and `viewbinder` instances. In this document, "repository" +and "data layer" will be used interchangably (and the same goes for the other +layers). + +The wifi logic is in `statusbar/pipeline/wifi` and the mobile logic is in +`statusbar/pipeline/mobile`. + +#### Repository (data layer) + +System callbacks, broadcast receivers, configuration values are all defined +here, and exposed through the appropriate interface. Where appropriate, we +define `Model` objects at this layer so that clients do not have to rely on +system-defined interfaces. + +#### Interactor (domain layer) + +Here is where we define the business logic and transform the data layer objects +into something consumable by the ViewModel classes. For example, +`MobileIconsInteractor` defines the CBRS filtering logic by exposing a +`filteredSubscriptions` list. + +#### ViewModel (UI layer) + +View models should define the final piece of business logic mapping to UI logic. +For example, the mobile view model checks the `IconInteractor.isRoaming` flow to +decide whether or not to show the roaming indicator. + +#### ViewBinder + +These have already been implemented and configured. ViewBinders replace the old +`applyMobileState` mechanism that existed in the `IconManager` classes of the +old pipeline. A view binder associates a ViewModel with a View, and keeps the +view up-to-date with the most recent information from the model. + +Any new fields added to the ViewModel classes need to be equivalently bound to +the view here. + +### Putting it all together + +Putting that altogether, we have this overall architecture diagram for the +icons: + + + +### Mobile icons architecture + +Because there can be multiple mobile connections at the same time, the mobile +pipeline is split up hierarchically. At each level (data, domain, and UI), there +is a singleton parent class that manages information relevant to **all** mobile +connections, and multiple instances of child classes that manage information for +a **single** mobile connection. + +For example, `MobileConnectionsRepository` is a singleton at the data layer that +stores information relevant to **all** mobile connections, and it also manages a +list of child `MobileConnectionRepository` classes. `MobileConnectionRepository` +is **not** a singleton, and each individual `MobileConnectionRepository` +instance fully qualifies the state of a **single** connection. This pattern is +repeated at the `Interactor` and `ViewModel` layers for mobile. + + + +Note: Since there is at most one wifi connection, the wifi pipeline is not split +up in the same way. + +## Customizations + +The new pipeline completely replaces these classes: + +* `WifiStatusTracker` +* `MobileStatusTracker` +* `NetworkSignalController` and `NetworkSignalControllerImpl` +* `MobileSignalController` +* `WifiSignalController` +* `StatusBarSignalPolicy` (including `SignalIconState`, `MobileIconState`, and + `WifiIconState`) + +Any customizations in any of these classes will need to be migrated to the new +pipeline. As a general rule, any change that would have gone into +`NetworkControllerImpl` would be done in `MobileConnectionsRepository`, and any +change for `MobileSignalController` can be done in `MobileConnectionRepository` +(see above on the relationship between those repositories). + +### Sample customization: New service + +Some customizations require listening to additional services to get additional +data. This new architecture makes it easy to add additional services to the +status bar data pipeline to get icon customizations. + +Below is a general guide to how a new service should be added. However, there +may be exceptions to this guide for specific use cases. + +1. In the data layer (`repository` classes), add a new `StateFlow` that listens + to the service: + + ```kotlin + class MobileConnectionsRepositoryImpl { + ... + val fooVal: StateFlow<Int> = + conflatedCallbackFlow { + val callback = object : FooServiceCallback(), FooListener { + override fun onFooChanged(foo: Int) { + trySend(foo) + } + } + + fooService.registerCallback(callback) + + awaitClose { fooService.unregisterCallback(callback) } + } + .stateIn(scope, started = SharingStarted.WhileSubscribed(), FOO_DEFAULT_VAL) + } + ``` + +1. In the domain layer (`interactor` classes), either use this new flow to + process values, or just expose the flow as-is for the UI layer. + + For example, if `bar` should only be true when `foo` is positive: + + ```kotlin + class MobileIconsInteractor { + ... + val bar: StateFlow<Boolean> = + mobileConnectionsRepo + .mapLatest { foo -> foo > 0 } + .stateIn(scope, SharingStarted.WhileSubscribed(), initialValue = false) + } + ``` + +1. In the UI layer (`viewmodel` classes), update the existing flows to process + the new value from the interactor. + + For example, if the icon should be hidden when `bar` is true: + + ```kotlin + class MobileIconViewModel { + ... + iconId: Flow<Int> = combine( + iconInteractor.level, + iconInteractor.numberOfLevels, + iconInteractor.bar, + ) { level, numberOfLevels, bar -> + if (bar) { + null + } else { + calcIcon(level, numberOfLevels) + } + } + ``` + +## Demo mode + +SystemUI demo mode is a first-class citizen in the new pipeline. It is +implemented as an entirely separate repository, +`DemoMobileConnectionsRepository`. When the system moves into demo mode, the +implementation of the data layer is switched to the demo repository via the +`MobileRepositorySwitcher` class. + +Because the demo mode repositories implement the same interfaces as the +production classes, any changes made above will have to be implemented for demo +mode as well. + +1. Following from above, if `fooVal` is added to the + `MobileConnectionsRepository` interface: + + ```kotlin + class DemoMobileConnectionsRepository { + private val _fooVal = MutableStateFlow(FOO_DEFAULT_VALUE) + override val fooVal: StateFlow<Int> = _fooVal.asStateFlow() + + // Process the state. **See below on how to add the command to the CLI** + fun processEnabledMobileState(state: Mobile) { + ... + _fooVal.value = state.fooVal + } + } + ``` + +1. (Optional) If you want to enable the command line interface for setting and + testing this value in demo mode, you can add parsing logic to + `DemoModeMobileConnectionDataSource` and `FakeNetworkEventModel`: + + ```kotlin + sealed interface FakeNetworkEventModel { + data class Mobile( + ... + // Add new fields here + val fooVal: Int? + ) + } + ``` + + ```kotlin + class DemoModeMobileConnectionDataSource { + // Currently, the demo commands are implemented as an extension function on Bundle + private fun Bundle.activeMobileEvent(): Mobile { + ... + val fooVal = getString("fooVal")?.toInt() + return Mobile( + ... + fooVal = fooVal, + ) + } + } + ``` + +If step 2 is implemented, then you will be able to pass demo commands via the +command line: + +``` +adb shell am broadcast -a com.android.systemui.demo -e command network -e mobile show -e fooVal <test value> +``` + +## Migration plan + +For Android U, the new pipeline will be enabled and default. However, the old +pipeline code will still be around just in case the new pipeline doesn’t do well +in the testing phase. + +For Android V, the old pipeline will be completely removed and the new pipeline +will be the one source of truth. + +Our ask for OEMs is to default to using the new pipeline in Android U. If there +are customizations that seem difficult to migrate over to the new pipeline, +please file a bug with us and we’d be more than happy to consult on the best +solution. The new pipeline was designed with customizability in mind, so our +hope is that working the new pipeline will be easier and faster. + +Note: The new pipeline currently only supports the wifi and mobile icons. The +other system status bar icons may be migrated to a similar architecture in the +future. diff --git a/packages/SystemUI/docs/status-bar-mobile-pipeline.png b/packages/SystemUI/docs/status-bar-mobile-pipeline.png Binary files differnew file mode 100644 index 000000000000..620563de3daa --- /dev/null +++ b/packages/SystemUI/docs/status-bar-mobile-pipeline.png diff --git a/packages/SystemUI/docs/status-bar-pipeline.png b/packages/SystemUI/docs/status-bar-pipeline.png Binary files differnew file mode 100644 index 000000000000..1c568c9bcda9 --- /dev/null +++ b/packages/SystemUI/docs/status-bar-pipeline.png diff --git a/packages/SystemUI/docs/status-bar.png b/packages/SystemUI/docs/status-bar.png Binary files differnew file mode 100644 index 000000000000..3a5af0e2c3e0 --- /dev/null +++ b/packages/SystemUI/docs/status-bar.png diff --git a/packages/SystemUI/ktfmt_includes.txt b/packages/SystemUI/ktfmt_includes.txt index 7243ca4cece0..4c271ea6a464 100644 --- a/packages/SystemUI/ktfmt_includes.txt +++ b/packages/SystemUI/ktfmt_includes.txt @@ -460,7 +460,6 @@ -packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt -packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiActivityModel.kt -packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt --packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt -packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt -packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiConstants.kt -packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt @@ -486,7 +485,6 @@ -packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowStateController.kt -packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewInfo.kt -packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt --packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt -packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarRootView.kt -packages/SystemUI/src/com/android/systemui/toast/ToastDefaultAnimation.kt -packages/SystemUI/src/com/android/systemui/toast/ToastLogger.kt diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 1ef523bf55a3..7073f6ace7cd 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -661,7 +661,9 @@ <item>7</item> <!-- WAKE_REASON_WAKE_MOTION --> <item>9</item> <!-- WAKE_REASON_LID --> <item>10</item> <!-- WAKE_REASON_DISPLAY_GROUP_ADDED --> - <item>12</item> <!-- WAKE_REASON_UNFOLD_DEVICE --> + <item>15</item> <!-- WAKE_REASON_TAP --> + <item>16</item> <!-- WAKE_REASON_LIFT --> + <item>17</item> <!-- WAKE_REASON_BIOMETRIC --> </integer-array> <!-- Whether the communal service should be enabled --> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 07e8badda230..45147ca13238 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2301,13 +2301,13 @@ <string name="accessibility_floating_button_docking_tooltip">Move button to the edge to hide it temporarily</string> <!-- Text for the undo action button of the message view of the accessibility floating menu to perform undo operation. [CHAR LIMIT=30]--> <string name="accessibility_floating_button_undo">Undo</string> - + <!-- Text for the message view with undo action of the accessibility floating menu to show which feature shortcut was removed. [CHAR LIMIT=30]--> + <string name="accessibility_floating_button_undo_message_label_text"><xliff:g id="feature name" example="Magnification">%s</xliff:g> shortcut removed</string> <!-- Text for the message view with undo action of the accessibility floating menu to show how many features shortcuts were removed. [CHAR LIMIT=30]--> - <string name="accessibility_floating_button_undo_message_text">{count, plural, - =1 {{label} shortcut removed} + <string name="accessibility_floating_button_undo_message_number_text">{count, plural, + =1 {# shortcut removed} other {# shortcuts removed} }</string> - <!-- Action in accessibility menu to move the accessibility floating button to the top left of the screen. [CHAR LIMIT=30] --> <string name="accessibility_floating_button_action_move_top_left">Move top left</string> <!-- Action in accessibility menu to move the accessibility floating button to the top right of the screen. [CHAR LIMIT=30] --> @@ -2466,6 +2466,8 @@ <string name="media_transfer_playing_different_device">Playing on <xliff:g id="deviceName" example="My Tablet">%1$s</xliff:g></string> <!-- Text informing the user that the media transfer has failed because something went wrong. [CHAR LIsMIT=50] --> <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> <!-- 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> diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java index 8ee893c14727..359da13a9799 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButtonController.java @@ -249,7 +249,8 @@ public class RotationButtonController { } public void setRotationLockedAtAngle(int rotationSuggestion) { - RotationPolicy.setRotationLockAtAngle(mContext, true, rotationSuggestion); + RotationPolicy.setRotationLockAtAngle(mContext, /* enabled= */ isRotationLocked(), + /* rotation= */ rotationSuggestion); } public boolean isRotationLocked() { diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java index 061ca4f7d850..67e3400670ba 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java @@ -177,6 +177,8 @@ public class KeyguardPINView extends KeyguardPinBasedInputView { @Override public void startAppearAnimation() { + setAlpha(1f); + setTranslationY(0); if (mAppearAnimator.isRunning()) { mAppearAnimator.cancel(); } @@ -213,7 +215,6 @@ public class KeyguardPINView extends KeyguardPinBasedInputView { /** Animate subviews according to expansion or time. */ private void animate(float progress) { - setAlpha(progress); Interpolator standardDecelerate = Interpolators.STANDARD_DECELERATE; Interpolator legacyDecelerate = Interpolators.LEGACY_DECELERATE; diff --git a/packages/SystemUI/src/com/android/keyguard/mediator/ScreenOnCoordinator.kt b/packages/SystemUI/src/com/android/keyguard/mediator/ScreenOnCoordinator.kt index 98ac2c0bd026..0f00a040b094 100644 --- a/packages/SystemUI/src/com/android/keyguard/mediator/ScreenOnCoordinator.kt +++ b/packages/SystemUI/src/com/android/keyguard/mediator/ScreenOnCoordinator.kt @@ -17,8 +17,10 @@ package com.android.keyguard.mediator import android.annotation.BinderThread +import android.os.Handler import android.os.Trace import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.unfold.SysUIUnfoldComponent import com.android.systemui.util.concurrency.PendingTasksContainer import com.android.systemui.util.kotlin.getOrNull @@ -33,7 +35,8 @@ import javax.inject.Inject */ @SysUISingleton class ScreenOnCoordinator @Inject constructor( - unfoldComponent: Optional<SysUIUnfoldComponent> + unfoldComponent: Optional<SysUIUnfoldComponent>, + @Main private val mainHandler: Handler ) { private val unfoldLightRevealAnimation = unfoldComponent.map( @@ -55,7 +58,11 @@ class ScreenOnCoordinator @Inject constructor( unfoldLightRevealAnimation?.onScreenTurningOn(pendingTasks.registerTask("unfold-reveal")) foldAodAnimationController?.onScreenTurningOn(pendingTasks.registerTask("fold-to-aod")) - pendingTasks.onTasksComplete { onDrawn.run() } + pendingTasks.onTasksComplete { + mainHandler.post { + onDrawn.run() + } + } Trace.endSection() } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java index 777d10c7acfd..6d54d389a38d 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java @@ -182,9 +182,9 @@ public class AccessibilityFloatingMenuController implements if (mFloatingMenu == null) { if (mFeatureFlags.isEnabled(A11Y_FLOATING_MENU_FLING_SPRING_ANIMATIONS)) { final Display defaultDisplay = mDisplayManager.getDisplay(DEFAULT_DISPLAY); - mFloatingMenu = new MenuViewLayerController( - mContext.createWindowContext(defaultDisplay, - TYPE_NAVIGATION_BAR_PANEL, /* options= */ null), mWindowManager, + final Context windowContext = mContext.createWindowContext(defaultDisplay, + TYPE_NAVIGATION_BAR_PANEL, /* options= */ null); + mFloatingMenu = new MenuViewLayerController(windowContext, mWindowManager, mAccessibilityManager); } else { mFloatingMenu = new AccessibilityFloatingMenu(mContext); diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationController.java index ee048e1a02d3..c2bc1408274f 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationController.java @@ -19,8 +19,6 @@ package com.android.systemui.accessibility.floatingmenu; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; -import android.content.ComponentCallbacks; -import android.content.res.Configuration; import android.view.MotionEvent; import androidx.annotation.NonNull; @@ -34,7 +32,7 @@ import com.android.wm.shell.common.magnetictarget.MagnetizedObject; * Controls the interaction between {@link MagnetizedObject} and * {@link MagnetizedObject.MagneticTarget}. */ -class DismissAnimationController implements ComponentCallbacks { +class DismissAnimationController { private static final float COMPLETELY_OPAQUE = 1.0f; private static final float COMPLETELY_TRANSPARENT = 0.0f; private static final float CIRCLE_VIEW_DEFAULT_SCALE = 1.0f; @@ -105,16 +103,6 @@ class DismissAnimationController implements ComponentCallbacks { mMagnetizedObject.addTarget(magneticTarget); } - @Override - public void onConfigurationChanged(@NonNull Configuration newConfig) { - updateResources(); - } - - @Override - public void onLowMemory() { - // Do nothing - } - void showDismissView(boolean show) { if (show) { mDismissView.show(); @@ -165,7 +153,7 @@ class DismissAnimationController implements ComponentCallbacks { } } - private void updateResources() { + void updateResources() { final float maxDismissSize = mDismissView.getResources().getDimensionPixelSize( R.dimen.dismiss_circle_size); mMinDismissSize = mDismissView.getResources().getDimensionPixelSize( diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuEduTooltipView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuEduTooltipView.java index 440053450d2f..5ec024ebc917 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuEduTooltipView.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuEduTooltipView.java @@ -21,6 +21,7 @@ import static android.view.View.MeasureSpec.AT_MOST; import static android.view.View.MeasureSpec.UNSPECIFIED; import android.annotation.SuppressLint; +import android.content.ComponentCallbacks; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; @@ -48,7 +49,7 @@ import com.android.systemui.recents.TriangleShape; * . It's just shown on the left or right of the anchor view. */ @SuppressLint("ViewConstructor") -class MenuEduTooltipView extends FrameLayout { +class MenuEduTooltipView extends FrameLayout implements ComponentCallbacks { private int mFontSize; private int mTextViewMargin; private int mTextViewPadding; @@ -73,9 +74,7 @@ class MenuEduTooltipView extends FrameLayout { } @Override - protected void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - + public void onConfigurationChanged(@NonNull Configuration newConfig) { updateResources(); updateMessageView(); updateArrowView(); @@ -83,6 +82,25 @@ class MenuEduTooltipView extends FrameLayout { updateLocationAndVisibility(); } + @Override + public void onLowMemory() { + // Do nothing. + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + getContext().registerComponentCallbacks(this); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + + getContext().unregisterComponentCallbacks(this); + } + void show(CharSequence message) { mMessageView.setText(message); diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java index 05e1d3f0e126..f79c3d27b2ec 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java @@ -30,13 +30,21 @@ import static com.android.systemui.accessibility.floatingmenu.MenuViewAppearance import android.annotation.FloatRange; import android.annotation.IntDef; +import android.content.ComponentCallbacks; import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.res.Configuration; import android.database.ContentObserver; +import android.os.Build; import android.os.Handler; import android.os.Looper; import android.os.UserHandle; import android.provider.Settings; import android.text.TextUtils; +import android.util.Log; +import android.view.accessibility.AccessibilityManager; + +import androidx.annotation.NonNull; import com.android.internal.accessibility.dialog.AccessibilityTarget; import com.android.internal.annotations.VisibleForTesting; @@ -50,6 +58,9 @@ import java.util.List; * Stores and observe the settings contents for the menu view. */ class MenuInfoRepository { + private static final String TAG = "MenuInfoRepository"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG) || Build.IS_DEBUGGABLE; + @FloatRange(from = 0.0, to = 1.0) private static final float DEFAULT_MENU_POSITION_X_PERCENT = 1.0f; @@ -60,6 +71,10 @@ class MenuInfoRepository { private static final int DEFAULT_MIGRATION_TOOLTIP_VALUE_PROMPT = MigrationPrompt.DISABLED; private final Context mContext; + private final Configuration mConfiguration; + private final AccessibilityManager mAccessibilityManager; + private final AccessibilityManager.AccessibilityServicesStateChangeListener + mA11yServicesStateChangeListener = manager -> onTargetFeaturesChanged(); private final Handler mHandler = new Handler(Looper.getMainLooper()); private final OnSettingsContentsChanged mSettingsContentsCallback; private Position mPercentagePosition; @@ -74,12 +89,12 @@ class MenuInfoRepository { int ENABLED = 1; } - private final ContentObserver mMenuTargetFeaturesContentObserver = + @VisibleForTesting + final ContentObserver mMenuTargetFeaturesContentObserver = new ContentObserver(mHandler) { @Override public void onChange(boolean selfChange) { - mSettingsContentsCallback.onTargetFeaturesChanged( - getTargets(mContext, ACCESSIBILITY_BUTTON)); + onTargetFeaturesChanged(); } }; @@ -102,8 +117,35 @@ class MenuInfoRepository { } }; - MenuInfoRepository(Context context, OnSettingsContentsChanged settingsContentsChanged) { + @VisibleForTesting + final ComponentCallbacks mComponentCallbacks = new ComponentCallbacks() { + @Override + public void onConfigurationChanged(@NonNull Configuration newConfig) { + final int diff = newConfig.diff(mConfiguration); + + if (DEBUG) { + Log.d(TAG, "onConfigurationChanged = " + Configuration.configurationDiffToString( + diff)); + } + + if ((diff & ActivityInfo.CONFIG_LOCALE) != 0) { + onTargetFeaturesChanged(); + } + + mConfiguration.setTo(newConfig); + } + + @Override + public void onLowMemory() { + // Do nothing. + } + }; + + MenuInfoRepository(Context context, AccessibilityManager accessibilityManager, + OnSettingsContentsChanged settingsContentsChanged) { mContext = context; + mAccessibilityManager = accessibilityManager; + mConfiguration = new Configuration(context.getResources().getConfiguration()); mSettingsContentsCallback = settingsContentsChanged; mPercentagePosition = getStartPosition(); @@ -172,6 +214,11 @@ class MenuInfoRepository { UserHandle.USER_CURRENT); } + private void onTargetFeaturesChanged() { + mSettingsContentsCallback.onTargetFeaturesChanged( + getTargets(mContext, ACCESSIBILITY_BUTTON)); + } + private Position getStartPosition() { final String absolutePositionString = Prefs.getString(mContext, Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION, /* defaultValue= */ null); @@ -181,7 +228,7 @@ class MenuInfoRepository { : Position.fromString(absolutePositionString); } - void registerContentObservers() { + void registerObserversAndCallbacks() { mContext.getContentResolver().registerContentObserver( Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS), /* notifyForDescendants */ false, mMenuTargetFeaturesContentObserver, @@ -202,12 +249,20 @@ class MenuInfoRepository { Settings.Secure.getUriFor(ACCESSIBILITY_FLOATING_MENU_OPACITY), /* notifyForDescendants */ false, mMenuFadeOutContentObserver, UserHandle.USER_CURRENT); + mContext.registerComponentCallbacks(mComponentCallbacks); + + mAccessibilityManager.addAccessibilityServicesStateChangeListener( + mA11yServicesStateChangeListener); } - void unregisterContentObservers() { + void unregisterObserversAndCallbacks() { mContext.getContentResolver().unregisterContentObserver(mMenuTargetFeaturesContentObserver); mContext.getContentResolver().unregisterContentObserver(mMenuSizeContentObserver); mContext.getContentResolver().unregisterContentObserver(mMenuFadeOutContentObserver); + mContext.unregisterComponentCallbacks(mComponentCallbacks); + + mAccessibilityManager.removeAccessibilityServicesStateChangeListener( + mA11yServicesStateChangeListener); } interface OnSettingsContentsChanged { diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuMessageView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuMessageView.java index 9875ad06f1ed..3e2b06b39bad 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuMessageView.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuMessageView.java @@ -20,6 +20,7 @@ import static android.util.TypedValue.COMPLEX_UNIT_PX; import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; import android.annotation.IntDef; +import android.content.ComponentCallbacks; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.Configuration; @@ -33,6 +34,8 @@ import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.TextView; +import androidx.annotation.NonNull; + import com.android.settingslib.Utils; import com.android.systemui.R; @@ -44,7 +47,7 @@ import java.lang.annotation.RetentionPolicy; * the {@link MenuView}. */ class MenuMessageView extends LinearLayout implements - ViewTreeObserver.OnComputeInternalInsetsListener { + ViewTreeObserver.OnComputeInternalInsetsListener, ComponentCallbacks { private final TextView mTextView; private final Button mUndoButton; @@ -61,6 +64,7 @@ class MenuMessageView extends LinearLayout implements MenuMessageView(Context context) { super(context); + setLayoutDirection(LAYOUT_DIRECTION_LOCALE); setVisibility(GONE); mTextView = new TextView(context); @@ -72,13 +76,16 @@ class MenuMessageView extends LinearLayout implements } @Override - protected void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - + public void onConfigurationChanged(@NonNull Configuration newConfig) { updateResources(); } @Override + public void onLowMemory() { + // Do nothing. + } + + @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); @@ -92,6 +99,7 @@ class MenuMessageView extends LinearLayout implements updateResources(); + getContext().registerComponentCallbacks(this); getViewTreeObserver().addOnComputeInternalInsetsListener(this); } @@ -99,6 +107,7 @@ class MenuMessageView extends LinearLayout implements protected void onDetachedFromWindow() { super.onDetachedFromWindow(); + getContext().unregisterComponentCallbacks(this); getViewTreeObserver().removeOnComputeInternalInsetsListener(this); } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java index 986aa51ecce1..28269d943ee3 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java @@ -19,6 +19,7 @@ package com.android.systemui.accessibility.floatingmenu; import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; import android.annotation.SuppressLint; +import android.content.ComponentCallbacks; import android.content.Context; import android.content.res.Configuration; import android.graphics.PointF; @@ -46,7 +47,7 @@ import java.util.List; */ @SuppressLint("ViewConstructor") class MenuView extends FrameLayout implements - ViewTreeObserver.OnComputeInternalInsetsListener { + ViewTreeObserver.OnComputeInternalInsetsListener, ComponentCallbacks { private static final int INDEX_MENU_ITEM = 0; private final List<AccessibilityTarget> mTargetFeatures = new ArrayList<>(); private final AccessibilityTargetAdapter mAdapter; @@ -106,14 +107,31 @@ class MenuView extends FrameLayout implements } @Override - protected void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - + public void onConfigurationChanged(@NonNull Configuration newConfig) { loadLayoutResources(); mTargetFeaturesView.setOverScrollMode(mMenuViewAppearance.getMenuScrollMode()); } + @Override + public void onLowMemory() { + // Do nothing. + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + getContext().registerComponentCallbacks(this); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + + getContext().unregisterComponentCallbacks(this); + } + void setOnTargetFeaturesChangeListener(OnTargetFeaturesChangeListener listener) { mFeaturesChangeListener = listener; } @@ -299,7 +317,7 @@ class MenuView extends FrameLayout implements mMenuViewModel.getSizeTypeData().observeForever(mSizeTypeObserver); mMenuViewModel.getMoveToTuckedData().observeForever(mMoveToTuckedObserver); setVisibility(VISIBLE); - mMenuViewModel.registerContentObservers(); + mMenuViewModel.registerObserversAndCallbacks(); getViewTreeObserver().addOnComputeInternalInsetsListener(this); getViewTreeObserver().addOnDrawListener(mSystemGestureExcludeUpdater); } @@ -312,7 +330,7 @@ class MenuView extends FrameLayout implements mMenuViewModel.getTargetFeaturesData().removeObserver(mTargetFeaturesObserver); mMenuViewModel.getSizeTypeData().removeObserver(mSizeTypeObserver); mMenuViewModel.getMoveToTuckedData().removeObserver(mMoveToTuckedObserver); - mMenuViewModel.unregisterContentObservers(); + mMenuViewModel.unregisterObserversAndCallbacks(); getViewTreeObserver().removeOnComputeInternalInsetsListener(this); getViewTreeObserver().removeOnDrawListener(mSystemGestureExcludeUpdater); } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java index 6f5b39cc7d56..15a8d09e5fd7 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java @@ -25,20 +25,22 @@ import static com.android.internal.accessibility.common.ShortcutConstants.Access import static com.android.internal.accessibility.util.AccessibilityUtils.getAccessibilityServiceFragmentType; import static com.android.internal.accessibility.util.AccessibilityUtils.setAccessibilityServiceState; import static com.android.systemui.accessibility.floatingmenu.MenuMessageView.Index; +import static com.android.systemui.util.PluralMessageFormaterKt.icuMessageFormat; import android.accessibilityservice.AccessibilityServiceInfo; import android.annotation.IntDef; import android.annotation.StringDef; import android.annotation.SuppressLint; +import android.content.ComponentCallbacks; import android.content.Context; import android.content.Intent; import android.content.res.Configuration; +import android.content.res.Resources; import android.graphics.Rect; import android.os.Handler; import android.os.Looper; import android.os.UserHandle; import android.provider.Settings; -import android.util.PluralsMessageFormatter; import android.view.MotionEvent; import android.view.View; import android.view.ViewTreeObserver; @@ -61,9 +63,7 @@ import com.android.wm.shell.common.magnetictarget.MagnetizedObject; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.Optional; /** @@ -74,7 +74,7 @@ import java.util.Optional; */ @SuppressLint("ViewConstructor") class MenuViewLayer extends FrameLayout implements - ViewTreeObserver.OnComputeInternalInsetsListener, View.OnClickListener { + ViewTreeObserver.OnComputeInternalInsetsListener, View.OnClickListener, ComponentCallbacks { private static final int SHOW_MESSAGE_DELAY_MS = 3000; private final WindowManager mWindowManager; @@ -137,8 +137,8 @@ class MenuViewLayer extends FrameLayout implements AccessibilityServiceInfo.FEEDBACK_ALL_MASK); serviceInfoList.forEach(info -> { if (getAccessibilityServiceFragmentType(info) == INVISIBLE_TOGGLE) { - setAccessibilityServiceState(mContext, info.getComponentName(), /* enabled= */ - false); + setAccessibilityServiceState(getContext(), + info.getComponentName(), /* enabled= */ false); } }); @@ -150,11 +150,14 @@ class MenuViewLayer extends FrameLayout implements AccessibilityManager accessibilityManager, IAccessibilityFloatingMenu floatingMenu) { super(context); + // Simplifies the translation positioning and animations + setLayoutDirection(LAYOUT_DIRECTION_LTR); + mWindowManager = windowManager; mAccessibilityManager = accessibilityManager; mFloatingMenu = floatingMenu; - mMenuViewModel = new MenuViewModel(context); + mMenuViewModel = new MenuViewModel(context, accessibilityManager); mMenuViewAppearance = new MenuViewAppearance(context, windowManager); mMenuView = new MenuView(context, mMenuViewModel, mMenuViewAppearance); mMenuAnimationController = mMenuView.getMenuAnimationController(); @@ -209,20 +212,30 @@ class MenuViewLayer extends FrameLayout implements } @Override - protected void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); + public void onConfigurationChanged(@NonNull Configuration newConfig) { mDismissView.updateResources(); + mDismissAnimationController.updateResources(); + } + + @Override + public void onLowMemory() { + // Do nothing. } private String getMessageText(List<AccessibilityTarget> newTargetFeatures) { Preconditions.checkArgument(newTargetFeatures.size() > 0, "The list should at least have one feature."); - final Map<String, Object> arguments = new HashMap<>(); - arguments.put("count", newTargetFeatures.size()); - arguments.put("label", newTargetFeatures.get(0).getLabel()); - return PluralsMessageFormatter.format(getResources(), arguments, - R.string.accessibility_floating_button_undo_message_text); + final int featuresSize = newTargetFeatures.size(); + final Resources resources = getResources(); + if (featuresSize == 1) { + return resources.getString( + R.string.accessibility_floating_button_undo_message_label_text, + newTargetFeatures.get(0).getLabel()); + } + + return icuMessageFormat(resources, + R.string.accessibility_floating_button_undo_message_number_text, featuresSize); } @Override @@ -246,7 +259,7 @@ class MenuViewLayer extends FrameLayout implements mMenuViewModel.getMigrationTooltipVisibilityData().observeForever( mMigrationTooltipObserver); mMessageView.setUndoListener(view -> undo()); - mContext.registerComponentCallbacks(mDismissAnimationController); + getContext().registerComponentCallbacks(this); } @Override @@ -261,7 +274,7 @@ class MenuViewLayer extends FrameLayout implements mMenuViewModel.getMigrationTooltipVisibilityData().removeObserver( mMigrationTooltipObserver); mHandler.removeCallbacksAndMessages(/* token= */ null); - mContext.unregisterComponentCallbacks(mDismissAnimationController); + getContext().unregisterComponentCallbacks(this); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewModel.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewModel.java index 5fea3b0ba2f9..eec84672f17c 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewModel.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewModel.java @@ -17,6 +17,7 @@ package com.android.systemui.accessibility.floatingmenu; import android.content.Context; +import android.view.accessibility.AccessibilityManager; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; @@ -41,8 +42,9 @@ class MenuViewModel implements MenuInfoRepository.OnSettingsContentsChanged { private final MutableLiveData<Position> mPercentagePositionData = new MutableLiveData<>(); private final MenuInfoRepository mInfoRepository; - MenuViewModel(Context context) { - mInfoRepository = new MenuInfoRepository(context, /* settingsContentsChanged= */ this); + MenuViewModel(Context context, AccessibilityManager accessibilityManager) { + mInfoRepository = new MenuInfoRepository(context, + accessibilityManager, /* settingsContentsChanged= */ this); } @Override @@ -111,11 +113,11 @@ class MenuViewModel implements MenuInfoRepository.OnSettingsContentsChanged { return mTargetFeaturesData; } - void registerContentObservers() { - mInfoRepository.registerContentObservers(); + void registerObserversAndCallbacks() { + mInfoRepository.registerObserversAndCallbacks(); } - void unregisterContentObservers() { - mInfoRepository.unregisterContentObservers(); + void unregisterObserversAndCallbacks() { + mInfoRepository.unregisterObserversAndCallbacks(); } }
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java index 76c1158373b7..96bfa4323f95 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java @@ -643,6 +643,7 @@ public class FrameworkServicesModule { @Provides @Singleton + @Nullable static BluetoothAdapter provideBluetoothAdapter(BluetoothManager bluetoothManager) { return bluetoothManager.getAdapter(); } diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 44fd3534973d..764a3d0a566d 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -419,7 +419,13 @@ object Flags { unreleasedFlag(1204, "persist.wm.debug.predictive_back_sysui_enable", teamfood = true) // TODO(b/255697805): Tracking Bug - @JvmField val TRACKPAD_GESTURE_BACK = unreleasedFlag(1205, "trackpad_gesture_back", teamfood = false) + @JvmField + val TRACKPAD_GESTURE_BACK = unreleasedFlag(1205, "trackpad_gesture_back", teamfood = false) + + // TODO(b/263826204): Tracking Bug + @JvmField + val WM_ENABLE_PREDICTIVE_BACK_BOUNCER_ANIM = + unreleasedFlag(1206, "persist.wm.debug.predictive_back_bouncer_anim", teamfood = true) // 1300 - screenshots // TODO(b/254512719): Tracking Bug diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt index 2a3a33eff274..2cf5fb98d07e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractor.kt @@ -77,6 +77,7 @@ constructor( /** Runnable to show the primary bouncer. */ val showRunnable = Runnable { + repository.setPrimaryVisible(true) repository.setPrimaryShow( KeyguardBouncerModel( promptReason = repository.bouncerPromptReason ?: 0, @@ -85,7 +86,6 @@ constructor( ) ) repository.setPrimaryShowingSoon(false) - repository.setPrimaryVisible(true) primaryBouncerCallbackInteractor.dispatchVisibilityChanged(View.VISIBLE) } diff --git a/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java b/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java index 90fc1d7ed49e..3587c4d8cc77 100644 --- a/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java +++ b/packages/SystemUI/src/com/android/systemui/power/PowerNotificationWarnings.java @@ -71,6 +71,7 @@ import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.statusbar.phone.SystemUIDialog; import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.util.NotificationChannels; +import com.android.systemui.util.settings.GlobalSettings; import com.android.systemui.volume.Events; import java.io.PrintWriter; @@ -175,6 +176,7 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { private ActivityStarter mActivityStarter; private final BroadcastSender mBroadcastSender; private final UiEventLogger mUiEventLogger; + private GlobalSettings mGlobalSettings; private final Lazy<BatteryController> mBatteryControllerLazy; private final DialogLaunchAnimator mDialogLaunchAnimator; @@ -184,7 +186,8 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { @Inject public PowerNotificationWarnings(Context context, ActivityStarter activityStarter, BroadcastSender broadcastSender, Lazy<BatteryController> batteryControllerLazy, - DialogLaunchAnimator dialogLaunchAnimator, UiEventLogger uiEventLogger) { + DialogLaunchAnimator dialogLaunchAnimator, UiEventLogger uiEventLogger, + GlobalSettings globalSettings) { mContext = context; mNoMan = mContext.getSystemService(NotificationManager.class); mPowerMan = (PowerManager) context.getSystemService(Context.POWER_SERVICE); @@ -196,6 +199,7 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { mDialogLaunchAnimator = dialogLaunchAnimator; mUseSevereDialog = mContext.getResources().getBoolean(R.bool.config_severe_battery_dialog); mUiEventLogger = uiEventLogger; + mGlobalSettings = globalSettings; } @Override @@ -281,6 +285,9 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI { } protected void showWarningNotification() { + if (mGlobalSettings.getInt(Global.LOW_POWER_MODE_REMINDER_ENABLED, 1) == 0) { + return; + } if (showSevereLowBatteryDialog()) { mBroadcastSender.sendBroadcast(new Intent(ACTION_ENABLE_SEVERE_BATTERY_DIALOG) .setPackage(mContext.getPackageName()) diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java index 893574a59d94..da18b5734e81 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java @@ -19,7 +19,8 @@ import static android.app.StatusBarManager.DISABLE2_QUICK_SETTINGS; import static com.android.systemui.media.dagger.MediaModule.QS_PANEL; import static com.android.systemui.media.dagger.MediaModule.QUICK_QS_PANEL; import static com.android.systemui.statusbar.disableflags.DisableFlagsLogger.DisableState; - +import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; +import static com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.content.res.Configuration; @@ -124,7 +125,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca * we're on keyguard but use {@link #isKeyguardState()} instead since that is more accurate * during state transitions which often call into us. */ - private int mState; + private int mStatusBarState = -1; private QSContainerImplController mQSContainerImplController; private int[] mTmpLocation = new int[2]; private int mLastViewHeight; @@ -457,7 +458,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca private boolean isKeyguardState() { // We want the freshest state here since otherwise we'll have some weirdness if earlier // listeners trigger updates - return mStatusBarStateController.getState() == StatusBarState.KEYGUARD; + return mStatusBarStateController.getCurrentOrUpcomingState() == KEYGUARD; } private void updateShowCollapsedOnKeyguard() { @@ -672,8 +673,8 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca mQSAnimator.setPosition(expansion); } if (!mInSplitShade - || mStatusBarStateController.getState() == StatusBarState.KEYGUARD - || mStatusBarStateController.getState() == StatusBarState.SHADE_LOCKED) { + || mStatusBarStateController.getState() == KEYGUARD + || mStatusBarStateController.getState() == SHADE_LOCKED) { // At beginning, state is 0 and will apply wrong squishiness to MediaHost in lockscreen // and media player expect no change by squishiness in lock screen shade. Don't bother // squishing mQsMediaHost when not in split shade to prevent problems with stale state. @@ -703,8 +704,8 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca // Large screens in landscape. // Need to check upcoming state as for unlocked -> AOD transition current state is // not updated yet, but we're transitioning and UI should already follow KEYGUARD state - if (mTransitioningToFullShade || mStatusBarStateController.getCurrentOrUpcomingState() - == StatusBarState.KEYGUARD) { + if (mTransitioningToFullShade + || mStatusBarStateController.getCurrentOrUpcomingState() == KEYGUARD) { // Always use "mFullShadeProgress" on keyguard, because // "panelExpansionFractions" is always 1 on keyguard split shade. return mLockscreenToShadeProgress; @@ -757,8 +758,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca } private boolean headerWillBeAnimating() { - return mState == StatusBarState.KEYGUARD && mShowCollapsedOnKeyguard - && !isKeyguardState(); + return mStatusBarState == KEYGUARD && mShowCollapsedOnKeyguard && !isKeyguardState(); } @Override @@ -891,9 +891,23 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca }; @Override + public void onUpcomingStateChanged(int upcomingState) { + if (upcomingState == KEYGUARD) { + // refresh state of QS as soon as possible - while it's still upcoming - so in case of + // transition to KEYGUARD (e.g. from unlocked to AOD) all objects are aware they should + // already behave like on keyguard. Otherwise we might be doing extra work, + // e.g. QSAnimator making QS visible and then quickly invisible + onStateChanged(upcomingState); + } + } + + @Override public void onStateChanged(int newState) { - mState = newState; - setKeyguardShowing(newState == StatusBarState.KEYGUARD); + if (newState == mStatusBarState) { + return; + } + mStatusBarState = newState; + setKeyguardShowing(newState == KEYGUARD); updateShowCollapsedOnKeyguard(); } @@ -921,7 +935,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca indentingPw.println("mTemp: " + Arrays.toString(mLocationTemp)); indentingPw.println("mShowCollapsedOnKeyguard: " + mShowCollapsedOnKeyguard); indentingPw.println("mLastKeyguardAndExpanded: " + mLastKeyguardAndExpanded); - indentingPw.println("mState: " + StatusBarState.toString(mState)); + indentingPw.println("mStatusBarState: " + StatusBarState.toString(mStatusBarState)); indentingPw.println("mTmpLocation: " + Arrays.toString(mTmpLocation)); indentingPw.println("mLastViewHeight: " + mLastViewHeight); indentingPw.println("mLastHeaderTranslation: " + mLastHeaderTranslation); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt index 4d914fe0adef..15fed3244d97 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt @@ -49,9 +49,9 @@ class StatusBarPipelineFlags @Inject constructor(private val featureFlags: Featu featureFlags.isEnabled(Flags.NEW_STATUS_BAR_WIFI_ICON_BACKEND) || useNewWifiIcon() /** - * Returns true if we should apply some coloring to the wifi icon that was rendered with the new + * Returns true if we should apply some coloring to the icons that were rendered with the new * pipeline to help with debugging. */ - fun useWifiDebugColoring(): Boolean = + fun useDebugColoring(): Boolean = featureFlags.isEnabled(Flags.NEW_STATUS_BAR_ICONS_DEBUG_COLORING) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt index 0d01715715c0..0993ab3701f6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.pipeline.dagger +import android.net.wifi.WifiManager import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.log.table.TableLogBuffer @@ -35,8 +36,11 @@ import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxyImpl import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl +import com.android.systemui.statusbar.pipeline.wifi.data.repository.RealWifiRepository import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositorySwitcher +import com.android.systemui.statusbar.pipeline.wifi.data.repository.prod.DisabledWifiRepository +import com.android.systemui.statusbar.pipeline.wifi.data.repository.prod.WifiRepositoryImpl import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractorImpl import dagger.Binds @@ -78,9 +82,23 @@ abstract class StatusBarPipelineModule { @ClassKey(MobileUiAdapter::class) abstract fun bindFeature(impl: MobileUiAdapter): CoreStartable - @Module companion object { - @JvmStatic + @Provides + @SysUISingleton + fun provideRealWifiRepository( + wifiManager: WifiManager?, + disabledWifiRepository: DisabledWifiRepository, + wifiRepositoryImplFactory: WifiRepositoryImpl.Factory, + ): RealWifiRepository { + // If we have a null [WifiManager], then the wifi repository should be permanently + // disabled. + return if (wifiManager == null) { + disabledWifiRepository + } else { + wifiRepositoryImplFactory.create(wifiManager) + } + } + @Provides @SysUISingleton @WifiTableLog @@ -88,7 +106,6 @@ abstract class StatusBarPipelineModule { return factory.create("WifiTableLog", 100) } - @JvmStatic @Provides @SysUISingleton @AirplaneTableLog diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt index dd93541d7c8f..59603874efde 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/ResolvedNetworkType.kt @@ -17,7 +17,6 @@ package com.android.systemui.statusbar.pipeline.mobile.data.model import android.telephony.Annotation.NetworkType -import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy /** @@ -26,21 +25,17 @@ import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy * methods on [MobileMappingsProxy] to generate an icon lookup key. */ sealed interface ResolvedNetworkType { - @NetworkType val type: Int val lookupKey: String object UnknownNetworkType : ResolvedNetworkType { - override val type: Int = NETWORK_TYPE_UNKNOWN override val lookupKey: String = "unknown" } data class DefaultNetworkType( - @NetworkType override val type: Int, override val lookupKey: String, ) : ResolvedNetworkType data class OverrideNetworkType( - @NetworkType override val type: Int, override val lookupKey: String, ) : ResolvedNetworkType } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt index 40e9ba1a46c7..d04996b4d6ce 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt @@ -17,7 +17,6 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository import android.telephony.SubscriptionInfo -import android.telephony.SubscriptionManager import android.telephony.TelephonyCallback import android.telephony.TelephonyManager import com.android.systemui.log.table.TableLogBuffer @@ -52,13 +51,12 @@ interface MobileConnectionRepository { * listener + model. */ val connectionInfo: Flow<MobileConnectionModel> + + /** The total number of levels. Used with [SignalDrawable]. */ + val numberOfLevels: StateFlow<Int> + /** Observable tracking [TelephonyManager.isDataConnectionAllowed] */ val dataEnabled: StateFlow<Boolean> - /** - * True if this connection represents the default subscription per - * [SubscriptionManager.getDefaultDataSubscriptionId] - */ - val isDefaultDataSubscription: StateFlow<Boolean> /** * See [TelephonyManager.getCdmaEnhancedRoamingIndicatorDisplayNumber]. This bit only matters if @@ -70,4 +68,9 @@ interface MobileConnectionRepository { /** The service provider name for this network connection, or the default name */ val networkName: StateFlow<NetworkNameModel> + + companion object { + /** The default number of levels to use for [numberOfLevels]. */ + const val DEFAULT_NUM_LEVELS = 4 + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt index 498c0b93fce8..97b4c2cadbe5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt @@ -18,7 +18,6 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository import android.provider.Settings import android.telephony.CarrierConfigManager -import android.telephony.SubscriptionManager import com.android.settingslib.SignalIcon.MobileIconGroup import com.android.settingslib.mobile.MobileMappings import com.android.settingslib.mobile.MobileMappings.Config @@ -38,9 +37,6 @@ interface MobileConnectionsRepository { /** Observable for the subscriptionId of the current mobile data connection */ val activeMobileDataSubscriptionId: StateFlow<Int> - /** Tracks [SubscriptionManager.getDefaultDataSubscriptionId] */ - val defaultDataSubId: StateFlow<Int> - /** The current connectivity status for the default mobile network connection */ val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel> diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt index db9d24ff7aba..0c8593d60cf5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt @@ -139,11 +139,6 @@ constructor( override val defaultMobileIconGroup: Flow<SignalIcon.MobileIconGroup> = activeRepo.flatMapLatest { it.defaultMobileIconGroup } - override val defaultDataSubId: StateFlow<Int> = - activeRepo - .flatMapLatest { it.defaultDataSubId } - .stateIn(scope, SharingStarted.WhileSubscribed(), realRepository.defaultDataSubId.value) - override val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel> = activeRepo .flatMapLatest { it.defaultMobileNetworkConnectivity } 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 0b5f9d5ae59e..0e164e7ee859 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 @@ -34,6 +34,7 @@ import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetwork import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.Mobile @@ -139,14 +140,6 @@ constructor( private fun <K, V> Map<K, V>.reverse() = entries.associateBy({ it.value }) { it.key } - // TODO(b/261029387): add a command for this value - override val defaultDataSubId = - activeMobileDataSubscriptionId.stateIn( - scope, - SharingStarted.WhileSubscribed(), - INVALID_SUBSCRIPTION_ID - ) - // TODO(b/261029387): not yet supported override val defaultMobileNetworkConnectivity = MutableStateFlow(MobileConnectivityModel()) @@ -199,7 +192,6 @@ constructor( val connection = getRepoForSubId(subId) // This is always true here, because we split out disabled states at the data-source level connection.dataEnabled.value = true - connection.isDefaultDataSubscription.value = state.dataType != null connection.networkName.value = NetworkNameModel.Derived(state.name) connection.cdmaRoaming.value = state.roaming @@ -261,15 +253,13 @@ constructor( private fun SignalIcon.MobileIconGroup?.toResolvedNetworkType(): ResolvedNetworkType { val key = mobileMappingsReverseLookup.value[this] ?: "dis" - return DefaultNetworkType(DEMO_NET_TYPE, key) + return DefaultNetworkType(key) } companion object { private const val TAG = "DemoMobileConnectionsRepo" private const val DEFAULT_SUB_ID = 1 - - private const val DEMO_NET_TYPE = 1234 } } @@ -279,9 +269,9 @@ class DemoMobileConnectionRepository( ) : MobileConnectionRepository { override val connectionInfo = MutableStateFlow(MobileConnectionModel()) - override val dataEnabled = MutableStateFlow(true) + override val numberOfLevels = MutableStateFlow(DEFAULT_NUM_LEVELS) - override val isDefaultDataSubscription = MutableStateFlow(true) + override val dataEnabled = MutableStateFlow(true) override val cdmaRoaming = MutableStateFlow(false) 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 5cfff82253c5..0fa0fea0bebf 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 @@ -48,6 +48,7 @@ import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetwork import com.android.systemui.statusbar.pipeline.mobile.data.model.toDataConnectionType import com.android.systemui.statusbar.pipeline.mobile.data.model.toNetworkNameModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel @@ -63,6 +64,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onEach @@ -78,7 +80,6 @@ class MobileConnectionRepositoryImpl( private val telephonyManager: TelephonyManager, private val globalSettings: GlobalSettings, broadcastDispatcher: BroadcastDispatcher, - defaultDataSubId: StateFlow<Int>, globalMobileDataSettingChangedEvent: Flow<Unit>, mobileMappingsProxy: MobileMappingsProxy, bgDispatcher: CoroutineDispatcher, @@ -185,14 +186,12 @@ class MobileConnectionRepositoryImpl( OVERRIDE_NETWORK_TYPE_NONE ) { DefaultNetworkType( - telephonyDisplayInfo.networkType, mobileMappingsProxy.toIconKey( telephonyDisplayInfo.networkType ) ) } else { OverrideNetworkType( - telephonyDisplayInfo.overrideNetworkType, mobileMappingsProxy.toIconKeyOverride( telephonyDisplayInfo.overrideNetworkType ) @@ -214,6 +213,12 @@ class MobileConnectionRepositoryImpl( .stateIn(scope, SharingStarted.WhileSubscribed(), state) } + // This will become variable based on [CarrierConfigManager.KEY_INFLATE_SIGNAL_STRENGTH_BOOL] + // once it's wired up inside of [CarrierConfigTracker]. + override val numberOfLevels: StateFlow<Int> = + flowOf(DEFAULT_NUM_LEVELS) + .stateIn(scope, SharingStarted.WhileSubscribed(), DEFAULT_NUM_LEVELS) + /** Produces whenever the mobile data setting changes for this subId */ private val localMobileDataSettingChangedEvent: Flow<Unit> = conflatedCallbackFlow { val observer = @@ -284,20 +289,6 @@ class MobileConnectionRepositoryImpl( private fun dataConnectionAllowed(): Boolean = telephonyManager.isDataConnectionAllowed - override val isDefaultDataSubscription: StateFlow<Boolean> = run { - val initialValue = defaultDataSubId.value == subId - defaultDataSubId - .mapLatest { it == subId } - .distinctUntilChanged() - .logDiffsForTable( - mobileLogger, - columnPrefix = "", - columnName = "isDefaultDataSub", - initialValue = initialValue, - ) - .stateIn(scope, SharingStarted.WhileSubscribed(), initialValue) - } - class Factory @Inject constructor( @@ -315,7 +306,6 @@ class MobileConnectionRepositoryImpl( subId: Int, defaultNetworkName: NetworkNameModel, networkNameSeparator: String, - defaultDataSubId: StateFlow<Int>, globalMobileDataSettingChangedEvent: Flow<Unit>, ): MobileConnectionRepository { val mobileLogger = logFactory.create(tableBufferLogName(subId), 100) @@ -328,7 +318,6 @@ class MobileConnectionRepositoryImpl( telephonyManager.createForSubscriptionId(subId), globalSettings, broadcastDispatcher, - defaultDataSubId, globalMobileDataSettingChangedEvent, mobileMappingsProxy, bgDispatcher, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt index d407abeb2315..c88c70064238 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt @@ -35,7 +35,6 @@ import android.telephony.TelephonyCallback import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener import android.telephony.TelephonyManager import androidx.annotation.VisibleForTesting -import com.android.internal.telephony.PhoneConstants import com.android.settingslib.SignalIcon.MobileIconGroup import com.android.settingslib.mobile.MobileMappings.Config import com.android.systemui.R @@ -60,7 +59,6 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.asExecutor import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.distinctUntilChanged @@ -142,24 +140,10 @@ constructor( .logInputChange(logger, "onActiveDataSubscriptionIdChanged") .stateIn(scope, started = SharingStarted.WhileSubscribed(), INVALID_SUBSCRIPTION_ID) - private val defaultDataSubIdChangeEvent: MutableSharedFlow<Unit> = - MutableSharedFlow(extraBufferCapacity = 1) - - override val defaultDataSubId: StateFlow<Int> = + private val defaultDataSubIdChangedEvent = broadcastDispatcher - .broadcastFlow( - IntentFilter(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED) - ) { intent, _ -> - intent.getIntExtra(PhoneConstants.SUBSCRIPTION_KEY, INVALID_SUBSCRIPTION_ID) - } - .distinctUntilChanged() + .broadcastFlow(IntentFilter(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)) .logInputChange(logger, "ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED") - .onEach { defaultDataSubIdChangeEvent.tryEmit(Unit) } - .stateIn( - scope, - SharingStarted.WhileSubscribed(), - SubscriptionManager.getDefaultDataSubscriptionId() - ) private val carrierConfigChangedEvent = broadcastDispatcher @@ -167,7 +151,7 @@ constructor( .logInputChange(logger, "ACTION_CARRIER_CONFIG_CHANGED") override val defaultDataSubRatConfig: StateFlow<Config> = - merge(defaultDataSubIdChangeEvent, carrierConfigChangedEvent) + merge(defaultDataSubIdChangedEvent, carrierConfigChangedEvent) .mapLatest { Config.readConfig(context) } .distinctUntilChanged() .logInputChange(logger, "defaultDataSubRatConfig") @@ -272,7 +256,6 @@ constructor( subId, defaultNetworkName, networkNameSeparator, - defaultDataSubId, globalMobileDataSettingChangedEvent, ) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt index 31ac7a16a940..9427c6b9fece 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt @@ -23,12 +23,11 @@ import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState.Connected import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel -import com.android.systemui.util.CarrierConfigTracker import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine @@ -171,11 +170,12 @@ class MobileIconInteractorImpl( } .stateIn(scope, SharingStarted.WhileSubscribed(), 0) - /** - * This will become variable based on [CarrierConfigManager.KEY_INFLATE_SIGNAL_STRENGTH_BOOL] - * once it's wired up inside of [CarrierConfigTracker] - */ - override val numberOfLevels: StateFlow<Int> = MutableStateFlow(4) + override val numberOfLevels: StateFlow<Int> = + connectionRepository.numberOfLevels.stateIn( + scope, + SharingStarted.WhileSubscribed(), + connectionRepository.numberOfLevels.value, + ) override val isDataConnected: StateFlow<Boolean> = connectionInfo diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt index ab442b5ab4de..3e81c7c7cefd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.pipeline.mobile.ui.binder import android.content.res.ColorStateList import android.view.View import android.view.View.GONE +import android.view.View.INVISIBLE import android.view.View.VISIBLE import android.view.ViewGroup import android.widget.ImageView @@ -30,7 +31,13 @@ import com.android.settingslib.graph.SignalDrawable import com.android.systemui.R import com.android.systemui.common.ui.binder.IconViewBinder import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.statusbar.StatusBarIconView +import com.android.systemui.statusbar.StatusBarIconView.STATE_DOT +import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN +import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModel +import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewBinding +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch @@ -40,7 +47,8 @@ object MobileIconBinder { fun bind( view: ViewGroup, viewModel: LocationBasedMobileViewModel, - ) { + ): ModernStatusBarViewBinding { + val mobileGroupView = view.requireViewById<ViewGroup>(R.id.mobile_group) val activityContainer = view.requireViewById<View>(R.id.inout_container) val activityIn = view.requireViewById<ImageView>(R.id.mobile_in) val activityOut = view.requireViewById<ImageView>(R.id.mobile_out) @@ -49,12 +57,39 @@ object MobileIconBinder { val mobileDrawable = SignalDrawable(view.context).also { iconView.setImageDrawable(it) } val roamingView = view.requireViewById<ImageView>(R.id.mobile_roaming) val roamingSpace = view.requireViewById<Space>(R.id.mobile_roaming_space) + val dotView = view.requireViewById<StatusBarIconView>(R.id.status_bar_dot) view.isVisible = true iconView.isVisible = true + // TODO(b/238425913): We should log this visibility state. + @StatusBarIconView.VisibleState + val visibilityState: MutableStateFlow<Int> = MutableStateFlow(STATE_HIDDEN) + + val iconTint: MutableStateFlow<Int> = MutableStateFlow(viewModel.defaultColor) + val decorTint: MutableStateFlow<Int> = MutableStateFlow(viewModel.defaultColor) + view.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.STARTED) { + launch { + visibilityState.collect { state -> + when (state) { + STATE_ICON -> { + mobileGroupView.visibility = VISIBLE + dotView.visibility = GONE + } + STATE_DOT -> { + mobileGroupView.visibility = INVISIBLE + dotView.visibility = VISIBLE + } + STATE_HIDDEN -> { + mobileGroupView.visibility = INVISIBLE + dotView.visibility = INVISIBLE + } + } + } + } + // Set the icon for the triangle launch { viewModel.iconId.distinctUntilChanged().collect { iconId -> @@ -89,15 +124,43 @@ object MobileIconBinder { // Set the tint launch { - viewModel.tint.collect { tint -> + iconTint.collect { tint -> val tintList = ColorStateList.valueOf(tint) iconView.imageTintList = tintList networkTypeView.imageTintList = tintList roamingView.imageTintList = tintList activityIn.imageTintList = tintList activityOut.imageTintList = tintList + dotView.setDecorColor(tint) } } + + launch { decorTint.collect { tint -> dotView.setDecorColor(tint) } } + } + } + + return object : ModernStatusBarViewBinding { + override fun getShouldIconBeVisible(): Boolean { + // If this view model exists, then the icon should be visible. + return true + } + + override fun onVisibilityStateChanged(@StatusBarIconView.VisibleState state: Int) { + visibilityState.value = state + } + + override fun onIconTintChanged(newTint: Int) { + if (viewModel.useDebugColoring) { + return + } + iconTint.value = newTint + } + + override fun onDecorTintChanged(newTint: Int) { + if (viewModel.useDebugColoring) { + return + } + decorTint.value = newTint } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt index e86fee24fe4d..ed9a1884a7b4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt @@ -17,50 +17,20 @@ package com.android.systemui.statusbar.pipeline.mobile.ui.view import android.content.Context -import android.graphics.Rect import android.util.AttributeSet import android.view.LayoutInflater import com.android.systemui.R -import com.android.systemui.statusbar.BaseStatusBarFrameLayout -import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON import com.android.systemui.statusbar.pipeline.mobile.ui.binder.MobileIconBinder import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModel -import java.util.ArrayList +import com.android.systemui.statusbar.pipeline.shared.ui.view.ModernStatusBarView class ModernStatusBarMobileView( context: Context, attrs: AttributeSet?, -) : BaseStatusBarFrameLayout(context, attrs) { +) : ModernStatusBarView(context, attrs) { var subId: Int = -1 - private lateinit var slot: String - override fun getSlot() = slot - - override fun onDarkChanged(areas: ArrayList<Rect>?, darkIntensity: Float, tint: Int) { - // TODO - } - - override fun setStaticDrawableColor(color: Int) { - // TODO - } - - override fun setDecorColor(color: Int) { - // TODO - } - - override fun setVisibleState(state: Int, animate: Boolean) { - // TODO - } - - override fun getVisibleState(): Int { - return STATE_ICON - } - - override fun isIconVisible(): Boolean { - return true - } - companion object { /** @@ -77,9 +47,8 @@ class ModernStatusBarMobileView( .inflate(R.layout.status_bar_mobile_signal_group_new, null) as ModernStatusBarMobileView) .also { - it.slot = slot it.subId = viewModel.subscriptionId - MobileIconBinder.bind(it, viewModel) + it.initView(slot) { MobileIconBinder.bind(it, viewModel) } } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModel.kt index b0dc41f45488..24cd9304f8dd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModel.kt @@ -18,11 +18,7 @@ package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel import android.graphics.Color import com.android.systemui.statusbar.phone.StatusBarLocation -import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger -import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flowOf +import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags /** * A view model for an individual mobile icon that embeds the notion of a [StatusBarLocation]. This @@ -33,50 +29,51 @@ import kotlinx.coroutines.flow.flowOf */ abstract class LocationBasedMobileViewModel( val commonImpl: MobileIconViewModelCommon, - val logger: ConnectivityPipelineLogger, + statusBarPipelineFlags: StatusBarPipelineFlags, + debugTint: Int, ) : MobileIconViewModelCommon by commonImpl { - abstract val tint: Flow<Int> + val useDebugColoring: Boolean = statusBarPipelineFlags.useDebugColoring() + + val defaultColor: Int = + if (useDebugColoring) { + debugTint + } else { + Color.WHITE + } companion object { fun viewModelForLocation( commonImpl: MobileIconViewModelCommon, - logger: ConnectivityPipelineLogger, + statusBarPipelineFlags: StatusBarPipelineFlags, loc: StatusBarLocation, ): LocationBasedMobileViewModel = when (loc) { - StatusBarLocation.HOME -> HomeMobileIconViewModel(commonImpl, logger) - StatusBarLocation.KEYGUARD -> KeyguardMobileIconViewModel(commonImpl, logger) - StatusBarLocation.QS -> QsMobileIconViewModel(commonImpl, logger) + StatusBarLocation.HOME -> + HomeMobileIconViewModel(commonImpl, statusBarPipelineFlags) + StatusBarLocation.KEYGUARD -> + KeyguardMobileIconViewModel(commonImpl, statusBarPipelineFlags) + StatusBarLocation.QS -> QsMobileIconViewModel(commonImpl, statusBarPipelineFlags) } } } class HomeMobileIconViewModel( commonImpl: MobileIconViewModelCommon, - logger: ConnectivityPipelineLogger, -) : MobileIconViewModelCommon, LocationBasedMobileViewModel(commonImpl, logger) { - override val tint: Flow<Int> = - flowOf(Color.CYAN) - .distinctUntilChanged() - .logOutputChange(logger, "HOME tint(${commonImpl.subscriptionId})") -} + statusBarPipelineFlags: StatusBarPipelineFlags, +) : + MobileIconViewModelCommon, + LocationBasedMobileViewModel(commonImpl, statusBarPipelineFlags, debugTint = Color.CYAN) class QsMobileIconViewModel( commonImpl: MobileIconViewModelCommon, - logger: ConnectivityPipelineLogger, -) : MobileIconViewModelCommon, LocationBasedMobileViewModel(commonImpl, logger) { - override val tint: Flow<Int> = - flowOf(Color.GREEN) - .distinctUntilChanged() - .logOutputChange(logger, "QS tint(${commonImpl.subscriptionId})") -} + statusBarPipelineFlags: StatusBarPipelineFlags, +) : + MobileIconViewModelCommon, + LocationBasedMobileViewModel(commonImpl, statusBarPipelineFlags, debugTint = Color.GREEN) class KeyguardMobileIconViewModel( commonImpl: MobileIconViewModelCommon, - logger: ConnectivityPipelineLogger, -) : MobileIconViewModelCommon, LocationBasedMobileViewModel(commonImpl, logger) { - override val tint: Flow<Int> = - flowOf(Color.MAGENTA) - .distinctUntilChanged() - .logOutputChange(logger, "KEYGUARD tint(${commonImpl.subscriptionId})") -} + statusBarPipelineFlags: StatusBarPipelineFlags, +) : + MobileIconViewModelCommon, + LocationBasedMobileViewModel(commonImpl, statusBarPipelineFlags, debugTint = Color.MAGENTA) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt index b9318b181aaf..24370d221ade 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel import androidx.annotation.VisibleForTesting import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.statusbar.phone.StatusBarLocation +import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants @@ -41,6 +42,7 @@ constructor( private val logger: ConnectivityPipelineLogger, private val constants: ConnectivityConstants, @Application private val scope: CoroutineScope, + private val statusBarPipelineFlags: StatusBarPipelineFlags, ) { @VisibleForTesting val mobileIconSubIdCache = mutableMapOf<Int, MobileIconViewModel>() @@ -60,7 +62,11 @@ constructor( ) .also { mobileIconSubIdCache[subId] = it } - return LocationBasedMobileViewModel.viewModelForLocation(common, logger, location) + return LocationBasedMobileViewModel.viewModelForLocation( + common, + statusBarPipelineFlags, + location, + ) } private fun removeInvalidModelsFromCache(subIds: List<Int>) { @@ -75,6 +81,7 @@ constructor( private val logger: ConnectivityPipelineLogger, private val constants: ConnectivityConstants, @Application private val scope: CoroutineScope, + private val statusBarPipelineFlags: StatusBarPipelineFlags, ) { fun create(subscriptionIdsFlow: StateFlow<List<Int>>): MobileIconsViewModel { return MobileIconsViewModel( @@ -83,6 +90,7 @@ constructor( logger, constants, scope, + statusBarPipelineFlags, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/ModernStatusBarViewBinding.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/ModernStatusBarViewBinding.kt new file mode 100644 index 000000000000..f67876b50233 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/ModernStatusBarViewBinding.kt @@ -0,0 +1,40 @@ +/* + * 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.statusbar.pipeline.shared.ui.binder + +import com.android.systemui.statusbar.StatusBarIconView + +/** + * Defines interface for an object that acts as the binding between a modern status bar view and its + * view-model. + * + * Users of the view binder classes in the modern status bar pipeline should use this to control the + * binder after it is bound. + */ +interface ModernStatusBarViewBinding { + /** Returns true if the icon should be visible and false otherwise. */ + fun getShouldIconBeVisible(): Boolean + + /** Notifies that the visibility state has changed. */ + fun onVisibilityStateChanged(@StatusBarIconView.VisibleState state: Int) + + /** Notifies that the icon tint has been updated. */ + fun onIconTintChanged(newTint: Int) + + /** Notifies that the decor tint has been updated (used only for the dot). */ + fun onDecorTintChanged(newTint: Int) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarView.kt new file mode 100644 index 000000000000..cc0ec548716d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarView.kt @@ -0,0 +1,115 @@ +/* + * 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.statusbar.pipeline.shared.ui.view + +import android.content.Context +import android.graphics.Rect +import android.util.AttributeSet +import android.view.Gravity +import com.android.systemui.R +import com.android.systemui.plugins.DarkIconDispatcher +import com.android.systemui.statusbar.BaseStatusBarFrameLayout +import com.android.systemui.statusbar.StatusBarIconView +import com.android.systemui.statusbar.StatusBarIconView.STATE_DOT +import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN +import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewBinding + +/** + * A new and more modern implementation of [BaseStatusBarFrameLayout] that gets updated by view + * binders communicating via [ModernStatusBarViewBinding]. + */ +open class ModernStatusBarView(context: Context, attrs: AttributeSet?) : + BaseStatusBarFrameLayout(context, attrs) { + + private lateinit var slot: String + private lateinit var binding: ModernStatusBarViewBinding + + @StatusBarIconView.VisibleState + private var iconVisibleState: Int = STATE_HIDDEN + set(value) { + if (field == value) { + return + } + field = value + binding.onVisibilityStateChanged(value) + } + + override fun getSlot() = slot + + override fun onDarkChanged(areas: ArrayList<Rect>?, darkIntensity: Float, tint: Int) { + val newTint = DarkIconDispatcher.getTint(areas, this, tint) + binding.onIconTintChanged(newTint) + binding.onDecorTintChanged(newTint) + } + + override fun setStaticDrawableColor(color: Int) { + binding.onIconTintChanged(color) + } + + override fun setDecorColor(color: Int) { + binding.onDecorTintChanged(color) + } + + override fun setVisibleState(@StatusBarIconView.VisibleState state: Int, animate: Boolean) { + iconVisibleState = state + } + + @StatusBarIconView.VisibleState + override fun getVisibleState(): Int { + return iconVisibleState + } + + override fun isIconVisible(): Boolean { + return binding.getShouldIconBeVisible() + } + + /** + * Initializes this view. + * + * Creates a dot view, and uses [bindingCreator] to get and set the binding. + */ + fun initView(slot: String, bindingCreator: () -> ModernStatusBarViewBinding) { + // The dot view requires [slot] to be set, and the [binding] may require an instantiated dot + // view. So, this is the required order. + this.slot = slot + initDotView() + this.binding = bindingCreator.invoke() + } + + /** + * Creates a [StatusBarIconView] that is always in DOT mode and adds it to this view. + * + * Mostly duplicated from [com.android.systemui.statusbar.StatusBarWifiView] and + * [com.android.systemui.statusbar.StatusBarMobileView]. + */ + private fun initDotView() { + // TODO(b/238425913): Could we just have this dot view be part of the layout with a dot + // drawable so we don't need to inflate it manually? Would that not work with animations? + val dotView = + StatusBarIconView(mContext, slot, null).also { + it.id = R.id.status_bar_dot + // Hard-code this view to always be in the DOT state so that whenever it's visible + // it will show a dot + it.visibleState = STATE_DOT + } + + val width = mContext.resources.getDimensionPixelSize(R.dimen.status_bar_icon_size) + val lp = LayoutParams(width, width) + lp.gravity = Gravity.CENTER_VERTICAL or Gravity.START + addView(dotView, lp) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt index a682a5711a6f..4251d18357f7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt @@ -23,6 +23,33 @@ import com.android.systemui.log.table.Diffable /** Provides information about the current wifi network. */ sealed class WifiNetworkModel : Diffable<WifiNetworkModel> { + /** + * A model representing that we couldn't fetch any wifi information. + * + * This is only used with [DisabledWifiRepository], where [WifiManager] is null. + */ + object Unavailable : WifiNetworkModel() { + override fun toString() = "WifiNetwork.Unavailable" + override fun logDiffs(prevVal: WifiNetworkModel, row: TableRowLogger) { + if (prevVal is Unavailable) { + return + } + + logFull(row) + } + + override fun logFull(row: TableRowLogger) { + row.logChange(COL_NETWORK_TYPE, TYPE_UNAVAILABLE) + row.logChange(COL_NETWORK_ID, NETWORK_ID_DEFAULT) + row.logChange(COL_VALIDATED, false) + row.logChange(COL_LEVEL, LEVEL_DEFAULT) + row.logChange(COL_SSID, null) + row.logChange(COL_PASSPOINT_ACCESS_POINT, false) + row.logChange(COL_ONLINE_SIGN_UP, false) + row.logChange(COL_PASSPOINT_NAME, null) + } + } + /** A model representing that we have no active wifi network. */ object Inactive : WifiNetworkModel() { override fun toString() = "WifiNetwork.Inactive" @@ -87,13 +114,8 @@ sealed class WifiNetworkModel : Diffable<WifiNetworkModel> { /** * The wifi signal level, guaranteed to be 0 <= level <= 4. - * - * Null if we couldn't fetch the level for some reason. - * - * TODO(b/238425913): The level will only be null if we have a null WifiManager. Is there a - * way we can guarantee a non-null WifiManager? */ - val level: Int? = null, + val level: Int, /** See [android.net.wifi.WifiInfo.ssid]. */ val ssid: String? = null, @@ -108,7 +130,7 @@ sealed class WifiNetworkModel : Diffable<WifiNetworkModel> { val passpointProviderFriendlyName: String? = null, ) : WifiNetworkModel() { init { - require(level == null || level in MIN_VALID_LEVEL..MAX_VALID_LEVEL) { + require(level in MIN_VALID_LEVEL..MAX_VALID_LEVEL) { "0 <= wifi level <= 4 required; level was $level" } } @@ -125,11 +147,7 @@ sealed class WifiNetworkModel : Diffable<WifiNetworkModel> { row.logChange(COL_VALIDATED, isValidated) } if (prevVal !is Active || prevVal.level != level) { - if (level != null) { - row.logChange(COL_LEVEL, level) - } else { - row.logChange(COL_LEVEL, LEVEL_DEFAULT) - } + row.logChange(COL_LEVEL, level) } if (prevVal !is Active || prevVal.ssid != ssid) { row.logChange(COL_SSID, ssid) @@ -190,6 +208,7 @@ sealed class WifiNetworkModel : Diffable<WifiNetworkModel> { } const val TYPE_CARRIER_MERGED = "CarrierMerged" +const val TYPE_UNAVAILABLE = "Unavailable" const val TYPE_INACTIVE = "Inactive" const val TYPE_ACTIVE = "Active" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt index 53525f254e1d..ac4d55c3a29c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt @@ -34,3 +34,13 @@ interface WifiRepository { /** Observable for the current wifi network activity. */ val wifiActivity: StateFlow<DataActivityModel> } + +/** + * A no-op interface used for Dagger bindings. + * + * [WifiRepositorySwitcher] needs to inject the "real" wifi repository, which could either be the + * full [WifiRepositoryImpl] or just [DisabledWifiRepository]. Having this interface lets us bind + * [RealWifiRepository], and then [WifiRepositorySwitcher] will automatically get the correct real + * repository. + */ +interface RealWifiRepository : WifiRepository diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcher.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcher.kt index be86620e01f3..2cb81c809716 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcher.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcher.kt @@ -58,7 +58,7 @@ import kotlinx.coroutines.flow.stateIn class WifiRepositorySwitcher @Inject constructor( - private val realImpl: WifiRepositoryImpl, + private val realImpl: RealWifiRepository, private val demoImpl: DemoWifiRepository, private val demoModeController: DemoModeController, @Application scope: CoroutineScope, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt index 7890074cf8a2..be3d7d4e65c4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt @@ -89,7 +89,7 @@ constructor( WifiNetworkModel.Active( networkId = DEMO_NET_ID, isValidated = validated ?: true, - level = level, + level = level ?: 0, ssid = ssid, // These fields below aren't supported in demo mode, since they aren't needed to satisfy diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepository.kt new file mode 100644 index 000000000000..5d4a6664a19a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepository.kt @@ -0,0 +1,49 @@ +/* + * 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.statusbar.pipeline.wifi.data.repository.prod + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel +import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel +import com.android.systemui.statusbar.pipeline.wifi.data.repository.RealWifiRepository +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** + * Implementation of wifi repository used when wifi is permanently disabled on the device. + * + * This repo should only exist when [WifiManager] is null, which means that we can never fetch any + * wifi information. + */ +@SysUISingleton +class DisabledWifiRepository @Inject constructor() : RealWifiRepository { + override val isWifiEnabled: StateFlow<Boolean> = MutableStateFlow(false).asStateFlow() + + override val isWifiDefault: StateFlow<Boolean> = MutableStateFlow(false).asStateFlow() + + override val wifiNetwork: StateFlow<WifiNetworkModel> = MutableStateFlow(NETWORK).asStateFlow() + + override val wifiActivity: StateFlow<DataActivityModel> = + MutableStateFlow(ACTIVITY).asStateFlow() + + companion object { + private val NETWORK = WifiNetworkModel.Unavailable + private val ACTIVITY = DataActivityModel(hasActivityIn = false, hasActivityOut = false) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt index c8c94e102999..c47c20d280c7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImpl.kt @@ -29,7 +29,6 @@ import android.net.NetworkRequest import android.net.wifi.WifiInfo import android.net.wifi.WifiManager import android.net.wifi.WifiManager.TrafficStateCallback -import android.util.Log import com.android.settingslib.Utils import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow @@ -40,11 +39,11 @@ import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.log.table.logDiffsForTable import com.android.systemui.statusbar.pipeline.dagger.WifiTableLog import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger -import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.SB_LOGGING_TAG import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logInputChange import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel import com.android.systemui.statusbar.pipeline.shared.data.model.toWifiDataActivityModel import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel +import com.android.systemui.statusbar.pipeline.wifi.data.repository.RealWifiRepository import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository import java.util.concurrent.Executor import javax.inject.Inject @@ -53,12 +52,9 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.stateIn @@ -68,178 +64,177 @@ import kotlinx.coroutines.flow.stateIn @OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton @SuppressLint("MissingPermission") -class WifiRepositoryImpl @Inject constructor( +class WifiRepositoryImpl +@Inject +constructor( broadcastDispatcher: BroadcastDispatcher, connectivityManager: ConnectivityManager, logger: ConnectivityPipelineLogger, @WifiTableLog wifiTableLogBuffer: TableLogBuffer, @Main mainExecutor: Executor, @Application scope: CoroutineScope, - wifiManager: WifiManager?, -) : WifiRepository { + wifiManager: WifiManager, +) : RealWifiRepository { - private val wifiStateChangeEvents: Flow<Unit> = broadcastDispatcher.broadcastFlow( - IntentFilter(WifiManager.WIFI_STATE_CHANGED_ACTION) - ) - .logInputChange(logger, "WIFI_STATE_CHANGED_ACTION intent") + private val wifiStateChangeEvents: Flow<Unit> = + broadcastDispatcher + .broadcastFlow(IntentFilter(WifiManager.WIFI_STATE_CHANGED_ACTION)) + .logInputChange(logger, "WIFI_STATE_CHANGED_ACTION intent") private val wifiNetworkChangeEvents: MutableSharedFlow<Unit> = MutableSharedFlow(extraBufferCapacity = 1) + // Because [WifiManager] doesn't expose a wifi enabled change listener, we do it + // internally by fetching [WifiManager.isWifiEnabled] whenever we think the state may + // have changed. override val isWifiEnabled: StateFlow<Boolean> = - if (wifiManager == null) { - MutableStateFlow(false).asStateFlow() - } else { - // Because [WifiManager] doesn't expose a wifi enabled change listener, we do it - // internally by fetching [WifiManager.isWifiEnabled] whenever we think the state may - // have changed. - merge(wifiNetworkChangeEvents, wifiStateChangeEvents) - .mapLatest { wifiManager.isWifiEnabled } - .distinctUntilChanged() - .logDiffsForTable( - wifiTableLogBuffer, - columnPrefix = "", - columnName = "isWifiEnabled", - initialValue = wifiManager.isWifiEnabled, - ) - .stateIn( - scope = scope, - started = SharingStarted.WhileSubscribed(), - initialValue = wifiManager.isWifiEnabled - ) - } + merge(wifiNetworkChangeEvents, wifiStateChangeEvents) + .mapLatest { wifiManager.isWifiEnabled } + .distinctUntilChanged() + .logDiffsForTable( + wifiTableLogBuffer, + columnPrefix = "", + columnName = "isWifiEnabled", + initialValue = wifiManager.isWifiEnabled, + ) + .stateIn( + scope = scope, + started = SharingStarted.WhileSubscribed(), + initialValue = wifiManager.isWifiEnabled, + ) - override val isWifiDefault: StateFlow<Boolean> = conflatedCallbackFlow { - // Note: This callback doesn't do any logging because we already log every network change - // in the [wifiNetwork] callback. - val callback = object : ConnectivityManager.NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) { - override fun onCapabilitiesChanged( - network: Network, - networkCapabilities: NetworkCapabilities - ) { - // This method will always be called immediately after the network becomes the - // default, in addition to any time the capabilities change while the network is - // the default. - // If this network contains valid wifi info, then wifi is the default network. - val wifiInfo = networkCapabilitiesToWifiInfo(networkCapabilities) - trySend(wifiInfo != null) - } + override val isWifiDefault: StateFlow<Boolean> = + conflatedCallbackFlow { + // Note: This callback doesn't do any logging because we already log every network + // change in the [wifiNetwork] callback. + val callback = + object : ConnectivityManager.NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) { + override fun onCapabilitiesChanged( + network: Network, + networkCapabilities: NetworkCapabilities + ) { + // This method will always be called immediately after the network + // becomes the default, in addition to any time the capabilities change + // while the network is the default. + // If this network contains valid wifi info, then wifi is the default + // network. + val wifiInfo = networkCapabilitiesToWifiInfo(networkCapabilities) + trySend(wifiInfo != null) + } - override fun onLost(network: Network) { - // The system no longer has a default network, so wifi is definitely not default. - trySend(false) - } - } + override fun onLost(network: Network) { + // The system no longer has a default network, so wifi is definitely not + // default. + trySend(false) + } + } - connectivityManager.registerDefaultNetworkCallback(callback) - awaitClose { connectivityManager.unregisterNetworkCallback(callback) } - } - .distinctUntilChanged() - .logDiffsForTable( - wifiTableLogBuffer, - columnPrefix = "", - columnName = "isWifiDefault", - initialValue = false, - ) - .stateIn( - scope, - started = SharingStarted.WhileSubscribed(), - initialValue = false - ) + connectivityManager.registerDefaultNetworkCallback(callback) + awaitClose { connectivityManager.unregisterNetworkCallback(callback) } + } + .distinctUntilChanged() + .logDiffsForTable( + wifiTableLogBuffer, + columnPrefix = "", + columnName = "isWifiDefault", + initialValue = false, + ) + .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = false) - override val wifiNetwork: StateFlow<WifiNetworkModel> = conflatedCallbackFlow { - var currentWifi: WifiNetworkModel = WIFI_NETWORK_DEFAULT + override val wifiNetwork: StateFlow<WifiNetworkModel> = + conflatedCallbackFlow { + var currentWifi: WifiNetworkModel = WIFI_NETWORK_DEFAULT - val callback = object : ConnectivityManager.NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) { - override fun onCapabilitiesChanged( - network: Network, - networkCapabilities: NetworkCapabilities - ) { - logger.logOnCapabilitiesChanged(network, networkCapabilities) + val callback = + object : ConnectivityManager.NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) { + override fun onCapabilitiesChanged( + network: Network, + networkCapabilities: NetworkCapabilities + ) { + logger.logOnCapabilitiesChanged(network, networkCapabilities) - wifiNetworkChangeEvents.tryEmit(Unit) + wifiNetworkChangeEvents.tryEmit(Unit) - val wifiInfo = networkCapabilitiesToWifiInfo(networkCapabilities) - if (wifiInfo?.isPrimary == true) { - val wifiNetworkModel = createWifiNetworkModel( - wifiInfo, - network, - networkCapabilities, - wifiManager, - ) - logger.logTransformation( - WIFI_NETWORK_CALLBACK_NAME, - oldValue = currentWifi, - newValue = wifiNetworkModel - ) - currentWifi = wifiNetworkModel - trySend(wifiNetworkModel) - } - } + val wifiInfo = networkCapabilitiesToWifiInfo(networkCapabilities) + if (wifiInfo?.isPrimary == true) { + val wifiNetworkModel = + createWifiNetworkModel( + wifiInfo, + network, + networkCapabilities, + wifiManager, + ) + logger.logTransformation( + WIFI_NETWORK_CALLBACK_NAME, + oldValue = currentWifi, + newValue = wifiNetworkModel, + ) + currentWifi = wifiNetworkModel + trySend(wifiNetworkModel) + } + } - override fun onLost(network: Network) { - logger.logOnLost(network) + override fun onLost(network: Network) { + logger.logOnLost(network) - wifiNetworkChangeEvents.tryEmit(Unit) + wifiNetworkChangeEvents.tryEmit(Unit) - val wifi = currentWifi - if (wifi is WifiNetworkModel.Active && wifi.networkId == network.getNetId()) { - val newNetworkModel = WifiNetworkModel.Inactive - logger.logTransformation( - WIFI_NETWORK_CALLBACK_NAME, - oldValue = wifi, - newValue = newNetworkModel - ) - currentWifi = newNetworkModel - trySend(newNetworkModel) - } - } - } + val wifi = currentWifi + if ( + wifi is WifiNetworkModel.Active && + wifi.networkId == network.getNetId() + ) { + val newNetworkModel = WifiNetworkModel.Inactive + logger.logTransformation( + WIFI_NETWORK_CALLBACK_NAME, + oldValue = wifi, + newValue = newNetworkModel, + ) + currentWifi = newNetworkModel + trySend(newNetworkModel) + } + } + } - connectivityManager.registerNetworkCallback(WIFI_NETWORK_CALLBACK_REQUEST, callback) + connectivityManager.registerNetworkCallback(WIFI_NETWORK_CALLBACK_REQUEST, callback) - awaitClose { connectivityManager.unregisterNetworkCallback(callback) } - } - .distinctUntilChanged() - .logDiffsForTable( - wifiTableLogBuffer, - columnPrefix = "wifiNetwork", - initialValue = WIFI_NETWORK_DEFAULT, - ) - // There will be multiple wifi icons in different places that will frequently - // subscribe/unsubscribe to flows as the views attach/detach. Using [stateIn] ensures that - // new subscribes will get the latest value immediately upon subscription. Otherwise, the - // views could show stale data. See b/244173280. - .stateIn( - scope, - started = SharingStarted.WhileSubscribed(), - initialValue = WIFI_NETWORK_DEFAULT - ) + awaitClose { connectivityManager.unregisterNetworkCallback(callback) } + } + .distinctUntilChanged() + .logDiffsForTable( + wifiTableLogBuffer, + columnPrefix = "wifiNetwork", + initialValue = WIFI_NETWORK_DEFAULT, + ) + // There will be multiple wifi icons in different places that will frequently + // subscribe/unsubscribe to flows as the views attach/detach. Using [stateIn] ensures + // that new subscribes will get the latest value immediately upon subscription. + // Otherwise, the views could show stale data. See b/244173280. + .stateIn( + scope, + started = SharingStarted.WhileSubscribed(), + initialValue = WIFI_NETWORK_DEFAULT, + ) override val wifiActivity: StateFlow<DataActivityModel> = - if (wifiManager == null) { - Log.w(SB_LOGGING_TAG, "Null WifiManager; skipping activity callback") - flowOf(ACTIVITY_DEFAULT) - } else { - conflatedCallbackFlow { - val callback = TrafficStateCallback { state -> - logger.logInputChange("onTrafficStateChange", prettyPrintActivity(state)) - trySend(state.toWifiDataActivityModel()) - } - wifiManager.registerTrafficStateCallback(mainExecutor, callback) - awaitClose { wifiManager.unregisterTrafficStateCallback(callback) } + conflatedCallbackFlow { + val callback = TrafficStateCallback { state -> + logger.logInputChange("onTrafficStateChange", prettyPrintActivity(state)) + trySend(state.toWifiDataActivityModel()) } + wifiManager.registerTrafficStateCallback(mainExecutor, callback) + awaitClose { wifiManager.unregisterTrafficStateCallback(callback) } } - .logDiffsForTable( - wifiTableLogBuffer, - columnPrefix = ACTIVITY_PREFIX, - initialValue = ACTIVITY_DEFAULT, - ) - .stateIn( - scope, - started = SharingStarted.WhileSubscribed(), - initialValue = ACTIVITY_DEFAULT - ) + .logDiffsForTable( + wifiTableLogBuffer, + columnPrefix = ACTIVITY_PREFIX, + initialValue = ACTIVITY_DEFAULT, + ) + .stateIn( + scope, + started = SharingStarted.WhileSubscribed(), + initialValue = ACTIVITY_DEFAULT, + ) companion object { private const val ACTIVITY_PREFIX = "wifiActivity" @@ -271,19 +266,19 @@ class WifiRepositoryImpl @Inject constructor( wifiInfo: WifiInfo, network: Network, networkCapabilities: NetworkCapabilities, - wifiManager: WifiManager?, + wifiManager: WifiManager, ): WifiNetworkModel { return if (wifiInfo.isCarrierMerged) { WifiNetworkModel.CarrierMerged } else { WifiNetworkModel.Active( - network.getNetId(), - isValidated = networkCapabilities.hasCapability(NET_CAPABILITY_VALIDATED), - level = wifiManager?.calculateSignalLevel(wifiInfo.rssi), - wifiInfo.ssid, - wifiInfo.isPasspointAp, - wifiInfo.isOsuAp, - wifiInfo.passpointProviderFriendlyName + network.getNetId(), + isValidated = networkCapabilities.hasCapability(NET_CAPABILITY_VALIDATED), + level = wifiManager.calculateSignalLevel(wifiInfo.rssi), + wifiInfo.ssid, + wifiInfo.isPasspointAp, + wifiInfo.isOsuAp, + wifiInfo.passpointProviderFriendlyName ) } } @@ -308,4 +303,28 @@ class WifiRepositoryImpl @Inject constructor( private const val WIFI_NETWORK_CALLBACK_NAME = "wifiNetworkModel" } + + @SysUISingleton + class Factory + @Inject + constructor( + private val broadcastDispatcher: BroadcastDispatcher, + private val connectivityManager: ConnectivityManager, + private val logger: ConnectivityPipelineLogger, + @WifiTableLog private val wifiTableLogBuffer: TableLogBuffer, + @Main private val mainExecutor: Executor, + @Application private val scope: CoroutineScope, + ) { + fun create(wifiManager: WifiManager): WifiRepositoryImpl { + return WifiRepositoryImpl( + broadcastDispatcher, + connectivityManager, + logger, + wifiTableLogBuffer, + mainExecutor, + scope, + wifiManager, + ) + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt index 93041ceb4200..980560ab5d58 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt @@ -65,6 +65,7 @@ class WifiInteractorImpl @Inject constructor( override val ssid: Flow<String?> = wifiRepository.wifiNetwork.map { info -> when (info) { + is WifiNetworkModel.Unavailable -> null is WifiNetworkModel.Inactive -> null is WifiNetworkModel.CarrierMerged -> null is WifiNetworkModel.Active -> when { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt index cc67c84772a5..2aff12c8721d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt @@ -30,6 +30,7 @@ import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.StatusBarIconView.STATE_DOT import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON +import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewBinding import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel import kotlinx.coroutines.InternalCoroutinesApi @@ -49,31 +50,12 @@ import kotlinx.coroutines.launch @Suppress("EXPERIMENTAL_IS_NOT_ENABLED") object WifiViewBinder { - /** - * Defines interface for an object that acts as the binding between the view and its view-model. - * - * Users of the [WifiViewBinder] class should use this to control the binder after it is bound. - */ - interface Binding { - /** Returns true if the wifi icon should be visible and false otherwise. */ - fun getShouldIconBeVisible(): Boolean - - /** Notifies that the visibility state has changed. */ - fun onVisibilityStateChanged(@StatusBarIconView.VisibleState state: Int) - - /** Notifies that the icon tint has been updated. */ - fun onIconTintChanged(newTint: Int) - - /** Notifies that the decor tint has been updated (used only for the dot). */ - fun onDecorTintChanged(newTint: Int) - } - /** Binds the view to the view-model, continuing to update the former based on the latter. */ @JvmStatic fun bind( view: ViewGroup, viewModel: LocationBasedWifiViewModel, - ): Binding { + ): ModernStatusBarViewBinding { val groupView = view.requireViewById<ViewGroup>(R.id.wifi_group) val iconView = view.requireViewById<ImageView>(R.id.wifi_signal) val dotView = view.requireViewById<StatusBarIconView>(R.id.status_bar_dot) @@ -148,7 +130,7 @@ object WifiViewBinder { } } - return object : Binding { + return object : ModernStatusBarViewBinding { override fun getShouldIconBeVisible(): Boolean { return viewModel.wifiIcon.value is WifiIcon.Visible } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt index be7782c37cfd..7a734862fe1b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt @@ -16,17 +16,12 @@ package com.android.systemui.statusbar.pipeline.wifi.ui.view +import android.annotation.SuppressLint import android.content.Context -import android.graphics.Rect import android.util.AttributeSet -import android.view.Gravity import android.view.LayoutInflater import com.android.systemui.R -import com.android.systemui.plugins.DarkIconDispatcher -import com.android.systemui.statusbar.BaseStatusBarFrameLayout -import com.android.systemui.statusbar.StatusBarIconView -import com.android.systemui.statusbar.StatusBarIconView.STATE_DOT -import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN +import com.android.systemui.statusbar.pipeline.shared.ui.view.ModernStatusBarView import com.android.systemui.statusbar.pipeline.wifi.ui.binder.WifiViewBinder import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel @@ -36,83 +31,14 @@ import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWi */ class ModernStatusBarWifiView( context: Context, - attrs: AttributeSet? -) : BaseStatusBarFrameLayout(context, attrs) { - - private lateinit var slot: String - private lateinit var binding: WifiViewBinder.Binding - - @StatusBarIconView.VisibleState - private var iconVisibleState: Int = STATE_HIDDEN - set(value) { - if (field == value) { - return - } - field = value - binding.onVisibilityStateChanged(value) - } - - override fun getSlot() = slot - - override fun onDarkChanged(areas: ArrayList<Rect>?, darkIntensity: Float, tint: Int) { - val newTint = DarkIconDispatcher.getTint(areas, this, tint) - binding.onIconTintChanged(newTint) - binding.onDecorTintChanged(newTint) - } - - override fun setStaticDrawableColor(color: Int) { - binding.onIconTintChanged(color) - } - - override fun setDecorColor(color: Int) { - binding.onDecorTintChanged(color) - } - - override fun setVisibleState(@StatusBarIconView.VisibleState state: Int, animate: Boolean) { - iconVisibleState = state - } - - @StatusBarIconView.VisibleState - override fun getVisibleState(): Int { - return iconVisibleState - } - - override fun isIconVisible(): Boolean { - return binding.getShouldIconBeVisible() - } - - private fun initView( - slotName: String, - wifiViewModel: LocationBasedWifiViewModel, - ) { - slot = slotName - initDotView() - binding = WifiViewBinder.bind(this, wifiViewModel) - } - - // Mostly duplicated from [com.android.systemui.statusbar.StatusBarWifiView]. - private fun initDotView() { - // TODO(b/238425913): Could we just have this dot view be part of - // R.layout.new_status_bar_wifi_group with a dot drawable so we don't need to inflate it - // manually? Would that not work with animations? - val dotView = StatusBarIconView(mContext, slot, null).also { - it.id = R.id.status_bar_dot - // Hard-code this view to always be in the DOT state so that whenever it's visible it - // will show a dot - it.visibleState = STATE_DOT - } - - val width = mContext.resources.getDimensionPixelSize(R.dimen.status_bar_icon_size) - val lp = LayoutParams(width, width) - lp.gravity = Gravity.CENTER_VERTICAL or Gravity.START - addView(dotView, lp) - } - + attrs: AttributeSet?, +) : ModernStatusBarView(context, attrs) { companion object { /** * Inflates a new instance of [ModernStatusBarWifiView], binds it to a view model, and * returns it. */ + @SuppressLint("InflateParams") @JvmStatic fun constructAndBind( context: Context, @@ -123,7 +49,7 @@ class ModernStatusBarWifiView( LayoutInflater.from(context).inflate(R.layout.new_status_bar_wifi_group, null) as ModernStatusBarWifiView ).also { - it.initView(slot, wifiViewModel) + it.initView(slot) { WifiViewBinder.bind(it, wifiViewModel) } } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt index a4615cc897cf..02c3a652cc8d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt @@ -47,7 +47,7 @@ abstract class LocationBasedWifiViewModel( /** True if the airplane spacer view should be visible. */ val isAirplaneSpacerVisible: Flow<Boolean>, ) { - val useDebugColoring: Boolean = statusBarPipelineFlags.useWifiDebugColoring() + val useDebugColoring: Boolean = statusBarPipelineFlags.useDebugColoring() val defaultColor: Int = if (useDebugColoring) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt index ab464cc78905..824b5972ba4b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt @@ -82,6 +82,7 @@ constructor( /** Returns the icon to use based on the given network. */ private fun WifiNetworkModel.icon(): WifiIcon { return when (this) { + is WifiNetworkModel.Unavailable -> WifiIcon.Hidden is WifiNetworkModel.CarrierMerged -> WifiIcon.Hidden is WifiNetworkModel.Inactive -> WifiIcon.Visible( res = WIFI_NO_NETWORK, @@ -89,27 +90,23 @@ constructor( "${context.getString(WIFI_NO_CONNECTION)},${context.getString(NO_INTERNET)}" ) ) - is WifiNetworkModel.Active -> - when (this.level) { - null -> WifiIcon.Hidden - else -> { - val levelDesc = context.getString(WIFI_CONNECTION_STRENGTH[this.level]) - when { - this.isValidated -> - WifiIcon.Visible( - WIFI_FULL_ICONS[this.level], - ContentDescription.Loaded(levelDesc) - ) - else -> - WifiIcon.Visible( - WIFI_NO_INTERNET_ICONS[this.level], - ContentDescription.Loaded( - "$levelDesc,${context.getString(NO_INTERNET)}" - ) - ) - } - } + is WifiNetworkModel.Active -> { + val levelDesc = context.getString(WIFI_CONNECTION_STRENGTH[this.level]) + when { + this.isValidated -> + WifiIcon.Visible( + WIFI_FULL_ICONS[this.level], + ContentDescription.Loaded(levelDesc), + ) + else -> + WifiIcon.Visible( + WIFI_NO_INTERNET_ICONS[this.level], + ContentDescription.Loaded( + "$levelDesc,${context.getString(NO_INTERNET)}" + ), + ) } + } } } diff --git a/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt b/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt index 3e111e6de785..302d6a9ca1b7 100644 --- a/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt +++ b/packages/SystemUI/src/com/android/systemui/stylus/StylusManager.kt @@ -38,7 +38,7 @@ class StylusManager @Inject constructor( private val inputManager: InputManager, - private val bluetoothAdapter: BluetoothAdapter, + private val bluetoothAdapter: BluetoothAdapter?, @Background private val handler: Handler, @Background private val executor: Executor, ) : InputManager.InputDeviceListener, BluetoothAdapter.OnMetadataChangedListener { @@ -141,7 +141,7 @@ constructor( } private fun onStylusBluetoothConnected(btAddress: String) { - val device: BluetoothDevice = bluetoothAdapter.getRemoteDevice(btAddress) ?: return + val device: BluetoothDevice = bluetoothAdapter?.getRemoteDevice(btAddress) ?: return try { bluetoothAdapter.addOnMetadataChangedListener(device, executor, this) } catch (e: IllegalArgumentException) { @@ -150,7 +150,7 @@ constructor( } private fun onStylusBluetoothDisconnected(btAddress: String) { - val device: BluetoothDevice = bluetoothAdapter.getRemoteDevice(btAddress) ?: return + val device: BluetoothDevice = bluetoothAdapter?.getRemoteDevice(btAddress) ?: return try { bluetoothAdapter.removeOnMetadataChangedListener(device, this) } catch (e: IllegalArgumentException) { diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt index 52980c3c1f9b..04b1a5016989 100644 --- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt @@ -65,26 +65,27 @@ import javax.inject.Inject * in the list of notifications until the user dismisses them. * * Only one chipbar may be shown at a time. - * TODO(b/245610654): Should we just display whichever chipbar was most recently requested, or do we - * need to maintain a priority ordering? */ @SysUISingleton -open class ChipbarCoordinator @Inject constructor( - context: Context, - logger: ChipbarLogger, - windowManager: WindowManager, - @Main mainExecutor: DelayableExecutor, - accessibilityManager: AccessibilityManager, - configurationController: ConfigurationController, - dumpManager: DumpManager, - powerManager: PowerManager, - private val falsingManager: FalsingManager, - private val falsingCollector: FalsingCollector, - private val viewUtil: ViewUtil, - private val vibratorHelper: VibratorHelper, - wakeLockBuilder: WakeLock.Builder, - systemClock: SystemClock, -) : TemporaryViewDisplayController<ChipbarInfo, ChipbarLogger>( +open class ChipbarCoordinator +@Inject +constructor( + context: Context, + logger: ChipbarLogger, + windowManager: WindowManager, + @Main mainExecutor: DelayableExecutor, + accessibilityManager: AccessibilityManager, + configurationController: ConfigurationController, + dumpManager: DumpManager, + powerManager: PowerManager, + private val falsingManager: FalsingManager, + private val falsingCollector: FalsingCollector, + private val viewUtil: ViewUtil, + private val vibratorHelper: VibratorHelper, + wakeLockBuilder: WakeLock.Builder, + systemClock: SystemClock, +) : + TemporaryViewDisplayController<ChipbarInfo, ChipbarLogger>( context, logger, windowManager, @@ -96,18 +97,14 @@ open class ChipbarCoordinator @Inject constructor( R.layout.chipbar, wakeLockBuilder, systemClock, -) { + ) { private lateinit var parent: ChipbarRootView - override val windowLayoutParams = commonWindowLayoutParams.apply { - gravity = Gravity.TOP.or(Gravity.CENTER_HORIZONTAL) - } + override val windowLayoutParams = + commonWindowLayoutParams.apply { gravity = Gravity.TOP.or(Gravity.CENTER_HORIZONTAL) } - override fun updateView( - newInfo: ChipbarInfo, - currentView: ViewGroup - ) { + override fun updateView(newInfo: ChipbarInfo, currentView: ViewGroup) { logger.logViewUpdate( newInfo.windowTitle, newInfo.text.loadText(context), @@ -123,12 +120,13 @@ open class ChipbarCoordinator @Inject constructor( // Detect falsing touches on the chip. parent = currentView.requireViewById(R.id.chipbar_root_view) - parent.touchHandler = object : Gefingerpoken { - override fun onTouchEvent(ev: MotionEvent?): Boolean { - falsingCollector.onTouchEvent(ev) - return false + parent.touchHandler = + object : Gefingerpoken { + override fun onTouchEvent(ev: MotionEvent?): Boolean { + falsingCollector.onTouchEvent(ev) + return false + } } - } // ---- Start icon ---- val iconView = currentView.requireViewById<CachingIconView>(R.id.start_icon) @@ -155,10 +153,12 @@ open class ChipbarCoordinator @Inject constructor( if (newInfo.endItem is ChipbarEndItem.Button) { TextViewBinder.bind(buttonView, newInfo.endItem.text) - val onClickListener = View.OnClickListener { clickedView -> - if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return@OnClickListener - newInfo.endItem.onClickListener.onClick(clickedView) - } + val onClickListener = + View.OnClickListener { clickedView -> + if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) + return@OnClickListener + newInfo.endItem.onClickListener.onClick(clickedView) + } buttonView.setOnClickListener(onClickListener) buttonView.visibility = View.VISIBLE @@ -168,21 +168,27 @@ open class ChipbarCoordinator @Inject constructor( // ---- Overall accessibility ---- val iconDesc = newInfo.startIcon.icon.contentDescription - val loadedIconDesc = if (iconDesc != null) { - "${iconDesc.loadContentDescription(context)} " - } else { - "" - } + val loadedIconDesc = + if (iconDesc != null) { + "${iconDesc.loadContentDescription(context)} " + } else { + "" + } + val endItemDesc = + if (newInfo.endItem is ChipbarEndItem.Loading) { + ". ${context.resources.getString(R.string.media_transfer_loading)}." + } else { + "" + } val chipInnerView = currentView.getInnerView() - chipInnerView.contentDescription = "$loadedIconDesc${newInfo.text.loadText(context)}" + chipInnerView.contentDescription = + "$loadedIconDesc${newInfo.text.loadText(context)}$endItemDesc" chipInnerView.accessibilityLiveRegion = ACCESSIBILITY_LIVE_REGION_ASSERTIVE maybeGetAccessibilityFocus(newInfo, currentView) // ---- Haptics ---- - newInfo.vibrationEffect?.let { - vibratorHelper.vibrate(it) - } + newInfo.vibrationEffect?.let { vibratorHelper.vibrate(it) } } private fun maybeGetAccessibilityFocus(info: ChipbarInfo?, view: ViewGroup) { diff --git a/packages/SystemUI/src/com/android/systemui/user/CreateUserActivity.java b/packages/SystemUI/src/com/android/systemui/user/CreateUserActivity.java index cd21a45be0ce..c570ec8d2cd7 100644 --- a/packages/SystemUI/src/com/android/systemui/user/CreateUserActivity.java +++ b/packages/SystemUI/src/com/android/systemui/user/CreateUserActivity.java @@ -64,7 +64,7 @@ public class CreateUserActivity extends Activity { private Dialog mGrantAdminDialog; private Dialog mSetupUserDialog; private final OnBackInvokedCallback mBackCallback = this::onBackInvoked; - private Boolean mGrantAdminRights; + private boolean mGrantAdminRights; @Inject public CreateUserActivity(UserCreator userCreator, EditUserInfoController editUserInfoController, IActivityManager activityManager, @@ -83,8 +83,7 @@ public class CreateUserActivity extends Activity { if (savedInstanceState != null) { mEditUserInfoController.onRestoreInstanceState(savedInstanceState); } - - if (mUserCreator.isHeadlessSystemUserMode()) { + if (mUserCreator.isMultipleAdminEnabled()) { mGrantAdminDialog = buildGrantAdminDialog(); mGrantAdminDialog.show(); } else { diff --git a/packages/SystemUI/src/com/android/systemui/user/UserCreator.kt b/packages/SystemUI/src/com/android/systemui/user/UserCreator.kt index 277f670597d3..1811c4d9f930 100644 --- a/packages/SystemUI/src/com/android/systemui/user/UserCreator.kt +++ b/packages/SystemUI/src/com/android/systemui/user/UserCreator.kt @@ -87,7 +87,7 @@ constructor( userManager.setUserAdmin(userId) } - fun isHeadlessSystemUserMode(): Boolean { - return UserManager.isHeadlessSystemUserMode() + fun isMultipleAdminEnabled(): Boolean { + return UserManager.isMultipleAdminEnabled() } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/mediator/ScreenOnCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/mediator/ScreenOnCoordinatorTest.kt index 34e78eb8c2eb..e9a2789bb5c8 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/mediator/ScreenOnCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/mediator/ScreenOnCoordinatorTest.kt @@ -16,29 +16,25 @@ package com.android.keyguard.mediator +import android.os.Handler +import android.os.Looper import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest - import com.android.systemui.SysuiTestCase -import com.android.systemui.keyguard.ScreenLifecycle import com.android.systemui.unfold.FoldAodAnimationController import com.android.systemui.unfold.SysUIUnfoldComponent import com.android.systemui.unfold.UnfoldLightRevealOverlayAnimation -import com.android.systemui.util.concurrency.FakeExecution import com.android.systemui.util.mockito.capture - -import java.util.Optional - import org.junit.Before import org.junit.Test import org.junit.runner.RunWith - import org.mockito.ArgumentCaptor import org.mockito.Captor import org.mockito.Mock import org.mockito.Mockito.`when` import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations +import java.util.Optional @SmallTest @RunWith(AndroidTestingRunner::class) @@ -55,6 +51,8 @@ class ScreenOnCoordinatorTest : SysuiTestCase() { @Captor private lateinit var readyCaptor: ArgumentCaptor<Runnable> + private val testHandler = Handler(Looper.getMainLooper()) + private lateinit var screenOnCoordinator: ScreenOnCoordinator @Before @@ -68,6 +66,7 @@ class ScreenOnCoordinatorTest : SysuiTestCase() { screenOnCoordinator = ScreenOnCoordinator( Optional.of(unfoldComponent), + testHandler ) } @@ -77,6 +76,7 @@ class ScreenOnCoordinatorTest : SysuiTestCase() { onUnfoldOverlayReady() onFoldAodReady() + waitHandlerIdle(testHandler) // Should be called when both unfold overlay and keyguard drawn ready verify(runnable).run() @@ -87,8 +87,10 @@ class ScreenOnCoordinatorTest : SysuiTestCase() { // Recreate with empty unfoldComponent screenOnCoordinator = ScreenOnCoordinator( Optional.empty(), + testHandler ) screenOnCoordinator.onScreenTurningOn(runnable) + waitHandlerIdle(testHandler) // Should be called when only keyguard drawn verify(runnable).run() @@ -103,4 +105,8 @@ class ScreenOnCoordinatorTest : SysuiTestCase() { verify(foldAodAnimationController).onScreenTurningOn(capture(readyCaptor)) readyCaptor.value.run() } + + private fun waitHandlerIdle(handler: Handler) { + handler.runWithScissors({}, /* timeout= */ 0) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationControllerTest.java index a36105e11514..a4b9b0849457 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DismissAnimationControllerTest.java @@ -22,6 +22,7 @@ import static org.mockito.Mockito.verify; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.WindowManager; +import android.view.accessibility.AccessibilityManager; import androidx.test.filters.SmallTest; @@ -29,8 +30,12 @@ import com.android.systemui.SysuiTestCase; import com.android.wm.shell.bubbles.DismissView; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; /** Tests for {@link DismissAnimationController}. */ @SmallTest @@ -40,10 +45,16 @@ public class DismissAnimationControllerTest extends SysuiTestCase { private DismissAnimationController mDismissAnimationController; private DismissView mDismissView; + @Rule + public MockitoRule mockito = MockitoJUnit.rule(); + + @Mock + private AccessibilityManager mAccessibilityManager; + @Before public void setUp() throws Exception { final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class); - final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext); + final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext, mAccessibilityManager); final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext, stubWindowManager); final MenuView stubMenuView = new MenuView(mContext, stubMenuViewModel, diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java index 0cdd6e2ce85e..7356184d4879 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java @@ -31,6 +31,7 @@ import android.testing.TestableLooper; import android.view.View; import android.view.ViewPropertyAnimator; import android.view.WindowManager; +import android.view.accessibility.AccessibilityManager; import androidx.dynamicanimation.animation.DynamicAnimation; import androidx.dynamicanimation.animation.FlingAnimation; @@ -43,9 +44,13 @@ import com.android.systemui.SysuiTestCase; import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; import java.util.Optional; @@ -61,12 +66,18 @@ public class MenuAnimationControllerTest extends SysuiTestCase { private MenuView mMenuView; private TestMenuAnimationController mMenuAnimationController; + @Rule + public MockitoRule mockito = MockitoJUnit.rule(); + + @Mock + private AccessibilityManager mAccessibilityManager; + @Before public void setUp() throws Exception { final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class); final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext, stubWindowManager); - final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext); + final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext, mAccessibilityManager); mMenuView = spy(new MenuView(mContext, stubMenuViewModel, stubMenuViewAppearance)); mViewPropertyAnimator = spy(mMenuView.animate()); diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepositoryTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepositoryTest.java index e62a3295a7e2..06340afb4892 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepositoryTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepositoryTest.java @@ -16,16 +16,23 @@ package com.android.systemui.accessibility.floatingmenu; +import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME; + import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.verify; +import android.content.Context; +import android.content.res.Configuration; import android.testing.AndroidTestingRunner; +import android.view.accessibility.AccessibilityManager; import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; +import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -34,6 +41,10 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + /** Tests for {@link MenuInfoRepository}. */ @RunWith(AndroidTestingRunner.class) @SmallTest @@ -42,13 +53,28 @@ public class MenuInfoRepositoryTest extends SysuiTestCase { public MockitoRule mockito = MockitoJUnit.rule(); @Mock + private AccessibilityManager mAccessibilityManager; + + @Mock private MenuInfoRepository.OnSettingsContentsChanged mMockSettingsContentsChanged; private MenuInfoRepository mMenuInfoRepository; + private final List<String> mShortcutTargets = new ArrayList<>(); @Before public void setUp() { - mMenuInfoRepository = new MenuInfoRepository(mContext, mMockSettingsContentsChanged); + mContext.addMockSystemService(Context.ACCESSIBILITY_SERVICE, mAccessibilityManager); + mShortcutTargets.add(MAGNIFICATION_CONTROLLER_NAME); + doReturn(mShortcutTargets).when(mAccessibilityManager).getAccessibilityShortcutTargets( + anyInt()); + + mMenuInfoRepository = new MenuInfoRepository(mContext, mAccessibilityManager, + mMockSettingsContentsChanged); + } + + @After + public void tearDown() { + mShortcutTargets.clear(); } @Test @@ -64,4 +90,14 @@ public class MenuInfoRepositoryTest extends SysuiTestCase { verify(mMockSettingsContentsChanged).onFadeEffectInfoChanged(any(MenuFadeEffectInfo.class)); } + + @Test + public void localeChange_verifyTargetFeaturesChanged() { + final Configuration configuration = new Configuration(); + configuration.setLocale(Locale.TAIWAN); + + mMenuInfoRepository.mComponentCallbacks.onConfigurationChanged(configuration); + + verify(mMockSettingsContentsChanged).onTargetFeaturesChanged(any()); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java index 78ee627a9a2f..f17b1cfe3c88 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java @@ -29,6 +29,7 @@ import android.graphics.Rect; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.WindowManager; +import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityNodeInfo; import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; @@ -56,6 +57,9 @@ public class MenuItemAccessibilityDelegateTest extends SysuiTestCase { public MockitoRule mockito = MockitoJUnit.rule(); @Mock + private AccessibilityManager mAccessibilityManager; + + @Mock private DismissAnimationController.DismissCallback mStubDismissCallback; private RecyclerView mStubListView; @@ -69,7 +73,7 @@ public class MenuItemAccessibilityDelegateTest extends SysuiTestCase { final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class); final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext, stubWindowManager); - final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext); + final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext, mAccessibilityManager); final int halfScreenHeight = stubWindowManager.getCurrentWindowMetrics().getBounds().height() / 2; diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java index d29ebb86686f..ed9562d83872 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java @@ -31,6 +31,7 @@ import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.MotionEvent; import android.view.WindowManager; +import android.view.accessibility.AccessibilityManager; import androidx.recyclerview.widget.RecyclerView; import androidx.test.filters.SmallTest; @@ -42,8 +43,12 @@ import com.android.wm.shell.bubbles.DismissView; import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; import java.util.ArrayList; import java.util.Collections; @@ -64,10 +69,16 @@ public class MenuListViewTouchHandlerTest extends SysuiTestCase { private RecyclerView mStubListView; private DismissView mDismissView; + @Rule + public MockitoRule mockito = MockitoJUnit.rule(); + + @Mock + private AccessibilityManager mAccessibilityManager; + @Before public void setUp() throws Exception { final WindowManager windowManager = mContext.getSystemService(WindowManager.class); - final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext); + final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext, mAccessibilityManager); final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext, windowManager); mStubMenuView = new MenuView(mContext, stubMenuViewModel, stubMenuViewAppearance); diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java index 742ee53e99b6..5a1a6db92742 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java @@ -29,6 +29,7 @@ import android.graphics.drawable.GradientDrawable; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.WindowManager; +import android.view.accessibility.AccessibilityManager; import androidx.test.filters.SmallTest; @@ -37,8 +38,12 @@ import com.android.systemui.SysuiTestCase; import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; /** Tests for {@link MenuView}. */ @RunWith(AndroidTestingRunner.class) @@ -52,12 +57,18 @@ public class MenuViewTest extends SysuiTestCase { private String mLastPosition; private MenuViewAppearance mStubMenuViewAppearance; + @Rule + public MockitoRule mockito = MockitoJUnit.rule(); + + @Mock + private AccessibilityManager mAccessibilityManager; + @Before public void setUp() throws Exception { mUiModeManager = mContext.getSystemService(UiModeManager.class); mNightMode = mUiModeManager.getNightMode(); mUiModeManager.setNightMode(MODE_NIGHT_YES); - final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext); + final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext, mAccessibilityManager); final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class); mStubMenuViewAppearance = new MenuViewAppearance(mContext, stubWindowManager); mMenuView = spy(new MenuView(mContext, stubMenuViewModel, mStubMenuViewAppearance)); diff --git a/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java b/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java index a56990f40b90..3528e14dbd80 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java @@ -41,6 +41,7 @@ import android.os.BatteryManager; import android.os.Bundle; import android.os.Handler; import android.os.UserHandle; +import android.provider.Settings; import android.test.suitebuilder.annotation.SmallTest; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; @@ -55,6 +56,8 @@ import com.android.systemui.broadcast.BroadcastSender; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.util.NotificationChannels; +import com.android.systemui.util.settings.FakeSettings; +import com.android.systemui.util.settings.GlobalSettings; import org.junit.Before; import org.junit.Test; @@ -73,6 +76,7 @@ public class PowerNotificationWarningsTest extends SysuiTestCase { public static final String FORMATTED_45M = "0h 45m"; public static final String FORMATTED_HOUR = "1h 0m"; private final NotificationManager mMockNotificationManager = mock(NotificationManager.class); + private final GlobalSettings mGlobalSettings = new FakeSettings(); private PowerNotificationWarnings mPowerNotificationWarnings; @Mock @@ -104,7 +108,8 @@ public class PowerNotificationWarningsTest extends SysuiTestCase { ActivityStarter starter = mDependency.injectMockDependency(ActivityStarter.class); BroadcastSender broadcastSender = mDependency.injectMockDependency(BroadcastSender.class); mPowerNotificationWarnings = new PowerNotificationWarnings(wrapper, starter, - broadcastSender, () -> mBatteryController, mDialogLaunchAnimator, mUiEventLogger); + broadcastSender, () -> mBatteryController, mDialogLaunchAnimator, mUiEventLogger, + mGlobalSettings); BatteryStateSnapshot snapshot = new BatteryStateSnapshot(100, false, false, 1, BatteryManager.BATTERY_HEALTH_GOOD, 5, 15); mPowerNotificationWarnings.updateSnapshot(snapshot); @@ -146,6 +151,16 @@ public class PowerNotificationWarningsTest extends SysuiTestCase { } @Test + public void testDisableLowBatteryReminder_noNotification() { + mGlobalSettings.putInt(Settings.Global.LOW_POWER_MODE_REMINDER_ENABLED, 0); + + mPowerNotificationWarnings.showLowBatteryWarning(false); + + verify(mMockNotificationManager, times(0)) + .notifyAsUser(anyString(), eq(SystemMessage.NOTE_POWER_LOW), any(), any()); + } + + @Test public void testShowLowBatteryNotification_NotifyAsUser() { mPowerNotificationWarnings.showLowBatteryWarning(false); verify(mMockNotificationManager, times(1)) diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java index ffe918d36d6f..42ef9c2914ce 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java @@ -151,7 +151,7 @@ public class QSFragmentTest extends SysuiBaseFragmentTest { @Test public void transitionToFullShade_setsAlphaUsingShadeInterpolator() { QSFragment fragment = resumeAndGetFragment(); - setStatusBarState(StatusBarState.SHADE); + setStatusBarCurrentAndUpcomingState(StatusBarState.SHADE); boolean isTransitioningToFullShade = true; float transitionProgress = 0.5f; float squishinessFraction = 0.5f; @@ -167,7 +167,7 @@ public class QSFragmentTest extends SysuiBaseFragmentTest { public void transitionToFullShade_onKeyguard_noBouncer_setsAlphaUsingLinearInterpolator() { QSFragment fragment = resumeAndGetFragment(); - setStatusBarState(KEYGUARD); + setStatusBarCurrentAndUpcomingState(KEYGUARD); when(mQSPanelController.isBouncerInTransit()).thenReturn(false); boolean isTransitioningToFullShade = true; float transitionProgress = 0.5f; @@ -183,7 +183,7 @@ public class QSFragmentTest extends SysuiBaseFragmentTest { public void transitionToFullShade_onKeyguard_bouncerActive_setsAlphaUsingBouncerInterpolator() { QSFragment fragment = resumeAndGetFragment(); - setStatusBarState(KEYGUARD); + setStatusBarCurrentAndUpcomingState(KEYGUARD); when(mQSPanelController.isBouncerInTransit()).thenReturn(true); boolean isTransitioningToFullShade = true; float transitionProgress = 0.5f; @@ -482,6 +482,15 @@ public class QSFragmentTest extends SysuiBaseFragmentTest { assertEquals(175, mediaHostClip.bottom); } + @Test + public void testQsUpdatesQsAnimatorWithUpcomingState() { + QSFragment fragment = resumeAndGetFragment(); + setStatusBarCurrentAndUpcomingState(SHADE); + fragment.onUpcomingStateChanged(KEYGUARD); + + verify(mQSAnimator).setOnKeyguard(true); + } + @Override protected Fragment instantiate(Context context, String className, Bundle arguments) { MockitoAnnotations.initMocks(this); @@ -591,8 +600,9 @@ public class QSFragmentTest extends SysuiBaseFragmentTest { return getFragment(); } - private void setStatusBarState(int statusBarState) { + private void setStatusBarCurrentAndUpcomingState(int statusBarState) { when(mStatusBarStateController.getState()).thenReturn(statusBarState); + when(mStatusBarStateController.getCurrentOrUpcomingState()).thenReturn(statusBarState); getFragment().onStateChanged(statusBarState); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt index d6a9ee325b2e..53cd71f1bdf9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS import kotlinx.coroutines.flow.MutableStateFlow // TODO(b/261632894): remove this in favor of the real impl or DemoMobileConnectionRepository @@ -29,12 +30,11 @@ class FakeMobileConnectionRepository( private val _connectionInfo = MutableStateFlow(MobileConnectionModel()) override val connectionInfo = _connectionInfo + override val numberOfLevels = MutableStateFlow(DEFAULT_NUM_LEVELS) + private val _dataEnabled = MutableStateFlow(true) override val dataEnabled = _dataEnabled - private val _isDefaultDataSubscription = MutableStateFlow(true) - override val isDefaultDataSubscription = _isDefaultDataSubscription - override val cdmaRoaming = MutableStateFlow(false) override val networkName = @@ -47,8 +47,4 @@ class FakeMobileConnectionRepository( fun setDataEnabled(enabled: Boolean) { _dataEnabled.value = enabled } - - fun setIsDefaultDataSubscription(isDefault: Boolean) { - _isDefaultDataSubscription.value = isDefault - } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt index 7f93328ee95e..49d4bdc88c82 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt @@ -57,9 +57,6 @@ class FakeMobileConnectionsRepository( private val _activeMobileDataSubscriptionId = MutableStateFlow(INVALID_SUBSCRIPTION_ID) override val activeMobileDataSubscriptionId = _activeMobileDataSubscriptionId - private val _defaultDataSubId = MutableStateFlow(INVALID_SUBSCRIPTION_ID) - override val defaultDataSubId = _defaultDataSubId - private val _mobileConnectivity = MutableStateFlow(MobileConnectivityModel()) override val defaultMobileNetworkConnectivity = _mobileConnectivity @@ -84,10 +81,6 @@ class FakeMobileConnectionsRepository( _subscriptions.value = subs } - fun setDefaultDataSubId(id: Int) { - _defaultDataSubId.value = id - } - fun setMobileConnectivity(model: MobileConnectivityModel) { _mobileConnectivity.value = model } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt index c63dd2a2318c..d6b8c0dbc59d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt @@ -61,6 +61,7 @@ import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetwork import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.UnknownNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.model.toNetworkNameModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel @@ -117,7 +118,6 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { telephonyManager, globalSettings, fakeBroadcastDispatcher, - connectionsRepo.defaultDataSubId, connectionsRepo.globalMobileDataSettingChangedEvent, mobileMappings, IMMEDIATE, @@ -319,7 +319,7 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { val callback = getTelephonyCallbackForType<TelephonyCallback.DisplayInfoListener>() val type = NETWORK_TYPE_LTE - val expected = DefaultNetworkType(type, mobileMappings.toIconKey(type)) + val expected = DefaultNetworkType(mobileMappings.toIconKey(type)) val ti = mock<TelephonyDisplayInfo>().also { whenever(it.networkType).thenReturn(type) } callback.onDisplayInfoChanged(ti) @@ -336,7 +336,7 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { val callback = getTelephonyCallbackForType<TelephonyCallback.DisplayInfoListener>() val type = OVERRIDE_NETWORK_TYPE_LTE_CA - val expected = OverrideNetworkType(type, mobileMappings.toIconKeyOverride(type)) + val expected = OverrideNetworkType(mobileMappings.toIconKeyOverride(type)) val ti = mock<TelephonyDisplayInfo>().also { whenever(it.networkType).thenReturn(type) @@ -380,33 +380,6 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { } @Test - fun isDefaultDataSubscription_isDefault() = - runBlocking(IMMEDIATE) { - connectionsRepo.setDefaultDataSubId(SUB_1_ID) - - var latest: Boolean? = null - val job = underTest.isDefaultDataSubscription.onEach { latest = it }.launchIn(this) - - assertThat(latest).isTrue() - - job.cancel() - } - - @Test - fun isDefaultDataSubscription_isNotDefault() = - runBlocking(IMMEDIATE) { - // Our subId is SUB_1_ID - connectionsRepo.setDefaultDataSubId(123) - - var latest: Boolean? = null - val job = underTest.isDefaultDataSubscription.onEach { latest = it }.launchIn(this) - - assertThat(latest).isFalse() - - job.cancel() - } - - @Test fun isDataConnectionAllowed_subIdSettingUpdate_valueUpdated() = runBlocking(IMMEDIATE) { val subIdSettingName = "${Settings.Global.MOBILE_DATA}$SUB_1_ID" @@ -431,6 +404,17 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { } @Test + fun numberOfLevels_isDefault() = + runBlocking(IMMEDIATE) { + var latest: Int? = null + val job = underTest.numberOfLevels.onEach { latest = it }.launchIn(this) + + assertThat(latest).isEqualTo(DEFAULT_NUM_LEVELS) + + job.cancel() + } + + @Test fun `roaming - cdma - queries telephony manager`() = runBlocking(IMMEDIATE) { var latest: Boolean? = null 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 b8cd7a4f6e0a..0da15e239932 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 @@ -307,35 +307,6 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { } @Test - fun testDefaultDataSubId_updatesOnBroadcast() = - runBlocking(IMMEDIATE) { - var latest: Int? = null - val job = underTest.defaultDataSubId.onEach { latest = it }.launchIn(this) - - fakeBroadcastDispatcher.registeredReceivers.forEach { receiver -> - receiver.onReceive( - context, - Intent(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED) - .putExtra(PhoneConstants.SUBSCRIPTION_KEY, SUB_2_ID) - ) - } - - assertThat(latest).isEqualTo(SUB_2_ID) - - fakeBroadcastDispatcher.registeredReceivers.forEach { receiver -> - receiver.onReceive( - context, - Intent(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED) - .putExtra(PhoneConstants.SUBSCRIPTION_KEY, SUB_1_ID) - ) - } - - assertThat(latest).isEqualTo(SUB_1_ID) - - job.cancel() - } - - @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/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt index ff72715b281f..a29146b01668 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt @@ -21,6 +21,7 @@ import com.android.settingslib.SignalIcon import com.android.settingslib.mobile.TelephonyIcons import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel import kotlinx.coroutines.flow.MutableStateFlow @@ -65,7 +66,7 @@ class FakeMobileIconInteractor( private val _level = MutableStateFlow(CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN) override val level = _level - private val _numberOfLevels = MutableStateFlow(4) + private val _numberOfLevels = MutableStateFlow(DEFAULT_NUM_LEVELS) override val numberOfLevels = _numberOfLevels fun setIconGroup(group: SignalIcon.MobileIconGroup) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt index 5abe33523cc6..61e13b85db6c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt @@ -17,7 +17,6 @@ package com.android.systemui.statusbar.pipeline.mobile.domain.interactor import android.telephony.CellSignalStrength -import android.telephony.SubscriptionInfo import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN import androidx.test.filters.SmallTest import com.android.settingslib.SignalIcon.MobileIconGroup @@ -34,7 +33,6 @@ import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobi import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor.Companion.THREE_G import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy import com.android.systemui.util.mockito.mock -import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -178,12 +176,26 @@ class MobileIconInteractorTest : SysuiTestCase() { } @Test + fun numberOfLevels_comesFromRepo() = + runBlocking(IMMEDIATE) { + var latest: Int? = null + val job = underTest.numberOfLevels.onEach { latest = it }.launchIn(this) + + connectionRepository.numberOfLevels.value = 5 + assertThat(latest).isEqualTo(5) + + connectionRepository.numberOfLevels.value = 4 + assertThat(latest).isEqualTo(4) + + job.cancel() + } + + @Test fun iconGroup_three_g() = runBlocking(IMMEDIATE) { connectionRepository.setConnectionInfo( MobileConnectionModel( - resolvedNetworkType = - DefaultNetworkType(THREE_G, mobileMappingsProxy.toIconKey(THREE_G)) + resolvedNetworkType = DefaultNetworkType(mobileMappingsProxy.toIconKey(THREE_G)) ), ) @@ -200,8 +212,7 @@ class MobileIconInteractorTest : SysuiTestCase() { runBlocking(IMMEDIATE) { connectionRepository.setConnectionInfo( MobileConnectionModel( - resolvedNetworkType = - DefaultNetworkType(THREE_G, mobileMappingsProxy.toIconKey(THREE_G)) + resolvedNetworkType = DefaultNetworkType(mobileMappingsProxy.toIconKey(THREE_G)) ), ) @@ -212,7 +223,6 @@ class MobileIconInteractorTest : SysuiTestCase() { MobileConnectionModel( resolvedNetworkType = DefaultNetworkType( - FOUR_G, mobileMappingsProxy.toIconKey(FOUR_G), ), ), @@ -230,10 +240,7 @@ class MobileIconInteractorTest : SysuiTestCase() { connectionRepository.setConnectionInfo( MobileConnectionModel( resolvedNetworkType = - OverrideNetworkType( - FIVE_G_OVERRIDE, - mobileMappingsProxy.toIconKeyOverride(FIVE_G_OVERRIDE) - ) + OverrideNetworkType(mobileMappingsProxy.toIconKeyOverride(FIVE_G_OVERRIDE)) ), ) @@ -251,10 +258,7 @@ class MobileIconInteractorTest : SysuiTestCase() { connectionRepository.setConnectionInfo( MobileConnectionModel( resolvedNetworkType = - DefaultNetworkType( - NETWORK_TYPE_UNKNOWN, - mobileMappingsProxy.toIconKey(NETWORK_TYPE_UNKNOWN) - ), + DefaultNetworkType(mobileMappingsProxy.toIconKey(NETWORK_TYPE_UNKNOWN)), ), ) @@ -509,8 +513,6 @@ class MobileIconInteractorTest : SysuiTestCase() { private const val CDMA_LEVEL = 2 private const val SUB_1_ID = 1 - private val SUB_1 = - mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) } private val DEFAULT_NAME = NetworkNameModel.Default("test default name") private val DERIVED_NAME = NetworkNameModel.Derived("test derived name") diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileViewTest.kt new file mode 100644 index 000000000000..a2c1209f5a40 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileViewTest.kt @@ -0,0 +1,189 @@ +/* + * 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.statusbar.pipeline.mobile.ui.view + +import android.content.res.ColorStateList +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.testing.TestableLooper.RunWithLooper +import android.testing.ViewUtils +import android.view.View +import android.widget.ImageView +import androidx.test.filters.SmallTest +import com.android.systemui.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.statusbar.StatusBarIconView +import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconInteractor +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModel +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconViewModel +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.QsMobileIconViewModel +import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@RunWithLooper(setAsMainLooper = true) +@OptIn(ExperimentalCoroutinesApi::class) +class ModernStatusBarMobileViewTest : SysuiTestCase() { + + private lateinit var testableLooper: TestableLooper + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) + + @Mock private lateinit var statusBarPipelineFlags: StatusBarPipelineFlags + @Mock private lateinit var tableLogBuffer: TableLogBuffer + @Mock private lateinit var logger: ConnectivityPipelineLogger + @Mock private lateinit var constants: ConnectivityConstants + + private lateinit var viewModel: LocationBasedMobileViewModel + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + testableLooper = TestableLooper.get(this) + + val interactor = FakeMobileIconInteractor(tableLogBuffer) + + val viewModelCommon = + MobileIconViewModel( + subscriptionId = 1, + interactor, + logger, + constants, + testScope.backgroundScope, + ) + viewModel = QsMobileIconViewModel(viewModelCommon, statusBarPipelineFlags) + } + + // Note: The following tests are more like integration tests, since they stand up a full + // [WifiViewModel] and test the interactions between the view, view-binder, and view-model. + + @Test + fun setVisibleState_icon_iconShownDotHidden() { + val view = ModernStatusBarMobileView.constructAndBind(context, SLOT_NAME, viewModel) + + view.setVisibleState(StatusBarIconView.STATE_ICON, /* animate= */ false) + + ViewUtils.attachView(view) + testableLooper.processAllMessages() + + assertThat(view.getGroupView().visibility).isEqualTo(View.VISIBLE) + assertThat(view.getDotView().visibility).isEqualTo(View.GONE) + + ViewUtils.detachView(view) + } + + @Test + fun setVisibleState_dot_iconHiddenDotShown() { + val view = ModernStatusBarMobileView.constructAndBind(context, SLOT_NAME, viewModel) + + view.setVisibleState(StatusBarIconView.STATE_DOT, /* animate= */ false) + + ViewUtils.attachView(view) + testableLooper.processAllMessages() + + assertThat(view.getGroupView().visibility).isEqualTo(View.INVISIBLE) + assertThat(view.getDotView().visibility).isEqualTo(View.VISIBLE) + + ViewUtils.detachView(view) + } + + @Test + fun setVisibleState_hidden_iconAndDotHidden() { + val view = ModernStatusBarMobileView.constructAndBind(context, SLOT_NAME, viewModel) + + view.setVisibleState(StatusBarIconView.STATE_HIDDEN, /* animate= */ false) + + ViewUtils.attachView(view) + testableLooper.processAllMessages() + + assertThat(view.getGroupView().visibility).isEqualTo(View.INVISIBLE) + assertThat(view.getDotView().visibility).isEqualTo(View.INVISIBLE) + + ViewUtils.detachView(view) + } + + @Test + fun isIconVisible_alwaysTrue() { + val view = ModernStatusBarMobileView.constructAndBind(context, SLOT_NAME, viewModel) + + ViewUtils.attachView(view) + testableLooper.processAllMessages() + + assertThat(view.isIconVisible).isTrue() + + ViewUtils.detachView(view) + } + + @Test + fun onDarkChanged_iconHasNewColor() { + whenever(statusBarPipelineFlags.useDebugColoring()).thenReturn(false) + val view = ModernStatusBarMobileView.constructAndBind(context, SLOT_NAME, viewModel) + ViewUtils.attachView(view) + testableLooper.processAllMessages() + + val color = 0x12345678 + view.onDarkChanged(arrayListOf(), 1.0f, color) + testableLooper.processAllMessages() + + assertThat(view.getIconView().imageTintList).isEqualTo(ColorStateList.valueOf(color)) + + ViewUtils.detachView(view) + } + + @Test + fun setStaticDrawableColor_iconHasNewColor() { + whenever(statusBarPipelineFlags.useDebugColoring()).thenReturn(false) + val view = ModernStatusBarMobileView.constructAndBind(context, SLOT_NAME, viewModel) + ViewUtils.attachView(view) + testableLooper.processAllMessages() + + val color = 0x23456789 + view.setStaticDrawableColor(color) + testableLooper.processAllMessages() + + assertThat(view.getIconView().imageTintList).isEqualTo(ColorStateList.valueOf(color)) + + ViewUtils.detachView(view) + } + + private fun View.getGroupView(): View { + return this.requireViewById(R.id.mobile_group) + } + + private fun View.getIconView(): ImageView { + return this.requireViewById(R.id.mobile_signal) + } + + private fun View.getDotView(): View { + return this.requireViewById(R.id.status_bar_dot) + } +} + +private const val SLOT_NAME = "TestSlotName" diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelTest.kt index 043d55a73076..c960a06e6bb2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelTest.kt @@ -20,6 +20,7 @@ import androidx.test.filters.SmallTest import com.android.settingslib.mobile.TelephonyIcons import com.android.systemui.SysuiTestCase import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconInteractor import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconViewModelTest.Companion.defaultSignal import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants @@ -45,6 +46,7 @@ class LocationBasedMobileIconViewModelTest : SysuiTestCase() { private lateinit var qsIcon: QsMobileIconViewModel private lateinit var keyguardIcon: KeyguardMobileIconViewModel private lateinit var interactor: FakeMobileIconInteractor + @Mock private lateinit var statusBarPipelineFlags: StatusBarPipelineFlags @Mock private lateinit var logger: ConnectivityPipelineLogger @Mock private lateinit var constants: ConnectivityConstants @Mock private lateinit var tableLogBuffer: TableLogBuffer @@ -68,9 +70,9 @@ class LocationBasedMobileIconViewModelTest : SysuiTestCase() { commonImpl = MobileIconViewModel(SUB_1_ID, interactor, logger, constants, testScope.backgroundScope) - homeIcon = HomeMobileIconViewModel(commonImpl, logger) - qsIcon = QsMobileIconViewModel(commonImpl, logger) - keyguardIcon = KeyguardMobileIconViewModel(commonImpl, logger) + homeIcon = HomeMobileIconViewModel(commonImpl, statusBarPipelineFlags) + qsIcon = QsMobileIconViewModel(commonImpl, statusBarPipelineFlags) + keyguardIcon = KeyguardMobileIconViewModel(commonImpl, statusBarPipelineFlags) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt index d6cb76260f0b..58b50c7e7e6d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.statusbar.phone.StatusBarLocation +import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy @@ -45,6 +46,7 @@ class MobileIconsViewModelTest : SysuiTestCase() { private lateinit var underTest: MobileIconsViewModel private val interactor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock()) + @Mock private lateinit var statusBarPipelineFlags: StatusBarPipelineFlags @Mock private lateinit var logger: ConnectivityPipelineLogger @Mock private lateinit var constants: ConnectivityConstants @@ -67,6 +69,7 @@ class MobileIconsViewModelTest : SysuiTestCase() { logger, constants, testScope.backgroundScope, + statusBarPipelineFlags, ) interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarViewTest.kt new file mode 100644 index 000000000000..3fe69837a761 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/view/ModernStatusBarViewTest.kt @@ -0,0 +1,153 @@ +/* + * 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.statusbar.pipeline.shared.ui.view + +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper.RunWithLooper +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.StatusBarIconView.STATE_DOT +import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN +import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON +import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewBinding +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@RunWithLooper(setAsMainLooper = true) +class ModernStatusBarViewTest : SysuiTestCase() { + + private lateinit var binding: TestBinding + + @Test + fun initView_hasCorrectSlot() { + val view = ModernStatusBarView(context, null) + val binding = TestBinding() + + view.initView("slotName") { binding } + + assertThat(view.slot).isEqualTo("slotName") + } + + @Test + fun getVisibleState_icon_returnsIcon() { + val view = createAndInitView() + + view.setVisibleState(STATE_ICON, /* animate= */ false) + + assertThat(view.visibleState).isEqualTo(STATE_ICON) + } + + @Test + fun getVisibleState_dot_returnsDot() { + val view = createAndInitView() + + view.setVisibleState(STATE_DOT, /* animate= */ false) + + assertThat(view.visibleState).isEqualTo(STATE_DOT) + } + + @Test + fun getVisibleState_hidden_returnsHidden() { + val view = createAndInitView() + + view.setVisibleState(STATE_HIDDEN, /* animate= */ false) + + assertThat(view.visibleState).isEqualTo(STATE_HIDDEN) + } + + @Test + fun onDarkChanged_bindingReceivesIconAndDecorTint() { + val view = createAndInitView() + + view.onDarkChanged(arrayListOf(), 1.0f, 0x12345678) + + assertThat(binding.iconTint).isEqualTo(0x12345678) + assertThat(binding.decorTint).isEqualTo(0x12345678) + } + + @Test + fun setStaticDrawableColor_bindingReceivesIconTint() { + val view = createAndInitView() + + view.setStaticDrawableColor(0x12345678) + + assertThat(binding.iconTint).isEqualTo(0x12345678) + } + + @Test + fun setDecorColor_bindingReceivesDecorColor() { + val view = createAndInitView() + + view.setDecorColor(0x23456789) + + assertThat(binding.decorTint).isEqualTo(0x23456789) + } + + @Test + fun isIconVisible_usesBinding_true() { + val view = createAndInitView() + + binding.shouldIconBeVisibleInternal = true + + assertThat(view.isIconVisible).isEqualTo(true) + } + + @Test + fun isIconVisible_usesBinding_false() { + val view = createAndInitView() + + binding.shouldIconBeVisibleInternal = false + + assertThat(view.isIconVisible).isEqualTo(false) + } + + private fun createAndInitView(): ModernStatusBarView { + val view = ModernStatusBarView(context, null) + binding = TestBinding() + view.initView(SLOT_NAME) { binding } + return view + } + + inner class TestBinding : ModernStatusBarViewBinding { + var iconTint: Int? = null + var decorTint: Int? = null + var onVisibilityStateChangedCalled: Boolean = false + + var shouldIconBeVisibleInternal: Boolean = true + + override fun onIconTintChanged(newTint: Int) { + iconTint = newTint + } + + override fun onDecorTintChanged(newTint: Int) { + decorTint = newTint + } + + override fun onVisibilityStateChanged(state: Int) { + onVisibilityStateChangedCalled = true + } + + override fun getShouldIconBeVisible(): Boolean { + return shouldIconBeVisibleInternal + } + } +} + +private const val SLOT_NAME = "TestSlotName" diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModelTest.kt index 30fd308433e4..30ac8d432e8a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModelTest.kt @@ -34,12 +34,6 @@ class WifiNetworkModelTest : SysuiTestCase() { } } - @Test - fun active_levelNull_noException() { - WifiNetworkModel.Active(NETWORK_ID, level = null) - // No assert, just need no crash - } - @Test(expected = IllegalArgumentException::class) fun active_levelNegative_exceptionThrown() { WifiNetworkModel.Active(NETWORK_ID, level = MIN_VALID_LEVEL - 1) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepositoryTest.kt new file mode 100644 index 000000000000..3c4e85bd231e --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepositoryTest.kt @@ -0,0 +1,57 @@ +/* + * 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.statusbar.pipeline.wifi.data.repository.prod + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel +import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test + +@SmallTest +class DisabledWifiRepositoryTest : SysuiTestCase() { + + private lateinit var underTest: DisabledWifiRepository + + @Before + fun setUp() { + underTest = DisabledWifiRepository() + } + + @Test + fun enabled_alwaysFalse() { + assertThat(underTest.isWifiEnabled.value).isEqualTo(false) + } + + @Test + fun default_alwaysFalse() { + assertThat(underTest.isWifiDefault.value).isEqualTo(false) + } + + @Test + fun network_alwaysUnavailable() { + assertThat(underTest.wifiNetwork.value).isEqualTo(WifiNetworkModel.Unavailable) + } + + @Test + fun activity_alwaysFalse() { + assertThat(underTest.wifiActivity.value) + .isEqualTo(DataActivityModel(hasActivityIn = false, hasActivityOut = false)) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt index befb2901d4d5..8f07615b19b2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt @@ -33,7 +33,6 @@ import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel -import com.android.systemui.statusbar.pipeline.wifi.data.repository.prod.WifiRepositoryImpl.Companion.ACTIVITY_DEFAULT import com.android.systemui.statusbar.pipeline.wifi.data.repository.prod.WifiRepositoryImpl.Companion.WIFI_NETWORK_DEFAULT import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any @@ -98,13 +97,6 @@ class WifiRepositoryImplTest : SysuiTestCase() { } @Test - fun isWifiEnabled_nullWifiManager_getsFalse() = runBlocking(IMMEDIATE) { - underTest = createRepo(wifiManagerToUse = null) - - assertThat(underTest.isWifiEnabled.value).isFalse() - } - - @Test fun isWifiEnabled_initiallyGetsWifiManagerValue() = runBlocking(IMMEDIATE) { whenever(wifiManager.isWifiEnabled).thenReturn(true) @@ -721,21 +713,6 @@ class WifiRepositoryImplTest : SysuiTestCase() { } @Test - fun wifiActivity_nullWifiManager_receivesDefault() = runBlocking(IMMEDIATE) { - underTest = createRepo(wifiManagerToUse = null) - - var latest: DataActivityModel? = null - val job = underTest - .wifiActivity - .onEach { latest = it } - .launchIn(this) - - assertThat(latest).isEqualTo(ACTIVITY_DEFAULT) - - job.cancel() - } - - @Test fun wifiActivity_callbackGivesNone_activityFlowHasNone() = runBlocking(IMMEDIATE) { var latest: DataActivityModel? = null val job = underTest @@ -801,7 +778,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { job.cancel() } - private fun createRepo(wifiManagerToUse: WifiManager? = wifiManager): WifiRepositoryImpl { + private fun createRepo(): WifiRepositoryImpl { return WifiRepositoryImpl( broadcastDispatcher, connectivityManager, @@ -809,7 +786,7 @@ class WifiRepositoryImplTest : SysuiTestCase() { tableLogger, executor, scope, - wifiManagerToUse, + wifiManager, ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt index 2ecb17b7fae0..01d59f96c221 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt @@ -52,6 +52,22 @@ class WifiInteractorImplTest : SysuiTestCase() { } @Test + fun ssid_unavailableNetwork_outputsNull() = + runBlocking(IMMEDIATE) { + wifiRepository.setWifiNetwork(WifiNetworkModel.Unavailable) + + var latest: String? = "default" + val job = underTest + .ssid + .onEach { latest = it } + .launchIn(this) + + assertThat(latest).isNull() + + job.cancel() + } + + @Test fun ssid_inactiveNetwork_outputsNull() = runBlocking(IMMEDIATE) { wifiRepository.setWifiNetwork(WifiNetworkModel.Inactive) @@ -85,6 +101,7 @@ class WifiInteractorImplTest : SysuiTestCase() { fun ssid_isPasspointAccessPoint_outputsPasspointName() = runBlocking(IMMEDIATE) { wifiRepository.setWifiNetwork(WifiNetworkModel.Active( networkId = 1, + level = 1, isPasspointAccessPoint = true, passpointProviderFriendlyName = "friendly", )) @@ -104,6 +121,7 @@ class WifiInteractorImplTest : SysuiTestCase() { fun ssid_isOnlineSignUpForPasspoint_outputsPasspointName() = runBlocking(IMMEDIATE) { wifiRepository.setWifiNetwork(WifiNetworkModel.Active( networkId = 1, + level = 1, isOnlineSignUpForPasspointAccessPoint = true, passpointProviderFriendlyName = "friendly", )) @@ -123,6 +141,7 @@ class WifiInteractorImplTest : SysuiTestCase() { fun ssid_unknownSsid_outputsNull() = runBlocking(IMMEDIATE) { wifiRepository.setWifiNetwork(WifiNetworkModel.Active( networkId = 1, + level = 1, ssid = WifiManager.UNKNOWN_SSID, )) @@ -141,6 +160,7 @@ class WifiInteractorImplTest : SysuiTestCase() { fun ssid_validSsid_outputsSsid() = runBlocking(IMMEDIATE) { wifiRepository.setWifiNetwork(WifiNetworkModel.Active( networkId = 1, + level = 1, ssid = "MyAwesomeWifiNetwork", )) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt index 59c10cd6df7c..b8ace2f04a61 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt @@ -17,7 +17,6 @@ package com.android.systemui.statusbar.pipeline.wifi.ui.view import android.content.res.ColorStateList -import android.graphics.Rect import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.testing.TestableLooper.RunWithLooper @@ -27,7 +26,6 @@ import android.widget.ImageView import androidx.test.filters.SmallTest import com.android.systemui.R import com.android.systemui.SysuiTestCase -import com.android.systemui.lifecycle.InstantTaskExecutorRule import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.statusbar.StatusBarIconView.STATE_DOT import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN @@ -52,8 +50,6 @@ import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import org.junit.Before -import org.junit.Ignore -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock @@ -70,7 +66,8 @@ class ModernStatusBarWifiViewTest : SysuiTestCase() { private lateinit var statusBarPipelineFlags: StatusBarPipelineFlags @Mock private lateinit var logger: ConnectivityPipelineLogger - @Mock private lateinit var tableLogBuffer: TableLogBuffer + @Mock + private lateinit var tableLogBuffer: TableLogBuffer @Mock private lateinit var connectivityConstants: ConnectivityConstants @Mock @@ -83,9 +80,6 @@ class ModernStatusBarWifiViewTest : SysuiTestCase() { private lateinit var scope: CoroutineScope private lateinit var airplaneModeViewModel: AirplaneModeViewModel - @JvmField @Rule - val instantTaskExecutor = InstantTaskExecutorRule() - @Before fun setUp() { MockitoAnnotations.initMocks(this) @@ -118,40 +112,6 @@ class ModernStatusBarWifiViewTest : SysuiTestCase() { ).home } - @Test - fun constructAndBind_hasCorrectSlot() { - val view = ModernStatusBarWifiView.constructAndBind(context, "slotName", viewModel) - - assertThat(view.slot).isEqualTo("slotName") - } - - @Test - fun getVisibleState_icon_returnsIcon() { - val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel) - - view.setVisibleState(STATE_ICON, /* animate= */ false) - - assertThat(view.visibleState).isEqualTo(STATE_ICON) - } - - @Test - fun getVisibleState_dot_returnsDot() { - val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel) - - view.setVisibleState(STATE_DOT, /* animate= */ false) - - assertThat(view.visibleState).isEqualTo(STATE_DOT) - } - - @Test - fun getVisibleState_hidden_returnsHidden() { - val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel) - - view.setVisibleState(STATE_HIDDEN, /* animate= */ false) - - assertThat(view.visibleState).isEqualTo(STATE_HIDDEN) - } - // Note: The following tests are more like integration tests, since they stand up a full // [WifiViewModel] and test the interactions between the view, view-binder, and view-model. @@ -235,24 +195,24 @@ class ModernStatusBarWifiViewTest : SysuiTestCase() { } @Test - @Ignore("b/262660044") fun onDarkChanged_iconHasNewColor() { - whenever(statusBarPipelineFlags.useWifiDebugColoring()).thenReturn(false) + whenever(statusBarPipelineFlags.useDebugColoring()).thenReturn(false) val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel) ViewUtils.attachView(view) testableLooper.processAllMessages() - val areas = ArrayList(listOf(Rect(0, 0, 1000, 1000))) val color = 0x12345678 - view.onDarkChanged(areas, 1.0f, color) + view.onDarkChanged(arrayListOf(), 1.0f, color) testableLooper.processAllMessages() assertThat(view.getIconView().imageTintList).isEqualTo(ColorStateList.valueOf(color)) + + ViewUtils.detachView(view) } @Test fun setStaticDrawableColor_iconHasNewColor() { - whenever(statusBarPipelineFlags.useWifiDebugColoring()).thenReturn(false) + whenever(statusBarPipelineFlags.useDebugColoring()).thenReturn(false) val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel) ViewUtils.attachView(view) testableLooper.processAllMessages() @@ -262,6 +222,8 @@ class ModernStatusBarWifiViewTest : SysuiTestCase() { testableLooper.processAllMessages() assertThat(view.getIconView().imageTintList).isEqualTo(ColorStateList.valueOf(color)) + + ViewUtils.detachView(view) } private fun View.getIconGroupView(): View { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt index 12b93819fc5e..726e813ec414 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelIconParameterizedTest.kt @@ -379,6 +379,12 @@ internal class WifiViewModelIconParameterizedTest(private val testCase: TestCase expected = null, ), + // network = Unavailable => not shown + TestCase( + network = WifiNetworkModel.Unavailable, + expected = null, + ), + // network = Active & validated = false => not shown TestCase( network = WifiNetworkModel.Active(NETWORK_ID, isValidated = false, level = 3), @@ -397,12 +403,6 @@ internal class WifiViewModelIconParameterizedTest(private val testCase: TestCase description = "Full internet level 4 icon", ), ), - - // network has null level => not shown - TestCase( - network = WifiNetworkModel.Active(NETWORK_ID, isValidated = true, level = null), - expected = null, - ), ) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt index 41584347c0f2..e5cfec9c08c0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt @@ -228,7 +228,7 @@ class WifiViewModelTest : SysuiTestCase() { whenever(connectivityConstants.shouldShowActivityConfig).thenReturn(true) createAndSetViewModel() - wifiRepository.setWifiNetwork(WifiNetworkModel.Active(NETWORK_ID, ssid = null)) + wifiRepository.setWifiNetwork(WifiNetworkModel.Active(NETWORK_ID, ssid = null, level = 1)) var activityIn: Boolean? = null val activityInJob = underTest @@ -553,7 +553,8 @@ class WifiViewModelTest : SysuiTestCase() { companion object { private const val NETWORK_ID = 2 - private val ACTIVE_VALID_WIFI_NETWORK = WifiNetworkModel.Active(NETWORK_ID, ssid = "AB") + private val ACTIVE_VALID_WIFI_NETWORK = + WifiNetworkModel.Active(NETWORK_ID, ssid = "AB", level = 1) } } 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 58b55602a39c..984de5b67bf5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusManagerTest.kt @@ -202,7 +202,7 @@ class StylusManagerTest : SysuiTestCase() { stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID) stylusManager.registerCallback(otherStylusCallback) - stylusManager.onInputDeviceAdded(STYLUS_DEVICE_ID) + stylusManager.onInputDeviceRemoved(STYLUS_DEVICE_ID) verify(stylusCallback, times(1)).onStylusRemoved(STYLUS_DEVICE_ID) verify(otherStylusCallback, times(1)).onStylusRemoved(STYLUS_DEVICE_ID) diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt index d3411c2b4416..90178c6a0096 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt @@ -124,7 +124,7 @@ class ChipbarCoordinatorTest : SysuiTestCase() { ) ) - val contentDescView = getChipbarView().requireViewById<ViewGroup>(R.id.chipbar_inner) + val contentDescView = getChipbarView().getInnerView() assertThat(contentDescView.contentDescription.toString()).contains("loadedCD") assertThat(contentDescView.contentDescription.toString()).contains("text") } @@ -139,11 +139,43 @@ class ChipbarCoordinatorTest : SysuiTestCase() { ) ) - val contentDescView = getChipbarView().requireViewById<ViewGroup>(R.id.chipbar_inner) + val contentDescView = getChipbarView().getInnerView() assertThat(contentDescView.contentDescription.toString()).isEqualTo("text") } @Test + fun displayView_contentDescription_endIsLoading() { + underTest.displayView( + createChipbarInfo( + Icon.Resource(R.drawable.ic_cake, ContentDescription.Loaded("loadedCD")), + Text.Loaded("text"), + endItem = ChipbarEndItem.Loading, + ) + ) + + val contentDescView = getChipbarView().getInnerView() + val loadingDesc = context.resources.getString(R.string.media_transfer_loading) + assertThat(contentDescView.contentDescription.toString()).contains("text") + assertThat(contentDescView.contentDescription.toString()).contains(loadingDesc) + } + + @Test + fun displayView_contentDescription_endNotLoading() { + underTest.displayView( + createChipbarInfo( + Icon.Resource(R.drawable.ic_cake, ContentDescription.Loaded("loadedCD")), + Text.Loaded("text"), + endItem = ChipbarEndItem.Error, + ) + ) + + val contentDescView = getChipbarView().getInnerView() + val loadingDesc = context.resources.getString(R.string.media_transfer_loading) + assertThat(contentDescView.contentDescription.toString()).contains("text") + assertThat(contentDescView.contentDescription.toString()).doesNotContain(loadingDesc) + } + + @Test fun displayView_loadedIcon_correctlyRendered() { val drawable = context.getDrawable(R.drawable.ic_celebration)!! @@ -417,6 +449,8 @@ class ChipbarCoordinatorTest : SysuiTestCase() { ) } + private fun ViewGroup.getInnerView() = this.requireViewById<ViewGroup>(R.id.chipbar_inner) + private fun ViewGroup.getStartIconView() = this.requireViewById<ImageView>(R.id.start_icon) private fun ViewGroup.getChipText(): String = diff --git a/services/api/current.txt b/services/api/current.txt index 090a4499a591..b173726411f6 100644 --- a/services/api/current.txt +++ b/services/api/current.txt @@ -40,6 +40,7 @@ package com.android.server.am { public interface ActivityManagerLocal { method public boolean bindSdkSandboxService(@NonNull android.content.Intent, @NonNull android.content.ServiceConnection, int, @NonNull String, @NonNull String, int) throws android.os.RemoteException; method public boolean canStartForegroundService(int, int, @NonNull String); + method public void killSdkSandboxClientAppProcess(@NonNull android.os.IBinder); } } @@ -212,6 +213,19 @@ package com.android.server.role { } +package com.android.server.security { + + public class FileIntegrityService extends com.android.server.SystemService { + method public void onStart(); + method public static void setUpFsVerity(@NonNull String) throws java.io.IOException; + } + + public class KeyChainSystemService extends com.android.server.SystemService { + method public void onStart(); + } + +} + package com.android.server.stats { public final class StatsHelper { diff --git a/services/backup/java/com/android/server/backup/internal/BackupHandler.java b/services/backup/java/com/android/server/backup/internal/BackupHandler.java index 3ff6ba7e59c0..38c7dd1e230b 100644 --- a/services/backup/java/com/android/server/backup/internal/BackupHandler.java +++ b/services/backup/java/com/android/server/backup/internal/BackupHandler.java @@ -375,7 +375,7 @@ public class BackupHandler extends Handler { case MSG_RUN_GET_RESTORE_SETS: { // Like other async operations, this is entered with the wakelock held - RestoreSet[] sets = null; + List<RestoreSet> sets = null; RestoreGetSetsParams params = (RestoreGetSetsParams) msg.obj; String callerLogString = "BH/MSG_RUN_GET_RESTORE_SETS"; try { @@ -394,7 +394,12 @@ public class BackupHandler extends Handler { } finally { if (params.observer != null) { try { - params.observer.restoreSetsAvailable(sets); + if (sets == null) { + params.observer.restoreSetsAvailable(null); + } else { + params.observer.restoreSetsAvailable( + sets.toArray(new RestoreSet[0])); + } } catch (RemoteException re) { Slog.e(TAG, "Unable to report listing to observer"); } catch (Exception e) { diff --git a/services/backup/java/com/android/server/backup/restore/ActiveRestoreSession.java b/services/backup/java/com/android/server/backup/restore/ActiveRestoreSession.java index d3e4f138f5da..70d7fac09a4f 100644 --- a/services/backup/java/com/android/server/backup/restore/ActiveRestoreSession.java +++ b/services/backup/java/com/android/server/backup/restore/ActiveRestoreSession.java @@ -45,6 +45,7 @@ import com.android.server.backup.params.RestoreParams; import com.android.server.backup.transport.TransportConnection; import com.android.server.backup.utils.BackupEligibilityRules; +import java.util.List; import java.util.function.BiFunction; /** @@ -60,7 +61,7 @@ public class ActiveRestoreSession extends IRestoreSession.Stub { private final int mUserId; private final BackupEligibilityRules mBackupEligibilityRules; @Nullable private final String mPackageName; - public RestoreSet[] mRestoreSets = null; + public List<RestoreSet> mRestoreSets = null; boolean mEnded = false; boolean mTimedOut = false; @@ -174,10 +175,10 @@ public class ActiveRestoreSession extends IRestoreSession.Stub { } synchronized (mBackupManagerService.getQueueLock()) { - for (int i = 0; i < mRestoreSets.length; i++) { - if (token == mRestoreSets[i].token) { + for (int i = 0; i < mRestoreSets.size(); i++) { + if (token == mRestoreSets.get(i).token) { final long oldId = Binder.clearCallingIdentity(); - RestoreSet restoreSet = mRestoreSets[i]; + RestoreSet restoreSet = mRestoreSets.get(i); try { return sendRestoreToHandlerLocked( (transportClient, listener) -> @@ -267,10 +268,10 @@ public class ActiveRestoreSession extends IRestoreSession.Stub { } synchronized (mBackupManagerService.getQueueLock()) { - for (int i = 0; i < mRestoreSets.length; i++) { - if (token == mRestoreSets[i].token) { + for (int i = 0; i < mRestoreSets.size(); i++) { + if (token == mRestoreSets.get(i).token) { final long oldId = Binder.clearCallingIdentity(); - RestoreSet restoreSet = mRestoreSets[i]; + RestoreSet restoreSet = mRestoreSets.get(i); try { return sendRestoreToHandlerLocked( (transportClient, listener) -> @@ -390,7 +391,7 @@ public class ActiveRestoreSession extends IRestoreSession.Stub { } } - public void setRestoreSets(RestoreSet[] restoreSets) { + public void setRestoreSets(List<RestoreSet> restoreSets) { mRestoreSets = restoreSets; } diff --git a/services/backup/java/com/android/server/backup/transport/BackupTransportClient.java b/services/backup/java/com/android/server/backup/transport/BackupTransportClient.java index 21005bbf8af9..daf3415229ea 100644 --- a/services/backup/java/com/android/server/backup/transport/BackupTransportClient.java +++ b/services/backup/java/com/android/server/backup/transport/BackupTransportClient.java @@ -180,11 +180,11 @@ public class BackupTransportClient { /** * See {@link IBackupTransport#getAvailableRestoreSets()} */ - public RestoreSet[] getAvailableRestoreSets() throws RemoteException { + public List<RestoreSet> getAvailableRestoreSets() throws RemoteException { AndroidFuture<List<RestoreSet>> resultFuture = mTransportFutures.newFuture(); mTransportBinder.getAvailableRestoreSets(resultFuture); List<RestoreSet> result = getFutureResult(resultFuture); - return result == null ? null : result.toArray(new RestoreSet[] {}); + return result; } /** diff --git a/services/companion/java/com/android/server/companion/virtual/CameraAccessController.java b/services/companion/java/com/android/server/companion/virtual/CameraAccessController.java index 2904f28fca01..312ab548c746 100644 --- a/services/companion/java/com/android/server/companion/virtual/CameraAccessController.java +++ b/services/companion/java/com/android/server/companion/virtual/CameraAccessController.java @@ -19,6 +19,7 @@ package com.android.server.companion.virtual; import static android.hardware.camera2.CameraInjectionSession.InjectionStatusCallback.ERROR_INJECTION_UNSUPPORTED; import android.annotation.NonNull; +import android.annotation.UserIdInt; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; @@ -95,6 +96,23 @@ class CameraAccessController extends CameraManager.AvailabilityCallback implemen } /** + * Returns the userId for which the camera access should be blocked. + */ + @UserIdInt + public int getUserId() { + return mContext.getUserId(); + } + + /** + * Returns the number of observers currently relying on this controller. + */ + public int getObserverCount() { + synchronized (mLock) { + return mObserverCount; + } + } + + /** * Starts watching for camera access by uids running on a virtual device, if we were not * already doing so. */ diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java index cdd54719e15f..db163dcae395 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java @@ -110,6 +110,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub private final int mDeviceId; private final InputController mInputController; private final SensorController mSensorController; + private final CameraAccessController mCameraAccessController; private VirtualAudioController mVirtualAudioController; @VisibleForTesting final Set<Integer> mVirtualDisplayIds = new ArraySet<>(); @@ -165,6 +166,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub IBinder token, int ownerUid, int deviceId, + CameraAccessController cameraAccessController, OnDeviceCloseListener onDeviceCloseListener, PendingTrampolineCallback pendingTrampolineCallback, IVirtualDeviceActivityListener activityListener, @@ -178,6 +180,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub deviceId, /* inputController= */ null, /* sensorController= */ null, + cameraAccessController, onDeviceCloseListener, pendingTrampolineCallback, activityListener, @@ -194,6 +197,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub int deviceId, InputController inputController, SensorController sensorController, + CameraAccessController cameraAccessController, OnDeviceCloseListener onDeviceCloseListener, PendingTrampolineCallback pendingTrampolineCallback, IVirtualDeviceActivityListener activityListener, @@ -223,6 +227,8 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub } else { mSensorController = sensorController; } + mCameraAccessController = cameraAccessController; + mCameraAccessController.startObservingIfNeeded(); mOnDeviceCloseListener = onDeviceCloseListener; try { token.linkToDeath(this, 0); @@ -243,6 +249,11 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub return flags; } + /** Returns the camera access controller of this device. */ + CameraAccessController getCameraAccessController() { + return mCameraAccessController; + } + /** Returns the device display name. */ CharSequence getDisplayName() { return mAssociationInfo.getDisplayName(); @@ -359,6 +370,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub } mOnDeviceCloseListener.onClose(mDeviceId); mAppToken.unlinkToDeath(this, 0); + mCameraAccessController.stopObservingIfNeeded(); final long ident = Binder.clearCallingIdentity(); try { @@ -376,6 +388,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub @Override public void onRunningAppsChanged(ArraySet<Integer> runningUids) { + mCameraAccessController.blockCameraAccessIfNeeded(runningUids); mRunningAppsChangedCallback.accept(runningUids); } 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 d31729872071..758345f716c3 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java @@ -27,7 +27,6 @@ import android.annotation.SuppressLint; import android.app.ActivityOptions; import android.companion.AssociationInfo; import android.companion.CompanionDeviceManager; -import android.companion.CompanionDeviceManager.OnAssociationsChangedListener; import android.companion.virtual.IVirtualDevice; import android.companion.virtual.IVirtualDeviceActivityListener; import android.companion.virtual.IVirtualDeviceManager; @@ -70,6 +69,7 @@ import java.util.List; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; @SuppressLint("LongLogTag") @@ -87,20 +87,6 @@ public class VirtualDeviceManagerService extends SystemService { VirtualDeviceManager.DEVICE_ID_DEFAULT + 1); /** - * Mapping from user IDs to CameraAccessControllers. - */ - @GuardedBy("mVirtualDeviceManagerLock") - private final SparseArray<CameraAccessController> mCameraAccessControllers = - new SparseArray<>(); - - /** - * Mapping from device IDs to CameraAccessControllers. - */ - @GuardedBy("mVirtualDeviceManagerLock") - private final SparseArray<CameraAccessController> mCameraAccessControllersByDeviceId = - new SparseArray<>(); - - /** * Mapping from device IDs to virtual devices. */ @GuardedBy("mVirtualDeviceManagerLock") @@ -112,21 +98,6 @@ public class VirtualDeviceManagerService extends SystemService { @GuardedBy("mVirtualDeviceManagerLock") private final SparseArray<ArraySet<Integer>> mAppsOnVirtualDevices = new SparseArray<>(); - /** - * Mapping from user ID to CDM associations. The associations come from - * {@link CompanionDeviceManager#getAllAssociations()}, which contains associations across all - * packages. - */ - private final ConcurrentHashMap<Integer, List<AssociationInfo>> mAllAssociations = - new ConcurrentHashMap<>(); - - /** - * Mapping from user ID to its change listener. The listeners are added when the user is - * started and removed when the user stops. - */ - private final SparseArray<OnAssociationsChangedListener> mOnAssociationsChangedListeners = - new SparseArray<>(); - public VirtualDeviceManagerService(Context context) { super(context); mImpl = new VirtualDeviceManagerImpl(); @@ -177,54 +148,9 @@ public class VirtualDeviceManagerService extends SystemService { } } - @Override - public void onUserStarting(@NonNull TargetUser user) { - super.onUserStarting(user); - Context userContext = getContext().createContextAsUser(user.getUserHandle(), 0); - synchronized (mVirtualDeviceManagerLock) { - final CompanionDeviceManager cdm = - userContext.getSystemService(CompanionDeviceManager.class); - final int userId = user.getUserIdentifier(); - mAllAssociations.put(userId, cdm.getAllAssociations()); - OnAssociationsChangedListener listener = - associations -> mAllAssociations.put(userId, associations); - mOnAssociationsChangedListeners.put(userId, listener); - cdm.addOnAssociationsChangedListener(Runnable::run, listener); - CameraAccessController cameraAccessController = new CameraAccessController( - userContext, mLocalService, this::onCameraAccessBlocked); - mCameraAccessControllers.put(user.getUserIdentifier(), cameraAccessController); - } - } - - @Override - public void onUserStopping(@NonNull TargetUser user) { - super.onUserStopping(user); - synchronized (mVirtualDeviceManagerLock) { - int userId = user.getUserIdentifier(); - mAllAssociations.remove(userId); - final CompanionDeviceManager cdm = getContext().createContextAsUser( - user.getUserHandle(), 0) - .getSystemService(CompanionDeviceManager.class); - OnAssociationsChangedListener listener = mOnAssociationsChangedListeners.get(userId); - if (listener != null) { - cdm.removeOnAssociationsChangedListener(listener); - mOnAssociationsChangedListeners.remove(userId); - } - CameraAccessController cameraAccessController = mCameraAccessControllers.get( - user.getUserIdentifier()); - if (cameraAccessController != null) { - cameraAccessController.close(); - mCameraAccessControllers.remove(user.getUserIdentifier()); - } else { - Slog.w(TAG, "Cannot unregister cameraAccessController for user " + user); - } - } - } - void onCameraAccessBlocked(int appUid) { synchronized (mVirtualDeviceManagerLock) { - int size = mVirtualDevices.size(); - for (int i = 0; i < size; i++) { + for (int i = 0; i < mVirtualDevices.size(); i++) { CharSequence deviceName = mVirtualDevices.valueAt(i).getDisplayName(); mVirtualDevices.valueAt(i).showToastWhereUidIsRunning(appUid, getContext().getString( @@ -235,6 +161,21 @@ public class VirtualDeviceManagerService extends SystemService { } } + CameraAccessController getCameraAccessController(UserHandle userHandle) { + int userId = userHandle.getIdentifier(); + synchronized (mVirtualDeviceManagerLock) { + for (int i = 0; i < mVirtualDevices.size(); i++) { + final CameraAccessController cameraAccessController = + mVirtualDevices.valueAt(i).getCameraAccessController(); + if (cameraAccessController.getUserId() == userId) { + return cameraAccessController; + } + } + } + Context userContext = getContext().createContextAsUser(userHandle, 0); + return new CameraAccessController(userContext, mLocalService, this::onCameraAccessBlocked); + } + @VisibleForTesting VirtualDeviceManagerInternal getLocalServiceInstance() { return mLocalService; @@ -255,8 +196,34 @@ public class VirtualDeviceManagerService extends SystemService { } } - class VirtualDeviceManagerImpl extends IVirtualDeviceManager.Stub implements - VirtualDeviceImpl.PendingTrampolineCallback { + @VisibleForTesting + void removeVirtualDevice(int deviceId) { + synchronized (mVirtualDeviceManagerLock) { + mAppsOnVirtualDevices.remove(deviceId); + mVirtualDevices.remove(deviceId); + } + } + + class VirtualDeviceManagerImpl extends IVirtualDeviceManager.Stub { + + private final VirtualDeviceImpl.PendingTrampolineCallback mPendingTrampolineCallback = + new VirtualDeviceImpl.PendingTrampolineCallback() { + @Override + public void startWaitingForPendingTrampoline(PendingTrampoline pendingTrampoline) { + PendingTrampoline existing = mPendingTrampolines.put( + pendingTrampoline.mPendingIntent.getCreatorPackage(), + pendingTrampoline); + if (existing != null) { + existing.mResultReceiver.send( + VirtualDeviceManager.LAUNCH_FAILURE_NO_ACTIVITY, null); + } + } + + @Override + public void stopWaitingForPendingTrampoline(PendingTrampoline pendingTrampoline) { + mPendingTrampolines.remove(pendingTrampoline.mPendingIntent.getCreatorPackage()); + } + }; @Override // Binder call public IVirtualDevice createVirtualDevice( @@ -279,25 +246,16 @@ public class VirtualDeviceManagerService extends SystemService { throw new IllegalArgumentException("No association with ID " + associationId); } synchronized (mVirtualDeviceManagerLock) { - final int userId = UserHandle.getUserId(callingUid); + final UserHandle userHandle = getCallingUserHandle(); final CameraAccessController cameraAccessController = - mCameraAccessControllers.get(userId); + getCameraAccessController(userHandle); final int deviceId = sNextUniqueIndex.getAndIncrement(); + final Consumer<ArraySet<Integer>> runningAppsChangedCallback = + runningUids -> notifyRunningAppsChanged(deviceId, runningUids); VirtualDeviceImpl virtualDevice = new VirtualDeviceImpl(getContext(), - associationInfo, token, callingUid, deviceId, - /* onDeviceCloseListener= */ this::onDeviceClosed, - this, activityListener, - runningUids -> { - cameraAccessController.blockCameraAccessIfNeeded(runningUids); - notifyRunningAppsChanged(deviceId, runningUids); - }, - params); - if (cameraAccessController != null) { - cameraAccessController.startObservingIfNeeded(); - mCameraAccessControllersByDeviceId.put(deviceId, cameraAccessController); - } else { - Slog.w(TAG, "cameraAccessController not found for user " + userId); - } + associationInfo, token, callingUid, deviceId, cameraAccessController, + this::onDeviceClosed, mPendingTrampolineCallback, activityListener, + runningAppsChangedCallback, params); mVirtualDevices.put(deviceId, virtualDevice); return virtualDevice; } @@ -409,8 +367,18 @@ public class VirtualDeviceManagerService extends SystemService { @Nullable private AssociationInfo getAssociationInfo(String packageName, int associationId) { - final int callingUserId = getCallingUserHandle().getIdentifier(); - final List<AssociationInfo> associations = mAllAssociations.get(callingUserId); + final UserHandle userHandle = getCallingUserHandle(); + final CompanionDeviceManager cdm = + getContext().createContextAsUser(userHandle, 0) + .getSystemService(CompanionDeviceManager.class); + List<AssociationInfo> associations; + final long identity = Binder.clearCallingIdentity(); + try { + associations = cdm.getAllAssociations(); + } finally { + Binder.restoreCallingIdentity(identity); + } + final int callingUserId = userHandle.getIdentifier(); if (associations != null) { final int associationSize = associations.size(); for (int i = 0; i < associationSize; i++) { @@ -427,25 +395,15 @@ public class VirtualDeviceManagerService extends SystemService { } private void onDeviceClosed(int deviceId) { - synchronized (mVirtualDeviceManagerLock) { - mVirtualDevices.remove(deviceId); - Intent i = new Intent(VirtualDeviceManager.ACTION_VIRTUAL_DEVICE_REMOVED); - i.putExtra(VirtualDeviceManager.EXTRA_VIRTUAL_DEVICE_ID, deviceId); - i.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); - final long identity = Binder.clearCallingIdentity(); - try { - getContext().sendBroadcastAsUser(i, UserHandle.ALL); - } finally { - Binder.restoreCallingIdentity(identity); - } - mAppsOnVirtualDevices.remove(deviceId); - final CameraAccessController cameraAccessController = - mCameraAccessControllersByDeviceId.removeReturnOld(deviceId); - if (cameraAccessController != null) { - cameraAccessController.stopObservingIfNeeded(); - } else { - Slog.w(TAG, "cameraAccessController not found for device Id " + deviceId); - } + removeVirtualDevice(deviceId); + Intent i = new Intent(VirtualDeviceManager.ACTION_VIRTUAL_DEVICE_REMOVED); + i.putExtra(VirtualDeviceManager.EXTRA_VIRTUAL_DEVICE_ID, deviceId); + i.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); + final long identity = Binder.clearCallingIdentity(); + try { + getContext().sendBroadcastAsUser(i, UserHandle.ALL); + } finally { + Binder.restoreCallingIdentity(identity); } } @@ -474,22 +432,6 @@ public class VirtualDeviceManagerService extends SystemService { } } } - - @Override - public void startWaitingForPendingTrampoline(PendingTrampoline pendingTrampoline) { - PendingTrampoline existing = mPendingTrampolines.put( - pendingTrampoline.mPendingIntent.getCreatorPackage(), - pendingTrampoline); - if (existing != null) { - existing.mResultReceiver.send( - VirtualDeviceManager.LAUNCH_FAILURE_NO_ACTIVITY, null); - } - } - - @Override - public void stopWaitingForPendingTrampoline(PendingTrampoline pendingTrampoline) { - mPendingTrampolines.remove(pendingTrampoline.mPendingIntent.getCreatorPackage()); - } } private final class LocalService extends VirtualDeviceManagerInternal { diff --git a/services/core/java/com/android/server/BinaryTransparencyService.java b/services/core/java/com/android/server/BinaryTransparencyService.java index 373080a6cff5..ff6fd4b9900e 100644 --- a/services/core/java/com/android/server/BinaryTransparencyService.java +++ b/services/core/java/com/android/server/BinaryTransparencyService.java @@ -273,7 +273,7 @@ public class BinaryTransparencyService extends SystemService { String[] signerDigestHexStrings = computePackageSignerSha256Digests( packageInfo.signingInfo); - // log to Westworld + // log to statsd FrameworkStatsLog.write(FrameworkStatsLog.APEX_INFO_GATHERED, packageInfo.packageName, packageInfo.getLongVersionCode(), diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java index 7b8ca912ecf2..9bedbd0ec584 100644 --- a/services/core/java/com/android/server/TelephonyRegistry.java +++ b/services/core/java/com/android/server/TelephonyRegistry.java @@ -1000,10 +1000,6 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { @Override public void notifySubscriptionInfoChanged() { if (VDBG) log("notifySubscriptionInfoChanged:"); - if (!checkNotifyPermission("notifySubscriptionInfoChanged()")) { - return; - } - synchronized (mRecords) { if (!mHasNotifySubscriptionInfoChangedOccurred) { log("notifySubscriptionInfoChanged: first invocation mRecords.size=" @@ -1030,10 +1026,6 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { @Override public void notifyOpportunisticSubscriptionInfoChanged() { if (VDBG) log("notifyOpptSubscriptionInfoChanged:"); - if (!checkNotifyPermission("notifyOpportunisticSubscriptionInfoChanged()")) { - return; - } - synchronized (mRecords) { if (!mHasNotifyOpportunisticSubscriptionInfoChangedOccurred) { log("notifyOpptSubscriptionInfoChanged: first invocation mRecords.size=" diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index 4ebd7146252a..bcea40e5a9db 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -54,6 +54,7 @@ import static android.os.PowerExemptionManager.REASON_INSTR_BACKGROUND_FGS_PERMI import static android.os.PowerExemptionManager.REASON_OPT_OUT_REQUESTED; import static android.os.PowerExemptionManager.REASON_OP_ACTIVATE_PLATFORM_VPN; import static android.os.PowerExemptionManager.REASON_OP_ACTIVATE_VPN; +import static android.os.PowerExemptionManager.REASON_PACKAGE_INSTALLER; import static android.os.PowerExemptionManager.REASON_PROC_STATE_PERSISTENT; import static android.os.PowerExemptionManager.REASON_PROC_STATE_PERSISTENT_UI; import static android.os.PowerExemptionManager.REASON_PROC_STATE_TOP; @@ -194,6 +195,7 @@ import com.android.internal.notification.SystemNotificationChannels; import com.android.internal.os.SomeArgs; import com.android.internal.os.TimeoutRecord; import com.android.internal.os.TransferPipe; +import com.android.internal.util.ArrayUtils; import com.android.internal.util.DumpUtils; import com.android.internal.util.FastPrintWriter; import com.android.internal.util.FrameworkStatsLog; @@ -202,6 +204,7 @@ import com.android.server.LocalServices; import com.android.server.SystemService; import com.android.server.am.ActivityManagerService.ItemMatcher; import com.android.server.am.LowMemDetector.MemFactor; +import com.android.server.pm.KnownPackages; import com.android.server.uri.NeededUriGrants; import com.android.server.wm.ActivityServiceConnectionsHolder; @@ -2382,6 +2385,13 @@ public final class ActiveServices { .getPotentialUserAllowedExemptionReason(callerUid, packageName); } } + if (reason == REASON_DENIED) { + if (ArrayUtils.contains(mAm.getPackageManagerInternal().getKnownPackageNames( + KnownPackages.PACKAGE_INSTALLER, UserHandle.USER_SYSTEM), packageName)) { + reason = REASON_PACKAGE_INSTALLER; + } + } + switch (reason) { case REASON_SYSTEM_UID: case REASON_SYSTEM_ALLOW_LISTED: @@ -2397,6 +2407,7 @@ public final class ActiveServices { case REASON_ACTIVE_DEVICE_ADMIN: case REASON_ROLE_EMERGENCY: case REASON_ALLOWLISTED_PACKAGE: + case REASON_PACKAGE_INSTALLER: return PERMISSION_GRANTED; default: return PERMISSION_DENIED; diff --git a/services/core/java/com/android/server/am/ActivityManagerLocal.java b/services/core/java/com/android/server/am/ActivityManagerLocal.java index 9f2cc7f9cb44..5175a31c16b5 100644 --- a/services/core/java/com/android/server/am/ActivityManagerLocal.java +++ b/services/core/java/com/android/server/am/ActivityManagerLocal.java @@ -23,6 +23,7 @@ import android.annotation.SystemApi; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; +import android.os.IBinder; import android.os.RemoteException; /** @@ -95,6 +96,15 @@ public interface ActivityManagerLocal { throws RemoteException; /** + * Kill an app process associated with an SDK sandbox. + * + * @param clientApplicationThreadBinder binder value of the + * {@link android.app.IApplicationThread} of a client app process associated with a + * sandbox. This is obtained using {@link Context#getIApplicationThreadBinder()}. + */ + void killSdkSandboxClientAppProcess(@NonNull IBinder clientApplicationThreadBinder); + + /** * Start a foreground service delegate. * @param options foreground service delegate options. * @param connection a service connection served as callback to caller. diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 191460c385ad..fc6d30bf58c9 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -84,7 +84,6 @@ import static android.os.Process.ZYGOTE_POLICY_FLAG_LATENCY_SENSITIVE; import static android.os.Process.ZYGOTE_POLICY_FLAG_SYSTEM_PROCESS; import static android.os.Process.ZYGOTE_PROCESS; import static android.os.Process.getTotalMemory; -import static android.os.Process.isSdkSandboxUid; import static android.os.Process.isThreadInProcess; import static android.os.Process.killProcess; import static android.os.Process.killProcessQuiet; @@ -259,8 +258,10 @@ import android.content.pm.ProviderInfo; import android.content.pm.ProviderInfoList; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; +import android.content.pm.SharedLibraryInfo; import android.content.pm.TestUtilityService; import android.content.pm.UserInfo; +import android.content.pm.VersionedPackage; import android.content.res.CompatibilityInfo; import android.content.res.Configuration; import android.content.res.Resources; @@ -9040,39 +9041,55 @@ public class ActivityManagerService extends IActivityManager.Stub sb.append("Instant-App: true\n"); } - if (isSdkSandboxUid(process.uid)) { - final int appUid = Process.getAppUidForSdkSandboxUid(process.uid); + if (process.isSdkSandbox) { + final String clientPackage = process.sdkSandboxClientAppPackage; try { - String[] clientPackages = pm.getPackagesForUid(appUid); - // In shared UID case, don't add the package information - if (clientPackages.length == 1) { - appendSdkSandboxClientPackageHeader(sb, clientPackages[0], callingUserId); + final PackageInfo pi = pm.getPackageInfo(clientPackage, + PackageManager.GET_SHARED_LIBRARY_FILES, callingUserId); + if (pi != null) { + appendSdkSandboxClientPackageHeader(sb, pi); + appendSdkSandboxLibraryHeaders(sb, pi); + } else { + Slog.e(TAG, + "PackageInfo is null for SDK sandbox client: " + clientPackage); } } catch (RemoteException e) { - Slog.e(TAG, "Error getting packages for client app uid: " + appUid, e); + Slog.e(TAG, + "Error getting package info for SDK sandbox client: " + clientPackage, + e); } sb.append("SdkSandbox: true\n"); } } } - private void appendSdkSandboxClientPackageHeader(StringBuilder sb, String pkg, int userId) { - final IPackageManager pm = AppGlobals.getPackageManager(); - sb.append("SdkSandbox-Client-Package: ").append(pkg); - try { - final PackageInfo pi = pm.getPackageInfo(pkg, 0, userId); - if (pi != null) { - sb.append(" v").append(pi.getLongVersionCode()); - if (pi.versionName != null) { - sb.append(" (").append(pi.versionName).append(")"); - } - } - } catch (RemoteException e) { - Slog.e(TAG, "Error getting package info for SDK sandbox client: " + pkg, e); + private void appendSdkSandboxClientPackageHeader(StringBuilder sb, + PackageInfo clientPackageInfo) { + sb.append("SdkSandbox-Client-Package: ").append(clientPackageInfo.packageName); + sb.append(" v").append(clientPackageInfo.getLongVersionCode()); + if (clientPackageInfo.versionName != null) { + sb.append(" (").append(clientPackageInfo.versionName).append(")"); } sb.append("\n"); } + private void appendSdkSandboxLibraryHeaders(StringBuilder sb, + PackageInfo clientPackageInfo) { + final ApplicationInfo info = clientPackageInfo.applicationInfo; + final List<SharedLibraryInfo> sharedLibraries = info.getSharedLibraryInfos(); + for (int j = 0, size = sharedLibraries.size(); j < size; j++) { + final SharedLibraryInfo sharedLibrary = sharedLibraries.get(j); + if (!sharedLibrary.isSdk()) { + continue; + } + + sb.append("SdkSandbox-Library: ").append(sharedLibrary.getPackageName()); + final VersionedPackage versionedPackage = sharedLibrary.getDeclaringPackage(); + sb.append(" v").append(versionedPackage.getLongVersionCode()); + sb.append("\n"); + } + } + private static String processClass(ProcessRecord process) { if (process == null || process.getPid() == MY_PID) { return "system_server"; @@ -13953,7 +13970,7 @@ public class ActivityManagerService extends IActivityManager.Stub } } else { BroadcastFilter bf = (BroadcastFilter)target; - if (bf.requiredPermission == null) { + if (bf.exported && bf.requiredPermission == null) { allProtected = false; break; } @@ -16958,6 +16975,20 @@ public class ActivityManagerService extends IActivityManager.Stub } @Override + public void killSdkSandboxClientAppProcess(IBinder clientApplicationThreadBinder) { + synchronized (ActivityManagerService.this) { + ProcessRecord r = getRecordForAppLOSP(clientApplicationThreadBinder); + if (r != null) { + r.killLocked( + "sdk sandbox died", + ApplicationExitInfo.REASON_DEPENDENCY_DIED, + ApplicationExitInfo.SUBREASON_SDK_SANDBOX_DIED, + true); + } + } + } + + @Override public void onUserRemoved(@UserIdInt int userId) { // Clean up any ActivityTaskManager state (by telling it the user is stopped) mAtmInternal.onUserStopped(userId); diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 0f7a72590376..78bff9524b10 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -106,6 +106,7 @@ import android.media.IAudioService; import android.media.ICapturePresetDevicesRoleDispatcher; import android.media.ICommunicationDeviceDispatcher; import android.media.IDeviceVolumeBehaviorDispatcher; +import android.media.IDevicesForAttributesCallback; import android.media.IMuteAwaitConnectionCallback; import android.media.IPlaybackConfigDispatcher; import android.media.IPreferredMixerAttributesDispatcher; @@ -3124,6 +3125,25 @@ public class AudioService extends IAudioService.Stub return mAudioSystem.getDevicesForAttributes(attributes, forVolume); } + /** + * @see AudioManager#addOnDevicesForAttributesChangedListener( + * AudioAttributes, Executor, OnDevicesForAttributesChangedListener) + */ + public void addOnDevicesForAttributesChangedListener(AudioAttributes attributes, + IDevicesForAttributesCallback callback) { + mAudioSystem.addOnDevicesForAttributesChangedListener( + attributes, false /* forVolume */, callback); + } + + /** + * @see AudioManager#removeOnDevicesForAttributesChangedListener( + * OnDevicesForAttributesChangedListener) + */ + public void removeOnDevicesForAttributesChangedListener( + IDevicesForAttributesCallback callback) { + mAudioSystem.removeOnDevicesForAttributesChangedListener(callback); + } + // pre-condition: event.getKeyCode() is one of KeyEvent.KEYCODE_VOLUME_UP, // KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_VOLUME_MUTE public void handleVolumeKey(@NonNull KeyEvent event, boolean isOnTv, diff --git a/services/core/java/com/android/server/audio/AudioSystemAdapter.java b/services/core/java/com/android/server/audio/AudioSystemAdapter.java index 7fefc556a02f..7839ada9d5b2 100644 --- a/services/core/java/com/android/server/audio/AudioSystemAdapter.java +++ b/services/core/java/com/android/server/audio/AudioSystemAdapter.java @@ -22,10 +22,15 @@ import android.media.AudioAttributes; import android.media.AudioDeviceAttributes; import android.media.AudioMixerAttributes; import android.media.AudioSystem; +import android.media.IDevicesForAttributesCallback; import android.media.ISoundDose; import android.media.ISoundDoseCallback; import android.media.audiopolicy.AudioMix; +import android.os.IBinder; +import android.os.RemoteCallbackList; +import android.os.RemoteException; import android.os.SystemClock; +import android.util.ArrayMap; import android.util.Log; import android.util.Pair; @@ -64,8 +69,21 @@ public class AudioSystemAdapter implements AudioSystem.RoutingUpdateCallback, private static final boolean USE_CACHE_FOR_GETDEVICES = true; private ConcurrentHashMap<Pair<AudioAttributes, Boolean>, ArrayList<AudioDeviceAttributes>> + mLastDevicesForAttr = new ConcurrentHashMap<>(); + private ConcurrentHashMap<Pair<AudioAttributes, Boolean>, ArrayList<AudioDeviceAttributes>> mDevicesForAttrCache; + private final Object mDeviceCacheLock = new Object(); private int[] mMethodCacheHit; + /** + * Map that stores all attributes + forVolume pairs that are registered for + * routing change callback. The key is the {@link IBinder} that corresponds + * to the remote callback. + */ + private final ArrayMap<IBinder, List<Pair<AudioAttributes, Boolean>>> mRegisteredAttributesMap = + new ArrayMap<>(); + private final RemoteCallbackList<IDevicesForAttributesCallback> + mDevicesForAttributesCallbacks = new RemoteCallbackList<>(); + private static final Object sRoutingListenerLock = new Object(); @GuardedBy("sRoutingListenerLock") private static @Nullable OnRoutingUpdatedListener sRoutingListener; @@ -96,6 +114,34 @@ public class AudioSystemAdapter implements AudioSystem.RoutingUpdateCallback, if (listener != null) { listener.onRoutingUpdatedFromNative(); } + + synchronized (mRegisteredAttributesMap) { + final int nbCallbacks = mDevicesForAttributesCallbacks.beginBroadcast(); + + for (int i = 0; i < nbCallbacks; i++) { + IDevicesForAttributesCallback cb = + mDevicesForAttributesCallbacks.getBroadcastItem(i); + List<Pair<AudioAttributes, Boolean>> attrList = + mRegisteredAttributesMap.get(cb.asBinder()); + + if (attrList == null) { + throw new IllegalStateException("Attribute list must not be null"); + } + + for (Pair<AudioAttributes, Boolean> attr : attrList) { + ArrayList<AudioDeviceAttributes> devices = + getDevicesForAttributes(attr.first, attr.second); + if (!mLastDevicesForAttr.containsKey(attr) + || !sameDeviceList(devices, mLastDevicesForAttr.get(attr))) { + try { + cb.onDevicesForAttributesChanged( + attr.first, attr.second, devices); + } catch (RemoteException e) { } + } + } + } + mDevicesForAttributesCallbacks.finishBroadcast(); + } } interface OnRoutingUpdatedListener { @@ -109,6 +155,66 @@ public class AudioSystemAdapter implements AudioSystem.RoutingUpdateCallback, } /** + * @see AudioManager#addOnDevicesForAttributesChangedListener( + * AudioAttributes, Executor, OnDevicesForAttributesChangedListener) + */ + public void addOnDevicesForAttributesChangedListener(AudioAttributes attributes, + boolean forVolume, @NonNull IDevicesForAttributesCallback listener) { + List<Pair<AudioAttributes, Boolean>> res; + final Pair<AudioAttributes, Boolean> attr = new Pair(attributes, forVolume); + synchronized (mRegisteredAttributesMap) { + res = mRegisteredAttributesMap.get(listener.asBinder()); + if (res == null) { + res = new ArrayList<>(); + mRegisteredAttributesMap.put(listener.asBinder(), res); + mDevicesForAttributesCallbacks.register(listener); + } + + if (!res.contains(attr)) { + res.add(attr); + } + } + + // Make query on registration to populate cache + getDevicesForAttributes(attributes, forVolume); + } + + /** + * @see AudioManager#removeOnDevicesForAttributesChangedListener( + * OnDevicesForAttributesChangedListener) + */ + public void removeOnDevicesForAttributesChangedListener( + @NonNull IDevicesForAttributesCallback listener) { + synchronized (mRegisteredAttributesMap) { + if (!mRegisteredAttributesMap.containsKey(listener.asBinder())) { + Log.w(TAG, "listener to be removed is not found."); + return; + } + mRegisteredAttributesMap.remove(listener.asBinder()); + mDevicesForAttributesCallbacks.unregister(listener); + } + } + + /** + * Helper function to compare lists of {@link AudioDeviceAttributes} + * @return true if the two lists contains the same devices, false otherwise. + */ + private boolean sameDeviceList(@NonNull List<AudioDeviceAttributes> a, + @NonNull List<AudioDeviceAttributes> b) { + for (AudioDeviceAttributes device : a) { + if (!b.contains(device)) { + return false; + } + } + for (AudioDeviceAttributes device : b) { + if (!a.contains(device)) { + return false; + } + } + return true; + } + + /** * Implementation of AudioSystem.VolumeRangeInitRequestCallback */ @Override @@ -159,8 +265,11 @@ public class AudioSystemAdapter implements AudioSystem.RoutingUpdateCallback, if (DEBUG_CACHE) { Log.d(TAG, "---- clearing cache ----------"); } - if (mDevicesForAttrCache != null) { - synchronized (mDevicesForAttrCache) { + synchronized (mDeviceCacheLock) { + if (mDevicesForAttrCache != null) { + // Save latest cache to determine routing updates + mLastDevicesForAttr.putAll(mDevicesForAttrCache); + mDevicesForAttrCache.clear(); } } @@ -189,7 +298,7 @@ public class AudioSystemAdapter implements AudioSystem.RoutingUpdateCallback, if (USE_CACHE_FOR_GETDEVICES) { ArrayList<AudioDeviceAttributes> res; final Pair<AudioAttributes, Boolean> key = new Pair(attributes, forVolume); - synchronized (mDevicesForAttrCache) { + synchronized (mDeviceCacheLock) { res = mDevicesForAttrCache.get(key); if (res == null) { res = AudioSystem.getDevicesForAttributes(attributes, forVolume); diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index 5e9f0e7bf5bd..110eb1ec214e 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -628,7 +628,6 @@ public final class DisplayManagerService extends SystemService { recordTopInsetLocked(mLogicalDisplayMapper.getDisplayLocked(Display.DEFAULT_DISPLAY)); updateSettingsLocked(); - updateUserDisabledHdrTypesFromSettingsLocked(); updateUserPreferredDisplayModeSettingsLocked(); } @@ -852,6 +851,15 @@ public final class DisplayManagerService extends SystemService { for (int i = 0; i < userDisabledHdrTypeStrings.length; i++) { mUserDisabledHdrTypes[i] = Integer.parseInt(userDisabledHdrTypeStrings[i]); } + + if (!mAreUserDisabledHdrTypesAllowed) { + mLogicalDisplayMapper.forEachLocked( + display -> { + display.setUserDisabledHdrTypes(mUserDisabledHdrTypes); + handleLogicalDisplayChangedLocked(display); + }); + } + } catch (NumberFormatException e) { Slog.e(TAG, "Failed to parse USER_DISABLED_HDR_FORMATS. " + "Clearing the setting.", e); @@ -879,6 +887,15 @@ public final class DisplayManagerService extends SystemService { Settings.Global.USER_PREFERRED_RESOLUTION_WIDTH, Display.INVALID_DISPLAY_WIDTH); Display.Mode mode = new Display.Mode(width, height, refreshRate); mUserPreferredMode = isResolutionAndRefreshRateValid(mode) ? mode : null; + if (mUserPreferredMode != null) { + mDisplayDeviceRepo.forEachLocked((DisplayDevice device) -> { + device.setUserPreferredDisplayModeLocked(mode); + }); + } else { + mLogicalDisplayMapper.forEachLocked((LogicalDisplay display) -> { + configurePreferredDisplayModeLocked(display); + }); + } } private DisplayInfo getDisplayInfoForFrameRateOverride(DisplayEventReceiver.FrameRateOverride[] diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java index f4eed2b47ec4..602059ad90e0 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController.java +++ b/services/core/java/com/android/server/display/DisplayPowerController.java @@ -852,6 +852,10 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call mAutomaticBrightnessController.stop(); } + if (mScreenOffBrightnessSensorController != null) { + mScreenOffBrightnessSensorController.stop(); + } + if (mBrightnessSetting != null) { mBrightnessSetting.unregisterListener(mBrightnessSettingListener); } @@ -1093,6 +1097,9 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call mBrightnessEventRingBuffer = new RingBuffer<>(BrightnessEvent.class, RINGBUFFER_MAX); + if (mScreenOffBrightnessSensorController != null) { + mScreenOffBrightnessSensorController.stop(); + } loadScreenOffBrightnessSensor(); int[] sensorValueToLux = mDisplayDeviceConfig.getScreenOffBrightnessSensorValueToLux(); if (mScreenOffBrightnessSensor != null && sensorValueToLux != null) { @@ -2707,6 +2714,10 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call dumpBrightnessEvents(pw); } + if (mScreenOffBrightnessSensorController != null) { + mScreenOffBrightnessSensorController.dump(pw); + } + if (mHbmController != null) { mHbmController.dump(pw); } diff --git a/services/core/java/com/android/server/display/DisplayPowerController2.java b/services/core/java/com/android/server/display/DisplayPowerController2.java index 09136b096653..bb8132f33fc5 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController2.java +++ b/services/core/java/com/android/server/display/DisplayPowerController2.java @@ -2330,6 +2330,11 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal if (mDisplayBrightnessController != null) { mDisplayBrightnessController.dump(pw); } + + pw.println(); + if (mDisplayStateController != null) { + mDisplayStateController.dumpsys(pw); + } } diff --git a/services/core/java/com/android/server/display/ScreenOffBrightnessSensorController.java b/services/core/java/com/android/server/display/ScreenOffBrightnessSensorController.java index 6f50dac07b99..42defac5bab8 100644 --- a/services/core/java/com/android/server/display/ScreenOffBrightnessSensorController.java +++ b/services/core/java/com/android/server/display/ScreenOffBrightnessSensorController.java @@ -92,6 +92,10 @@ public class ScreenOffBrightnessSensorController implements SensorEventListener } } + void stop() { + setLightSensorEnabled(false); + } + float getAutomaticScreenBrightness() { if (mLastSensorValue < 0 || mLastSensorValue >= mSensorValueToLux.length || (!mRegistered @@ -109,7 +113,7 @@ public class ScreenOffBrightnessSensorController implements SensorEventListener /** Dump current state */ public void dump(PrintWriter pw) { - pw.println("ScreenOffBrightnessSensorController:"); + pw.println("Screen Off Brightness Sensor Controller:"); IndentingPrintWriter idpw = new IndentingPrintWriter(pw); idpw.increaseIndent(); idpw.println("registered=" + mRegistered); diff --git a/services/core/java/com/android/server/display/state/DisplayStateController.java b/services/core/java/com/android/server/display/state/DisplayStateController.java index 546478e480e0..b1a1c601cc0c 100644 --- a/services/core/java/com/android/server/display/state/DisplayStateController.java +++ b/services/core/java/com/android/server/display/state/DisplayStateController.java @@ -98,7 +98,7 @@ public class DisplayStateController { */ public void dumpsys(PrintWriter pw) { pw.println(); - pw.println("DisplayPowerProximityStateController:"); + pw.println("DisplayStateController:"); pw.println(" mPerformScreenOffTransition:" + mPerformScreenOffTransition); IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " "); if (mDisplayPowerProximityStateController != null) { diff --git a/services/core/java/com/android/server/grammaticalinflection/OWNERS b/services/core/java/com/android/server/grammaticalinflection/OWNERS new file mode 100644 index 000000000000..5f16ba9123b7 --- /dev/null +++ b/services/core/java/com/android/server/grammaticalinflection/OWNERS @@ -0,0 +1,4 @@ +# Bug template url: https://b.corp.google.com/issues/new?component=1082762&template=1601534 +allenwtsu@google.com +goldmanj@google.com +calvinpan@google.com diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index e1e99a1cb3b7..8dc2f52c5839 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -44,6 +44,7 @@ import android.hardware.input.IInputDeviceBatteryState; import android.hardware.input.IInputDevicesChangedListener; import android.hardware.input.IInputManager; import android.hardware.input.IInputSensorEventListener; +import android.hardware.input.IKeyboardBacklightListener; import android.hardware.input.ITabletModeChangedListener; import android.hardware.input.InputDeviceIdentifier; import android.hardware.input.InputManager; @@ -2227,6 +2228,24 @@ public class InputManagerService extends IInputManager.Stub } @Override + @EnforcePermission(Manifest.permission.MONITOR_KEYBOARD_BACKLIGHT) + public void registerKeyboardBacklightListener(IKeyboardBacklightListener listener) { + super.registerKeyboardBacklightListener_enforcePermission(); + Objects.requireNonNull(listener); + mKeyboardBacklightController.registerKeyboardBacklightListener(listener, + Binder.getCallingPid()); + } + + @Override + @EnforcePermission(Manifest.permission.MONITOR_KEYBOARD_BACKLIGHT) + public void unregisterKeyboardBacklightListener(IKeyboardBacklightListener listener) { + super.unregisterKeyboardBacklightListener_enforcePermission(); + Objects.requireNonNull(listener); + mKeyboardBacklightController.unregisterKeyboardBacklightListener(listener, + Binder.getCallingPid()); + } + + @Override public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return; IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " "); diff --git a/services/core/java/com/android/server/input/KeyboardBacklightController.java b/services/core/java/com/android/server/input/KeyboardBacklightController.java index b207e27b4005..77b0d4f39ae3 100644 --- a/services/core/java/com/android/server/input/KeyboardBacklightController.java +++ b/services/core/java/com/android/server/input/KeyboardBacklightController.java @@ -16,14 +16,19 @@ package com.android.server.input; +import android.annotation.BinderThread; import android.annotation.ColorInt; import android.content.Context; import android.graphics.Color; +import android.hardware.input.IKeyboardBacklightListener; +import android.hardware.input.IKeyboardBacklightState; import android.hardware.input.InputManager; import android.hardware.lights.Light; import android.os.Handler; +import android.os.IBinder; import android.os.Looper; import android.os.Message; +import android.os.RemoteException; import android.util.IndentingPrintWriter; import android.util.Log; import android.util.Slog; @@ -68,6 +73,11 @@ final class KeyboardBacklightController implements InputManager.InputDeviceListe private final Handler mHandler; private final SparseArray<Light> mKeyboardBacklights = new SparseArray<>(); + // List of currently registered keyboard backlight listeners + @GuardedBy("mKeyboardBacklightListenerRecords") + private final SparseArray<KeyboardBacklightListenerRecord> mKeyboardBacklightListenerRecords = + new SparseArray<>(); + static { // Fixed brightness levels to avoid issues when converting back and forth from the // device brightness range to [0-255] @@ -129,6 +139,9 @@ final class KeyboardBacklightController implements InputManager.InputDeviceListe Slog.d(TAG, "Changing brightness from " + currBrightness + " to " + newBrightness); } + notifyKeyboardBacklightChanged(deviceId, BRIGHTNESS_LEVELS.headSet(newBrightness).size(), + true/* isTriggeredByKeyPress */); + synchronized (mDataStore) { try { mDataStore.setKeyboardBacklightBrightness(inputDevice.getDescriptor(), @@ -217,6 +230,62 @@ final class KeyboardBacklightController implements InputManager.InputDeviceListe return null; } + /** Register the keyboard backlight listener for a process. */ + @BinderThread + public void registerKeyboardBacklightListener(IKeyboardBacklightListener listener, + int pid) { + synchronized (mKeyboardBacklightListenerRecords) { + if (mKeyboardBacklightListenerRecords.get(pid) != null) { + throw new IllegalStateException("The calling process has already registered " + + "a KeyboardBacklightListener."); + } + KeyboardBacklightListenerRecord record = new KeyboardBacklightListenerRecord(pid, + listener); + try { + listener.asBinder().linkToDeath(record, 0); + } catch (RemoteException ex) { + throw new RuntimeException(ex); + } + mKeyboardBacklightListenerRecords.put(pid, record); + } + } + + /** Unregister the keyboard backlight listener for a process. */ + @BinderThread + public void unregisterKeyboardBacklightListener(IKeyboardBacklightListener listener, + int pid) { + synchronized (mKeyboardBacklightListenerRecords) { + KeyboardBacklightListenerRecord record = mKeyboardBacklightListenerRecords.get(pid); + if (record == null) { + throw new IllegalStateException("The calling process has no registered " + + "KeyboardBacklightListener."); + } + if (record.mListener != listener) { + throw new IllegalStateException("The calling process has a different registered " + + "KeyboardBacklightListener."); + } + record.mListener.asBinder().unlinkToDeath(record, 0); + mKeyboardBacklightListenerRecords.remove(pid); + } + } + + private void notifyKeyboardBacklightChanged(int deviceId, int currentBacklightLevel, + boolean isTriggeredByKeyPress) { + synchronized (mKeyboardBacklightListenerRecords) { + for (int i = 0; i < mKeyboardBacklightListenerRecords.size(); i++) { + mKeyboardBacklightListenerRecords.valueAt(i).notifyKeyboardBacklightChanged( + deviceId, new KeyboardBacklightState(currentBacklightLevel), + isTriggeredByKeyPress); + } + } + } + + private void onKeyboardBacklightListenerDied(int pid) { + synchronized (mKeyboardBacklightListenerRecords) { + mKeyboardBacklightListenerRecords.remove(pid); + } + } + void dump(PrintWriter pw) { IndentingPrintWriter ipw = new IndentingPrintWriter(pw); ipw.println(TAG + ": " + mKeyboardBacklights.size() + " keyboard backlights"); @@ -227,4 +296,49 @@ final class KeyboardBacklightController implements InputManager.InputDeviceListe } ipw.decreaseIndent(); } + + // A record of a registered Keyboard backlight listener from one process. + private class KeyboardBacklightListenerRecord implements IBinder.DeathRecipient { + public final int mPid; + public final IKeyboardBacklightListener mListener; + + KeyboardBacklightListenerRecord(int pid, IKeyboardBacklightListener listener) { + mPid = pid; + mListener = listener; + } + + @Override + public void binderDied() { + if (DEBUG) { + Slog.d(TAG, "Keyboard backlight listener for pid " + mPid + " died."); + } + onKeyboardBacklightListenerDied(mPid); + } + + public void notifyKeyboardBacklightChanged(int deviceId, IKeyboardBacklightState state, + boolean isTriggeredByKeyPress) { + try { + mListener.onBrightnessChanged(deviceId, state, isTriggeredByKeyPress); + } catch (RemoteException ex) { + Slog.w(TAG, "Failed to notify process " + mPid + + " that keyboard backlight changed, assuming it died.", ex); + binderDied(); + } + } + } + + private static class KeyboardBacklightState extends IKeyboardBacklightState { + + KeyboardBacklightState(int brightnessLevel) { + this.brightnessLevel = brightnessLevel; + this.maxBrightnessLevel = NUM_BRIGHTNESS_CHANGE_STEPS; + } + + @Override + public String toString() { + return "KeyboardBacklightState{brightnessLevel=" + brightnessLevel + + ", maxBrightnessLevel=" + maxBrightnessLevel + + "}"; + } + } } diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index c15b538ce605..5b9a6639bff6 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -48,12 +48,12 @@ 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.InputMethodBindingController.TIME_TO_RECONNECT; import static com.android.server.inputmethod.InputMethodUtils.isSoftInputModeStateVisibleAllowed; -import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_IME_VISIBILITY; import static java.lang.annotation.RetentionPolicy.SOURCE; diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java index 1998af649070..cc485baef0c9 100644 --- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java +++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java @@ -29,6 +29,7 @@ import android.Manifest; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; +import android.app.ActivityThread; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -51,6 +52,7 @@ import android.os.Looper; import android.os.PowerManager; import android.os.RemoteException; import android.os.UserHandle; +import android.provider.DeviceConfig; import android.text.TextUtils; import android.util.ArrayMap; import android.util.Log; @@ -89,10 +91,19 @@ class MediaRouter2ServiceImpl { // TODO: (In Android S or later) if we add callback methods for generic failures // in MediaRouter2, remove this constant and replace the usages with the real request IDs. private static final long DUMMY_REQUEST_ID = -1; - private static final int PACKAGE_IMPORTANCE_FOR_DISCOVERY = IMPORTANCE_FOREGROUND_SERVICE; private static final int DUMP_EVENTS_MAX_COUNT = 70; + private static final String MEDIA_BETTER_TOGETHER_NAMESPACE = "media_better_together"; + + private static final String KEY_SCANNING_PACKAGE_MINIMUM_IMPORTANCE = + "scanning_package_minimum_importance"; + + private static int sPackageImportanceForScanning = DeviceConfig.getInt( + MEDIA_BETTER_TOGETHER_NAMESPACE, + /* name */ KEY_SCANNING_PACKAGE_MINIMUM_IMPORTANCE, + /* defaultValue */ IMPORTANCE_FOREGROUND_SERVICE); + private final Context mContext; private final UserManagerInternal mUserManagerInternal; private final Object mLock = new Object(); @@ -140,7 +151,7 @@ class MediaRouter2ServiceImpl { mContext = context; mActivityManager = mContext.getSystemService(ActivityManager.class); mActivityManager.addOnUidImportanceListener(mOnUidImportanceListener, - PACKAGE_IMPORTANCE_FOR_DISCOVERY); + sPackageImportanceForScanning); mPowerManager = mContext.getSystemService(PowerManager.class); mUserManagerInternal = LocalServices.getService(UserManagerInternal.class); @@ -149,15 +160,26 @@ class MediaRouter2ServiceImpl { screenOnOffIntentFilter.addAction(ACTION_SCREEN_OFF); mContext.registerReceiver(mScreenOnOffReceiver, screenOnOffIntentFilter); + + DeviceConfig.addOnPropertiesChangedListener(MEDIA_BETTER_TOGETHER_NAMESPACE, + ActivityThread.currentApplication().getMainExecutor(), + this::onDeviceConfigChange); } // Start of methods that implement MediaRouter2 operations. @NonNull - public boolean verifyPackageName(@NonNull String clientPackageName) { + public boolean verifyPackageExists(@NonNull String clientPackageName) { + final int pid = Binder.getCallingPid(); + final int uid = Binder.getCallingUid(); final long token = Binder.clearCallingIdentity(); try { + mContext.enforcePermission( + Manifest.permission.MEDIA_CONTENT_CONTROL, + pid, + uid, + "Must hold MEDIA_CONTENT_CONTROL permission."); PackageManager pm = mContext.getPackageManager(); pm.getPackageInfo(clientPackageName, PackageManager.PackageInfoFlags.of(0)); return true; @@ -169,20 +191,6 @@ class MediaRouter2ServiceImpl { } @NonNull - public void enforceMediaContentControlPermission() { - final int pid = Binder.getCallingPid(); - final int uid = Binder.getCallingUid(); - final long token = Binder.clearCallingIdentity(); - - try { - mContext.enforcePermission(Manifest.permission.MEDIA_CONTENT_CONTROL, pid, uid, - "Must hold MEDIA_CONTENT_CONTROL permission."); - } finally { - Binder.restoreCallingIdentity(token); - } - } - - @NonNull public List<MediaRoute2Info> getSystemRoutes() { final int uid = Binder.getCallingUid(); final int userId = UserHandle.getUserHandleForUid(uid).getIdentifier(); @@ -1386,6 +1394,12 @@ class MediaRouter2ServiceImpl { // End of locked methods that are used by both MediaRouter2 and MediaRouter2Manager. + private void onDeviceConfigChange(@NonNull DeviceConfig.Properties properties) { + sPackageImportanceForScanning = properties.getInt( + /* name */ KEY_SCANNING_PACKAGE_MINIMUM_IMPORTANCE, + /* defaultValue */ IMPORTANCE_FOREGROUND_SERVICE); + } + static long toUniqueRequestId(int requesterId, int originalRequestId) { return ((long) requesterId << 32) | originalRequestId; } @@ -1938,12 +1952,12 @@ class MediaRouter2ServiceImpl { @NonNull RoutingSessionInfo oldSession, @NonNull MediaRoute2Info route) { try { if (route.isSystemRoute() && !routerRecord.mHasModifyAudioRoutingPermission) { - routerRecord.mRouter.requestCreateSessionByManager(uniqueRequestId, - oldSession, mSystemProvider.getDefaultRoute()); - } else { - routerRecord.mRouter.requestCreateSessionByManager(uniqueRequestId, - oldSession, route); + // The router lacks permission to modify system routing, so we hide system + // route info from them. + route = mSystemProvider.getDefaultRoute(); } + routerRecord.mRouter.requestCreateSessionByManager( + uniqueRequestId, oldSession, route); } catch (RemoteException ex) { Slog.w(TAG, "getSessionHintsForCreatingSessionOnHandler: " + "Failed to request. Router probably died.", ex); @@ -2563,7 +2577,7 @@ class MediaRouter2ServiceImpl { isManagerScanning = managerRecords.stream().anyMatch(manager -> manager.mIsScanning && service.mActivityManager .getPackageImportance(manager.mPackageName) - <= PACKAGE_IMPORTANCE_FOR_DISCOVERY); + <= sPackageImportanceForScanning); if (isManagerScanning) { discoveryPreferences = routerRecords.stream() @@ -2572,7 +2586,7 @@ class MediaRouter2ServiceImpl { } else { discoveryPreferences = routerRecords.stream().filter(record -> service.mActivityManager.getPackageImportance(record.mPackageName) - <= PACKAGE_IMPORTANCE_FOR_DISCOVERY) + <= sPackageImportanceForScanning) .map(record -> record.mDiscoveryPreference) .collect(Collectors.toList()); } diff --git a/services/core/java/com/android/server/media/MediaRouterService.java b/services/core/java/com/android/server/media/MediaRouterService.java index ad82e1c786e3..3ad0e44e6ea3 100644 --- a/services/core/java/com/android/server/media/MediaRouterService.java +++ b/services/core/java/com/android/server/media/MediaRouterService.java @@ -380,14 +380,8 @@ public final class MediaRouterService extends IMediaRouterService.Stub // Binder call @Override - public boolean verifyPackageName(String clientPackageName) { - return mService2.verifyPackageName(clientPackageName); - } - - // Binder call - @Override - public void enforceMediaContentControlPermission() { - mService2.enforceMediaContentControlPermission(); + public boolean verifyPackageExists(String clientPackageName) { + return mService2.verifyPackageExists(clientPackageName); } // Binder call diff --git a/services/core/java/com/android/server/pm/AppStateHelper.java b/services/core/java/com/android/server/pm/AppStateHelper.java index 32479ee459ff..1a2b01eb11c5 100644 --- a/services/core/java/com/android/server/pm/AppStateHelper.java +++ b/services/core/java/com/android/server/pm/AppStateHelper.java @@ -37,7 +37,7 @@ import com.android.internal.util.ArrayUtils; import com.android.server.LocalServices; import java.util.ArrayList; -import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; @@ -159,13 +159,21 @@ public class AppStateHelper { return false; } - private static boolean containsAny(Collection<String> list, Collection<String> which) { - if (list.isEmpty()) { - return false; - } - for (var element : which) { - if (list.contains(element)) { + /** + * True if {@code arr} contains any element in {@code which}. + * Both {@code arr} and {@code which} must be sorted in advance. + */ + private static boolean containsAny(String[] arr, List<String> which) { + int s1 = arr.length; + int s2 = which.size(); + for (int i = 0, j = 0; i < s1 && j < s2; ) { + int val = arr[i].compareTo(which.get(j)); + if (val == 0) { return true; + } else if (val < 0) { + ++i; + } else { + ++j; } } return false; @@ -174,9 +182,9 @@ public class AppStateHelper { private void addLibraryDependency(ArraySet<String> results, List<String> libPackageNames) { var pmInternal = LocalServices.getService(PackageManagerInternal.class); - var libraryNames = new ArraySet<String>(); - var staticSharedLibraryNames = new ArraySet<String>(); - var sdkLibraryNames = new ArraySet<String>(); + var libraryNames = new ArrayList<String>(); + var staticSharedLibraryNames = new ArrayList<String>(); + var sdkLibraryNames = new ArrayList<String>(); for (var packageName : libPackageNames) { var pkg = pmInternal.getAndroidPackage(packageName); if (pkg == null) { @@ -199,11 +207,19 @@ public class AppStateHelper { return; } - pmInternal.forEachPackage(pkg -> { - if (containsAny(pkg.getUsesLibraries(), libraryNames) - || containsAny(pkg.getUsesOptionalLibraries(), libraryNames) - || containsAny(pkg.getUsesStaticLibraries(), staticSharedLibraryNames) - || containsAny(pkg.getUsesSdkLibraries(), sdkLibraryNames)) { + Collections.sort(libraryNames); + Collections.sort(sdkLibraryNames); + Collections.sort(staticSharedLibraryNames); + + pmInternal.forEachPackageState(pkgState -> { + var pkg = pkgState.getPkg(); + if (pkg == null) { + return; + } + if (containsAny(pkg.getUsesLibrariesSorted(), libraryNames) + || containsAny(pkg.getUsesOptionalLibrariesSorted(), libraryNames) + || containsAny(pkg.getUsesStaticLibrariesSorted(), staticSharedLibraryNames) + || containsAny(pkg.getUsesSdkLibrariesSorted(), sdkLibraryNames)) { results.add(pkg.getPackageName()); } }); diff --git a/services/core/java/com/android/server/pm/AppsFilterBase.java b/services/core/java/com/android/server/pm/AppsFilterBase.java index 1021e07cf946..12f6a1816e3e 100644 --- a/services/core/java/com/android/server/pm/AppsFilterBase.java +++ b/services/core/java/com/android/server/pm/AppsFilterBase.java @@ -399,6 +399,24 @@ public abstract class AppsFilterBase implements AppsFilterSnapshot { Slog.wtf(TAG, "No setting found for non system uid " + callingUid); return true; } + + if (DEBUG_TRACING) { + Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "getAppId"); + } + final int callingAppId = UserHandle.getAppId(callingUid); + final int targetAppId = targetPkgSetting.getAppId(); + if (DEBUG_TRACING) { + Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER); + } + if (callingAppId == targetAppId + || callingAppId < Process.FIRST_APPLICATION_UID + || targetAppId < Process.FIRST_APPLICATION_UID) { + if (DEBUG_LOGGING) { + log(callingSetting, targetPkgSetting, "same app id or core app id"); + } + return false; + } + final PackageStateInternal callingPkgSetting; if (DEBUG_TRACING) { Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "callingSetting instanceof"); @@ -446,27 +464,6 @@ public abstract class AppsFilterBase implements AppsFilterSnapshot { } } - if (DEBUG_TRACING) { - Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "getAppId"); - } - final int callingAppId; - if (callingPkgSetting != null) { - callingAppId = callingPkgSetting.getAppId(); - } else { - // all should be the same - callingAppId = callingSharedPkgSettings.valueAt(0).getAppId(); - } - final int targetAppId = targetPkgSetting.getAppId(); - if (DEBUG_TRACING) { - Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER); - } - if (callingAppId == targetAppId) { - if (DEBUG_LOGGING) { - log(callingSetting, targetPkgSetting, "same app id"); - } - return false; - } - try { if (DEBUG_TRACING) { Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "requestsQueryAllPackages"); diff --git a/services/core/java/com/android/server/pm/BackgroundDexOptService.java b/services/core/java/com/android/server/pm/BackgroundDexOptService.java index cda7503400c7..fe122f87a423 100644 --- a/services/core/java/com/android/server/pm/BackgroundDexOptService.java +++ b/services/core/java/com/android/server/pm/BackgroundDexOptService.java @@ -614,11 +614,14 @@ public final class BackgroundDexOptService { size += getDirectorySize(path); if (!ArrayUtils.isEmpty(info.applicationInfo.splitSourceDirs)) { for (String splitSourceDir : info.applicationInfo.splitSourceDirs) { - path = Paths.get(splitSourceDir).toFile(); - if (path.isFile()) { - path = path.getParentFile(); + File pathSplitSourceDir = Paths.get(splitSourceDir).toFile(); + if (pathSplitSourceDir.isFile()) { + pathSplitSourceDir = pathSplitSourceDir.getParentFile(); } - size += getDirectorySize(path); + if (path.getAbsolutePath().equals(pathSplitSourceDir.getAbsolutePath())) { + continue; + } + size += getDirectorySize(pathSplitSourceDir); } } return size; diff --git a/services/core/java/com/android/server/pm/parsing/pkg/AndroidPackageInternal.java b/services/core/java/com/android/server/pm/parsing/pkg/AndroidPackageInternal.java index 8d43fe7b5f27..9eca7d651dea 100644 --- a/services/core/java/com/android/server/pm/parsing/pkg/AndroidPackageInternal.java +++ b/services/core/java/com/android/server/pm/parsing/pkg/AndroidPackageInternal.java @@ -16,6 +16,8 @@ package com.android.server.pm.parsing.pkg; +import android.annotation.NonNull; + import com.android.internal.content.om.OverlayConfig; import com.android.server.pm.pkg.AndroidPackage; @@ -31,5 +33,15 @@ import com.android.server.pm.pkg.AndroidPackage; */ public interface AndroidPackageInternal extends AndroidPackage, OverlayConfig.PackageProvider.Package { + @NonNull + String[] getUsesLibrariesSorted(); + + @NonNull + String[] getUsesOptionalLibrariesSorted(); + + @NonNull + String[] getUsesSdkLibrariesSorted(); + @NonNull + String[] getUsesStaticLibrariesSorted(); } 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 e361c9332972..ed9382b42206 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 @@ -93,6 +93,7 @@ import libcore.util.EmptyArray; import java.io.File; import java.security.PublicKey; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; @@ -405,6 +406,15 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, private List<AndroidPackageSplit> mSplits; @NonNull + private String[] mUsesLibrariesSorted; + @NonNull + private String[] mUsesOptionalLibrariesSorted; + @NonNull + private String[] mUsesSdkLibrariesSorted; + @NonNull + private String[] mUsesStaticLibrariesSorted; + + @NonNull public static PackageImpl forParsing(@NonNull String packageName, @NonNull String baseCodePath, @NonNull String codePath, @NonNull TypedArray manifestArray, boolean isCoreApp) { return new PackageImpl(packageName, baseCodePath, codePath, manifestArray, isCoreApp); @@ -1379,6 +1389,19 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, @NonNull @Override + public String[] getUsesLibrariesSorted() { + if (mUsesLibrariesSorted == null) { + // Note lazy-sorting here doesn't break immutability because it always + // return the same content. In the case of multi-threading, data race in accessing + // mUsesLibrariesSorted might result in unnecessary creation of sorted copies + // which is OK because the case is quite rare. + mUsesLibrariesSorted = sortLibraries(usesLibraries); + } + return mUsesLibrariesSorted; + } + + @NonNull + @Override public List<String> getUsesNativeLibraries() { return usesNativeLibraries; } @@ -1391,6 +1414,15 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, @NonNull @Override + public String[] getUsesOptionalLibrariesSorted() { + if (mUsesOptionalLibrariesSorted == null) { + mUsesOptionalLibrariesSorted = sortLibraries(usesOptionalLibraries); + } + return mUsesOptionalLibrariesSorted; + } + + @NonNull + @Override public List<String> getUsesOptionalNativeLibraries() { return usesOptionalNativeLibraries; } @@ -1405,6 +1437,15 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, @Override public List<String> getUsesSdkLibraries() { return usesSdkLibraries; } + @NonNull + @Override + public String[] getUsesSdkLibrariesSorted() { + if (mUsesSdkLibrariesSorted == null) { + mUsesSdkLibrariesSorted = sortLibraries(usesSdkLibraries); + } + return mUsesSdkLibrariesSorted; + } + @Nullable @Override public String[][] getUsesSdkLibrariesCertDigests() { return usesSdkLibrariesCertDigests; } @@ -1419,6 +1460,15 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, return usesStaticLibraries; } + @NonNull + @Override + public String[] getUsesStaticLibrariesSorted() { + if (mUsesStaticLibrariesSorted == null) { + mUsesStaticLibrariesSorted = sortLibraries(usesStaticLibraries); + } + return mUsesStaticLibrariesSorted; + } + @Nullable @Override public String[][] getUsesStaticLibrariesCertDigests() { @@ -2650,6 +2700,16 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, return this; } + private static String[] sortLibraries(List<String> libraryNames) { + int size = libraryNames.size(); + if (size == 0) { + return EmptyArray.STRING; + } + var arr = libraryNames.toArray(EmptyArray.STRING); + Arrays.sort(arr); + return arr; + } + private void assignDerivedFields2() { mBaseAppInfoFlags = PackageInfoUtils.appInfoFlags(this, null); mBaseAppInfoPrivateFlags = PackageInfoUtils.appInfoPrivateFlags(this, null); diff --git a/services/core/java/com/android/server/security/FileIntegrityService.java b/services/core/java/com/android/server/security/FileIntegrityService.java index 5ae697315ed1..6c0e1a43f938 100644 --- a/services/core/java/com/android/server/security/FileIntegrityService.java +++ b/services/core/java/com/android/server/security/FileIntegrityService.java @@ -18,6 +18,7 @@ package com.android.server.security; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SystemApi; import android.app.AppOpsManager; import android.content.Context; import android.content.pm.PackageManager; @@ -59,6 +60,7 @@ import java.util.ArrayList; * A {@link SystemService} that provides file integrity related operations. * @hide */ +@SystemApi(client = SystemApi.Client.SYSTEM_SERVER) public class FileIntegrityService extends SystemService { private static final String TAG = "FileIntegrityService"; @@ -71,7 +73,10 @@ public class FileIntegrityService extends SystemService { private final ArrayList<X509Certificate> mTrustedCertificates = new ArrayList<X509Certificate>(); - /** Gets the instance of the service */ + /** + * Gets the instance of the service. + * @hide + */ public static FileIntegrityService getService() { return LocalServices.getService(FileIntegrityService.class); } @@ -139,6 +144,7 @@ public class FileIntegrityService extends SystemService { } }; + /** @hide */ public FileIntegrityService(final Context context) { super(context); try { @@ -149,6 +155,7 @@ public class FileIntegrityService extends SystemService { LocalServices.addService(FileIntegrityService.class, this); } + /** @hide */ @Override public void onStart() { loadAllCertificates(); @@ -158,6 +165,7 @@ public class FileIntegrityService extends SystemService { /** * Returns whether the signature over the file's fs-verity digest can be verified by one of the * known certiticates. + * @hide */ public boolean verifyPkcs7DetachedSignature(String signaturePath, String filePath) throws IOException { @@ -183,6 +191,16 @@ public class FileIntegrityService extends SystemService { return false; } + /** + * Enables fs-verity, if supported by the filesystem. + * @see <a href="https://www.kernel.org/doc/html/latest/filesystems/fsverity.html"> + * @hide + */ + @SystemApi(client = SystemApi.Client.SYSTEM_SERVER) + public static void setUpFsVerity(@NonNull String filePath) throws IOException { + VerityUtils.setUpFsverity(filePath); + } + private void loadAllCertificates() { // A better alternative to load certificates would be to read from .fs-verity kernel // keyring, which fsverity_init loads to during earlier boot time from the same sources diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java index 8613b5027d57..966329e486f9 100644 --- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java +++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java @@ -1749,7 +1749,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { setExternalControl(true, vibHolder.stats); } if (DEBUG) { - Slog.e(TAG, "Playing external vibration: " + vib); + Slog.d(TAG, "Playing external vibration: " + vib); } // Vibrator will start receiving data from external channels after this point. // Report current time as the vibration start time, for debugging. @@ -1763,7 +1763,7 @@ public class VibratorManagerService extends IVibratorManagerService.Stub { if (mCurrentExternalVibration != null && mCurrentExternalVibration.isHoldingSameVibration(vib)) { if (DEBUG) { - Slog.e(TAG, "Stopping external vibration" + vib); + Slog.d(TAG, "Stopping external vibration: " + vib); } endExternalVibrateLocked( new Vibration.EndInfo(Vibration.Status.FINISHED), diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index fcb135e3c0d4..11013e84c732 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -1451,8 +1451,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A updatePictureInPictureMode(null, false); } else { mLastReportedMultiWindowMode = inMultiWindowMode; - ensureActivityConfiguration(0 /* globalChanges */, PRESERVE_WINDOWS, - false /* ignoreVisibility */); + ensureActivityConfiguration(0 /* globalChanges */, PRESERVE_WINDOWS); } } } @@ -3981,6 +3980,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } void finishRelaunching() { + mLetterboxUiController.setRelauchingAfterRequestedOrientationChanged(false); mTaskSupervisor.getActivityMetricsLogger().notifyActivityRelaunched(this); if (mPendingRelaunchCount > 0) { @@ -7724,13 +7724,17 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } void setRequestedOrientation(int requestedOrientation) { + if (mLetterboxUiController.shouldIgnoreRequestedOrientation(requestedOrientation)) { + return; + } setOrientation(requestedOrientation, this); // Push the new configuration to the requested app in case where it's not pushed, e.g. when // the request is handled at task level with letterbox. if (!getMergedOverrideConfiguration().equals( mLastReportedConfiguration.getMergedConfiguration())) { - ensureActivityConfiguration(0 /* globalChanges */, false /* preserveWindow */); + ensureActivityConfiguration(0 /* globalChanges */, false /* preserveWindow */, + false /* ignoreVisibility */, true /* isRequestedOrientationChanged */); } mAtmService.getTaskChangeNotificationController().notifyActivityRequestedOrientationChanged( @@ -9060,7 +9064,13 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A boolean ensureActivityConfiguration(int globalChanges, boolean preserveWindow) { return ensureActivityConfiguration(globalChanges, preserveWindow, - false /* ignoreVisibility */); + false /* ignoreVisibility */, false /* isRequestedOrientationChanged */); + } + + boolean ensureActivityConfiguration(int globalChanges, boolean preserveWindow, + boolean ignoreVisibility) { + return ensureActivityConfiguration(globalChanges, preserveWindow, ignoreVisibility, + false /* isRequestedOrientationChanged */); } /** @@ -9074,11 +9084,13 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A * (stopped state). This is useful for the case where we know the * activity will be visible soon and we want to ensure its configuration * before we make it visible. + * @param isRequestedOrientationChanged whether this is triggered in response to an app calling + * {@link android.app.Activity#setRequestedOrientation}. * @return False if the activity was relaunched and true if it wasn't relaunched because we * can't or the app handles the specific configuration that is changing. */ boolean ensureActivityConfiguration(int globalChanges, boolean preserveWindow, - boolean ignoreVisibility) { + boolean ignoreVisibility, boolean isRequestedOrientationChanged) { final Task rootTask = getRootTask(); if (rootTask.mConfigWillChange) { ProtoLog.v(WM_DEBUG_CONFIGURATION, "Skipping config check " @@ -9202,6 +9214,9 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } else { mRelaunchReason = RELAUNCH_REASON_NONE; } + if (isRequestedOrientationChanged) { + mLetterboxUiController.setRelauchingAfterRequestedOrientationChanged(true); + } if (mState == PAUSING) { // A little annoying: we are waiting for this activity to finish pausing. Let's not // do anything now, but just flag that it needs to be restarted when done pausing. diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index b4af69e14780..034f5c8128f6 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -2645,9 +2645,9 @@ class ActivityStarter { if (differentTopTask && !mAvoidMoveToFront) { mStartActivity.intent.addFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT); - if (mSourceRecord == null || (mSourceRootTask.getTopNonFinishingActivity() != null - && mSourceRootTask.getTopNonFinishingActivity().getTask() - == mSourceRecord.getTask())) { + // TODO(b/264487981): Consider using BackgroundActivityStartController to determine + // whether to bring the launching activity to the front. + if (mSourceRecord == null || inTopNonFinishingTask(mSourceRecord)) { // We really do want to push this one into the user's face, right now. if (mLaunchTaskBehind && mSourceRecord != null) { intentActivity.setTaskToAffiliateWith(mSourceRecord.getTask()); @@ -2706,6 +2706,20 @@ class ActivityStarter { mRootWindowContainer.getDefaultTaskDisplayArea(), mTargetRootTask); } + private boolean inTopNonFinishingTask(ActivityRecord r) { + if (r == null || r.getTask() == null) { + return false; + } + + final Task rTask = r.getTask(); + final Task parent = rTask.getCreatedByOrganizerTask() != null + ? rTask.getCreatedByOrganizerTask() : r.getRootTask(); + final ActivityRecord topNonFinishingActivity = parent != null + ? parent.getTopNonFinishingActivity() : null; + + return topNonFinishingActivity != null && topNonFinishingActivity.getTask() == rTask; + } + private void resumeTargetRootTaskIfNeeded() { if (mDoResume) { final ActivityRecord next = mTargetRootTask.topRunningActivity( diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java index 473a6e5ac0c5..103b9b29379b 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java +++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java @@ -1451,6 +1451,7 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { mUserLeaving = true; } + mService.deferWindowLayout(); final Transition newTransition = task.mTransitionController.isShellTransitionsEnabled() ? task.mTransitionController.isCollecting() ? null : task.mTransitionController.createTransition(TRANSIT_TO_FRONT) : null; @@ -1458,9 +1459,6 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { reason = reason + " findTaskToMoveToFront"; boolean reparented = false; if (task.isResizeable() && canUseActivityOptionsLaunchBounds(options)) { - final Rect bounds = options.getLaunchBounds(); - task.setBounds(bounds); - Task targetRootTask = mRootWindowContainer.getOrCreateRootTask(null, options, task, ON_TOP); @@ -1473,14 +1471,11 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { // task.reparent() should already placed the task on top, // still need moveTaskToFrontLocked() below for any transition settings. } - if (targetRootTask.shouldResizeRootTaskWithLaunchBounds()) { - targetRootTask.resize(bounds, !PRESERVE_WINDOWS, !DEFER_RESUME); - } else { - // WM resizeTask must be done after the task is moved to the correct stack, - // because Task's setBounds() also updates dim layer's bounds, but that has - // dependency on the root task. - task.resize(false /* relayout */, false /* forced */); - } + // The resizeTask must be done after the task is moved to the correct root task, + // because Task's setBounds() also updates dim layer's bounds, but that has + // dependency on the root task. + final Rect bounds = options.getLaunchBounds(); + task.setBounds(bounds); } if (!reparented) { @@ -1510,6 +1505,7 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { } } finally { mUserLeaving = false; + mService.continueWindowLayout(); } } @@ -2570,13 +2566,13 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { : null; boolean moveHomeTaskForward = true; synchronized (mService.mGlobalLock) { + final boolean isCallerRecents = mRecentTasks.isCallerRecents(callingUid); int activityType = ACTIVITY_TYPE_UNDEFINED; if (activityOptions != null) { activityType = activityOptions.getLaunchActivityType(); - final int windowingMode = activityOptions.getLaunchWindowingMode(); - if (activityOptions.freezeRecentTasksReordering() - && mService.checkPermission(MANAGE_ACTIVITY_TASKS, callingPid, callingUid) - == PERMISSION_GRANTED) { + if (activityOptions.freezeRecentTasksReordering() && (isCallerRecents + || ActivityTaskManagerService.checkPermission(MANAGE_ACTIVITY_TASKS, + callingPid, callingUid) == PERMISSION_GRANTED)) { mRecentTasks.setFreezeTaskListReordering(); } if (activityOptions.getLaunchRootTask() != null) { @@ -2619,7 +2615,9 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { mRootWindowContainer.startPowerModeLaunchIfNeeded( true /* forceSend */, targetActivity); final LaunchingState launchingState = - mActivityMetricsLogger.notifyActivityLaunching(task.intent); + mActivityMetricsLogger.notifyActivityLaunching(task.intent, + // Recents always has a new launching state (not combinable). + null /* caller */, isCallerRecents ? INVALID_UID : callingUid); try { mService.moveTaskToFrontLocked(null /* appThread */, null /* callingPackage */, task.mTaskId, 0, options); diff --git a/services/core/java/com/android/server/wm/AppTaskImpl.java b/services/core/java/com/android/server/wm/AppTaskImpl.java index 7bd8c538351d..6b5f068b88a7 100644 --- a/services/core/java/com/android/server/wm/AppTaskImpl.java +++ b/services/core/java/com/android/server/wm/AppTaskImpl.java @@ -99,7 +99,7 @@ class AppTaskImpl extends IAppTask.Stub { throw new IllegalArgumentException("Unable to find task ID " + mTaskId); } return mService.getRecentTasks().createRecentTaskInfo(task, - false /* stripExtras */, true /* getTasksAllowed */); + false /* stripExtras */); } finally { Binder.restoreCallingIdentity(origId); } diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 82237bb2c483..e7a5ee7f01d9 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -85,6 +85,7 @@ import static android.view.WindowManager.TRANSIT_CHANGE; 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.view.inputmethod.ImeTracker.DEBUG_IME_VISIBILITY; import static android.window.DisplayAreaOrganizer.FEATURE_IME; import static android.window.DisplayAreaOrganizer.FEATURE_ROOT; import static android.window.DisplayAreaOrganizer.FEATURE_WINDOWED_MAGNIFICATION; @@ -140,7 +141,6 @@ import static com.android.server.wm.WindowContainer.AnimationFlags.PARENTS; import static com.android.server.wm.WindowContainer.AnimationFlags.TRANSITION; import static com.android.server.wm.WindowContainerChildProto.DISPLAY_CONTENT; import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_DISPLAY; -import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_IME_VISIBILITY; import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_INPUT_METHOD; import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_LAYOUT; import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_LAYOUT_REPEATS; @@ -500,7 +500,6 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp // Accessed directly by all users. private boolean mLayoutNeeded; int pendingLayoutChanges; - boolean mLayoutAndAssignWindowLayersScheduled; /** * Used to gate application window layout until we have sent the complete configuration. @@ -1109,12 +1108,12 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp * mDisplayMetrics.densityDpi / DENSITY_DEFAULT; isDefaultDisplay = mDisplayId == DEFAULT_DISPLAY; mInsetsStateController = new InsetsStateController(this); + initializeDisplayBaseInfo(); mDisplayFrames = new DisplayFrames(mInsetsStateController.getRawInsetsState(), mDisplayInfo, calculateDisplayCutoutForRotation(mDisplayInfo.rotation), calculateRoundedCornersForRotation(mDisplayInfo.rotation), calculatePrivacyIndicatorBoundsForRotation(mDisplayInfo.rotation), calculateDisplayShapeForRotation(mDisplayInfo.rotation)); - initializeDisplayBaseInfo(); mHoldScreenWakeLock = mWmService.mPowerManager.newWakeLock( PowerManager.SCREEN_BRIGHT_WAKE_LOCK | PowerManager.ON_AFTER_RELEASE, @@ -6024,6 +6023,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp } } + @Nullable ActivityRecord topRunningActivity() { return topRunningActivity(false /* considerKeyguardState */); } diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java index cf3a6880e712..e6d8b3db4564 100644 --- a/services/core/java/com/android/server/wm/DisplayRotation.java +++ b/services/core/java/com/android/server/wm/DisplayRotation.java @@ -100,6 +100,8 @@ public class DisplayRotation { private final DisplayWindowSettings mDisplayWindowSettings; private final Context mContext; private final Object mLock; + @Nullable + private final DisplayRotationImmersiveAppCompatPolicy mCompatPolicyForImmersiveApps; public final boolean isDefaultDisplay; private final boolean mSupportAutoRotation; @@ -205,7 +207,7 @@ public class DisplayRotation { /** * A flag to indicate if the display rotation should be fixed to user specified rotation - * regardless of all other states (including app requrested orientation). {@code true} the + * regardless of all other states (including app requested orientation). {@code true} the * display rotation should be fixed to user specified rotation, {@code false} otherwise. */ private int mFixedToUserRotation = IWindowManager.FIXED_TO_USER_ROTATION_DEFAULT; @@ -232,6 +234,7 @@ public class DisplayRotation { mContext = context; mLock = lock; isDefaultDisplay = displayContent.isDefaultDisplay; + mCompatPolicyForImmersiveApps = initImmersiveAppCompatPolicy(service, displayContent); mSupportAutoRotation = mContext.getResources().getBoolean(R.bool.config_supportAutoRotation); @@ -255,6 +258,14 @@ public class DisplayRotation { } } + @VisibleForTesting + @Nullable + DisplayRotationImmersiveAppCompatPolicy initImmersiveAppCompatPolicy( + WindowManagerService service, DisplayContent displayContent) { + return DisplayRotationImmersiveAppCompatPolicy.createIfNeeded( + service.mLetterboxConfiguration, this, displayContent); + } + // Change the default value to the value specified in the sysprop // ro.bootanim.set_orientation_<display_id>. Four values are supported: ORIENTATION_0, // ORIENTATION_90, ORIENTATION_180 and ORIENTATION_270. @@ -1305,11 +1316,11 @@ public class DisplayRotation { return mAllowAllRotations; } - private boolean isLandscapeOrSeascape(int rotation) { + boolean isLandscapeOrSeascape(@Surface.Rotation final int rotation) { return rotation == mLandscapeRotation || rotation == mSeascapeRotation; } - private boolean isAnyPortrait(int rotation) { + boolean isAnyPortrait(@Surface.Rotation final int rotation) { return rotation == mPortraitRotation || rotation == mUpsideDownRotation; } @@ -1348,9 +1359,16 @@ public class DisplayRotation { return mFoldController != null && mFoldController.overrideFrozenRotation(); } - private boolean isRotationChoicePossible(int orientation) { - // Rotation choice is only shown when the user is in locked mode. - if (mUserRotationMode != WindowManagerPolicy.USER_ROTATION_LOCKED) return false; + private boolean isRotationChoiceAllowed(@Surface.Rotation final int proposedRotation) { + final boolean isRotationLockEnforced = mCompatPolicyForImmersiveApps != null + && mCompatPolicyForImmersiveApps.isRotationLockEnforced(proposedRotation); + + // Don't show rotation choice button if + if (!isRotationLockEnforced // not enforcing locked rotation + // and the screen rotation is not locked by the user. + && mUserRotationMode != WindowManagerPolicy.USER_ROTATION_LOCKED) { + return false; + } // Don't show rotation choice if we are in tabletop or book modes. if (isTabletopAutoRotateOverrideEnabled()) return false; @@ -1402,7 +1420,7 @@ public class DisplayRotation { } // Ensure that some rotation choice is possible for the given orientation. - switch (orientation) { + switch (mCurrentAppOrientation) { case ActivityInfo.SCREEN_ORIENTATION_FULL_USER: case ActivityInfo.SCREEN_ORIENTATION_USER: case ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED: @@ -1719,11 +1737,11 @@ public class DisplayRotation { @Override - public void onProposedRotationChanged(int rotation) { + public void onProposedRotationChanged(@Surface.Rotation int rotation) { ProtoLog.v(WM_DEBUG_ORIENTATION, "onProposedRotationChanged, rotation=%d", rotation); // Send interaction power boost to improve redraw performance. mService.mPowerManagerInternal.setPowerBoost(Boost.INTERACTION, 0); - if (isRotationChoicePossible(mCurrentAppOrientation)) { + if (isRotationChoiceAllowed(rotation)) { final boolean isValid = isValidRotationChoice(rotation); sendProposedRotationChangeToStatusBarInternal(rotation, isValid); } else { diff --git a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java index 7266d2194779..ba0413df6325 100644 --- a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java @@ -287,7 +287,7 @@ final class DisplayRotationCompatPolicy { * <li>The activity has fixed orientation but not "locked" or "nosensor" one. * </ul> */ - private boolean isTreatmentEnabledForActivity(@Nullable ActivityRecord activity) { + boolean isTreatmentEnabledForActivity(@Nullable ActivityRecord activity) { return activity != null && !activity.inMultiWindowMode() && activity.getRequestedConfigurationOrientation() != ORIENTATION_UNDEFINED // "locked" and "nosensor" values are often used by camera apps that can't diff --git a/services/core/java/com/android/server/wm/DisplayRotationImmersiveAppCompatPolicy.java b/services/core/java/com/android/server/wm/DisplayRotationImmersiveAppCompatPolicy.java new file mode 100644 index 000000000000..74494ddd9f59 --- /dev/null +++ b/services/core/java/com/android/server/wm/DisplayRotationImmersiveAppCompatPolicy.java @@ -0,0 +1,158 @@ +/* + * 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.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; +import static android.content.res.Configuration.ORIENTATION_PORTRAIT; +import static android.content.res.Configuration.ORIENTATION_UNDEFINED; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.res.Configuration.Orientation; +import android.view.Surface; +import android.view.WindowInsets.Type; + +/** + * Policy to decide whether to enforce screen rotation lock for optimisation of the screen rotation + * user experience for immersive applications for compatibility when ignoring orientation request. + * + * <p>This is needed because immersive apps, such as games, are often not optimized for all + * orientations and can have a poor UX when rotated (e.g., state loss or entering size-compat mode). + * Additionally, some games rely on sensors for the gameplay so users can trigger such rotations + * accidentally when auto rotation is on. + */ +final class DisplayRotationImmersiveAppCompatPolicy { + + @Nullable + static DisplayRotationImmersiveAppCompatPolicy createIfNeeded( + @NonNull final LetterboxConfiguration letterboxConfiguration, + @NonNull final DisplayRotation displayRotation, + @NonNull final DisplayContent displayContent) { + if (!letterboxConfiguration + .isDisplayRotationImmersiveAppCompatPolicyEnabled(/* checkDeviceConfig */ false)) { + return null; + } + + return new DisplayRotationImmersiveAppCompatPolicy( + letterboxConfiguration, displayRotation, displayContent); + } + + private final DisplayRotation mDisplayRotation; + private final LetterboxConfiguration mLetterboxConfiguration; + private final DisplayContent mDisplayContent; + + private DisplayRotationImmersiveAppCompatPolicy( + @NonNull final LetterboxConfiguration letterboxConfiguration, + @NonNull final DisplayRotation displayRotation, + @NonNull final DisplayContent displayContent) { + mDisplayRotation = displayRotation; + mLetterboxConfiguration = letterboxConfiguration; + mDisplayContent = displayContent; + } + + /** + * Decides whether it is necessary to lock screen rotation, preventing auto rotation, based on + * the top activity configuration and proposed screen rotation. + * + * <p>This is needed because immersive apps, such as games, are often not optimized for all + * orientations and can have a poor UX when rotated. Additionally, some games rely on sensors + * for the gameplay so users can trigger such rotations accidentally when auto rotation is on. + * + * <p>Screen rotation is locked when the following conditions are met: + * <ul> + * <li>Top activity requests to hide status and navigation bars + * <li>Top activity is fullscreen and in optimal orientation (without letterboxing) + * <li>Rotation will lead to letterboxing due to fixed orientation. + * <li>{@link DisplayContent#getIgnoreOrientationRequest} is {@code true} + * <li>This policy is enabled on the device, for details see + * {@link LetterboxConfiguration#isDisplayRotationImmersiveAppCompatPolicyEnabled} + * </ul> + * + * @param proposedRotation new proposed {@link Surface.Rotation} for the screen. + * @return {@code true}, if there is a need to lock screen rotation, {@code false} otherwise. + */ + boolean isRotationLockEnforced(@Surface.Rotation final int proposedRotation) { + if (!mLetterboxConfiguration.isDisplayRotationImmersiveAppCompatPolicyEnabled( + /* checkDeviceConfig */ true)) { + return false; + } + synchronized (mDisplayContent.mWmService.mGlobalLock) { + return isRotationLockEnforcedLocked(proposedRotation); + } + } + + private boolean isRotationLockEnforcedLocked(@Surface.Rotation final int proposedRotation) { + if (!mDisplayContent.getIgnoreOrientationRequest()) { + return false; + } + + final ActivityRecord activityRecord = mDisplayContent.topRunningActivity(); + if (activityRecord == null) { + return false; + } + + // Don't lock screen rotation if an activity hasn't requested to hide system bars. + if (!hasRequestedToHideStatusAndNavBars(activityRecord)) { + return false; + } + + // Don't lock screen rotation if activity is not in fullscreen. Checking windowing mode + // for a task rather than an activity to exclude activity embedding scenario. + if (activityRecord.getTask() == null + || activityRecord.getTask().getWindowingMode() != WINDOWING_MODE_FULLSCREEN) { + return false; + } + + // Don't lock screen rotation if activity is letterboxed. + if (activityRecord.areBoundsLetterboxed()) { + return false; + } + + if (activityRecord.getRequestedConfigurationOrientation() == ORIENTATION_UNDEFINED) { + return false; + } + + // Lock screen rotation only if, after rotation the activity's orientation won't match + // the screen orientation, forcing the activity to enter letterbox mode after rotation. + return activityRecord.getRequestedConfigurationOrientation() + != surfaceRotationToConfigurationOrientation(proposedRotation); + } + + /** + * Checks whether activity has requested to hide status and navigation bars. + */ + private boolean hasRequestedToHideStatusAndNavBars(@NonNull ActivityRecord activity) { + WindowState mainWindow = activity.findMainWindow(); + if (mainWindow == null) { + return false; + } + return (mainWindow.getRequestedVisibleTypes() + & (Type.statusBars() | Type.navigationBars())) == 0; + } + + @Orientation + private int surfaceRotationToConfigurationOrientation(@Surface.Rotation final int rotation) { + if (mDisplayRotation.isAnyPortrait(rotation)) { + return ORIENTATION_PORTRAIT; + } else if (mDisplayRotation.isLandscapeOrSeascape(rotation)) { + return ORIENTATION_LANDSCAPE; + } else { + return ORIENTATION_UNDEFINED; + } + } +} diff --git a/services/core/java/com/android/server/wm/InsetsPolicy.java b/services/core/java/com/android/server/wm/InsetsPolicy.java index 35e1fbb61b68..1df534f21c18 100644 --- a/services/core/java/com/android/server/wm/InsetsPolicy.java +++ b/services/core/java/com/android/server/wm/InsetsPolicy.java @@ -19,7 +19,6 @@ package com.android.server.wm; import static android.app.StatusBarManager.WINDOW_STATE_HIDDEN; import static android.app.StatusBarManager.WINDOW_STATE_SHOWING; 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.InsetsController.ANIMATION_TYPE_HIDE; @@ -42,7 +41,6 @@ import android.app.StatusBarManager; import android.app.WindowConfiguration; import android.content.ComponentName; import android.content.res.Resources; -import android.graphics.Rect; import android.util.ArrayMap; import android.util.IntArray; import android.util.SparseArray; @@ -276,21 +274,22 @@ class InsetsPolicy { /** * @see WindowState#getInsetsState() */ - InsetsState getInsetsForWindowMetrics(@NonNull WindowManager.LayoutParams attrs) { - final WindowToken token = mDisplayContent.getWindowToken(attrs.token); - if (token != null) { - final InsetsState rotatedState = token.getFixedRotationTransformInsetsState(); - if (rotatedState != null) { - return rotatedState; + void getInsetsForWindowMetrics(@Nullable WindowToken token, + @NonNull InsetsState outInsetsState) { + final InsetsState srcState = token != null && token.isFixedRotationTransforming() + ? token.getFixedRotationTransformInsetsState() + : mStateController.getRawInsetsState(); + outInsetsState.set(srcState, true /* copySources */); + for (int i = mShowingTransientTypes.size() - 1; i >= 0; i--) { + final InsetsSource source = outInsetsState.peekSource(mShowingTransientTypes.get(i)); + if (source != null) { + source.setVisible(false); } } - final boolean alwaysOnTop = token != null && token.isAlwaysOnTop(); - // Always use windowing mode fullscreen when get insets for window metrics to make sure it - // contains all insets types. - final InsetsState originalState = enforceInsetsPolicyForTarget(attrs, - WINDOWING_MODE_FULLSCREEN, alwaysOnTop, mStateController.getRawInsetsState()); - InsetsState state = adjustVisibilityForTransientTypes(originalState); - return adjustInsetsForRoundedCorners(token, state, state == originalState); + adjustInsetsForRoundedCorners(token, outInsetsState, false /* copyState */); + if (token != null && token.hasSizeCompatBounds()) { + outInsetsState.scale(1f / token.getCompatScale()); + } } /** @@ -423,10 +422,9 @@ class InsetsPolicy { final Task task = activityRecord != null ? activityRecord.getTask() : null; if (task != null && !task.getWindowConfiguration().tasksAreFloating()) { // Use task bounds to calculating rounded corners if the task is not floating. - final Rect roundedCornerFrame = new Rect(task.getBounds()); final InsetsState state = copyState ? new InsetsState(originalState) : originalState; - state.setRoundedCornerFrame(roundedCornerFrame); + state.setRoundedCornerFrame(task.getBounds()); return state; } } diff --git a/services/core/java/com/android/server/wm/InsetsSourceProvider.java b/services/core/java/com/android/server/wm/InsetsSourceProvider.java index 5171f5b02899..659f8d755c59 100644 --- a/services/core/java/com/android/server/wm/InsetsSourceProvider.java +++ b/services/core/java/com/android/server/wm/InsetsSourceProvider.java @@ -33,7 +33,6 @@ import static com.android.server.wm.InsetsSourceProviderProto.SEAMLESS_ROTATING; import static com.android.server.wm.InsetsSourceProviderProto.SERVER_VISIBLE; import static com.android.server.wm.InsetsSourceProviderProto.SOURCE; import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_INSETS_CONTROL; -import static com.android.server.wm.WindowManagerService.H.LAYOUT_AND_ASSIGN_WINDOW_LAYERS_IF_NEEDED; import android.annotation.NonNull; import android.annotation.Nullable; @@ -508,11 +507,6 @@ abstract class InsetsSourceProvider { return; } mClientVisible = clientVisible; - if (!mDisplayContent.mLayoutAndAssignWindowLayersScheduled) { - mDisplayContent.mLayoutAndAssignWindowLayersScheduled = true; - mDisplayContent.mWmService.mH.obtainMessage( - LAYOUT_AND_ASSIGN_WINDOW_LAYERS_IF_NEEDED, mDisplayContent).sendToTarget(); - } updateVisibility(); } diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/LetterboxConfiguration.java index 9b8423327215..f916ee40d538 100644 --- a/services/core/java/com/android/server/wm/LetterboxConfiguration.java +++ b/services/core/java/com/android/server/wm/LetterboxConfiguration.java @@ -18,6 +18,7 @@ package com.android.server.wm; import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM; import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME; +import static com.android.server.wm.LetterboxConfigurationDeviceConfig.KEY_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY; import android.annotation.IntDef; import android.annotation.NonNull; @@ -222,23 +223,40 @@ final class LetterboxConfiguration { // See RefreshCallbackItem for context. private boolean mIsCameraCompatRefreshCycleThroughStopEnabled = true; - LetterboxConfiguration(Context systemUiContext) { - this(systemUiContext, new LetterboxConfigurationPersister(systemUiContext, - () -> readLetterboxHorizontalReachabilityPositionFromConfig(systemUiContext, - /* forBookMode */ false), - () -> readLetterboxVerticalReachabilityPositionFromConfig(systemUiContext, - /* forTabletopMode */ false), - () -> readLetterboxHorizontalReachabilityPositionFromConfig(systemUiContext, - /* forBookMode */ true), - () -> readLetterboxVerticalReachabilityPositionFromConfig(systemUiContext, - /* forTabletopMode */ true) - )); + // Whether should ignore app requested orientation in response to an app + // calling Activity#setRequestedOrientation. See + // LetterboxUiController#shouldIgnoreRequestedOrientation for details. + private final boolean mIsPolicyForIgnoringRequestedOrientationEnabled; + + // Whether enabling rotation compat policy for immersive apps that prevents auto rotation + // into non-optimal screen orientation while in fullscreen. This is needed because immersive + // apps, such as games, are often not optimized for all orientations and can have a poor UX + // when rotated. Additionally, some games rely on sensors for the gameplay so users can trigger + // such rotations accidentally when auto rotation is on. + private final boolean mIsDisplayRotationImmersiveAppCompatPolicyEnabled; + + // Flags dynamically updated with {@link android.provider.DeviceConfig}. + @NonNull private final LetterboxConfigurationDeviceConfig mDeviceConfig; + + LetterboxConfiguration(@NonNull final Context systemUiContext) { + this(systemUiContext, + new LetterboxConfigurationPersister(systemUiContext, + () -> readLetterboxHorizontalReachabilityPositionFromConfig( + systemUiContext, /* forBookMode */ false), + () -> readLetterboxVerticalReachabilityPositionFromConfig( + systemUiContext, /* forTabletopMode */ false), + () -> readLetterboxHorizontalReachabilityPositionFromConfig( + systemUiContext, /* forBookMode */ true), + () -> readLetterboxVerticalReachabilityPositionFromConfig( + systemUiContext, /* forTabletopMode */ true))); } @VisibleForTesting - LetterboxConfiguration(Context systemUiContext, - LetterboxConfigurationPersister letterboxConfigurationPersister) { + LetterboxConfiguration(@NonNull final Context systemUiContext, + @NonNull final LetterboxConfigurationPersister letterboxConfigurationPersister) { mContext = systemUiContext; + mDeviceConfig = new LetterboxConfigurationDeviceConfig(systemUiContext.getMainExecutor()); + mFixedOrientationLetterboxAspectRatio = mContext.getResources().getFloat( R.dimen.config_fixedOrientationLetterboxAspectRatio); mLetterboxActivityCornersRadius = mContext.getResources().getInteger( @@ -274,10 +292,19 @@ final class LetterboxConfiguration { R.bool.config_letterboxIsEnabledForTranslucentActivities); mIsCameraCompatTreatmentEnabled = mContext.getResources().getBoolean( R.bool.config_isWindowManagerCameraCompatTreatmentEnabled); + mIsCompatFakeFocusEnabled = mContext.getResources().getBoolean( + R.bool.config_isCompatFakeFocusEnabled); + mIsPolicyForIgnoringRequestedOrientationEnabled = mContext.getResources().getBoolean( + R.bool.config_letterboxIsPolicyForIgnoringRequestedOrientationEnabled); + + mIsDisplayRotationImmersiveAppCompatPolicyEnabled = mContext.getResources().getBoolean( + R.bool.config_letterboxIsDisplayRotationImmersiveAppCompatPolicyEnabled); + mDeviceConfig.updateFlagActiveStatus( + /* isActive */ mIsDisplayRotationImmersiveAppCompatPolicyEnabled, + /* key */ KEY_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY); + mLetterboxConfigurationPersister = letterboxConfigurationPersister; mLetterboxConfigurationPersister.start(); - mIsCompatFakeFocusEnabled = mContext.getResources() - .getBoolean(R.bool.config_isCompatFakeFocusEnabled); } /** @@ -1034,6 +1061,15 @@ final class LetterboxConfiguration { mIsCompatFakeFocusEnabled = enabled; } + /** + * Whether should ignore app requested orientation in response to an app calling + * {@link android.app.Activity#setRequestedOrientation}. See {@link + * LetterboxUiController#shouldIgnoreRequestedOrientation} for details. + */ + boolean isPolicyForIgnoringRequestedOrientationEnabled() { + return mIsPolicyForIgnoringRequestedOrientationEnabled; + } + /** Whether camera compatibility treatment is enabled. */ boolean isCameraCompatTreatmentEnabled(boolean checkDeviceConfig) { return mIsCameraCompatTreatmentEnabled @@ -1044,7 +1080,7 @@ final class LetterboxConfiguration { // DeviceConfig.OnPropertiesChangedListener private static boolean isCameraCompatTreatmentAllowed() { return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_WINDOW_MANAGER, - "enable_camera_compat_treatment", false); + "enable_compat_camera_treatment", true); } /** Whether camera compatibility refresh is enabled. */ @@ -1088,4 +1124,20 @@ final class LetterboxConfiguration { mIsCameraCompatRefreshCycleThroughStopEnabled = true; } + /** + * Checks whether rotation compat policy for immersive apps that prevents auto rotation + * into non-optimal screen orientation while in fullscreen is enabled. + * + * <p>This is needed because immersive apps, such as games, are often not optimized for all + * orientations and can have a poor UX when rotated. Additionally, some games rely on sensors + * for the gameplay so users can trigger such rotations accidentally when auto rotation is on. + * + * @param checkDeviceConfig whether should check both static config and a dynamic property + * from {@link DeviceConfig} or only static value. + */ + boolean isDisplayRotationImmersiveAppCompatPolicyEnabled(final boolean checkDeviceConfig) { + return mIsDisplayRotationImmersiveAppCompatPolicyEnabled && (!checkDeviceConfig + || mDeviceConfig.getFlag(KEY_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY)); + } + } diff --git a/services/core/java/com/android/server/wm/LetterboxConfigurationDeviceConfig.java b/services/core/java/com/android/server/wm/LetterboxConfigurationDeviceConfig.java new file mode 100644 index 000000000000..cf123a1f9ace --- /dev/null +++ b/services/core/java/com/android/server/wm/LetterboxConfigurationDeviceConfig.java @@ -0,0 +1,120 @@ +/* + * 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 android.annotation.NonNull; +import android.provider.DeviceConfig; +import android.util.ArraySet; + + +import com.android.internal.annotations.VisibleForTesting; + +import java.util.Map; +import java.util.concurrent.Executor; + +/** + * Utility class that caches {@link DeviceConfig} flags for app compat features and listens + * to updates by implementing {@link DeviceConfig.OnPropertiesChangedListener}. + */ +final class LetterboxConfigurationDeviceConfig + implements DeviceConfig.OnPropertiesChangedListener { + + static final String KEY_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY = + "enable_display_rotation_immersive_app_compat_policy"; + private static final boolean DEFAULT_VALUE_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY = + true; + + @VisibleForTesting + static final Map<String, Boolean> sKeyToDefaultValueMap = Map.of( + KEY_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY, + DEFAULT_VALUE_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY + ); + + // Whether enabling rotation compat policy for immersive apps that prevents auto rotation + // into non-optimal screen orientation while in fullscreen. This is needed because immersive + // apps, such as games, are often not optimized for all orientations and can have a poor UX + // when rotated. Additionally, some games rely on sensors for the gameplay so users can trigger + // such rotations accidentally when auto rotation is on. + private boolean mIsDisplayRotationImmersiveAppCompatPolicyEnabled = + DEFAULT_VALUE_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY; + + // Set of active device configs that need to be updated in + // DeviceConfig.OnPropertiesChangedListener#onPropertiesChanged. + private final ArraySet<String> mActiveDeviceConfigsSet = new ArraySet<>(); + + LetterboxConfigurationDeviceConfig(@NonNull final Executor executor) { + DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_WINDOW_MANAGER, + executor, /* onPropertiesChangedListener */ this); + } + + @Override + public void onPropertiesChanged(@NonNull final DeviceConfig.Properties properties) { + for (int i = mActiveDeviceConfigsSet.size() - 1; i >= 0; i--) { + String key = mActiveDeviceConfigsSet.valueAt(i); + // Reads the new configuration, if the device config properties contain the key. + if (properties.getKeyset().contains(key)) { + readAndSaveValueFromDeviceConfig(key); + } + } + } + + /** + * Adds {@code key} to a set of flags that can be updated from the server if + * {@code isActive} is {@code true} and read it's current value from {@link DeviceConfig}. + */ + void updateFlagActiveStatus(boolean isActive, String key) { + if (!isActive) { + return; + } + mActiveDeviceConfigsSet.add(key); + readAndSaveValueFromDeviceConfig(key); + } + + /** + * Returns values of the {@code key} flag. + * + * @throws AssertionError {@code key} isn't recognised. + */ + boolean getFlag(String key) { + switch (key) { + case KEY_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY: + return mIsDisplayRotationImmersiveAppCompatPolicyEnabled; + default: + throw new AssertionError("Unexpected flag name: " + key); + } + } + + private void readAndSaveValueFromDeviceConfig(String key) { + Boolean defaultValue = sKeyToDefaultValueMap.get(key); + if (defaultValue == null) { + throw new AssertionError("Haven't found default value for flag: " + key); + } + switch (key) { + case KEY_ENABLE_DISPLAY_ROTATION_IMMERSIVE_APP_COMPAT_POLICY: + mIsDisplayRotationImmersiveAppCompatPolicyEnabled = + getDeviceConfig(key, defaultValue); + break; + default: + throw new AssertionError("Unexpected flag name: " + key); + } + } + + private boolean getDeviceConfig(String key, boolean defaultValue) { + return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_WINDOW_MANAGER, + key, defaultValue); + } +} diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java index fd7e082beed4..0c8a6453e6fb 100644 --- a/services/core/java/com/android/server/wm/LetterboxUiController.java +++ b/services/core/java/com/android/server/wm/LetterboxUiController.java @@ -17,10 +17,13 @@ package com.android.server.wm; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +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_COMPAT_IGNORE_REQUESTED_ORIENTATION; import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__BOTTOM; import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__CENTER; @@ -55,6 +58,8 @@ import static com.android.server.wm.LetterboxConfiguration.letterboxBackgroundTy import android.annotation.Nullable; import android.app.ActivityManager.TaskDescription; +import android.content.pm.ActivityInfo.ScreenOrientation; +import android.content.pm.PackageManager; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Color; @@ -74,6 +79,7 @@ import com.android.internal.statusbar.LetterboxDetails; import com.android.server.wm.LetterboxConfiguration.LetterboxBackgroundType; import java.io.PrintWriter; +import java.util.function.BooleanSupplier; /** Controls behaviour of the letterbox UI for {@link mActivityRecord}. */ // TODO(b/185262487): Improve test coverage of this class. Parts of it are tested in @@ -131,12 +137,37 @@ final class LetterboxUiController { // DisplayRotationCompatPolicy. private boolean mIsRefreshAfterRotationRequested; + @Nullable + private final Boolean mBooleanPropertyIgnoreRequestedOrientation; + + private boolean mIsRelauchingAfterRequestedOrientationChanged; + LetterboxUiController(WindowManagerService wmService, ActivityRecord activityRecord) { mLetterboxConfiguration = wmService.mLetterboxConfiguration; // Given activityRecord may not be fully constructed since LetterboxUiController // is created in its constructor. It shouldn't be used in this constructor but it's safe // to use it after since controller is only used in ActivityRecord. mActivityRecord = activityRecord; + + PackageManager packageManager = wmService.mContext.getPackageManager(); + mBooleanPropertyIgnoreRequestedOrientation = + readComponentProperty(packageManager, mActivityRecord.packageName, + mLetterboxConfiguration::isPolicyForIgnoringRequestedOrientationEnabled, + PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION); + } + + @Nullable + private static Boolean readComponentProperty(PackageManager packageManager, String packageName, + BooleanSupplier gatingCondition, String propertyName) { + if (!gatingCondition.getAsBoolean()) { + return null; + } + try { + return packageManager.getProperty(propertyName, packageName).getBoolean(); + } catch (PackageManager.NameNotFoundException e) { + // No such property name. + } + return null; } /** Cleans up {@link Letterbox} if it exists.*/ @@ -154,6 +185,72 @@ final class LetterboxUiController { } /** + * Whether should ignore app requested orientation in response to an app + * calling {@link android.app.Activity#setRequestedOrientation}. + * + * <p>This is needed to avoid getting into {@link android.app.Activity#setRequestedOrientation} + * loop when {@link DisplayContent#getIgnoreOrientationRequest} is enabled or device has + * landscape natural orientation which app developers don't expect. For example, the loop can + * look like this: + * <ol> + * <li>App sets default orientation to "unspecified" at runtime + * <li>App requests to "portrait" after checking some condition (e.g. display rotation). + * <li>(2) leads to fullscreen -> letterboxed bounds change and activity relaunch because + * app can't handle the corresponding config changes. + * <li>Loop goes back to (1) + * </ol> + * + * <p>This treatment is enabled when the following conditions are met: + * <ul> + * <li>Flag gating the treatment is enabled + * <li>Opt-out component property isn't enabled + * <li>Opt-in component property or per-app override are enabled + * <li>Activity is relaunched after {@link android.app.Activity#setRequestedOrientation} + * call from an app or camera compat force rotation treatment is active for the activity. + * </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)) { + return false; + } + if (mIsRelauchingAfterRequestedOrientationChanged) { + Slog.w(TAG, "Ignoring orientation update to " + + screenOrientationToString(requestedOrientation) + + " due to relaunching after setRequestedOrientation for " + mActivityRecord); + return true; + } + DisplayContent displayContent = mActivityRecord.mDisplayContent; + if (displayContent == null) { + return false; + } + if (displayContent.mDisplayRotationCompatPolicy != null + && displayContent.mDisplayRotationCompatPolicy + .isTreatmentEnabledForActivity(mActivityRecord)) { + Slog.w(TAG, "Ignoring orientation update to " + + screenOrientationToString(requestedOrientation) + + " due to camera compat treatment for " + mActivityRecord); + return true; + } + return false; + } + + /** + * Sets whether an activity is relaunching after the app has called {@link + * android.app.Activity#setRequestedOrientation}. + */ + void setRelauchingAfterRequestedOrientationChanged(boolean isRelaunching) { + mIsRelauchingAfterRequestedOrientationChanged = isRelaunching; + } + + /** * Whether activity "refresh" was requested but not finished in {@link #activityResumedLocked} * following the camera compat force rotation in {@link DisplayRotationCompatPolicy}. */ diff --git a/services/core/java/com/android/server/wm/RecentTasks.java b/services/core/java/com/android/server/wm/RecentTasks.java index 4be1c830f331..9e959180b7e3 100644 --- a/services/core/java/com/android/server/wm/RecentTasks.java +++ b/services/core/java/com/android/server/wm/RecentTasks.java @@ -976,7 +976,7 @@ class RecentTasks { continue; } - res.add(createRecentTaskInfo(task, true /* stripExtras */, getTasksAllowed)); + res.add(createRecentTaskInfo(task, true /* stripExtras */)); } return res; } @@ -1889,8 +1889,7 @@ class RecentTasks { /** * Creates a new RecentTaskInfo from a Task. */ - ActivityManager.RecentTaskInfo createRecentTaskInfo(Task tr, boolean stripExtras, - boolean getTasksAllowed) { + ActivityManager.RecentTaskInfo createRecentTaskInfo(Task tr, boolean stripExtras) { final ActivityManager.RecentTaskInfo rti = new ActivityManager.RecentTaskInfo(); // If the recent Task is detached, we consider it will be re-attached to the default // TaskDisplayArea because we currently only support recent overview in the default TDA. @@ -1902,9 +1901,6 @@ class RecentTasks { rti.id = rti.isRunning ? rti.taskId : INVALID_TASK_ID; rti.persistentId = rti.taskId; rti.lastSnapshotData.set(tr.mLastTaskSnapshotData); - if (!getTasksAllowed) { - Task.trimIneffectiveInfo(tr, rti); - } // Fill in organized child task info for the task created by organizer. if (tr.mCreatedByOrganizer) { diff --git a/services/core/java/com/android/server/wm/RunningTasks.java b/services/core/java/com/android/server/wm/RunningTasks.java index 1cc1a57756e9..614b405d6745 100644 --- a/services/core/java/com/android/server/wm/RunningTasks.java +++ b/services/core/java/com/android/server/wm/RunningTasks.java @@ -173,10 +173,6 @@ class RunningTasks implements Consumer<Task> { } // Fill in some deprecated values rti.id = rti.taskId; - - if (!mAllowed) { - Task.trimIneffectiveInfo(task, rti); - } return rti; } } diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java index 17b463febc13..b5c82a8f46fd 100644 --- a/services/core/java/com/android/server/wm/Session.java +++ b/services/core/java/com/android/server/wm/Session.java @@ -118,7 +118,7 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient { private float mLastReportedAnimatorScale; private String mPackageName; private String mRelayoutTag; - private final InsetsSourceControl[] mDummyControls = new InsetsSourceControl[0]; + private final InsetsSourceControl.Array mDummyControls = new InsetsSourceControl.Array(); final boolean mSetsUnrestrictedKeepClearAreas; public Session(WindowManagerService service, IWindowSessionCallback callback) { @@ -198,7 +198,7 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient { public int addToDisplay(IWindow window, WindowManager.LayoutParams attrs, int viewVisibility, int displayId, @InsetsType int requestedVisibleTypes, InputChannel outInputChannel, InsetsState outInsetsState, - InsetsSourceControl[] outActiveControls, Rect outAttachedFrame, + InsetsSourceControl.Array outActiveControls, Rect outAttachedFrame, float[] outSizeCompatScale) { return mService.addWindow(this, window, attrs, viewVisibility, displayId, UserHandle.getUserId(mUid), requestedVisibleTypes, outInputChannel, outInsetsState, @@ -209,7 +209,7 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient { public int addToDisplayAsUser(IWindow window, WindowManager.LayoutParams attrs, int viewVisibility, int displayId, int userId, @InsetsType int requestedVisibleTypes, InputChannel outInputChannel, InsetsState outInsetsState, - InsetsSourceControl[] outActiveControls, Rect outAttachedFrame, + InsetsSourceControl.Array outActiveControls, Rect outAttachedFrame, float[] outSizeCompatScale) { return mService.addWindow(this, window, attrs, viewVisibility, displayId, userId, requestedVisibleTypes, outInputChannel, outInsetsState, outActiveControls, @@ -246,7 +246,7 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient { int requestedWidth, int requestedHeight, int viewFlags, int flags, int seq, int lastSyncSeqId, ClientWindowFrames outFrames, MergedConfiguration mergedConfiguration, SurfaceControl outSurfaceControl, - InsetsState outInsetsState, InsetsSourceControl[] outActiveControls, + InsetsState outInsetsState, InsetsSourceControl.Array outActiveControls, Bundle outSyncSeqIdBundle) { if (false) Slog.d(TAG_WM, ">>>>>> ENTERED relayout from " + Binder.getCallingPid()); diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 8609e10bbe92..3b5b5a903544 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -423,9 +423,6 @@ class Task extends TaskFragment { // This number will be assigned when we evaluate OOM scores for all visible tasks. int mLayerRank = LAYER_RANK_INVISIBLE; - /** Helper object used for updating override configuration. */ - private Configuration mTmpConfig = new Configuration(); - /* Unique identifier for this task. */ final int mTaskId; /* User for which this task was created. */ @@ -796,16 +793,11 @@ class Task extends TaskFragment { Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "resizeTask_" + mTaskId); - boolean updatedConfig = false; - mTmpConfig.setTo(getResolvedOverrideConfiguration()); - if (setBounds(bounds) != BOUNDS_CHANGE_NONE) { - updatedConfig = !mTmpConfig.equals(getResolvedOverrideConfiguration()); - } // This variable holds information whether the configuration didn't change in a // significant way and the activity was kept the way it was. If it's false, it means // the activity had to be relaunched due to configuration change. boolean kept = true; - if (updatedConfig) { + if (setBounds(bounds, forced) != BOUNDS_CHANGE_NONE) { final ActivityRecord r = topRunningActivityLocked(); if (r != null) { kept = r.ensureActivityConfiguration(0 /* globalChanges */, @@ -822,8 +814,6 @@ class Task extends TaskFragment { } } } - resize(kept, forced); - saveLaunchingStateIfNeeded(); Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); @@ -2693,12 +2683,6 @@ class Task extends TaskFragment { return canSpecifyOrientation() && getDisplayArea().canSpecifyOrientation(orientation); } - void resize(boolean relayout, boolean forced) { - if (setBounds(getRequestedOverrideBounds(), forced) != BOUNDS_CHANGE_NONE && relayout) { - getDisplayContent().layoutAndAssignWindowLayersIfNeeded(); - } - } - @Override void onDisplayChanged(DisplayContent dc) { final boolean isRootTask = isRootTask(); @@ -3418,27 +3402,6 @@ class Task extends TaskFragment { info.isSleeping = shouldSleepActivities(); } - /** - * Removes the activity info if the activity belongs to a different uid, which is - * different from the app that hosts the task. - */ - static void trimIneffectiveInfo(Task task, TaskInfo info) { - final ActivityRecord baseActivity = task.getActivity(r -> !r.finishing, - false /* traverseTopToBottom */); - final int baseActivityUid = - baseActivity != null ? baseActivity.getUid() : task.effectiveUid; - - if (info.topActivityInfo != null - && task.effectiveUid != info.topActivityInfo.applicationInfo.uid) { - info.topActivity = null; - info.topActivityInfo = null; - } - - if (task.effectiveUid != baseActivityUid) { - info.baseActivity = null; - } - } - @Nullable PictureInPictureParams getPictureInPictureParams() { final Task topTask = getTopMostTask(); if (topTask == null) return null; @@ -4771,14 +4734,6 @@ class Task extends TaskFragment { } } - /** - * Returns true if this root task should be resized to match the bounds specified by - * {@link ActivityOptions#setLaunchBounds} when launching an activity into the root task. - */ - boolean shouldResizeRootTaskWithLaunchBounds() { - return inPinnedWindowingMode(); - } - void checkTranslucentActivityWaiting(ActivityRecord top) { if (mTranslucentActivityWaiting != top) { mUndrawnActivitiesBelowTopTranslucent.clear(); @@ -5603,29 +5558,6 @@ class Task extends TaskFragment { return true; } - // TODO: Can only be called from special methods in ActivityTaskSupervisor. - // Need to consolidate those calls points into this resize method so anyone can call directly. - void resize(Rect displayedBounds, boolean preserveWindows, boolean deferResume) { - Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "task.resize_" + getRootTaskId()); - mAtmService.deferWindowLayout(); - try { - // TODO: Why not just set this on the root task directly vs. on each tasks? - // Update override configurations of all tasks in the root task. - forAllTasks(task -> { - if (task.isResizeable()) { - task.setBounds(displayedBounds); - } - }, true /* traverseTopToBottom */); - - if (!deferResume) { - ensureVisibleActivitiesConfiguration(topRunningActivity(), preserveWindows); - } - } finally { - mAtmService.continueWindowLayout(); - Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); - } - } - boolean willActivityBeVisible(IBinder token) { final ActivityRecord r = ActivityRecord.forTokenLocked(token); if (r == null) { diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index b8878618b21c..dd489aa1447e 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -96,6 +96,7 @@ import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; import android.window.ITaskFragmentOrganizer; import android.window.ScreenCapture; +import android.window.TaskFragmentAnimationParams; import android.window.TaskFragmentInfo; import android.window.TaskFragmentOrganizerToken; @@ -306,6 +307,10 @@ class TaskFragment extends WindowContainer<WindowContainer> { @Nullable private final IBinder mFragmentToken; + /** The animation override params for animation running on this TaskFragment. */ + @NonNull + private TaskFragmentAnimationParams mAnimationParams = TaskFragmentAnimationParams.DEFAULT; + /** * The bounds of the embedded TaskFragment relative to the parent Task. * {@code null} if it is not {@link #mIsEmbedded} @@ -453,6 +458,15 @@ class TaskFragment extends WindowContainer<WindowContainer> { && organizer.asBinder().equals(mTaskFragmentOrganizer.asBinder()); } + void setAnimationParams(@NonNull TaskFragmentAnimationParams animationParams) { + mAnimationParams = animationParams; + } + + @NonNull + TaskFragmentAnimationParams getAnimationParams() { + return mAnimationParams; + } + TaskFragment getAdjacentTaskFragment() { return mAdjacentTaskFragment; } diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index 302538f31951..ad1e87933027 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -647,6 +647,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { t.setCornerRadius(targetLeash, 0); t.setShadowRadius(targetLeash, 0); t.setMatrix(targetLeash, 1, 0, 0, 1); + t.setAlpha(targetLeash, 1); // The bounds sent to the transition is always a real bounds. This means we lose // information about "null" bounds (inheriting from parent). Core will fix-up // non-organized window surface bounds; however, since Core can't touch organized diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java index 5de143d91539..7f9e808c4c93 100644 --- a/services/core/java/com/android/server/wm/WallpaperController.java +++ b/services/core/java/com/android/server/wm/WallpaperController.java @@ -756,9 +756,7 @@ class WallpaperController { private void updateWallpaperTokens(boolean visible) { for (int curTokenNdx = mWallpaperTokens.size() - 1; curTokenNdx >= 0; curTokenNdx--) { final WallpaperWindowToken token = mWallpaperTokens.get(curTokenNdx); - if (token.updateWallpaperWindows(visible)) { - token.mDisplayContent.assignWindowLayers(false /* setLayoutNeeded */); - } + token.updateWallpaperWindows(visible); } } diff --git a/services/core/java/com/android/server/wm/WallpaperWindowToken.java b/services/core/java/com/android/server/wm/WallpaperWindowToken.java index 87f4ad42d8bf..210d5a5f480f 100644 --- a/services/core/java/com/android/server/wm/WallpaperWindowToken.java +++ b/services/core/java/com/android/server/wm/WallpaperWindowToken.java @@ -100,7 +100,7 @@ class WallpaperWindowToken extends WindowToken { } /** Returns {@code true} if visibility is changed. */ - boolean updateWallpaperWindows(boolean visible) { + void updateWallpaperWindows(boolean visible) { boolean changed = false; if (mVisibleRequested != visible) { ProtoLog.d(WM_DEBUG_WALLPAPER, "Wallpaper token %s visible=%b", @@ -117,7 +117,6 @@ class WallpaperWindowToken extends WindowToken { linkFixedRotationTransform(wallpaperTarget.mToken); } } - return changed; } final WindowState wallpaperTarget = @@ -143,7 +142,6 @@ class WallpaperWindowToken extends WindowToken { } setVisible(visible); - return changed; } private void setVisible(boolean visible) { diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index 67370524dc5e..9e94fdf957b3 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -265,14 +265,6 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< } /** - * Callback which is triggered while changing the parent, after setting up the surface but - * before asking the parent to assign child layers. - */ - interface PreAssignChildLayersCallback { - void onPreAssignChildLayers(); - } - - /** * True if this an AppWindowToken and the activity which created this was launched with * ActivityOptions.setLaunchTaskBehind. * @@ -605,11 +597,6 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< */ @Override void onParentChanged(ConfigurationContainer newParent, ConfigurationContainer oldParent) { - onParentChanged(newParent, oldParent, null); - } - - void onParentChanged(ConfigurationContainer newParent, ConfigurationContainer oldParent, - PreAssignChildLayersCallback callback) { super.onParentChanged(newParent, oldParent); if (mParent == null) { return; @@ -627,13 +614,8 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< reparentSurfaceControl(getSyncTransaction(), mParent.mSurfaceControl); } - if (callback != null) { - callback.onPreAssignChildLayers(); - } - // Either way we need to ask the parent to assign us a Z-order. mParent.assignChildLayers(); - scheduleAnimation(); } void createSurfaceControl(boolean force) { diff --git a/services/core/java/com/android/server/wm/WindowManagerDebugConfig.java b/services/core/java/com/android/server/wm/WindowManagerDebugConfig.java index 6abb8fdfca6c..42b556f77ab6 100644 --- a/services/core/java/com/android/server/wm/WindowManagerDebugConfig.java +++ b/services/core/java/com/android/server/wm/WindowManagerDebugConfig.java @@ -56,7 +56,4 @@ public class WindowManagerDebugConfig { static final boolean SHOW_STACK_CRAWLS = false; static final boolean DEBUG_WINDOW_CROP = false; static final boolean DEBUG_UNKNOWN_APP_VISIBILITY = false; - - // TODO(b/239501597) : Have a system property to control this flag. - public static final boolean DEBUG_IME_VISIBILITY = false; } diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index b83f4231bd78..60b9f4b4117e 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -1423,9 +1423,9 @@ public class WindowManagerService extends IWindowManager.Stub public int addWindow(Session session, IWindow client, LayoutParams attrs, int viewVisibility, int displayId, int requestUserId, @InsetsType int requestedVisibleTypes, InputChannel outInputChannel, InsetsState outInsetsState, - InsetsSourceControl[] outActiveControls, Rect outAttachedFrame, + InsetsSourceControl.Array outActiveControls, Rect outAttachedFrame, float[] outSizeCompatScale) { - Arrays.fill(outActiveControls, null); + outActiveControls.set(null); int[] appOp = new int[1]; final boolean isRoundedCornerOverlay = (attrs.privateFlags & PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY) != 0; @@ -2215,10 +2215,10 @@ public class WindowManagerService extends IWindowManager.Stub int requestedWidth, int requestedHeight, int viewVisibility, int flags, int seq, int lastSyncSeqId, ClientWindowFrames outFrames, MergedConfiguration outMergedConfiguration, SurfaceControl outSurfaceControl, - InsetsState outInsetsState, InsetsSourceControl[] outActiveControls, + InsetsState outInsetsState, InsetsSourceControl.Array outActiveControls, Bundle outSyncIdBundle) { if (outActiveControls != null) { - Arrays.fill(outActiveControls, null); + outActiveControls.set(null); } int result = 0; boolean configChanged = false; @@ -2590,11 +2590,12 @@ public class WindowManagerService extends IWindowManager.Stub return result; } - private void getInsetsSourceControls(WindowState win, InsetsSourceControl[] outControls) { + private void getInsetsSourceControls(WindowState win, InsetsSourceControl.Array outArray) { final InsetsSourceControl[] controls = win.getDisplayContent().getInsetsStateController().getControlsForDispatch(win); if (controls != null) { - final int length = Math.min(controls.length, outControls.length); + final int length = controls.length; + final InsetsSourceControl[] outControls = new InsetsSourceControl[length]; for (int i = 0; i < length; i++) { // We will leave the critical section before returning the leash to the client, // so we need to copy the leash to prevent others release the one that we are @@ -2607,6 +2608,7 @@ public class WindowManagerService extends IWindowManager.Stub outControls[i].setParcelableFlags(PARCELABLE_WRITE_RETURN_VALUE); } } + outArray.set(outControls); } } @@ -5358,7 +5360,6 @@ public class WindowManagerService extends IWindowManager.Stub public static final int ANIMATION_FAILSAFE = 60; public static final int RECOMPUTE_FOCUS = 61; public static final int ON_POINTER_DOWN_OUTSIDE_FOCUS = 62; - public static final int LAYOUT_AND_ASSIGN_WINDOW_LAYERS_IF_NEEDED = 63; public static final int WINDOW_STATE_BLAST_SYNC_TIMEOUT = 64; public static final int REPARENT_TASK_TO_DEFAULT_DISPLAY = 65; public static final int INSETS_CHANGED = 66; @@ -5635,14 +5636,6 @@ public class WindowManagerService extends IWindowManager.Stub } break; } - case LAYOUT_AND_ASSIGN_WINDOW_LAYERS_IF_NEEDED: { - synchronized (mGlobalLock) { - final DisplayContent displayContent = (DisplayContent) msg.obj; - displayContent.mLayoutAndAssignWindowLayersScheduled = false; - displayContent.layoutAndAssignWindowLayersIfNeeded(); - } - break; - } case WINDOW_STATE_BLAST_SYNC_TIMEOUT: { synchronized (mGlobalLock) { final WindowState ws = (WindowState) msg.obj; @@ -8924,28 +8917,17 @@ public class WindowManagerService extends IWindowManager.Stub } @Override - public boolean getWindowInsets(WindowManager.LayoutParams attrs, int displayId, - InsetsState outInsetsState) { - final int uid = Binder.getCallingUid(); + public boolean getWindowInsets(int displayId, IBinder token, InsetsState outInsetsState) { final long origId = Binder.clearCallingIdentity(); try { synchronized (mGlobalLock) { - final DisplayContent dc = getDisplayContentOrCreate(displayId, attrs.token); + final DisplayContent dc = getDisplayContentOrCreate(displayId, token); if (dc == null) { throw new WindowManager.InvalidDisplayException("Display#" + displayId + "could not be found!"); } - final WindowToken token = dc.getWindowToken(attrs.token); - final float overrideScale = mAtmService.mCompatModePackages.getCompatScale( - attrs.packageName, uid); - final InsetsState state = dc.getInsetsPolicy().getInsetsForWindowMetrics(attrs); - outInsetsState.set(state, true /* copySources */); - if (WindowState.hasCompatScale(attrs, token, overrideScale)) { - final float compatScale = token != null && token.hasSizeCompatBounds() - ? token.getCompatScale() * overrideScale - : overrideScale; - outInsetsState.scale(1f / compatScale); - } + final WindowToken winToken = dc.getWindowToken(token); + dc.getInsetsPolicy().getInsetsForWindowMetrics(winToken, outInsetsState); return dc.getDisplayPolicy().areSystemBarsForcedConsumedLw(); } } finally { diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index b624e8064296..7d15902c7e41 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -21,6 +21,7 @@ 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_SET_ANIMATION_PARAMS; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_ADD_RECT_INSETS_PROVIDER; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_CHILDREN_TASKS_REPARENT; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_CLEAR_ADJACENT_ROOTS; @@ -44,6 +45,7 @@ import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP 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; @@ -88,7 +90,9 @@ import android.window.ITransitionMetricsReporter; import android.window.ITransitionPlayer; import android.window.IWindowContainerTransactionCallback; import android.window.IWindowOrganizerController; +import android.window.TaskFragmentAnimationParams; import android.window.TaskFragmentCreationParams; +import android.window.TaskFragmentOperation; import android.window.WindowContainerTransaction; import com.android.internal.annotations.VisibleForTesting; @@ -658,7 +662,8 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub } } - if (windowingMode > -1) { + final int prevWindowingMode = container.getWindowingMode(); + if (windowingMode > -1 && prevWindowingMode != windowingMode) { if (mService.isInLockTaskMode() && WindowConfiguration.inMultiWindowMode(windowingMode)) { throw new UnsupportedOperationException("Not supported to set multi-window" @@ -672,9 +677,8 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub return effects; } - final int prevMode = container.getWindowingMode(); container.setWindowingMode(windowingMode); - if (prevMode != container.getWindowingMode()) { + if (prevWindowingMode != container.getWindowingMode()) { // The activity in the container may become focusable or non-focusable due to // windowing modes changes (such as entering or leaving pinned windowing mode), // so also apply the lifecycle effects to this transaction. @@ -1138,6 +1142,10 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub fragment.setCompanionTaskFragment(companion); break; } + case HIERARCHY_OP_TYPE_SET_TASK_FRAGMENT_OPERATION: { + effects |= applyTaskFragmentOperation(hop, errorCallbackToken, organizer); + break; + } default: { // The other operations may change task order so they are skipped while in lock // task mode. The above operations are still allowed because they don't move @@ -1270,6 +1278,47 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub return effects; } + /** Applies change set through {@link WindowContainerTransaction#setTaskFragmentOperation}. */ + private int applyTaskFragmentOperation(@NonNull WindowContainerTransaction.HierarchyOp hop, + @Nullable IBinder errorCallbackToken, @Nullable ITaskFragmentOrganizer organizer) { + 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; + switch (opType) { + 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); + break; + } + taskFragment.setAnimationParams(animationParams); + break; + } + // TODO(b/263436063): move other TaskFragment related operation here. + } + return effect; + } + /** A helper method to send minimum dimension violation error to the client. */ private void sendMinimumDimensionViolation(TaskFragment taskFragment, Point minDimensions, IBinder errorCallbackToken, String reason) { @@ -1698,6 +1747,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub 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: diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index ae31ee8f288c..59c673791db3 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -364,7 +364,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP boolean mHidden = true; // Used to determine if to show child windows. private boolean mDragResizing; private boolean mDragResizingChangeReported = true; - private boolean mRedrawForSyncReported; + private boolean mRedrawForSyncReported = true; /** * Used to assosciate a given set of state changes sent from MSG_RESIZED @@ -1276,24 +1276,15 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP * @see ActivityRecord#hasSizeCompatBounds() */ boolean hasCompatScale() { - return hasCompatScale(mAttrs, mActivityRecord, mOverrideScale); - } - - /** - * @return {@code true} if the application runs in size compatibility mode. - * @see android.content.res.CompatibilityInfo#supportsScreen - * @see ActivityRecord#hasSizeCompatBounds() - */ - static boolean hasCompatScale(WindowManager.LayoutParams attrs, WindowToken token, - float overrideScale) { - if ((attrs.privateFlags & PRIVATE_FLAG_COMPATIBLE_WINDOW) != 0) { + if ((mAttrs.privateFlags & PRIVATE_FLAG_COMPATIBLE_WINDOW) != 0) { return true; } - if (attrs.type == TYPE_APPLICATION_STARTING) { + if (mAttrs.type == TYPE_APPLICATION_STARTING) { // Exclude starting window because it is not displayed by the application. return false; } - return token != null && token.hasSizeCompatBounds() || overrideScale != 1f; + return mActivityRecord != null && mActivityRecord.hasSizeCompatBounds() + || mOverrideScale != 1f; } /** @@ -5956,8 +5947,11 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP mSyncSeqId++; if (getSyncMethod() == BLASTSyncEngine.METHOD_BLAST) { mPrepareSyncSeqId = mSyncSeqId; + requestRedrawForSync(); + } else if (mHasSurface && mWinAnimator.mDrawState != DRAW_PENDING) { + // Only need to request redraw if the window has reported draw. + requestRedrawForSync(); } - requestRedrawForSync(); return true; } diff --git a/services/core/java/com/android/server/wm/WindowStateAnimator.java b/services/core/java/com/android/server/wm/WindowStateAnimator.java index a0ba8fda906f..5ecf737ede5f 100644 --- a/services/core/java/com/android/server/wm/WindowStateAnimator.java +++ b/services/core/java/com/android/server/wm/WindowStateAnimator.java @@ -428,10 +428,6 @@ class WindowStateAnimator { mShownAlpha = mAlpha; } - private boolean isInBlastSync() { - return mService.useBLASTSync() && mWin.useBLASTSync(); - } - void prepareSurfaceLocked(SurfaceControl.Transaction t) { final WindowState w = mWin; if (!hasSurface()) { diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java b/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java index e4581451288f..70a7a0227a49 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java @@ -1411,5 +1411,8 @@ class ActiveAdmin { pw.print("mtePolicy="); pw.println(mtePolicy); + + pw.print("accountTypesWithManagementDisabled="); + pw.println(accountTypesWithManagementDisabled); } } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java index 15c8c2705e6c..d796ddf7a462 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java @@ -16,9 +16,25 @@ package com.android.server.devicepolicy; +import static android.app.admin.PolicyUpdateReason.REASON_CONFLICTING_ADMIN_POLICY; +import static android.app.admin.PolicyUpdatesReceiver.EXTRA_POLICY_BUNDLE_KEY; +import static android.app.admin.PolicyUpdatesReceiver.EXTRA_POLICY_KEY; +import static android.app.admin.PolicyUpdatesReceiver.EXTRA_POLICY_SET_RESULT_KEY; +import static android.app.admin.PolicyUpdatesReceiver.EXTRA_POLICY_TARGET_USER_ID; +import static android.app.admin.PolicyUpdatesReceiver.EXTRA_POLICY_UPDATE_REASON_KEY; +import static android.app.admin.PolicyUpdatesReceiver.POLICY_SET_RESULT_FAILURE; +import static android.app.admin.PolicyUpdatesReceiver.POLICY_SET_RESULT_SUCCESS; + +import android.Manifest; import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.admin.PolicyUpdatesReceiver; +import android.app.admin.TargetUser; import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.Bundle; import android.os.Environment; import android.os.UserHandle; import android.util.AtomicFile; @@ -39,8 +55,10 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; /** * Class responsible for setting, resolving, and enforcing policies set by multiple management @@ -56,7 +74,7 @@ final class DevicePolicyEngine { /** * Map of <userId, Map<policyKey, policyState>> */ - private final SparseArray<Map<String, PolicyState<?>>> mUserPolicies; + private final SparseArray<Map<String, PolicyState<?>>> mLocalPolicies; /** * Map of <policyKey, policyState> @@ -65,7 +83,7 @@ final class DevicePolicyEngine { DevicePolicyEngine(@NonNull Context context) { mContext = Objects.requireNonNull(context); - mUserPolicies = new SparseArray<>(); + mLocalPolicies = new SparseArray<>(); mGlobalPolicies = new HashMap<>(); } @@ -92,8 +110,28 @@ final class DevicePolicyEngine { boolean policyChanged = policyState.setPolicy(enforcingAdmin, value); if (policyChanged) { - enforcePolicy(policyDefinition, policyState.getCurrentResolvedPolicy(), userId); + enforcePolicy( + policyDefinition, policyState.getCurrentResolvedPolicy(), userId); + sendPolicyChangedToAdmins( + policyState.getPoliciesSetByAdmins().keySet(), + enforcingAdmin, + policyDefinition, + userId == enforcingAdmin.getUserId() + ? TargetUser.LOCAL_USER_ID : TargetUser.PARENT_USER_ID); + } + boolean wasAdminPolicyEnforced = Objects.equals( + policyState.getCurrentResolvedPolicy(), value); + sendPolicyResultToAdmin( + enforcingAdmin, + policyDefinition, + wasAdminPolicyEnforced, + // TODO: we're always sending this for now, should properly handle errors. + REASON_CONFLICTING_ADMIN_POLICY, + userId == enforcingAdmin.getUserId() + ? TargetUser.LOCAL_USER_ID : TargetUser.PARENT_USER_ID); + + write(); return policyChanged; } } @@ -117,12 +155,28 @@ final class DevicePolicyEngine { synchronized (mLock) { PolicyState<V> policyState = getGlobalPolicyStateLocked(policyDefinition); - boolean policyChanged = policyState.setPolicy(enforcingAdmin, value); + boolean policyChanged = policyState.setPolicy(enforcingAdmin, value); if (policyChanged) { enforcePolicy(policyDefinition, policyState.getCurrentResolvedPolicy(), UserHandle.USER_ALL); + sendPolicyChangedToAdmins( + policyState.getPoliciesSetByAdmins().keySet(), + enforcingAdmin, + policyDefinition, + TargetUser.GLOBAL_USER_ID); } + boolean wasAdminPolicyEnforced = Objects.equals( + policyState.getCurrentResolvedPolicy(), value); + sendPolicyResultToAdmin( + enforcingAdmin, + policyDefinition, + wasAdminPolicyEnforced, + // TODO: we're always sending this for now, should properly handle errors. + REASON_CONFLICTING_ADMIN_POLICY, + TargetUser.GLOBAL_USER_ID); + + write(); return policyChanged; } } @@ -148,8 +202,26 @@ final class DevicePolicyEngine { boolean policyChanged = policyState.removePolicy(enforcingAdmin); if (policyChanged) { - enforcePolicy(policyDefinition, policyState.getCurrentResolvedPolicy(), userId); + enforcePolicy( + policyDefinition, policyState.getCurrentResolvedPolicy(), userId); + sendPolicyChangedToAdmins( + policyState.getPoliciesSetByAdmins().keySet(), + enforcingAdmin, + policyDefinition, + userId == enforcingAdmin.getUserId() + ? TargetUser.LOCAL_USER_ID : TargetUser.PARENT_USER_ID); } + // for a remove policy to be enforced, it means no current policy exists + boolean wasAdminPolicyEnforced = policyState.getCurrentResolvedPolicy() == null; + sendPolicyResultToAdmin( + enforcingAdmin, + policyDefinition, + wasAdminPolicyEnforced, + // TODO: we're always sending this for now, should properly handle errors. + REASON_CONFLICTING_ADMIN_POLICY, + userId == enforcingAdmin.getUserId() + ? TargetUser.LOCAL_USER_ID : TargetUser.PARENT_USER_ID); + write(); return policyChanged; } @@ -176,7 +248,23 @@ final class DevicePolicyEngine { if (policyChanged) { enforcePolicy(policyDefinition, policyState.getCurrentResolvedPolicy(), UserHandle.USER_ALL); + + sendPolicyChangedToAdmins( + policyState.getPoliciesSetByAdmins().keySet(), + enforcingAdmin, + policyDefinition, + TargetUser.GLOBAL_USER_ID); } + // for a remove policy to be enforced, it means no current policy exists + boolean wasAdminPolicyEnforced = policyState.getCurrentResolvedPolicy() == null; + sendPolicyResultToAdmin( + enforcingAdmin, + policyDefinition, + wasAdminPolicyEnforced, + // TODO: we're always sending this for now, should properly handle errors. + REASON_CONFLICTING_ADMIN_POLICY, + TargetUser.GLOBAL_USER_ID); + write(); return policyChanged; } @@ -215,19 +303,18 @@ final class DevicePolicyEngine { + policyDefinition.getPolicyKey() + " locally."); } - if (!mUserPolicies.contains(userId)) { - mUserPolicies.put(userId, new HashMap<>()); + if (!mLocalPolicies.contains(userId)) { + mLocalPolicies.put(userId, new HashMap<>()); } - if (!mUserPolicies.get(userId).containsKey(policyDefinition.getPolicyKey())) { - mUserPolicies.get(userId).put( + if (!mLocalPolicies.get(userId).containsKey(policyDefinition.getPolicyKey())) { + mLocalPolicies.get(userId).put( policyDefinition.getPolicyKey(), new PolicyState<>(policyDefinition)); } - return getPolicyState(mUserPolicies.get(userId), policyDefinition); + return getPolicyState(mLocalPolicies.get(userId), policyDefinition); } @NonNull private <V> PolicyState<V> getGlobalPolicyStateLocked(PolicyDefinition<V> policyDefinition) { - if (policyDefinition.isLocalOnlyPolicy()) { throw new IllegalArgumentException("Can't set local policy " + policyDefinition.getPolicyKey() + " globally."); @@ -260,9 +347,104 @@ final class DevicePolicyEngine { // TODO: null policyValue means remove any enforced policies, ensure callbacks handle this // properly policyDefinition.enforcePolicy(policyValue, mContext, userId); - // TODO: send broadcast or call callback to notify admins of policy change - // TODO: notify calling admin of result (e.g. success, runtime failure, policy set by - // a different admin) + } + + private <V> void sendPolicyResultToAdmin( + EnforcingAdmin admin, PolicyDefinition<V> policyDefinition, boolean success, + int reason, int targetUserId) { + Intent intent = new Intent(PolicyUpdatesReceiver.ACTION_DEVICE_POLICY_SET_RESULT); + intent.setPackage(admin.getPackageName()); + + List<ResolveInfo> receivers = mContext.getPackageManager().queryBroadcastReceiversAsUser( + intent, + PackageManager.ResolveInfoFlags.of(PackageManager.GET_RECEIVERS), + admin.getUserId()); + if (receivers.isEmpty()) { + Log.i(TAG, "Couldn't find any receivers that handle ACTION_DEVICE_POLICY_SET_RESULT" + + "in package " + admin.getPackageName()); + return; + } + + Bundle extras = new Bundle(); + extras.putString(EXTRA_POLICY_KEY, policyDefinition.getPolicyDefinitionKey()); + extras.putInt(EXTRA_POLICY_TARGET_USER_ID, targetUserId); + + if (policyDefinition.getCallbackArgs() != null + && !policyDefinition.getCallbackArgs().isEmpty()) { + extras.putBundle(EXTRA_POLICY_BUNDLE_KEY, policyDefinition.getCallbackArgs()); + } + extras.putInt( + EXTRA_POLICY_SET_RESULT_KEY, + success ? POLICY_SET_RESULT_SUCCESS : POLICY_SET_RESULT_FAILURE); + + if (!success) { + extras.putInt(EXTRA_POLICY_UPDATE_REASON_KEY, reason); + } + + intent.putExtras(extras); + maybeSendIntentToAdminReceivers(intent, UserHandle.of(admin.getUserId()), receivers); + } + + // TODO(b/261430877): Finalise the decision on which admins to send the updates to. + private <V> void sendPolicyChangedToAdmins( + Set<EnforcingAdmin> admins, EnforcingAdmin callingAdmin, + PolicyDefinition<V> policyDefinition, + int targetUserId) { + for (EnforcingAdmin admin: admins) { + // We're sending a separate broadcast for the calling admin with the result. + if (admin.equals(callingAdmin)) { + continue; + } + maybeSendOnPolicyChanged( + admin, policyDefinition, REASON_CONFLICTING_ADMIN_POLICY, targetUserId); + } + } + + private <V> void maybeSendOnPolicyChanged( + EnforcingAdmin admin, PolicyDefinition<V> policyDefinition, int reason, + int targetUserId) { + Intent intent = new Intent(PolicyUpdatesReceiver.ACTION_DEVICE_POLICY_CHANGED); + intent.setPackage(admin.getPackageName()); + + List<ResolveInfo> receivers = mContext.getPackageManager().queryBroadcastReceiversAsUser( + intent, + PackageManager.ResolveInfoFlags.of(PackageManager.GET_RECEIVERS), + admin.getUserId()); + if (receivers.isEmpty()) { + Log.i(TAG, "Couldn't find any receivers that handle ACTION_DEVICE_POLICY_CHANGED" + + "in package " + admin.getPackageName()); + return; + } + + Bundle extras = new Bundle(); + extras.putString(EXTRA_POLICY_KEY, policyDefinition.getPolicyDefinitionKey()); + extras.putInt(EXTRA_POLICY_TARGET_USER_ID, targetUserId); + + if (policyDefinition.getCallbackArgs() != null + && !policyDefinition.getCallbackArgs().isEmpty()) { + extras.putBundle(EXTRA_POLICY_BUNDLE_KEY, policyDefinition.getCallbackArgs()); + } + extras.putInt(EXTRA_POLICY_UPDATE_REASON_KEY, reason); + intent.putExtras(extras); + maybeSendIntentToAdminReceivers( + intent, UserHandle.of(admin.getUserId()), receivers); + } + + private void maybeSendIntentToAdminReceivers( + Intent intent, UserHandle userHandle, List<ResolveInfo> receivers) { + for (ResolveInfo resolveInfo : receivers) { + if (!Manifest.permission.BIND_DEVICE_ADMIN.equals( + resolveInfo.activityInfo.permission)) { + Log.w(TAG, "Receiver " + resolveInfo.activityInfo + " is not protected by" + + "BIND_DEVICE_ADMIN permission!"); + continue; + } + // TODO: If admins are always bound to, do I still need to set + // "BroadcastOptions.setBackgroundActivityStartsAllowed"? + // TODO: maybe protect it with a permission that is granted to the role so that we + // don't accidentally send a broadcast to an admin that no longer holds the role. + mContext.sendBroadcastAsUser(intent, userHandle); + } } private void write() { @@ -283,14 +465,14 @@ final class DevicePolicyEngine { private void clear() { synchronized (mLock) { mGlobalPolicies.clear(); - mUserPolicies.clear(); + mLocalPolicies.clear(); } } private class DevicePoliciesReaderWriter { private static final String DEVICE_POLICIES_XML = "device_policies.xml"; - private static final String TAG_USER_POLICY_ENTRY = "user-policy-entry"; - private static final String TAG_DEVICE_POLICY_ENTRY = "device-policy-entry"; + private static final String TAG_LOCAL_POLICY_ENTRY = "local-policy-entry"; + private static final String TAG_GLOBAL_POLICY_ENTRY = "global-policy-entry"; private static final String TAG_ADMINS_POLICY_ENTRY = "admins-policy-entry"; private static final String ATTR_USER_ID = "user-id"; private static final String ATTR_POLICY_ID = "policy-id"; @@ -332,17 +514,17 @@ final class DevicePolicyEngine { // TODO(b/256846294): Add versioning to read/write void writeInner(TypedXmlSerializer serializer) throws IOException { - writeUserPoliciesInner(serializer); - writeDevicePoliciesInner(serializer); + writeLocalPoliciesInner(serializer); + writeGlobalPoliciesInner(serializer); } - private void writeUserPoliciesInner(TypedXmlSerializer serializer) throws IOException { - if (mUserPolicies != null) { - for (int i = 0; i < mUserPolicies.size(); i++) { - int userId = mUserPolicies.keyAt(i); - for (Map.Entry<String, PolicyState<?>> policy : mUserPolicies.get( + private void writeLocalPoliciesInner(TypedXmlSerializer serializer) throws IOException { + if (mLocalPolicies != null) { + for (int i = 0; i < mLocalPolicies.size(); i++) { + int userId = mLocalPolicies.keyAt(i); + for (Map.Entry<String, PolicyState<?>> policy : mLocalPolicies.get( userId).entrySet()) { - serializer.startTag(/* namespace= */ null, TAG_USER_POLICY_ENTRY); + serializer.startTag(/* namespace= */ null, TAG_LOCAL_POLICY_ENTRY); serializer.attributeInt(/* namespace= */ null, ATTR_USER_ID, userId); serializer.attribute( @@ -352,16 +534,16 @@ final class DevicePolicyEngine { policy.getValue().saveToXml(serializer); serializer.endTag(/* namespace= */ null, TAG_ADMINS_POLICY_ENTRY); - serializer.endTag(/* namespace= */ null, TAG_USER_POLICY_ENTRY); + serializer.endTag(/* namespace= */ null, TAG_LOCAL_POLICY_ENTRY); } } } } - private void writeDevicePoliciesInner(TypedXmlSerializer serializer) throws IOException { + private void writeGlobalPoliciesInner(TypedXmlSerializer serializer) throws IOException { if (mGlobalPolicies != null) { for (Map.Entry<String, PolicyState<?>> policy : mGlobalPolicies.entrySet()) { - serializer.startTag(/* namespace= */ null, TAG_DEVICE_POLICY_ENTRY); + serializer.startTag(/* namespace= */ null, TAG_GLOBAL_POLICY_ENTRY); serializer.attribute(/* namespace= */ null, ATTR_POLICY_ID, policy.getKey()); @@ -369,7 +551,7 @@ final class DevicePolicyEngine { policy.getValue().saveToXml(serializer); serializer.endTag(/* namespace= */ null, TAG_ADMINS_POLICY_ENTRY); - serializer.endTag(/* namespace= */ null, TAG_DEVICE_POLICY_ENTRY); + serializer.endTag(/* namespace= */ null, TAG_GLOBAL_POLICY_ENTRY); } } } @@ -402,11 +584,11 @@ final class DevicePolicyEngine { while (XmlUtils.nextElementWithin(parser, outerDepth)) { String tag = parser.getName(); switch (tag) { - case TAG_USER_POLICY_ENTRY: - readUserPoliciesInner(parser); + case TAG_LOCAL_POLICY_ENTRY: + readLocalPoliciesInner(parser); break; - case TAG_DEVICE_POLICY_ENTRY: - readDevicePoliciesInner(parser); + case TAG_GLOBAL_POLICY_ENTRY: + readGlobalPoliciesInner(parser); break; default: Log.e(TAG, "Unknown tag " + tag); @@ -414,24 +596,24 @@ final class DevicePolicyEngine { } } - private void readUserPoliciesInner(TypedXmlPullParser parser) + private void readLocalPoliciesInner(TypedXmlPullParser parser) throws XmlPullParserException, IOException { int userId = parser.getAttributeInt(/* namespace= */ null, ATTR_USER_ID); String policyKey = parser.getAttributeValue( /* namespace= */ null, ATTR_POLICY_ID); - if (!mUserPolicies.contains(userId)) { - mUserPolicies.put(userId, new HashMap<>()); + if (!mLocalPolicies.contains(userId)) { + mLocalPolicies.put(userId, new HashMap<>()); } PolicyState<?> adminsPolicy = parseAdminsPolicy(parser); if (adminsPolicy != null) { - mUserPolicies.get(userId).put(policyKey, adminsPolicy); + mLocalPolicies.get(userId).put(policyKey, adminsPolicy); } else { Log.e(TAG, "Error parsing file, " + policyKey + "doesn't have an " + "AdminsPolicy."); } } - private void readDevicePoliciesInner(TypedXmlPullParser parser) + private void readGlobalPoliciesInner(TypedXmlPullParser parser) throws IOException, XmlPullParserException { String policyKey = parser.getAttributeValue(/* namespace= */ null, ATTR_POLICY_ID); PolicyState<?> adminsPolicy = parseAdminsPolicy(parser); diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index 8c2065e7f764..51f3e321338d 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -18,7 +18,13 @@ package com.android.server.devicepolicy; import static android.Manifest.permission.BIND_DEVICE_ADMIN; import static android.Manifest.permission.MANAGE_CA_CERTIFICATES; +import static android.Manifest.permission.MANAGE_DEVICE_POLICY_ACROSS_USERS; +import static android.Manifest.permission.MANAGE_DEVICE_POLICY_ACROSS_USERS_FULL; +import static android.Manifest.permission.MANAGE_DEVICE_POLICY_ACROSS_USERS_SECURITY_CRITICAL; +import static android.Manifest.permission.QUERY_ADMIN_POLICY; import static android.Manifest.permission.REQUEST_PASSWORD_COMPLEXITY; +import static android.Manifest.permission.SET_TIME; +import static android.Manifest.permission.SET_TIME_ZONE; import static android.accessibilityservice.AccessibilityServiceInfo.FEEDBACK_ALL_MASK; import static android.app.ActivityManager.LOCK_TASK_MODE_NONE; import static android.app.AppOpsManager.MODE_ALLOWED; @@ -137,6 +143,7 @@ import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE; import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE; import static android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES; +import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_DEFAULT; import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPRISE; import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK; @@ -715,7 +722,10 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { + "management app's authentication policy"; private static final String NOT_SYSTEM_CALLER_MSG = "Only the system can %s"; + private static final String PERMISSION_BASED_ACCESS_EXPERIMENT_FLAG = + "enable_permission_based_access"; private static final String ENABLE_COEXISTENCE_FLAG = "enable_coexistence"; + private static final boolean DEFAULT_VALUE_PERMISSION_BASED_ACCESS_FLAG = false; private static final boolean DEFAULT_ENABLE_COEXISTENCE_FLAG = false; // TODO(b/258425381) remove the flag after rollout. @@ -8033,9 +8043,15 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { Objects.requireNonNull(who, "ComponentName is null"); final CallerIdentity caller = getCallerIdentity(who); - Preconditions.checkCallAuthorization(isProfileOwnerOnUser0(caller) - || isProfileOwnerOfOrganizationOwnedDevice(caller) || isDefaultDeviceOwner(caller)); + if (isPermissionCheckFlagEnabled()) { + // The effect of this policy is device-wide. + enforcePermission(SET_TIME, UserHandle.USER_ALL); + } else { + Preconditions.checkCallAuthorization(isProfileOwnerOnUser0(caller) + || isProfileOwnerOfOrganizationOwnedDevice(caller) || isDefaultDeviceOwner( + caller)); + } mInjector.binderWithCleanCallingIdentity(() -> mInjector.settingsGlobalPutInt(Settings.Global.AUTO_TIME, enabled ? 1 : 0)); @@ -8057,8 +8073,14 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { Objects.requireNonNull(who, "ComponentName is null"); final CallerIdentity caller = getCallerIdentity(who); - Preconditions.checkCallAuthorization(isProfileOwnerOnUser0(caller) - || isProfileOwnerOfOrganizationOwnedDevice(caller) || isDefaultDeviceOwner(caller)); + + if (isPermissionCheckFlagEnabled()) { + enforceCanQuery(SET_TIME, UserHandle.USER_ALL); + } else { + Preconditions.checkCallAuthorization(isProfileOwnerOnUser0(caller) + || isProfileOwnerOfOrganizationOwnedDevice(caller) || isDefaultDeviceOwner( + caller)); + } return mInjector.settingsGlobalGetInt(Global.AUTO_TIME, 0) > 0; } @@ -8074,15 +8096,23 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { Objects.requireNonNull(who, "ComponentName is null"); final CallerIdentity caller = getCallerIdentity(who); - Preconditions.checkCallAuthorization(isProfileOwnerOnUser0(caller) - || isProfileOwnerOfOrganizationOwnedDevice(caller) || isDefaultDeviceOwner(caller)); + + if (isPermissionCheckFlagEnabled()) { + // The effect of this policy is device-wide. + enforcePermission(SET_TIME_ZONE, UserHandle.USER_ALL); + } else { + Preconditions.checkCallAuthorization(isProfileOwnerOnUser0(caller) + || isProfileOwnerOfOrganizationOwnedDevice(caller) || isDefaultDeviceOwner( + caller)); + } if (isCoexistenceEnabled(caller)) { mDevicePolicyEngine.setGlobalPolicy( PolicyDefinition.AUTO_TIMEZONE, // TODO(b/260573124): add correct enforcing admin when permission changes are // merged. - EnforcingAdmin.createEnterpriseEnforcingAdmin(caller.getComponentName()), + EnforcingAdmin.createEnterpriseEnforcingAdmin( + caller.getComponentName(), caller.getUserId()), enabled); } else { mInjector.binderWithCleanCallingIdentity(() -> @@ -8107,8 +8137,15 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { Objects.requireNonNull(who, "ComponentName is null"); final CallerIdentity caller = getCallerIdentity(who); - Preconditions.checkCallAuthorization(isProfileOwnerOnUser0(caller) - || isProfileOwnerOfOrganizationOwnedDevice(caller) || isDefaultDeviceOwner(caller)); + + if (isPermissionCheckFlagEnabled()) { + // The effect of this policy is device-wide. + enforceCanQuery(SET_TIME_ZONE, UserHandle.USER_ALL); + } else { + Preconditions.checkCallAuthorization(isProfileOwnerOnUser0(caller) + || isProfileOwnerOfOrganizationOwnedDevice(caller) || isDefaultDeviceOwner( + caller)); + } return mInjector.settingsGlobalGetInt(Global.AUTO_TIME_ZONE, 0) > 0; } @@ -12599,7 +12636,8 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { } if (isCoexistenceEnabled(caller)) { - EnforcingAdmin admin = EnforcingAdmin.createEnterpriseEnforcingAdmin(who); + EnforcingAdmin admin = EnforcingAdmin.createEnterpriseEnforcingAdmin( + who, caller.getUserId()); if (packages.length == 0) { mDevicePolicyEngine.removeLocalPolicy( PolicyDefinition.LOCK_TASK, @@ -12619,7 +12657,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { mDevicePolicyEngine.setLocalPolicy( PolicyDefinition.LOCK_TASK, - EnforcingAdmin.createEnterpriseEnforcingAdmin(who), + EnforcingAdmin.createEnterpriseEnforcingAdmin(who, caller.getUserId()), policy, caller.getUserId()); } @@ -12715,7 +12753,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { checkCanExecuteOrThrowUnsafe(DevicePolicyManager.OPERATION_SET_LOCK_TASK_FEATURES); } if (isCoexistenceEnabled(caller)) { - EnforcingAdmin admin = EnforcingAdmin.createEnterpriseEnforcingAdmin(who); + EnforcingAdmin admin = EnforcingAdmin.createEnterpriseEnforcingAdmin(who, userHandle); LockTaskPolicy currentPolicy = mDevicePolicyEngine.getLocalPolicy( PolicyDefinition.LOCK_TASK, caller.getUserId()).getPoliciesSetByAdmins().get(admin); @@ -12728,7 +12766,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { mDevicePolicyEngine.setLocalPolicy( PolicyDefinition.LOCK_TASK, - EnforcingAdmin.createEnterpriseEnforcingAdmin(who), + EnforcingAdmin.createEnterpriseEnforcingAdmin(who, userHandle), policy, caller.getUserId()); } else { @@ -13031,8 +13069,14 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { Objects.requireNonNull(who, "ComponentName is null"); final CallerIdentity caller = getCallerIdentity(who); - Preconditions.checkCallAuthorization( - isDefaultDeviceOwner(caller) || isProfileOwnerOfOrganizationOwnedDevice(caller)); + if (isPermissionCheckFlagEnabled()) { + // This is a global action. + enforcePermission(SET_TIME, UserHandle.USER_ALL); + } else { + Preconditions.checkCallAuthorization( + isDefaultDeviceOwner(caller) + || isProfileOwnerOfOrganizationOwnedDevice(caller)); + } // Don't allow set time when auto time is on. if (mInjector.settingsGlobalGetInt(Global.AUTO_TIME, 0) == 1) { @@ -13051,8 +13095,14 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { Objects.requireNonNull(who, "ComponentName is null"); final CallerIdentity caller = getCallerIdentity(who); - Preconditions.checkCallAuthorization( - isDefaultDeviceOwner(caller) || isProfileOwnerOfOrganizationOwnedDevice(caller)); + if (isPermissionCheckFlagEnabled()) { + // This is a global action. + enforcePermission(SET_TIME_ZONE, UserHandle.USER_ALL); + } else { + Preconditions.checkCallAuthorization( + isDefaultDeviceOwner(caller) + || isProfileOwnerOfOrganizationOwnedDevice(caller)); + } // Don't allow set timezone when auto timezone is on. if (mInjector.settingsGlobalGetInt(Global.AUTO_TIME_ZONE, 0) == 1) { @@ -13657,6 +13707,16 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { broadcastIntentToDevicePolicyManagerRoleHolder(intent, parentHandle); } + @Override + public void enforcePermission(String permission, int targetUserId) { + DevicePolicyManagerService.this.enforcePermission(permission, targetUserId); + } + + @Override + public boolean hasPermission(String permission, int targetUserId) { + return DevicePolicyManagerService.this.hasPermission(permission, targetUserId); + } + private void broadcastIntentToCrossProfileManifestReceivers( Intent intent, UserHandle userHandle, boolean requiresPermission) { final int userId = userHandle.getIdentifier(); @@ -14362,7 +14422,8 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { // TODO(b/260573124): Add correct enforcing admin when permission changes are // merged, and don't forget to handle delegates! Enterprise admins assume // component name isn't null. - EnforcingAdmin.createEnterpriseEnforcingAdmin(caller.getComponentName()), + EnforcingAdmin.createEnterpriseEnforcingAdmin( + caller.getComponentName(), caller.getUserId()), grantState, caller.getUserId()); // TODO: update javadoc to reflect that callback no longer return success/failure @@ -18408,14 +18469,25 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { } } + private String getDevicePolicyManagementRoleHolderPackageName(Context context) { RoleManager roleManager = context.getSystemService(RoleManager.class); - List<String> roleHolders = - roleManager.getRoleHolders(RoleManager.ROLE_DEVICE_POLICY_MANAGEMENT); - if (roleHolders.isEmpty()) { - return null; - } - return roleHolders.get(0); + + // Calling identity needs to be cleared as this method is used in the permissions checks. + return mInjector.binderWithCleanCallingIdentity(() -> { + List<String> roleHolders = + roleManager.getRoleHolders(RoleManager.ROLE_DEVICE_POLICY_MANAGEMENT); + if (roleHolders.isEmpty()) { + return null; + } + return roleHolders.get(0); + }); + } + + private boolean isDevicePolicyManagementRoleHolder(CallerIdentity caller) { + String devicePolicyManagementRoleHolderPackageName = + getDevicePolicyManagementRoleHolderPackageName(mContext); + return caller.getPackageName().equals(devicePolicyManagementRoleHolderPackageName); } private void resetInteractAcrossProfilesAppOps() { @@ -19513,6 +19585,192 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { }); } + // DPC types + private static final int DEFAULT_DEVICE_OWNER = 0; + private static final int FINANCED_DEVICE_OWNER = 1; + private static final int PROFILE_OWNER_OF_ORGANIZATION_OWNED_DEVICE = 2; + private static final int PROFILE_OWNER_ON_USER_0 = 3; + private static final int PROFILE_OWNER = 4; + + // Permissions of existing DPC types. + private static final List<String> DEFAULT_DEVICE_OWNER_PERMISSIONS = List.of( + MANAGE_DEVICE_POLICY_ACROSS_USERS_FULL, + MANAGE_DEVICE_POLICY_ACROSS_USERS, + MANAGE_DEVICE_POLICY_ACROSS_USERS_SECURITY_CRITICAL, + SET_TIME, + SET_TIME_ZONE); + private static final List<String> FINANCED_DEVICE_OWNER_PERMISSIONS = List.of( + MANAGE_DEVICE_POLICY_ACROSS_USERS_FULL, + MANAGE_DEVICE_POLICY_ACROSS_USERS, + MANAGE_DEVICE_POLICY_ACROSS_USERS_SECURITY_CRITICAL); + private static final List<String> PROFILE_OWNER_OF_ORGANIZATION_OWNED_DEVICE_PERMISSIONS = + List.of( + MANAGE_DEVICE_POLICY_ACROSS_USERS, + MANAGE_DEVICE_POLICY_ACROSS_USERS_SECURITY_CRITICAL, + SET_TIME, + SET_TIME_ZONE); + private static final List<String> PROFILE_OWNER_ON_USER_0_PERMISSIONS = List.of( + SET_TIME, + SET_TIME_ZONE); + private static final List<String> PROFILE_OWNER_PERMISSIONS = List.of( + MANAGE_DEVICE_POLICY_ACROSS_USERS_SECURITY_CRITICAL); + + private static final HashMap<Integer, List<String>> DPC_PERMISSIONS = new HashMap<>(); + { + DPC_PERMISSIONS.put(DEFAULT_DEVICE_OWNER, DEFAULT_DEVICE_OWNER_PERMISSIONS); + DPC_PERMISSIONS.put(FINANCED_DEVICE_OWNER, FINANCED_DEVICE_OWNER_PERMISSIONS); + DPC_PERMISSIONS.put(PROFILE_OWNER_OF_ORGANIZATION_OWNED_DEVICE, + PROFILE_OWNER_OF_ORGANIZATION_OWNED_DEVICE_PERMISSIONS); + DPC_PERMISSIONS.put(PROFILE_OWNER_ON_USER_0, PROFILE_OWNER_ON_USER_0_PERMISSIONS); + DPC_PERMISSIONS.put(PROFILE_OWNER, PROFILE_OWNER_PERMISSIONS); + } + + //TODO(b/254253251) Fill this map in as new permissions are added for policies. + private static final HashMap<String, Integer> ACTIVE_ADMIN_POLICIES = new HashMap<>(); + + private static final HashMap<String, String> CROSS_USER_PERMISSIONS = + new HashMap<>(); + { + // Auto time is intrinsically global so there is no cross-user permission. + CROSS_USER_PERMISSIONS.put(SET_TIME, null); + CROSS_USER_PERMISSIONS.put(SET_TIME_ZONE, null); + } + + /** + * Checks if the calling process has been granted permission to apply a device policy on a + * specific user. + * The given permission will be checked along with its associated cross-user permission if it + * exists and the target user is different to the calling user. + * + * @param permission The name of the permission being checked. + * @param targetUserId The userId of the user which the caller needs permission to act on. + * @throws SecurityException if the caller has not been granted the given permission, + * the associtated cross-user permission if the caller's user is different to the target user. + */ + private void enforcePermission(String permission, int targetUserId) + throws SecurityException { + if (!hasPermission(permission, targetUserId)) { + throw new SecurityException("Caller does not have the required permissions for " + + "this user. Permissions required: {" + + permission + + ", " + + CROSS_USER_PERMISSIONS.get(permission) + + "}"); + } + } + + /** + * Return whether the calling process has been granted permission to query a device policy on + * a specific user. + * + * @param permission The name of the permission being checked. + * @param targetUserId The userId of the user which the caller needs permission to act on. + * @throws SecurityException if the caller has not been granted the given permission, + * the associatated cross-user permission if the caller's user is different to the target user + * and if the user has not been granted {@link QUERY_ADMIN_POLICY}. + */ + private void enforceCanQuery(String permission, int targetUserId) throws SecurityException { + if (hasPermission(QUERY_ADMIN_POLICY)) { + return; + } + enforcePermission(permission, targetUserId); + } + + /** + * Return whether the calling process has been granted permission to apply a device policy on + * a specific user. + * + * @param permission The name of the permission being checked. + * @param targetUserId The userId of the user which the caller needs permission to act on. + */ + private boolean hasPermission(String permission, int targetUserId) { + boolean hasPermissionOnOwnUser = hasPermission(permission); + boolean hasPermissionOnTargetUser = true; + if (hasPermissionOnOwnUser & getCallerIdentity().getUserId() != targetUserId) { + hasPermissionOnTargetUser = hasPermission(CROSS_USER_PERMISSIONS.get(permission)); + } + return hasPermissionOnOwnUser && hasPermissionOnTargetUser; + } + + /** + * Return whether the calling process has been granted the given permission. + * + * @param permission The name of the permission being checked. + */ + private boolean hasPermission(String permission) { + if (permission == null) { + return true; + } + + CallerIdentity caller = getCallerIdentity(); + + // Check if the caller holds the permission + if (mContext.checkCallingOrSelfPermission(permission) == PERMISSION_GRANTED) { + return true; + } + // Check the permissions of DPCs + if (isDefaultDeviceOwner(caller)) { + return DPC_PERMISSIONS.get(DEFAULT_DEVICE_OWNER).contains(permission); + } + if (isFinancedDeviceOwner(caller)) { + return DPC_PERMISSIONS.get(FINANCED_DEVICE_OWNER).contains(permission); + } + if (isProfileOwnerOfOrganizationOwnedDevice(caller)) { + return DPC_PERMISSIONS.get(PROFILE_OWNER_OF_ORGANIZATION_OWNED_DEVICE).contains( + permission); + } + if (isProfileOwnerOnUser0(caller)) { + return DPC_PERMISSIONS.get(PROFILE_OWNER_ON_USER_0).contains(permission); + } + if (isProfileOwner(caller)) { + return DPC_PERMISSIONS.get(PROFILE_OWNER).contains(permission); + } + // Check the permission for the role-holder + if (isDevicePolicyManagementRoleHolder(caller)) { + return anyDpcHasPermission(permission, mContext.getUserId()); + } + // Check if the caller is an active admin that uses a certain policy. + if (ACTIVE_ADMIN_POLICIES.containsKey(permission)) { + return getActiveAdminForCallerLocked( + null, ACTIVE_ADMIN_POLICIES.get(permission), false) != null; + } + + return false; + } + + /** + * Returns whether there is a DPC on the given user that has been granted the given permission. + * + * @param permission The name of the permission being checked. + * @param userId The id of the user to check. + */ + private boolean anyDpcHasPermission(String permission, int userId) { + if (mOwners.isDefaultDeviceOwnerUserId(userId)) { + return DPC_PERMISSIONS.get(DEFAULT_DEVICE_OWNER).contains(permission); + } + if (mOwners.isFinancedDeviceOwnerUserId(userId)) { + return DPC_PERMISSIONS.get(FINANCED_DEVICE_OWNER).contains(permission); + } + if (mOwners.isProfileOwnerOfOrganizationOwnedDevice(userId)) { + return DPC_PERMISSIONS.get(PROFILE_OWNER_OF_ORGANIZATION_OWNED_DEVICE).contains( + permission); + } + if (userId == 0 && mOwners.hasProfileOwner(0)) { + return DPC_PERMISSIONS.get(PROFILE_OWNER_ON_USER_0).contains(permission); + } + if (mOwners.hasProfileOwner(userId)) { + return DPC_PERMISSIONS.get(PROFILE_OWNER).contains(permission); + } + return false; + } + + private boolean isPermissionCheckFlagEnabled() { + return DeviceConfig.getBoolean( + NAMESPACE_DEVICE_POLICY_MANAGER, + PERMISSION_BASED_ACCESS_EXPERIMENT_FLAG, + DEFAULT_VALUE_PERMISSION_BASED_ACCESS_FLAG); + } + // TODO(b/260560985): properly gate coexistence changes private boolean isCoexistenceEnabled(CallerIdentity caller) { return isCoexistenceFlagEnabled() diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/EnforcingAdmin.java b/services/devicepolicy/java/com/android/server/devicepolicy/EnforcingAdmin.java index 9261d59ce1e3..00e48eb67ab0 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/EnforcingAdmin.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/EnforcingAdmin.java @@ -69,16 +69,18 @@ final class EnforcingAdmin { return new EnforcingAdmin(packageName, userId); } - static EnforcingAdmin createEnterpriseEnforcingAdmin(@NonNull ComponentName componentName) { + static EnforcingAdmin createEnterpriseEnforcingAdmin( + @NonNull ComponentName componentName, int userId) { Objects.requireNonNull(componentName); return new EnforcingAdmin( - componentName.getPackageName(), componentName, Set.of(DPC_AUTHORITY)); + componentName.getPackageName(), componentName, Set.of(DPC_AUTHORITY), userId); } - static EnforcingAdmin createDeviceAdminEnforcingAdmin(ComponentName componentName) { + static EnforcingAdmin createDeviceAdminEnforcingAdmin(ComponentName componentName, int userId) { Objects.requireNonNull(componentName); return new EnforcingAdmin( - componentName.getPackageName(), componentName, Set.of(DEVICE_ADMIN_AUTHORITY)); + componentName.getPackageName(), componentName, Set.of(DEVICE_ADMIN_AUTHORITY), + userId); } static String getRoleAuthorityOf(String roleName) { @@ -86,7 +88,7 @@ final class EnforcingAdmin { } private EnforcingAdmin( - String packageName, ComponentName componentName, Set<String> authorities) { + String packageName, ComponentName componentName, Set<String> authorities, int userId) { Objects.requireNonNull(packageName); Objects.requireNonNull(componentName); Objects.requireNonNull(authorities); @@ -96,7 +98,7 @@ final class EnforcingAdmin { mPackageName = packageName; mComponentName = componentName; mAuthorities = new HashSet<>(authorities); - mUserId = -1; // not needed for non role authorities + mUserId = userId; } private EnforcingAdmin(String packageName, int userId) { @@ -145,6 +147,15 @@ final class EnforcingAdmin { return getAuthorities().contains(authority); } + @NonNull + String getPackageName() { + return mPackageName; + } + + int getUserId() { + return mUserId; + } + /** * For two EnforcingAdmins to be equal they must: * @@ -188,11 +199,11 @@ final class EnforcingAdmin { void saveToXml(TypedXmlSerializer serializer) throws IOException { serializer.attribute(/* namespace= */ null, ATTR_PACKAGE_NAME, mPackageName); serializer.attributeBoolean(/* namespace= */ null, ATTR_IS_ROLE, mIsRoleAuthority); - if (mIsRoleAuthority) { - serializer.attributeInt(/* namespace= */ null, ATTR_USER_ID, mUserId); - } else { + serializer.attributeInt(/* namespace= */ null, ATTR_USER_ID, mUserId); + if (!mIsRoleAuthority) { serializer.attribute( /* namespace= */ null, ATTR_CLASS_NAME, mComponentName.getClassName()); + // Role authorities get recomputed on load so no need to save them. serializer.attribute( /* namespace= */ null, ATTR_AUTHORITIES, @@ -205,15 +216,15 @@ final class EnforcingAdmin { String packageName = parser.getAttributeValue(/* namespace= */ null, ATTR_PACKAGE_NAME); boolean isRoleAuthority = parser.getAttributeBoolean(/* namespace= */ null, ATTR_IS_ROLE); String authoritiesStr = parser.getAttributeValue(/* namespace= */ null, ATTR_AUTHORITIES); + int userId = parser.getAttributeInt(/* namespace= */ null, ATTR_USER_ID); if (isRoleAuthority) { - int userId = parser.getAttributeInt(/* namespace= */ null, ATTR_USER_ID); return new EnforcingAdmin(packageName, userId); } else { String className = parser.getAttributeValue(/* namespace= */ null, ATTR_CLASS_NAME); ComponentName componentName = new ComponentName(packageName, className); Set<String> authorities = Set.of(authoritiesStr.split(ATTR_AUTHORITIES_SEPARATOR)); - return new EnforcingAdmin(packageName, componentName, authorities); + return new EnforcingAdmin(packageName, componentName, authorities, userId); } } } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java b/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java index 6f172e4515fc..581a19913530 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/Owners.java @@ -19,6 +19,7 @@ package com.android.server.devicepolicy; import static android.app.admin.DevicePolicyManager.DEPRECATE_USERMANAGERINTERNAL_DEVICEPOLICY_DEFAULT; import static android.app.admin.DevicePolicyManager.DEPRECATE_USERMANAGERINTERNAL_DEVICEPOLICY_FLAG; import static android.app.admin.DevicePolicyManager.DEVICE_OWNER_TYPE_DEFAULT; +import static android.app.admin.DevicePolicyManager.DEVICE_OWNER_TYPE_FINANCED; import static com.android.server.devicepolicy.DeviceStateCacheImpl.NO_DEVICE_OWNER; @@ -455,6 +456,23 @@ class Owners { } } + boolean isDefaultDeviceOwnerUserId(int userId) { + synchronized (mData) { + return mData.mDeviceOwner != null + && mData.mDeviceOwnerUserId == userId + && getDeviceOwnerType(getDeviceOwnerPackageName()) == DEVICE_OWNER_TYPE_DEFAULT; + } + } + + boolean isFinancedDeviceOwnerUserId(int userId) { + synchronized (mData) { + return mData.mDeviceOwner != null + && mData.mDeviceOwnerUserId == userId + && getDeviceOwnerType(getDeviceOwnerPackageName()) + == DEVICE_OWNER_TYPE_FINANCED; + } + } + boolean hasProfileOwner(int userId) { synchronized (mData) { return getProfileOwnerComponent(userId) != null; diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java index a787a0b3943b..c684af39a25f 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java @@ -19,12 +19,16 @@ package com.android.server.devicepolicy; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.admin.DevicePolicyManager; +import android.app.admin.PolicyUpdatesReceiver; import android.content.Context; +import android.os.Bundle; import com.android.internal.util.function.QuadFunction; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; +import org.xmlpull.v1.XmlPullParserException; + import java.io.IOException; import java.util.LinkedHashMap; import java.util.List; @@ -44,8 +48,9 @@ final class PolicyDefinition<V> { private static final String ATTR_POLICY_KEY = "policy-key"; private static final String ATTR_POLICY_DEFINITION_KEY = "policy-type-key"; - private static final String ATTR_CALLBACK_ARGS = "callback-args"; - private static final String ATTR_CALLBACK_ARGS_SEPARATOR = ";"; + private static final String ATTR_CALLBACK_ARGS_SIZE = "size"; + private static final String ATTR_CALLBACK_ARGS_KEY = "key"; + private static final String ATTR_CALLBACK_ARGS_VALUE = "value"; static PolicyDefinition<Boolean> AUTO_TIMEZONE = new PolicyDefinition<>( @@ -53,7 +58,7 @@ final class PolicyDefinition<V> { // auto timezone is enabled by default, hence disabling it is more restrictive. FALSE_MORE_RESTRICTIVE, POLICY_FLAG_GLOBAL_ONLY_POLICY, - (Boolean value, Context context, Integer userId, String[] args) -> + (Boolean value, Context context, Integer userId, Bundle args) -> PolicyEnforcerCallbacks.setAutoTimezoneEnabled(value, context), new BooleanPolicySerializer()); @@ -75,9 +80,11 @@ final class PolicyDefinition<V> { static PolicyDefinition<Integer> PERMISSION_GRANT( @NonNull String packageName, @NonNull String permission) { + Bundle callbackArgs = new Bundle(); + callbackArgs.putString(PolicyUpdatesReceiver.EXTRA_PACKAGE_NAME, packageName); + callbackArgs.putString(PolicyUpdatesReceiver.EXTRA_PERMISSION_NAME, permission); return PERMISSION_GRANT_NO_ARGS.setArgs( - DevicePolicyManager.PERMISSION_GRANT_POLICY(packageName, permission), - new String[]{packageName, permission}); + DevicePolicyManager.PERMISSION_GRANT_POLICY(packageName, permission), callbackArgs); } static PolicyDefinition<LockTaskPolicy> LOCK_TASK = new PolicyDefinition<>( @@ -87,13 +94,14 @@ final class PolicyDefinition<V> { EnforcingAdmin.getRoleAuthorityOf("DeviceLock"), EnforcingAdmin.DPC_AUTHORITY)), POLICY_FLAG_LOCAL_ONLY_POLICY, - (LockTaskPolicy value, Context context, Integer userId, String[] args) -> + (LockTaskPolicy value, Context context, Integer userId, Bundle args) -> PolicyEnforcerCallbacks.setLockTask(value, context, userId), new LockTaskPolicy.LockTaskPolicySerializer()); private static Map<String, PolicyDefinition<?>> sPolicyDefinitions = Map.of( DevicePolicyManager.AUTO_TIMEZONE_POLICY, AUTO_TIMEZONE, - DevicePolicyManager.PERMISSION_GRANT_POLICY_KEY, PERMISSION_GRANT_NO_ARGS + DevicePolicyManager.PERMISSION_GRANT_POLICY_KEY, PERMISSION_GRANT_NO_ARGS, + DevicePolicyManager.LOCK_TASK_POLICY, LOCK_TASK ); @@ -103,19 +111,30 @@ final class PolicyDefinition<V> { private final int mPolicyFlags; // A function that accepts policy to apple, context, userId, callback arguments, and returns // true if the policy has been enforced successfully. - private final QuadFunction<V, Context, Integer, String[], Boolean> mPolicyEnforcerCallback; - private final String[] mCallbackArgs; + private final QuadFunction<V, Context, Integer, Bundle, Boolean> mPolicyEnforcerCallback; + private final Bundle mCallbackArgs; private final PolicySerializer<V> mPolicySerializer; - private PolicyDefinition<V> setArgs(String key, String[] callbackArgs) { + private PolicyDefinition<V> setArgs(String key, Bundle callbackArgs) { return new PolicyDefinition<>(key, mPolicyDefinitionKey, mResolutionMechanism, mPolicyFlags, mPolicyEnforcerCallback, mPolicySerializer, callbackArgs); } + @NonNull String getPolicyKey() { return mPolicyKey; } + @NonNull + String getPolicyDefinitionKey() { + return mPolicyDefinitionKey; + } + + @Nullable + Bundle getCallbackArgs() { + return mCallbackArgs; + } + /** * Returns {@code true} if the policy is a global policy by nature and can't be applied locally. */ @@ -146,7 +165,7 @@ final class PolicyDefinition<V> { private PolicyDefinition( String key, ResolutionMechanism<V> resolutionMechanism, - QuadFunction<V, Context, Integer, String[], Boolean> policyEnforcerCallback, + QuadFunction<V, Context, Integer, Bundle, Boolean> policyEnforcerCallback, PolicySerializer<V> policySerializer) { this(key, resolutionMechanism, POLICY_FLAG_NONE, policyEnforcerCallback, policySerializer); } @@ -159,7 +178,7 @@ final class PolicyDefinition<V> { String key, ResolutionMechanism<V> resolutionMechanism, int policyFlags, - QuadFunction<V, Context, Integer, String[], Boolean> policyEnforcerCallback, + QuadFunction<V, Context, Integer, Bundle, Boolean> policyEnforcerCallback, PolicySerializer<V> policySerializer) { this(key, key, resolutionMechanism, policyFlags, policyEnforcerCallback, policySerializer, /* callbackArs= */ null); @@ -174,9 +193,9 @@ final class PolicyDefinition<V> { String policyDefinitionKey, ResolutionMechanism<V> resolutionMechanism, int policyFlags, - QuadFunction<V, Context, Integer, String[], Boolean> policyEnforcerCallback, + QuadFunction<V, Context, Integer, Bundle, Boolean> policyEnforcerCallback, PolicySerializer<V> policySerializer, - String[] callbackArgs) { + Bundle callbackArgs) { mPolicyKey = policyKey; mPolicyDefinitionKey = policyDefinitionKey; mResolutionMechanism = resolutionMechanism; @@ -193,24 +212,39 @@ final class PolicyDefinition<V> { serializer.attribute(/* namespace= */ null, ATTR_POLICY_KEY, mPolicyKey); serializer.attribute( /* namespace= */ null, ATTR_POLICY_DEFINITION_KEY, mPolicyDefinitionKey); + serializer.attributeInt( + /* namespace= */ null, ATTR_CALLBACK_ARGS_SIZE, + mCallbackArgs == null ? 0 : mCallbackArgs.size()); if (mCallbackArgs != null) { - serializer.attribute(/* namespace= */ null, ATTR_CALLBACK_ARGS, - String.join(ATTR_CALLBACK_ARGS_SEPARATOR, mCallbackArgs)); + int i = 0; + for (String key : mCallbackArgs.keySet()) { + serializer.attribute(/* namespace= */ null, + ATTR_CALLBACK_ARGS_KEY + i, key); + serializer.attribute(/* namespace= */ null, + ATTR_CALLBACK_ARGS_VALUE + i, mCallbackArgs.getString(key)); + i++; + } } } - static <V> PolicyDefinition<V> readFromXml(TypedXmlPullParser parser) { + static <V> PolicyDefinition<V> readFromXml(TypedXmlPullParser parser) + throws XmlPullParserException, IOException { String policyKey = parser.getAttributeValue(/* namespace= */ null, ATTR_POLICY_KEY); String policyDefinitionKey = parser.getAttributeValue( /* namespace= */ null, ATTR_POLICY_DEFINITION_KEY); - String callbackArgsStr = parser.getAttributeValue( - /* namespace= */ null, ATTR_CALLBACK_ARGS); - String[] callbackArgs = callbackArgsStr == null - ? null - : callbackArgsStr.split(ATTR_CALLBACK_ARGS_SEPARATOR); + int size = parser.getAttributeInt(/* namespace= */ null, ATTR_CALLBACK_ARGS_SIZE); + Bundle callbackArgs = new Bundle(); + + for (int i = 0; i < size; i++) { + String key = parser.getAttributeValue( + /* namespace= */ null, ATTR_CALLBACK_ARGS_KEY + i); + String value = parser.getAttributeValue( + /* namespace= */ null, ATTR_CALLBACK_ARGS_VALUE + i); + callbackArgs.putString(key, value); + } // TODO: can we avoid casting? - if (callbackArgs == null) { + if (callbackArgs.isEmpty()) { return (PolicyDefinition<V>) sPolicyDefinitions.get(policyDefinitionKey); } else { return (PolicyDefinition<V>) sPolicyDefinitions.get(policyDefinitionKey).setArgs( diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java index 74b6f9ea114f..c745b31afd9c 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java @@ -19,9 +19,11 @@ package com.android.server.devicepolicy; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.admin.DevicePolicyManager; +import android.app.admin.PolicyUpdatesReceiver; import android.content.Context; import android.content.pm.PackageManager; import android.os.Binder; +import android.os.Bundle; import android.os.UserHandle; import android.permission.AdminPermissionControlParams; import android.permission.PermissionControllerManager; @@ -53,14 +55,17 @@ final class PolicyEnforcerCallbacks { static boolean setPermissionGrantState( @Nullable Integer grantState, @NonNull Context context, int userId, - @NonNull String[] args) { + @NonNull Bundle args) { return Boolean.TRUE.equals(Binder.withCleanCallingIdentity(() -> { - if (args == null || args.length < 2) { + if (args == null + || !args.containsKey(PolicyUpdatesReceiver.EXTRA_PACKAGE_NAME) + || !args.containsKey(PolicyUpdatesReceiver.EXTRA_PERMISSION_NAME)) { throw new IllegalArgumentException("Package name and permission name must be " - + "provided as arguments"); + + "provided as arguments."); } - String packageName = args[0]; - String permissionName = args[1]; + + String packageName = args.getString(PolicyUpdatesReceiver.EXTRA_PACKAGE_NAME); + String permissionName = args.getString(PolicyUpdatesReceiver.EXTRA_PERMISSION_NAME); Objects.requireNonNull(packageName); Objects.requireNonNull(permissionName); Objects.requireNonNull(context); diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyState.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyState.java index d3dee98cf7ba..ffde5f858ce6 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyState.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyState.java @@ -119,10 +119,13 @@ final class PolicyState<V> { static <V> PolicyState<V> readFromXml(TypedXmlPullParser parser) throws IOException, XmlPullParserException { + PolicyDefinition<V> policyDefinition = PolicyDefinition.readFromXml(parser); - LinkedHashMap<EnforcingAdmin, V> adminsPolicy = new LinkedHashMap<>(); + V currentResolvedPolicy = policyDefinition.readPolicyValueFromXml( parser, ATTR_RESOLVED_POLICY); + + LinkedHashMap<EnforcingAdmin, V> policiesSetByAdmins = new LinkedHashMap<>(); int outerDepth = parser.getDepth(); while (XmlUtils.nextElementWithin(parser, outerDepth)) { String tag = parser.getName(); @@ -134,12 +137,12 @@ final class PolicyState<V> { if (XmlUtils.nextElementWithin(parser, adminPolicyDepth) && parser.getName().equals(TAG_ENFORCING_ADMIN_ENTRY)) { admin = EnforcingAdmin.readFromXml(parser); - adminsPolicy.put(admin, value); + policiesSetByAdmins.put(admin, value); } } else { Log.e(DevicePolicyEngine.TAG, "Unknown tag: " + tag); } } - return new PolicyState<V>(policyDefinition, adminsPolicy, currentResolvedPolicy); + return new PolicyState<V>(policyDefinition, policiesSetByAdmins, currentResolvedPolicy); } } diff --git a/services/robotests/backup/src/com/android/server/backup/restore/ActiveRestoreSessionTest.java b/services/robotests/backup/src/com/android/server/backup/restore/ActiveRestoreSessionTest.java index e0812d6a77ea..73ddbe8cec7c 100644 --- a/services/robotests/backup/src/com/android/server/backup/restore/ActiveRestoreSessionTest.java +++ b/services/robotests/backup/src/com/android/server/backup/restore/ActiveRestoreSessionTest.java @@ -78,6 +78,7 @@ import org.robolectric.shadows.ShadowLooper; import org.robolectric.shadows.ShadowPackageManager; import java.util.ArrayDeque; +import java.util.Arrays; @RunWith(RobolectricTestRunner.class) @Config( @@ -196,7 +197,7 @@ public class ActiveRestoreSessionTest { mShadowApplication.grantPermissions(android.Manifest.permission.BACKUP); TransportMock transportMock = setUpTransport(mTransport); when(transportMock.transport.getAvailableRestoreSets()) - .thenReturn(new RestoreSet[] {mRestoreSet1, mRestoreSet2}); + .thenReturn(Arrays.asList(mRestoreSet1, mRestoreSet2)); IRestoreSession restoreSession = createActiveRestoreSession(PACKAGE_1, mTransport); int result = restoreSession.getAvailableRestoreSets(mObserver, mMonitor); @@ -214,7 +215,8 @@ public class ActiveRestoreSessionTest { public void testGetAvailableRestoreSets_forEmptyRestoreSets() throws Exception { mShadowApplication.grantPermissions(android.Manifest.permission.BACKUP); TransportMock transportMock = setUpTransport(mTransport); - when(transportMock.transport.getAvailableRestoreSets()).thenReturn(new RestoreSet[0]); + when(transportMock.transport.getAvailableRestoreSets()).thenReturn( + Arrays.asList(new RestoreSet[0])); IRestoreSession restoreSession = createActiveRestoreSession(PACKAGE_1, mTransport); int result = restoreSession.getAvailableRestoreSets(mObserver, mMonitor); @@ -593,7 +595,7 @@ public class ActiveRestoreSessionTest { new ActiveRestoreSession( mBackupManagerService, packageName, transport.transportName, mBackupEligibilityRules); - restoreSession.setRestoreSets(restoreSets); + restoreSession.setRestoreSets(Arrays.asList(restoreSets)); return restoreSession; } 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 1619856e005a..f0e3f3f508a5 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 @@ -89,6 +89,10 @@ class AndroidPackageTest : ParcelableComponentTest(AndroidPackage::class, Packag "sortReceivers", "sortServices", "setAllComponentsDirectBootAware", + "getUsesLibrariesSorted", + "getUsesOptionalLibrariesSorted", + "getUsesSdkLibrariesSorted", + "getUsesStaticLibrariesSorted", // Tested through setting minor/major manually "setLongVersionCode", "getLongVersionCode", diff --git a/services/tests/mockingservicestests/src/com/android/server/companion/virtual/CameraAccessControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/companion/virtual/CameraAccessControllerTest.java index 145e66c92f14..757d27b6b569 100644 --- a/services/tests/mockingservicestests/src/com/android/server/companion/virtual/CameraAccessControllerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/companion/virtual/CameraAccessControllerTest.java @@ -18,6 +18,8 @@ package com.android.server.companion.virtual; import static android.hardware.camera2.CameraInjectionSession.InjectionStatusCallback.ERROR_INJECTION_UNSUPPORTED; +import static com.google.common.truth.Truth.assertThat; + import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; @@ -116,6 +118,11 @@ public class CameraAccessControllerTest { } @Test + public void getUserId_returnsCorrectId() { + assertThat(mController.getUserId()).isEqualTo(mContext.getUserId()); + } + + @Test public void onCameraOpened_uidNotRunning_noCameraBlocking() throws CameraAccessException { when(mDeviceManagerInternal.isAppRunningOnAnyVirtualDevice( eq(mTestAppInfo.uid))).thenReturn(false); diff --git a/services/tests/mockingservicestests/src/com/android/server/companion/virtual/OWNERS b/services/tests/mockingservicestests/src/com/android/server/companion/virtual/OWNERS new file mode 100644 index 000000000000..2e475a9a2742 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/companion/virtual/OWNERS @@ -0,0 +1 @@ +include /services/companion/java/com/android/server/companion/virtual/OWNERS
\ No newline at end of file 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 89eaa2c3d85a..759b0497044f 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 @@ -203,6 +203,7 @@ public class VirtualDeviceManagerServiceTest { private VirtualDeviceImpl mDeviceImpl; private InputController mInputController; private SensorController mSensorController; + private CameraAccessController mCameraAccessController; private AssociationInfo mAssociationInfo; private VirtualDeviceManagerService mVdms; private VirtualDeviceManagerInternal mLocalService; @@ -237,6 +238,8 @@ public class VirtualDeviceManagerServiceTest { @Mock private IAudioConfigChangedCallback mConfigChangedCallback; @Mock + private CameraAccessController.CameraAccessBlockedCallback mCameraAccessBlockedCallback; + @Mock private ApplicationInfo mApplicationInfoMock; @Mock IInputManager mIInputManagerMock; @@ -325,6 +328,8 @@ public class VirtualDeviceManagerServiceTest { new Handler(TestableLooper.get(this).getLooper()), mContext.getSystemService(WindowManager.class), threadVerifier); mSensorController = new SensorController(new Object(), VIRTUAL_DEVICE_ID_1); + mCameraAccessController = + new CameraAccessController(mContext, mLocalService, mCameraAccessBlockedCallback); mAssociationInfo = new AssociationInfo(/* associationId= */ 1, 0, null, MacAddress.BROADCAST_ADDRESS, "", null, null, true, false, false, 0, 0); @@ -394,12 +399,7 @@ public class VirtualDeviceManagerServiceTest { .setBlockedActivities(getBlockedActivities()) .setDevicePolicy(POLICY_TYPE_SENSORS, DEVICE_POLICY_CUSTOM) .build(); - mDeviceImpl = new VirtualDeviceImpl(mContext, - mAssociationInfo, new Binder(), /* ownerUid */ 0, VIRTUAL_DEVICE_ID_1, - mInputController, mSensorController, - /* onDeviceCloseListener= */ (int deviceId) -> {}, - mPendingTrampolineCallback, mActivityListener, mRunningAppsChangedCallback, params); - mVdms.addVirtualDevice(mDeviceImpl); + mDeviceImpl = createVirtualDevice(VIRTUAL_DEVICE_ID_1, DEVICE_OWNER_UID_1, params); assertThat(mVdm.getDevicePolicy(mDeviceImpl.getDeviceId(), POLICY_TYPE_SENSORS)) .isEqualTo(DEVICE_POLICY_CUSTOM); @@ -558,6 +558,21 @@ public class VirtualDeviceManagerServiceTest { } @Test + public void cameraAccessController_observerCountUpdated() { + assertThat(mCameraAccessController.getObserverCount()).isEqualTo(1); + + VirtualDeviceImpl secondDevice = + createVirtualDevice(VIRTUAL_DEVICE_ID_2, DEVICE_OWNER_UID_2); + assertThat(mCameraAccessController.getObserverCount()).isEqualTo(2); + + mDeviceImpl.close(); + assertThat(mCameraAccessController.getObserverCount()).isEqualTo(1); + + secondDevice.close(); + assertThat(mCameraAccessController.getObserverCount()).isEqualTo(0); + } + + @Test public void onVirtualDisplayRemovedLocked_doesNotThrowException() { mDeviceImpl.onVirtualDisplayCreatedLocked( mDeviceImpl.createWindowPolicyController(new ArrayList<>()), DISPLAY_ID_1); @@ -1543,14 +1558,18 @@ public class VirtualDeviceManagerServiceTest { } private VirtualDeviceImpl createVirtualDevice(int virtualDeviceId, int ownerUid) { - VirtualDeviceParams params = new VirtualDeviceParams - .Builder() + VirtualDeviceParams params = new VirtualDeviceParams.Builder() .setBlockedActivities(getBlockedActivities()) .build(); + return createVirtualDevice(virtualDeviceId, ownerUid, params); + } + + private VirtualDeviceImpl createVirtualDevice(int virtualDeviceId, int ownerUid, + VirtualDeviceParams params) { VirtualDeviceImpl virtualDeviceImpl = new VirtualDeviceImpl(mContext, mAssociationInfo, new Binder(), ownerUid, virtualDeviceId, - mInputController, mSensorController, - /* onDeviceCloseListener= */ (int deviceId) -> {}, + mInputController, mSensorController, mCameraAccessController, + /* onDeviceCloseListener= */ deviceId -> mVdms.removeVirtualDevice(deviceId), mPendingTrampolineCallback, mActivityListener, mRunningAppsChangedCallback, params); mVdms.addVirtualDevice(virtualDeviceImpl); return virtualDeviceImpl; diff --git a/services/tests/servicestests/src/com/android/server/display/ScreenOffBrightnessSensorControllerTest.java b/services/tests/servicestests/src/com/android/server/display/ScreenOffBrightnessSensorControllerTest.java index ea04a193e569..5b10dc4e0bab 100644 --- a/services/tests/servicestests/src/com/android/server/display/ScreenOffBrightnessSensorControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/display/ScreenOffBrightnessSensorControllerTest.java @@ -35,6 +35,7 @@ import androidx.test.runner.AndroidJUnit4; import com.android.server.testutils.OffsettableClock; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -73,6 +74,15 @@ public class ScreenOffBrightnessSensorControllerTest { ); } + @After + public void tearDown() { + if (mController != null) { + // Stop the update Brightness loop. + mController.stop(); + mController = null; + } + } + @Test public void testBrightness() throws Exception { when(mSensorManager.registerListener(any(SensorEventListener.class), eq(mLightSensor), diff --git a/services/tests/servicestests/src/com/android/server/input/KeyboardBacklightControllerTests.kt b/services/tests/servicestests/src/com/android/server/input/KeyboardBacklightControllerTests.kt index 44bdf5e49653..b5dad945ffac 100644 --- a/services/tests/servicestests/src/com/android/server/input/KeyboardBacklightControllerTests.kt +++ b/services/tests/servicestests/src/com/android/server/input/KeyboardBacklightControllerTests.kt @@ -20,6 +20,8 @@ import android.content.Context import android.content.ContextWrapper import android.graphics.Color import android.hardware.input.IInputManager +import android.hardware.input.IKeyboardBacklightListener +import android.hardware.input.IKeyboardBacklightState import android.hardware.input.InputManager import android.hardware.lights.Light import android.os.test.TestLooper @@ -27,10 +29,6 @@ import android.platform.test.annotations.Presubmit import android.view.InputDevice import androidx.test.core.app.ApplicationProvider import com.android.server.input.KeyboardBacklightController.BRIGHTNESS_LEVELS -import java.io.FileNotFoundException -import java.io.FileOutputStream -import java.io.IOException -import java.io.InputStream import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull @@ -45,6 +43,10 @@ import org.mockito.Mockito.eq import org.mockito.Mockito.spy import org.mockito.Mockito.`when` import org.mockito.junit.MockitoJUnit +import java.io.FileNotFoundException +import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStream private fun createKeyboard(deviceId: Int): InputDevice = InputDevice.Builder() @@ -90,6 +92,7 @@ class KeyboardBacklightControllerTests { private lateinit var dataStore: PersistentDataStore private lateinit var testLooper: TestLooper private var lightColorMap: HashMap<Int, Int> = HashMap() + private var lastBacklightState: KeyboardBacklightState? = null @Before fun setup() { @@ -310,4 +313,75 @@ class KeyboardBacklightControllerTests { lightColorMap[LIGHT_ID] ) } + + @Test + fun testKeyboardBacklightT_registerUnregisterListener() { + val keyboardWithBacklight = createKeyboard(DEVICE_ID) + val keyboardBacklight = createLight(LIGHT_ID, Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT) + `when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight) + `when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight)) + keyboardBacklightController.onInputDeviceAdded(DEVICE_ID) + // Initially backlight is at min + lightColorMap[LIGHT_ID] = Color.argb(BRIGHTNESS_LEVELS.first(), 0, 0, 0) + + // Register backlight listener + val listener = KeyboardBacklightListener() + keyboardBacklightController.registerKeyboardBacklightListener(listener, 0) + + lastBacklightState = null + keyboardBacklightController.incrementKeyboardBacklight(DEVICE_ID) + testLooper.dispatchNext() + + assertEquals( + "Backlight state device Id should be $DEVICE_ID", + DEVICE_ID, + lastBacklightState!!.deviceId + ) + assertEquals( + "Backlight state brightnessLevel should be " + 1, + 1, + lastBacklightState!!.brightnessLevel + ) + assertEquals( + "Backlight state maxBrightnessLevel should be " + (BRIGHTNESS_LEVELS.size - 1), + (BRIGHTNESS_LEVELS.size - 1), + lastBacklightState!!.maxBrightnessLevel + ) + assertEquals( + "Backlight state isTriggeredByKeyPress should be true", + true, + lastBacklightState!!.isTriggeredByKeyPress + ) + + // Unregister listener + keyboardBacklightController.unregisterKeyboardBacklightListener(listener, 0) + + lastBacklightState = null + keyboardBacklightController.incrementKeyboardBacklight(DEVICE_ID) + testLooper.dispatchNext() + + assertNull("Listener should not receive any updates", lastBacklightState) + } + + inner class KeyboardBacklightListener : IKeyboardBacklightListener.Stub() { + override fun onBrightnessChanged( + deviceId: Int, + state: IKeyboardBacklightState, + isTriggeredByKeyPress: Boolean + ) { + lastBacklightState = KeyboardBacklightState( + deviceId, + state.brightnessLevel, + state.maxBrightnessLevel, + isTriggeredByKeyPress + ) + } + } + + class KeyboardBacklightState( + val deviceId: Int, + val brightnessLevel: Int, + val maxBrightnessLevel: Int, + val isTriggeredByKeyPress: Boolean + ) } diff --git a/services/tests/servicestests/test-apps/SimpleServiceTestApp/AndroidManifest.xml b/services/tests/servicestests/test-apps/SimpleServiceTestApp/AndroidManifest.xml index 1bc47759d078..b1d0f3dee8c0 100644 --- a/services/tests/servicestests/test-apps/SimpleServiceTestApp/AndroidManifest.xml +++ b/services/tests/servicestests/test-apps/SimpleServiceTestApp/AndroidManifest.xml @@ -18,13 +18,17 @@ package="com.android.servicestests.apps.simpleservicetestapp"> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/> <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" /> <application> <service android:name=".SimpleService" android:exported="true" /> <service android:name=".SimpleFgService" - android:exported="true" /> + android:foregroundServiceType="specialUse" + android:exported="true"> + <property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE" android:value="test" /> + </service> <service android:name=".SimpleIsolatedService" android:isolatedProcess="true" android:exported="true" /> diff --git a/services/tests/wmtests/Android.bp b/services/tests/wmtests/Android.bp index 079d765868fd..2ce7cea08a3d 100644 --- a/services/tests/wmtests/Android.bp +++ b/services/tests/wmtests/Android.bp @@ -68,6 +68,10 @@ android_test { "android.test.runner", ], + defaults: [ + "modules-utils-testable-device-config-defaults", + ], + // These are not normally accessible from apps so they must be explicitly included. jni_libs: [ "libdexmakerjvmtiagent", diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationImmersiveAppCompatPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationImmersiveAppCompatPolicyTests.java new file mode 100644 index 000000000000..d29b18f89f77 --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationImmersiveAppCompatPolicyTests.java @@ -0,0 +1,219 @@ +/* + * 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.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; +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.content.res.Configuration.ORIENTATION_LANDSCAPE; +import static android.content.res.Configuration.ORIENTATION_PORTRAIT; +import static android.content.res.Configuration.ORIENTATION_UNDEFINED; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyBoolean; + +import android.platform.test.annotations.Presubmit; +import android.view.Surface; +import android.view.WindowInsets.Type; + +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Test class for {@link DisplayRotationImmersiveAppCompatPolicy}. + * + * Build/Install/Run: + * atest WmTests:DisplayRotationImmersiveAppCompatPolicyTests + */ +@SmallTest +@Presubmit +@RunWith(WindowTestRunner.class) +public class DisplayRotationImmersiveAppCompatPolicyTests extends WindowTestsBase { + + private DisplayRotationImmersiveAppCompatPolicy mPolicy; + + private LetterboxConfiguration mMockLetterboxConfiguration; + private ActivityRecord mMockActivityRecord; + private Task mMockTask; + private WindowState mMockWindowState; + + @Before + public void setUp() throws Exception { + mMockActivityRecord = mock(ActivityRecord.class); + mMockTask = mock(Task.class); + when(mMockTask.getWindowingMode()).thenReturn(WINDOWING_MODE_FULLSCREEN); + when(mMockActivityRecord.getTask()).thenReturn(mMockTask); + when(mMockActivityRecord.areBoundsLetterboxed()).thenReturn(false); + when(mMockActivityRecord.getRequestedConfigurationOrientation()).thenReturn( + ORIENTATION_LANDSCAPE); + mMockWindowState = mock(WindowState.class); + when(mMockWindowState.getRequestedVisibleTypes()).thenReturn(0); + when(mMockActivityRecord.findMainWindow()).thenReturn(mMockWindowState); + + spy(mDisplayContent); + doReturn(mMockActivityRecord).when(mDisplayContent).topRunningActivity(); + when(mDisplayContent.getIgnoreOrientationRequest()).thenReturn(true); + + mMockLetterboxConfiguration = mock(LetterboxConfiguration.class); + when(mMockLetterboxConfiguration.isDisplayRotationImmersiveAppCompatPolicyEnabled( + /* checkDeviceConfig */ anyBoolean())).thenReturn(true); + + mPolicy = DisplayRotationImmersiveAppCompatPolicy.createIfNeeded( + mMockLetterboxConfiguration, createDisplayRotationMock(), + mDisplayContent); + } + + private DisplayRotation createDisplayRotationMock() { + DisplayRotation mockDisplayRotation = mock(DisplayRotation.class); + + when(mockDisplayRotation.isAnyPortrait(Surface.ROTATION_0)).thenReturn(true); + when(mockDisplayRotation.isAnyPortrait(Surface.ROTATION_90)).thenReturn(false); + when(mockDisplayRotation.isAnyPortrait(Surface.ROTATION_180)).thenReturn(true); + when(mockDisplayRotation.isAnyPortrait(Surface.ROTATION_270)).thenReturn(false); + when(mockDisplayRotation.isLandscapeOrSeascape(Surface.ROTATION_0)).thenReturn(false); + when(mockDisplayRotation.isLandscapeOrSeascape(Surface.ROTATION_90)).thenReturn(true); + when(mockDisplayRotation.isLandscapeOrSeascape(Surface.ROTATION_180)).thenReturn(false); + when(mockDisplayRotation.isLandscapeOrSeascape(Surface.ROTATION_270)).thenReturn(true); + + return mockDisplayRotation; + } + + @Test + public void testIsRotationLockEnforced_landscapeActivity_lockedWhenRotatingToPortrait() { + // Base case: App is optimal in Landscape. + + // ROTATION_* is the target display orientation counted from the natural display + // orientation. Outside of test environment, ROTATION_0 means that proposed display + // rotation is the natural device orientation. + // DisplayRotationImmersiveAppCompatPolicy assesses whether the proposed target + // orientation ROTATION_* is optimal for the top fullscreen activity or not. + // For instance, ROTATION_0 means portrait screen orientation (see + // createDisplayRotationMock) which isn't optimal for a landscape-only activity so + // we should show a rotation suggestion button instead of rotating directly. + + // Rotation to portrait + assertTrue(mPolicy.isRotationLockEnforced(Surface.ROTATION_0)); + // Rotation to landscape + assertFalse(mPolicy.isRotationLockEnforced(Surface.ROTATION_90)); + // Rotation to portrait + assertTrue(mPolicy.isRotationLockEnforced(Surface.ROTATION_180)); + // Rotation to landscape + assertFalse(mPolicy.isRotationLockEnforced(Surface.ROTATION_270)); + } + + @Test + public void testIsRotationLockEnforced_portraitActivity_lockedWhenRotatingToLandscape() { + when(mMockActivityRecord.getRequestedConfigurationOrientation()).thenReturn( + ORIENTATION_PORTRAIT); + + // Rotation to portrait + assertFalse(mPolicy.isRotationLockEnforced(Surface.ROTATION_0)); + // Rotation to landscape + assertTrue(mPolicy.isRotationLockEnforced(Surface.ROTATION_90)); + // Rotation to portrait + assertFalse(mPolicy.isRotationLockEnforced(Surface.ROTATION_180)); + // Rotation to landscape + assertTrue(mPolicy.isRotationLockEnforced(Surface.ROTATION_270)); + } + + @Test + public void testIsRotationLockEnforced_responsiveActivity_lockNotEnforced() { + // Do not fix screen orientation + when(mMockActivityRecord.getRequestedConfigurationOrientation()).thenReturn( + ORIENTATION_UNDEFINED); + + assertIsRotationLockEnforcedReturnsFalseForAllRotations(); + } + + @Test + public void testIsRotationLockEnforced_statusBarVisible_lockNotEnforced() { + // Some system bars are visible + when(mMockWindowState.getRequestedVisibleTypes()).thenReturn(Type.statusBars()); + + assertIsRotationLockEnforcedReturnsFalseForAllRotations(); + } + + @Test + public void testIsRotationLockEnforced_navBarVisible_lockNotEnforced() { + // Some system bars are visible + when(mMockWindowState.getRequestedVisibleTypes()).thenReturn(Type.navigationBars()); + + assertIsRotationLockEnforcedReturnsFalseForAllRotations(); + } + + @Test + public void testIsRotationLockEnforced_activityIsLetterboxed_lockNotEnforced() { + // Activity is letterboxed + when(mMockActivityRecord.areBoundsLetterboxed()).thenReturn(true); + + assertIsRotationLockEnforcedReturnsFalseForAllRotations(); + } + + @Test + public void testIsRotationLockEnforced_notFullscreen_lockNotEnforced() { + when(mMockTask.getWindowingMode()).thenReturn(WINDOWING_MODE_MULTI_WINDOW); + + assertIsRotationLockEnforcedReturnsFalseForAllRotations(); + + when(mMockTask.getWindowingMode()).thenReturn(WINDOWING_MODE_PINNED); + + assertIsRotationLockEnforcedReturnsFalseForAllRotations(); + + when(mMockTask.getWindowingMode()).thenReturn(WINDOWING_MODE_FREEFORM); + + assertIsRotationLockEnforcedReturnsFalseForAllRotations(); + } + + @Test + public void testIsRotationLockEnforced_ignoreOrientationRequestDisabled_lockNotEnforced() { + when(mDisplayContent.getIgnoreOrientationRequest()).thenReturn(false); + + assertIsRotationLockEnforcedReturnsFalseForAllRotations(); + } + + @Test + public void testRotationChoiceEnforcedOnly_nullTopRunningActivity_lockNotEnforced() { + when(mDisplayContent.topRunningActivity()).thenReturn(null); + + assertIsRotationLockEnforcedReturnsFalseForAllRotations(); + } + + @Test + public void testRotationChoiceEnforcedOnly_featureFlagDisabled_lockNotEnforced() { + when(mMockLetterboxConfiguration.isDisplayRotationImmersiveAppCompatPolicyEnabled( + /* checkDeviceConfig */ true)).thenReturn(false); + + assertIsRotationLockEnforcedReturnsFalseForAllRotations(); + } + + private void assertIsRotationLockEnforcedReturnsFalseForAllRotations() { + assertFalse(mPolicy.isRotationLockEnforced(Surface.ROTATION_0)); + assertFalse(mPolicy.isRotationLockEnforced(Surface.ROTATION_90)); + assertFalse(mPolicy.isRotationLockEnforced(Surface.ROTATION_180)); + assertFalse(mPolicy.isRotationLockEnforced(Surface.ROTATION_270)); + } +} diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java index 491f876dceed..4ce43e1fc469 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationTests.java @@ -1096,8 +1096,16 @@ public class DisplayRotationTests { mMockDisplayAddress = mock(DisplayAddress.class); mMockDisplayWindowSettings = mock(DisplayWindowSettings.class); + mTarget = new DisplayRotation(sMockWm, mMockDisplayContent, mMockDisplayAddress, - mMockDisplayPolicy, mMockDisplayWindowSettings, mMockContext, new Object()); + mMockDisplayPolicy, mMockDisplayWindowSettings, mMockContext, new Object()) { + @Override + DisplayRotationImmersiveAppCompatPolicy initImmersiveAppCompatPolicy( + WindowManagerService service, DisplayContent displayContent) { + return null; + } + }; + reset(sMockWm); captureObservers(); diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationDeviceConfigTests.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationDeviceConfigTests.java new file mode 100644 index 000000000000..2b7a06bd35f3 --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxConfigurationDeviceConfigTests.java @@ -0,0 +1,109 @@ +/* + * 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 com.android.server.wm.LetterboxConfigurationDeviceConfig.sKeyToDefaultValueMap; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.platform.test.annotations.Presubmit; +import android.provider.DeviceConfig; + +import androidx.test.filters.SmallTest; + +import com.android.modules.utils.testing.TestableDeviceConfig; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import java.util.Map; + +/** + * Test class for {@link LetterboxConfigurationDeviceConfig}. + * + * atest WmTests:LetterboxConfigurationDeviceConfigTests + */ +@SmallTest +@Presubmit +public class LetterboxConfigurationDeviceConfigTests { + + private LetterboxConfigurationDeviceConfig mDeviceConfig; + + @Rule + public final TestableDeviceConfig.TestableDeviceConfigRule + mDeviceConfigRule = new TestableDeviceConfig.TestableDeviceConfigRule(); + + @Before + public void setUp() { + mDeviceConfig = new LetterboxConfigurationDeviceConfig(/* executor */ Runnable::run); + } + + @Test + public void testGetFlag_flagIsActive_flagChanges() throws Throwable { + for (Map.Entry<String, Boolean> entry : sKeyToDefaultValueMap.entrySet()) { + testGetFlagForKey_flagIsActive_flagChanges(entry.getKey(), entry.getValue()); + } + } + + private void testGetFlagForKey_flagIsActive_flagChanges(final String key, boolean defaultValue) + throws InterruptedException { + mDeviceConfig.updateFlagActiveStatus(/* isActive */ true, key); + + assertEquals("Unexpected default value for " + key, + mDeviceConfig.getFlag(key), defaultValue); + + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_WINDOW_MANAGER, key, + /* value */ Boolean.TRUE.toString(), /* makeDefault */ false); + + assertTrue("Flag " + key + "is not true after change", mDeviceConfig.getFlag(key)); + + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_WINDOW_MANAGER, key, + /* value */ Boolean.FALSE.toString(), /* makeDefault */ false); + + assertFalse("Flag " + key + "is not false after change", mDeviceConfig.getFlag(key)); + } + + @Test + public void testGetFlag_flagIsNotActive_alwaysReturnDefaultValue() throws Throwable { + for (Map.Entry<String, Boolean> entry : sKeyToDefaultValueMap.entrySet()) { + testGetFlagForKey_flagIsNotActive_alwaysReturnDefaultValue( + entry.getKey(), entry.getValue()); + } + } + + private void testGetFlagForKey_flagIsNotActive_alwaysReturnDefaultValue(final String key, + boolean defaultValue) throws InterruptedException { + assertEquals("Unexpected default value for " + key, + mDeviceConfig.getFlag(key), defaultValue); + + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_WINDOW_MANAGER, key, + /* value */ Boolean.TRUE.toString(), /* makeDefault */ false); + + assertEquals("Flag " + key + "is not set to default after change", + mDeviceConfig.getFlag(key), defaultValue); + + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_WINDOW_MANAGER, key, + /* value */ Boolean.FALSE.toString(), /* makeDefault */ false); + + assertEquals("Flag " + key + "is not set to default after change", + mDeviceConfig.getFlag(key), defaultValue); + } + +} diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java new file mode 100644 index 000000000000..6d778afee88c --- /dev/null +++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java @@ -0,0 +1,173 @@ +/* + * 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.content.pm.ActivityInfo.OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; +import static android.view.WindowManager.PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.eq; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyString; + +import android.compat.testing.PlatformCompatChangeRule; +import android.content.ComponentName; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.Property; +import android.platform.test.annotations.Presubmit; + +import androidx.test.filters.SmallTest; + +import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; +import org.junit.runner.RunWith; + + /** + * Test class for {@link LetterboxUiControllerTest}. + * + * Build/Install/Run: + * atest WmTests:LetterboxUiControllerTest + */ +@SmallTest +@Presubmit +@RunWith(WindowTestRunner.class) +public class LetterboxUiControllerTest extends WindowTestsBase { + + @Rule + public TestRule compatChangeRule = new PlatformCompatChangeRule(); + + private ActivityRecord mActivity; + private DisplayContent mDisplayContent; + private LetterboxUiController mController; + private LetterboxConfiguration mLetterboxConfiguration; + + @Before + public void setUp() throws Exception { + mActivity = setUpActivityWithComponent(); + + mLetterboxConfiguration = mWm.mLetterboxConfiguration; + spyOn(mLetterboxConfiguration); + + mController = new LetterboxUiController(mWm, mActivity); + } + + @Test + @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION}) + public void testShouldIgnoreRequestedOrientation_activityRelaunching_returnsTrue() { + prepareActivityThatShouldIgnoreRequestedOrientationDuringRelaunch(); + + assertTrue(mController.shouldIgnoreRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED)); + } + + @Test + @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION}) + public void testShouldIgnoreRequestedOrientation_cameraCompatTreatment_returnsTrue() { + doReturn(true).when(mLetterboxConfiguration).isCameraCompatTreatmentEnabled(anyBoolean()); + + // Recreate DisplayContent with DisplayRotationCompatPolicy + mActivity = setUpActivityWithComponent(); + mController = new LetterboxUiController(mWm, mActivity); + prepareActivityThatShouldIgnoreRequestedOrientationDuringRelaunch(); + mController.setRelauchingAfterRequestedOrientationChanged(false); + + spyOn(mDisplayContent.mDisplayRotationCompatPolicy); + doReturn(true).when(mDisplayContent.mDisplayRotationCompatPolicy) + .isTreatmentEnabledForActivity(eq(mActivity)); + + assertTrue(mController.shouldIgnoreRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED)); + } + + @Test + public void testShouldIgnoreRequestedOrientation_overrideDisabled_returnsFalse() { + prepareActivityThatShouldIgnoreRequestedOrientationDuringRelaunch(); + + assertFalse(mController.shouldIgnoreRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED)); + } + + @Test + public void testShouldIgnoreRequestedOrientation_propertyIsTrue_returnsTrue() + throws Exception { + doReturn(true).when(mLetterboxConfiguration) + .isPolicyForIgnoringRequestedOrientationEnabled(); + mockThatProperty(PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION, /* value */ true); + mController = new LetterboxUiController(mWm, mActivity); + prepareActivityThatShouldIgnoreRequestedOrientationDuringRelaunch(); + + assertTrue(mController.shouldIgnoreRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED)); + } + + @Test + @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION}) + public void testShouldIgnoreRequestedOrientation_propertyIsFalseAndOverride_returnsFalse() + throws Exception { + doReturn(true).when(mLetterboxConfiguration) + .isPolicyForIgnoringRequestedOrientationEnabled(); + mockThatProperty(PROPERTY_COMPAT_IGNORE_REQUESTED_ORIENTATION, /* value */ false); + + mController = new LetterboxUiController(mWm, mActivity); + prepareActivityThatShouldIgnoreRequestedOrientationDuringRelaunch(); + + assertFalse(mController.shouldIgnoreRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED)); + } + + @Test + @EnableCompatChanges({OVERRIDE_ENABLE_COMPAT_IGNORE_REQUESTED_ORIENTATION}) + public void testShouldIgnoreRequestedOrientation_flagIsDisabled_returnsFalse() { + prepareActivityThatShouldIgnoreRequestedOrientationDuringRelaunch(); + doReturn(false).when(mLetterboxConfiguration) + .isPolicyForIgnoringRequestedOrientationEnabled(); + + assertFalse(mController.shouldIgnoreRequestedOrientation(SCREEN_ORIENTATION_UNSPECIFIED)); + } + + private void mockThatProperty(String propertyName, boolean value) throws Exception { + Property property = new Property(propertyName, /* value */ value, /* packageName */ "", + /* className */ ""); + PackageManager pm = mWm.mContext.getPackageManager(); + spyOn(pm); + doReturn(property).when(pm).getProperty(eq(propertyName), anyString()); + } + + private void prepareActivityThatShouldIgnoreRequestedOrientationDuringRelaunch() { + doReturn(true).when(mLetterboxConfiguration) + .isPolicyForIgnoringRequestedOrientationEnabled(); + mController.setRelauchingAfterRequestedOrientationChanged(true); + } + + private ActivityRecord setUpActivityWithComponent() { + mDisplayContent = new TestDisplayContent + .Builder(mAtm, /* dw */ 1000, /* dh */ 2000).build(); + Task task = new TaskBuilder(mSupervisor).setDisplay(mDisplayContent).build(); + final ActivityRecord activity = new ActivityBuilder(mAtm) + .setOnTop(true) + .setTask(task) + // Set the component to be that of the test class in order to enable compat changes + .setComponent(ComponentName.createRelative(mContext, + com.android.server.wm.LetterboxUiControllerTest.class.getName())) + .build(); + return activity; + } +} diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java index 367f91bc5bc3..d364dbbbaac0 100644 --- a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java @@ -30,7 +30,6 @@ import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; import static android.content.pm.ActivityInfo.LAUNCH_MULTIPLE; import static android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; -import static android.os.Process.NOBODY_UID; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; @@ -1208,34 +1207,20 @@ public class RecentTasksTest extends WindowTestsBase { @Test public void testCreateRecentTaskInfo_detachedTask() { - final Task task = createTaskBuilder(".Task").build(); - new ActivityBuilder(mSupervisor.mService) - .setTask(task) - .setUid(NOBODY_UID) - .setComponent(getUniqueComponentName()) - .build(); + final Task task = createTaskBuilder(".Task").setCreateActivity(true).build(); final TaskDisplayArea tda = task.getDisplayArea(); assertTrue(task.isAttached()); assertTrue(task.supportsMultiWindow()); - RecentTaskInfo info = mRecentTasks.createRecentTaskInfo(task, true /* stripExtras */, - true /* getTasksAllowed */); + RecentTaskInfo info = mRecentTasks.createRecentTaskInfo(task, true); assertTrue(info.supportsMultiWindow); - info = mRecentTasks.createRecentTaskInfo(task, true /* stripExtras */, - false /* getTasksAllowed */); - - assertTrue(info.topActivity == null); - assertTrue(info.topActivityInfo == null); - assertTrue(info.baseActivity == null); - // The task can be put in split screen even if it is not attached now. task.removeImmediately(); - info = mRecentTasks.createRecentTaskInfo(task, true /* stripExtras */, - true /* getTasksAllowed */); + info = mRecentTasks.createRecentTaskInfo(task, true); assertTrue(info.supportsMultiWindow); @@ -1244,8 +1229,7 @@ public class RecentTasksTest extends WindowTestsBase { doReturn(false).when(tda).supportsNonResizableMultiWindow(); doReturn(false).when(task).isResizeable(); - info = mRecentTasks.createRecentTaskInfo(task, true /* stripExtras */, - true /* getTasksAllowed */); + info = mRecentTasks.createRecentTaskInfo(task, true); assertFalse(info.supportsMultiWindow); @@ -1253,8 +1237,7 @@ public class RecentTasksTest extends WindowTestsBase { // the device supports it. doReturn(true).when(tda).supportsNonResizableMultiWindow(); - info = mRecentTasks.createRecentTaskInfo(task, true /* stripExtras */, - true /* getTasksAllowed */); + info = mRecentTasks.createRecentTaskInfo(task, true); assertTrue(info.supportsMultiWindow); } diff --git a/services/tests/wmtests/src/com/android/server/wm/ScreenshotTests.java b/services/tests/wmtests/src/com/android/server/wm/ScreenshotTests.java index 736f8f7a5ed3..1ee0959447a6 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ScreenshotTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ScreenshotTests.java @@ -48,7 +48,6 @@ import android.view.IWindowManager; import android.view.PointerIcon; import android.view.SurfaceControl; import android.view.cts.surfacevalidator.BitmapPixelChecker; -import android.view.cts.surfacevalidator.PixelColor; import android.view.cts.surfacevalidator.SaveBitmapHelper; import android.window.ScreenCapture; import android.window.ScreenCapture.ScreenCaptureListener; @@ -132,7 +131,7 @@ public class ScreenshotTests { Bitmap swBitmap = screenshot.copy(Bitmap.Config.ARGB_8888, false); screenshot.recycle(); - BitmapPixelChecker bitmapPixelChecker = new BitmapPixelChecker(PixelColor.RED); + BitmapPixelChecker bitmapPixelChecker = new BitmapPixelChecker(Color.RED); Rect bounds = new Rect(0, 0, swBitmap.getWidth(), swBitmap.getHeight()); int numMatchingPixels = bitmapPixelChecker.getNumMatchingPixels(swBitmap, bounds); int sizeOfBitmap = bounds.width() * bounds.height(); @@ -182,7 +181,7 @@ public class ScreenshotTests { Bitmap swBitmap = screenshot.copy(Bitmap.Config.ARGB_8888, false); screenshot.recycle(); - BitmapPixelChecker bitmapPixelChecker = new BitmapPixelChecker(PixelColor.RED); + BitmapPixelChecker bitmapPixelChecker = new BitmapPixelChecker(Color.RED); Rect bounds = new Rect(point.x, point.y, BUFFER_WIDTH + point.x, BUFFER_HEIGHT + point.y); int numMatchingPixels = bitmapPixelChecker.getNumMatchingPixels(swBitmap, bounds); int pixelMatchSize = bounds.width() * bounds.height(); diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java index 2cce62e1685c..a48a0bcafcf3 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java @@ -94,6 +94,7 @@ import android.provider.DeviceConfig; import android.provider.DeviceConfig.Properties; import android.view.InsetsFrameProvider; import android.view.InsetsSource; +import android.view.InsetsState; import android.view.WindowManager; import androidx.test.filters.MediumTest; @@ -1466,6 +1467,12 @@ public class SizeCompatTests extends WindowTestsBase { assertEquals(new Rect(notchHeight, 0, 0, 0), mActivity.getLetterboxInsets()); assertTrue(displayPolicy.isFullyTransparentAllowed(w, ITYPE_STATUS_BAR)); assertActivityMaxBoundsSandboxed(); + + // The insets state for metrics should be rotated (landscape). + final InsetsState insetsState = new InsetsState(); + mActivity.mDisplayContent.getInsetsPolicy().getInsetsForWindowMetrics( + mActivity, insetsState); + assertEquals(dh, insetsState.getDisplayFrame().width()); } @Test 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 26fe5214a7ea..b70d8bd50917 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java @@ -23,6 +23,7 @@ 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_SET_ANIMATION_PARAMS; 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; @@ -73,6 +74,7 @@ import android.content.ComponentName; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.res.Configuration; +import android.graphics.Color; import android.graphics.Rect; import android.net.Uri; import android.os.Binder; @@ -83,8 +85,10 @@ import android.platform.test.annotations.Presubmit; import android.view.RemoteAnimationDefinition; import android.view.SurfaceControl; import android.window.ITaskFragmentOrganizer; +import android.window.TaskFragmentAnimationParams; import android.window.TaskFragmentCreationParams; import android.window.TaskFragmentInfo; +import android.window.TaskFragmentOperation; import android.window.TaskFragmentOrganizer; import android.window.TaskFragmentOrganizerToken; import android.window.TaskFragmentParentInfo; @@ -689,6 +693,59 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { } @Test + public void testApplyTransaction_enforceTaskFragmentOrganized_setTaskFragmentOperation() { + final Task task = createTask(mDisplayContent); + mTaskFragment = new TaskFragmentBuilder(mAtm) + .setParentTask(task) + .setFragmentToken(mFragmentToken) + .build(); + mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment); + final TaskFragmentOperation operation = new TaskFragmentOperation.Builder( + OP_TYPE_SET_ANIMATION_PARAMS) + .setAnimationParams(TaskFragmentAnimationParams.DEFAULT) + .build(); + mTransaction.setTaskFragmentOperation(mFragmentToken, operation); + mOrganizer.applyTransaction(mTransaction, TASK_FRAGMENT_TRANSIT_CHANGE, + false /* shouldApplyIndependently */); + + // Not allowed because TaskFragment is not organized by the caller organizer. + assertApplyTransactionDisallowed(mTransaction); + + mTaskFragment.setTaskFragmentOrganizer(mOrganizerToken, 10 /* uid */, + "Test:TaskFragmentOrganizer" /* processName */); + + assertApplyTransactionAllowed(mTransaction); + } + + @Test + public void testSetTaskFragmentOperation() { + final Task task = createTask(mDisplayContent); + mTaskFragment = new TaskFragmentBuilder(mAtm) + .setParentTask(task) + .setOrganizer(mOrganizer) + .setFragmentToken(mFragmentToken) + .build(); + assertEquals(TaskFragmentAnimationParams.DEFAULT, mTaskFragment.getAnimationParams()); + + mWindowOrganizerController.mLaunchTaskFragments.put(mFragmentToken, mTaskFragment); + final TaskFragmentAnimationParams animationParams = + new TaskFragmentAnimationParams.Builder() + .setAnimationBackgroundColor(Color.GREEN) + .build(); + final TaskFragmentOperation operation = new TaskFragmentOperation.Builder( + OP_TYPE_SET_ANIMATION_PARAMS) + .setAnimationParams(animationParams) + .build(); + mTransaction.setTaskFragmentOperation(mFragmentToken, operation); + mOrganizer.applyTransaction(mTransaction, TASK_FRAGMENT_TRANSIT_CHANGE, + false /* shouldApplyIndependently */); + assertApplyTransactionAllowed(mTransaction); + + assertEquals(animationParams, mTaskFragment.getAnimationParams()); + assertEquals(Color.GREEN, mTaskFragment.getAnimationParams().getAnimationBackgroundColor()); + } + + @Test public void testApplyTransaction_createTaskFragment_failForDifferentUid() { final ActivityRecord activity = createActivityRecord(mDisplayContent); final int uid = Binder.getCallingUid(); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java index 3d777f81579b..cac7745a5e86 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java @@ -214,7 +214,7 @@ public class WindowManagerServiceTests extends WindowTestsBase { final MergedConfiguration outConfig = new MergedConfiguration(); final SurfaceControl outSurfaceControl = new SurfaceControl(); final InsetsState outInsetsState = new InsetsState(); - final InsetsSourceControl[] outControls = new InsetsSourceControl[0]; + final InsetsSourceControl.Array outControls = new InsetsSourceControl.Array(); final Bundle outBundle = new Bundle(); mWm.relayoutWindow(win.mSession, win.mClient, win.mAttrs, w, h, View.GONE, 0, 0, 0, outFrames, outConfig, outSurfaceControl, outInsetsState, outControls, outBundle); @@ -351,7 +351,7 @@ public class WindowManagerServiceTests extends WindowTestsBase { mWm.addWindow(session, new TestIWindow(), params, View.VISIBLE, DEFAULT_DISPLAY, UserHandle.USER_SYSTEM, WindowInsets.Type.defaultVisible(), null, new InsetsState(), - new InsetsSourceControl[0], new Rect(), new float[1]); + new InsetsSourceControl.Array(), new Rect(), new float[1]); verify(mWm.mWindowContextListenerController, never()).registerWindowContainerListener(any(), any(), anyInt(), anyInt(), any()); diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectorSession.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java index 689423ad3e3e..4d6d3205db40 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectorSession.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/DetectorSession.java @@ -93,21 +93,27 @@ import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; /** - * A class that provides trusted hotword detector to communicate with the {@link - * HotwordDetectionService}. + * A class that provides sandboxed detector to communicate with the {@link + * HotwordDetectionService} and {@link VisualQueryDetectionService}. * - * This class provides the methods to do initialization with the {@link HotwordDetectionService} - * and handle external source detection. It also provides the methods to check if we can egress - * the data from the {@link HotwordDetectionService}. + * Trusted hotword detectors such as {@link SoftwareHotwordDetector} and + * {@link AlwaysOnHotwordDetector} will leverage this class to communitcate with + * {@link HotwordDetectionService}; similarly, {@link VisualQueryDetector} will communicate with + * {@link VisualQueryDetectionService}. + * + * This class provides the methods to do initialization with the {@link HotwordDetectionService} and + * {@link VisualQueryDetectionService} handles external source detection for + * {@link HotwordDetectionService}. It also provides the methods to check if we can egress the data + * from the {@link HotwordDetectionService} and {@link VisualQueryDetectionService}. * * The subclass should override the {@link #informRestartProcessLocked()} to handle the trusted * process restart. */ -abstract class HotwordDetectorSession { - private static final String TAG = "HotwordDetectorSession"; +abstract class DetectorSession { + private static final String TAG = "DetectorSession"; static final boolean DEBUG = false; - private static final String OP_MESSAGE = + private static final String HOTWORD_DETECTION_OP_MESSAGE = "Providing hotword detection result to VoiceInteractionService"; // The error codes are used for onError callback @@ -173,7 +179,7 @@ abstract class HotwordDetectorSession { @GuardedBy("mLock") ParcelFileDescriptor mCurrentAudioSink; @GuardedBy("mLock") - @NonNull HotwordDetectionConnection.ServiceConnection mRemoteHotwordDetectionService; + @NonNull HotwordDetectionConnection.ServiceConnection mRemoteDetectionService; boolean mDebugHotwordLogging = false; @GuardedBy("mLock") private double mProximityMeters = PROXIMITY_UNKNOWN; @@ -185,13 +191,13 @@ abstract class HotwordDetectorSession { boolean mPerformingExternalSourceHotwordDetection; @NonNull final IBinder mToken; - HotwordDetectorSession( - @NonNull HotwordDetectionConnection.ServiceConnection remoteHotwordDetectionService, + DetectorSession( + @NonNull HotwordDetectionConnection.ServiceConnection remoteDetectionService, @NonNull Object lock, @NonNull Context context, @NonNull IBinder token, @NonNull IHotwordRecognitionStatusCallback callback, int voiceInteractionServiceUid, Identity voiceInteractorIdentity, @NonNull ScheduledExecutorService scheduledExecutorService, boolean logging) { - mRemoteHotwordDetectionService = remoteHotwordDetectionService; + mRemoteDetectionService = remoteDetectionService; mLock = lock; mContext = context; mToken = token; @@ -219,7 +225,7 @@ abstract class HotwordDetectorSession { if (DEBUG) { Slog.d(TAG, "updateStateAfterProcessStartLocked"); } - AndroidFuture<Void> voidFuture = mRemoteHotwordDetectionService.postAsync(service -> { + AndroidFuture<Void> voidFuture = mRemoteDetectionService.postAsync(service -> { AndroidFuture<Void> future = new AndroidFuture<>(); IRemoteCallback statusCallback = new IRemoteCallback.Stub() { @Override @@ -319,7 +325,7 @@ abstract class HotwordDetectorSession { Slog.v(TAG, "call updateStateAfterProcessStartLocked"); updateStateAfterProcessStartLocked(options, sharedMemory); } else { - mRemoteHotwordDetectionService.run( + mRemoteDetectionService.run( service -> service.updateState(options, sharedMemory, /* callback= */ null)); } } @@ -407,7 +413,7 @@ abstract class HotwordDetectorSession { // TODO: handle cancellations well // TODO: what if we cancelled and started a new one? - mRemoteHotwordDetectionService.run( + mRemoteDetectionService.run( service -> { service.detectFromMicrophoneSource( serviceAudioSource, @@ -512,7 +518,7 @@ abstract class HotwordDetectorSession { void destroyLocked() { mDestroyed = true; mDebugHotwordLogging = false; - mRemoteHotwordDetectionService = null; + mRemoteDetectionService = null; if (mAttentionManagerInternal != null) { mAttentionManagerInternal.onStopProximityUpdates(mProximityCallbackInternal); } @@ -524,9 +530,9 @@ abstract class HotwordDetectorSession { } @SuppressWarnings("GuardedBy") - void updateRemoteHotwordDetectionServiceLocked( - @NonNull HotwordDetectionConnection.ServiceConnection remoteHotwordDetectionService) { - mRemoteHotwordDetectionService = remoteHotwordDetectionService; + void updateRemoteSandboxedDetectionServiceLocked( + @NonNull HotwordDetectionConnection.ServiceConnection remoteDetectionService) { + mRemoteDetectionService = remoteDetectionService; } void reportErrorLocked(int status) { @@ -628,9 +634,9 @@ abstract class HotwordDetectorSession { int hotwordOp = AppOpsManager.strOpToOp(AppOpsManager.OPSTR_RECORD_AUDIO_HOTWORD); mAppOpsManager.noteOpNoThrow(hotwordOp, mVoiceInteractorIdentity.uid, mVoiceInteractorIdentity.packageName, - mVoiceInteractorIdentity.attributionTag, OP_MESSAGE); + mVoiceInteractorIdentity.attributionTag, HOTWORD_DETECTION_OP_MESSAGE); enforcePermissionForDataDelivery(mContext, mVoiceInteractorIdentity, - CAPTURE_AUDIO_HOTWORD, OP_MESSAGE); + CAPTURE_AUDIO_HOTWORD, HOTWORD_DETECTION_OP_MESSAGE); } }); } diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/DspTrustedHotwordDetectorSession.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/DspTrustedHotwordDetectorSession.java index 84bd71601643..ad84f004e966 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/DspTrustedHotwordDetectorSession.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/DspTrustedHotwordDetectorSession.java @@ -32,7 +32,6 @@ import android.os.IBinder; import android.os.PersistableBundle; import android.os.RemoteException; import android.os.SharedMemory; -import android.service.voice.AlwaysOnHotwordDetector; import android.service.voice.HotwordDetectedResult; import android.service.voice.HotwordDetectionService; import android.service.voice.HotwordDetector; @@ -59,7 +58,7 @@ import java.util.concurrent.atomic.AtomicBoolean; * {@link android.service.voice.VoiceInteractionService#createAlwaysOnHotwordDetector(String, * Locale, PersistableBundle, SharedMemory, AlwaysOnHotwordDetector.Callback)}. */ -final class DspTrustedHotwordDetectorSession extends HotwordDetectorSession { +final class DspTrustedHotwordDetectorSession extends DetectorSession { private static final String TAG = "DspTrustedHotwordDetectorSession"; // The validation timeout value is 3 seconds for onDetect of DSP trigger event. @@ -182,7 +181,7 @@ final class DspTrustedHotwordDetectorSession extends HotwordDetectorSession { }; mValidatingDspTrigger = true; - mRemoteHotwordDetectionService.run(service -> { + mRemoteDetectionService.run(service -> { // We use the VALIDATION_TIMEOUT_MILLIS to inform that the client needs to invoke // the callback before timeout value. In order to reduce the latency impact between // server side and client side, we need to use another timeout value diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java index 276bccdb9f23..d501af7d83be 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java @@ -89,7 +89,7 @@ final class HotwordDetectionConnection { Executors.newSingleThreadScheduledExecutor(); @Nullable private final ScheduledFuture<?> mCancellationTaskFuture; private final IBinder.DeathRecipient mAudioServerDeathRecipient = this::audioServerDied; - @NonNull private final ServiceConnectionFactory mServiceConnectionFactory; + @NonNull private final ServiceConnectionFactory mHotwordDetectionServiceConnectionFactory; private final int mDetectorType; /** * Time after which each HotwordDetectionService process is stopped and replaced by a new one. @@ -99,7 +99,7 @@ final class HotwordDetectionConnection { final Object mLock; final int mVoiceInteractionServiceUid; - final ComponentName mDetectionComponentName; + final ComponentName mHotwordDetectionComponentName; final int mUser; final Context mContext; volatile HotwordDetectionServiceIdentity mIdentity; @@ -122,27 +122,30 @@ final class HotwordDetectionConnection { * to record the detectors. */ @GuardedBy("mLock") - private final SparseArray<HotwordDetectorSession> mHotwordDetectorSessions = + private final SparseArray<DetectorSession> mDetectorSessions = new SparseArray<>(); HotwordDetectionConnection(Object lock, Context context, int voiceInteractionServiceUid, - Identity voiceInteractorIdentity, ComponentName serviceName, int userId, + Identity voiceInteractorIdentity, ComponentName hotwordDetectionServiceName, int userId, boolean bindInstantServiceAllowed, int detectorType) { mLock = lock; mContext = context; mVoiceInteractionServiceUid = voiceInteractionServiceUid; mVoiceInteractorIdentity = voiceInteractorIdentity; - mDetectionComponentName = serviceName; + mHotwordDetectionComponentName = hotwordDetectionServiceName; mUser = userId; mDetectorType = detectorType; mReStartPeriodSeconds = DeviceConfig.getInt(DeviceConfig.NAMESPACE_VOICE_INTERACTION, KEY_RESTART_PERIOD_IN_SECONDS, 0); - final Intent intent = new Intent(HotwordDetectionService.SERVICE_INTERFACE); - intent.setComponent(mDetectionComponentName); + final Intent hotwordDetectionServiceIntent = + new Intent(HotwordDetectionService.SERVICE_INTERFACE); + hotwordDetectionServiceIntent.setComponent(mHotwordDetectionComponentName); initAudioFlingerLocked(); - mServiceConnectionFactory = new ServiceConnectionFactory(intent, bindInstantServiceAllowed); - mRemoteHotwordDetectionService = mServiceConnectionFactory.createLocked(); + mHotwordDetectionServiceConnectionFactory = + new ServiceConnectionFactory(hotwordDetectionServiceIntent, + bindInstantServiceAllowed); + mRemoteHotwordDetectionService = mHotwordDetectionServiceConnectionFactory.createLocked(); mLastRestartInstant = Instant.now(); if (mReStartPeriodSeconds <= 0) { @@ -176,8 +179,8 @@ final class HotwordDetectionConnection { try { mAudioFlinger.linkToDeath(mAudioServerDeathRecipient, /* flags= */ 0); } catch (RemoteException e) { - Slog.w(TAG, "Audio server died before we registered a DeathRecipient; retrying init.", - e); + Slog.w(TAG, "Audio server died before we registered a DeathRecipient; " + + "retrying init.", e); initAudioFlingerLocked(); } } @@ -200,10 +203,10 @@ final class HotwordDetectionConnection { void cancelLocked() { Slog.v(TAG, "cancelLocked"); clearDebugHotwordLoggingTimeoutLocked(); - runForEachHotwordDetectorSessionLocked((session) -> { + runForEachDetectorSessionLocked((session) -> { session.destroyLocked(); }); - mHotwordDetectorSessions.clear(); + mDetectorSessions.clear(); mDebugHotwordLogging = false; mRemoteHotwordDetectionService.unbind(); LocalServices.getService(PermissionManagerServiceInternal.class) @@ -223,7 +226,7 @@ final class HotwordDetectionConnection { @SuppressWarnings("GuardedBy") void updateStateLocked(PersistableBundle options, SharedMemory sharedMemory, @NonNull IBinder token) { - final HotwordDetectorSession session = getDetectorSessionByTokenLocked(token); + final DetectorSession session = getDetectorSessionByTokenLocked(token); if (session == null) { Slog.v(TAG, "Not found the detector by token"); return; @@ -259,7 +262,7 @@ final class HotwordDetectionConnection { if (DEBUG) { Slog.d(TAG, "startListeningFromExternalSourceLocked"); } - final HotwordDetectorSession session = getDetectorSessionByTokenLocked(token); + final DetectorSession session = getDetectorSessionByTokenLocked(token); if (session == null) { Slog.v(TAG, "Not found the detector by token"); return; @@ -321,7 +324,7 @@ final class HotwordDetectionConnection { Slog.v(TAG, "setDebugHotwordLoggingLocked: " + logging); clearDebugHotwordLoggingTimeoutLocked(); mDebugHotwordLogging = logging; - runForEachHotwordDetectorSessionLocked((session) -> { + runForEachDetectorSessionLocked((session) -> { session.setDebugHotwordLoggingLocked(logging); }); @@ -331,7 +334,7 @@ final class HotwordDetectionConnection { Slog.v(TAG, "Timeout to reset mDebugHotwordLogging to false"); synchronized (mLock) { mDebugHotwordLogging = false; - runForEachHotwordDetectorSessionLocked((session) -> { + runForEachDetectorSessionLocked((session) -> { session.setDebugHotwordLoggingLocked(false); }); } @@ -350,24 +353,24 @@ final class HotwordDetectionConnection { private void restartProcessLocked() { // TODO(b/244598068): Check HotwordAudioStreamManager first Slog.v(TAG, "Restarting hotword detection process"); - ServiceConnection oldConnection = mRemoteHotwordDetectionService; + ServiceConnection oldHotwordConnection = mRemoteHotwordDetectionService; HotwordDetectionServiceIdentity previousIdentity = mIdentity; mLastRestartInstant = Instant.now(); // Recreate connection to reset the cache. - mRemoteHotwordDetectionService = mServiceConnectionFactory.createLocked(); + mRemoteHotwordDetectionService = mHotwordDetectionServiceConnectionFactory.createLocked(); Slog.v(TAG, "Started the new process, dispatching processRestarted to detector"); - runForEachHotwordDetectorSessionLocked((session) -> { - session.updateRemoteHotwordDetectionServiceLocked(mRemoteHotwordDetectionService); + runForEachDetectorSessionLocked((session) -> { + session.updateRemoteSandboxedDetectionServiceLocked(mRemoteHotwordDetectionService); session.informRestartProcessLocked(); }); if (DEBUG) { Slog.i(TAG, "processRestarted is dispatched done, unbinding from the old process"); } - oldConnection.ignoreConnectionStatusEvents(); - oldConnection.unbind(); + oldHotwordConnection.ignoreConnectionStatusEvents(); + oldHotwordConnection.unbind(); if (previousIdentity != null) { removeServiceUidForAudioPolicy(previousIdentity.getIsolatedUid()); } @@ -437,12 +440,12 @@ final class HotwordDetectionConnection { pw.print(prefix); pw.print("mBound="); pw.println(mRemoteHotwordDetectionService.isBound()); pw.print(prefix); pw.print("mRestartCount="); - pw.println(mServiceConnectionFactory.mRestartCount); + pw.println(mHotwordDetectionServiceConnectionFactory.mRestartCount); pw.print(prefix); pw.print("mLastRestartInstant="); pw.println(mLastRestartInstant); pw.print(prefix); pw.print("mDetectorType="); pw.println(HotwordDetector.detectorTypeToString(mDetectorType)); - pw.print(prefix); pw.println("HotwordDetectorSession(s)"); - runForEachHotwordDetectorSessionLocked((session) -> { + pw.print(prefix); pw.println("DetectorSession(s)"); + runForEachDetectorSessionLocked((session) -> { session.dumpLocked(prefix, pw); }); } @@ -537,9 +540,9 @@ final class HotwordDetectionConnection { } } synchronized (HotwordDetectionConnection.this.mLock) { - runForEachHotwordDetectorSessionLocked((session) -> { + runForEachDetectorSessionLocked((session) -> { session.reportErrorLocked( - HotwordDetectorSession.HOTWORD_DETECTION_SERVICE_DIED); + DetectorSession.HOTWORD_DETECTION_SERVICE_DIED); }); } // Can improve to log exit reason if needed @@ -599,12 +602,12 @@ final class HotwordDetectionConnection { int detectorType) { // We only support one Dsp trusted hotword detector and one software hotword detector at // the same time, remove existing one. - HotwordDetectorSession removeSession = mHotwordDetectorSessions.get(detectorType); + DetectorSession removeSession = mDetectorSessions.get(detectorType); if (removeSession != null) { removeSession.destroyLocked(); - mHotwordDetectorSessions.remove(detectorType); + mDetectorSessions.remove(detectorType); } - final HotwordDetectorSession session; + final DetectorSession session; if (detectorType == HotwordDetector.DETECTOR_TYPE_TRUSTED_HOTWORD_DSP) { session = new DspTrustedHotwordDetectorSession(mRemoteHotwordDetectionService, mLock, mContext, token, callback, mVoiceInteractionServiceUid, @@ -615,30 +618,30 @@ final class HotwordDetectionConnection { mVoiceInteractionServiceUid, mVoiceInteractorIdentity, mScheduledExecutorService, mDebugHotwordLogging); } - mHotwordDetectorSessions.put(detectorType, session); + mDetectorSessions.put(detectorType, session); session.initialize(options, sharedMemory); } @SuppressWarnings("GuardedBy") void destroyDetectorLocked(@NonNull IBinder token) { - final HotwordDetectorSession session = getDetectorSessionByTokenLocked(token); + final DetectorSession session = getDetectorSessionByTokenLocked(token); if (session != null) { session.destroyLocked(); - final int index = mHotwordDetectorSessions.indexOfValue(session); - if (index < 0 || index > mHotwordDetectorSessions.size() - 1) { + final int index = mDetectorSessions.indexOfValue(session); + if (index < 0 || index > mDetectorSessions.size() - 1) { return; } - mHotwordDetectorSessions.removeAt(index); + mDetectorSessions.removeAt(index); } } @SuppressWarnings("GuardedBy") - private HotwordDetectorSession getDetectorSessionByTokenLocked(IBinder token) { + private DetectorSession getDetectorSessionByTokenLocked(IBinder token) { if (token == null) { return null; } - for (int i = 0; i < mHotwordDetectorSessions.size(); i++) { - final HotwordDetectorSession session = mHotwordDetectorSessions.valueAt(i); + for (int i = 0; i < mDetectorSessions.size(); i++) { + final DetectorSession session = mDetectorSessions.valueAt(i); if (!session.isDestroyed() && session.isSameToken(token)) { return session; } @@ -648,7 +651,7 @@ final class HotwordDetectionConnection { @SuppressWarnings("GuardedBy") private DspTrustedHotwordDetectorSession getDspTrustedHotwordDetectorSessionLocked() { - final HotwordDetectorSession session = mHotwordDetectorSessions.get( + final DetectorSession session = mDetectorSessions.get( HotwordDetector.DETECTOR_TYPE_TRUSTED_HOTWORD_DSP); if (session == null || session.isDestroyed()) { Slog.v(TAG, "Not found the Dsp detector"); @@ -659,7 +662,7 @@ final class HotwordDetectionConnection { @SuppressWarnings("GuardedBy") private SoftwareTrustedHotwordDetectorSession getSoftwareTrustedHotwordDetectorSessionLocked() { - final HotwordDetectorSession session = mHotwordDetectorSessions.get( + final DetectorSession session = mDetectorSessions.get( HotwordDetector.DETECTOR_TYPE_TRUSTED_HOTWORD_SOFTWARE); if (session == null || session.isDestroyed()) { Slog.v(TAG, "Not found the software detector"); @@ -669,10 +672,10 @@ final class HotwordDetectionConnection { } @SuppressWarnings("GuardedBy") - private void runForEachHotwordDetectorSessionLocked( - @NonNull Consumer<HotwordDetectorSession> action) { - for (int i = 0; i < mHotwordDetectorSessions.size(); i++) { - HotwordDetectorSession session = mHotwordDetectorSessions.valueAt(i); + private void runForEachDetectorSessionLocked( + @NonNull Consumer<DetectorSession> action) { + for (int i = 0; i < mDetectorSessions.size(); i++) { + DetectorSession session = mDetectorSessions.valueAt(i); action.accept(session); } } diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/SoftwareTrustedHotwordDetectorSession.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/SoftwareTrustedHotwordDetectorSession.java index 4eb997a610a4..3ad963d21943 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/SoftwareTrustedHotwordDetectorSession.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/SoftwareTrustedHotwordDetectorSession.java @@ -55,7 +55,7 @@ import java.util.concurrent.ScheduledExecutorService; * {@link android.service.voice.VoiceInteractionService#createHotwordDetector(PersistableBundle, * SharedMemory, HotwordDetector.Callback)}. */ -final class SoftwareTrustedHotwordDetectorSession extends HotwordDetectorSession { +final class SoftwareTrustedHotwordDetectorSession extends DetectorSession { private static final String TAG = "SoftwareTrustedHotwordDetectorSession"; private IMicrophoneHotwordDetectionVoiceInteractionCallback mSoftwareCallback; @@ -155,7 +155,7 @@ final class SoftwareTrustedHotwordDetectorSession extends HotwordDetectorSession } }; - mRemoteHotwordDetectionService.run( + mRemoteDetectionService.run( service -> service.detectFromMicrophoneSource( null, AUDIO_SOURCE_MICROPHONE, @@ -179,7 +179,7 @@ final class SoftwareTrustedHotwordDetectorSession extends HotwordDetectorSession } mPerformingSoftwareHotwordDetection = false; - mRemoteHotwordDetectionService.run(ISandboxedDetectionService::stopDetection); + mRemoteDetectionService.run(ISandboxedDetectionService::stopDetection); closeExternalAudioStreamLocked("stopping requested"); } diff --git a/telephony/java/android/telephony/CellBroadcastIdRange.java b/telephony/java/android/telephony/CellBroadcastIdRange.java index eaf4f1c4bf41..abee80f76f83 100644 --- a/telephony/java/android/telephony/CellBroadcastIdRange.java +++ b/telephony/java/android/telephony/CellBroadcastIdRange.java @@ -15,6 +15,7 @@ */ package android.telephony; +import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.SystemApi; import android.os.Parcel; @@ -29,7 +30,9 @@ import java.util.Objects; @SystemApi public final class CellBroadcastIdRange implements Parcelable { + @IntRange(from = 0, to = 0xFFFF) private int mStartId; + @IntRange(from = 0, to = 0xFFFF) private int mEndId; private int mType; private boolean mIsEnabled; @@ -38,18 +41,19 @@ public final class CellBroadcastIdRange implements Parcelable { * Create a new CellBroacastRange * * @param startId first message identifier as specified in TS 23.041 (3GPP) - * or C.R1001-G (3GPP2) + * or C.R1001-G (3GPP2). The value must be between 0 and 0xFFFF. * @param endId last message identifier as specified in TS 23.041 (3GPP) - * or C.R1001-G (3GPP2) + * or C.R1001-G (3GPP2). The value must be between 0 and 0xFFFF. * @param type the message format as defined in {@link SmsCbMessage} * @param isEnabled whether the range is enabled * * @throws IllegalArgumentException if endId < startId or invalid value */ - public CellBroadcastIdRange(int startId, int endId, + public CellBroadcastIdRange(@IntRange(from = 0, to = 0xFFFF) int startId, + @IntRange(from = 0, to = 0xFFFF) int endId, @android.telephony.SmsCbMessage.MessageFormat int type, boolean isEnabled) throws IllegalArgumentException { - if (startId < 0 || endId < 0) { + if (startId < 0 || endId < 0 || startId > 0xFFFF || endId > 0xFFFF) { throw new IllegalArgumentException("invalid id"); } if (endId < startId) { @@ -65,6 +69,7 @@ public final class CellBroadcastIdRange implements Parcelable { * Return the first message identifier of this range as specified in TS 23.041 (3GPP) * or C.R1001-G (3GPP2) */ + @IntRange(from = 0, to = 0xFFFF) public int getStartId() { return mStartId; } @@ -73,6 +78,7 @@ public final class CellBroadcastIdRange implements Parcelable { * Return the last message identifier of this range as specified in TS 23.041 (3GPP) * or C.R1001-G (3GPP2) */ + @IntRange(from = 0, to = 0xFFFF) public int getEndId() { return mEndId; } diff --git a/telephony/java/android/telephony/SmsManager.java b/telephony/java/android/telephony/SmsManager.java index e6f134921cd9..a2a110d3f758 100644 --- a/telephony/java/android/telephony/SmsManager.java +++ b/telephony/java/android/telephony/SmsManager.java @@ -2014,7 +2014,7 @@ public final class SmsManager { * @see #disableCellBroadcastRange(int, int, int) * * @throws IllegalArgumentException if endMessageId < startMessageId - * @deprecated Use {@link TelephonyManager#setCellBroadcastRanges} instead. + * @deprecated Use {@link TelephonyManager#setCellBroadcastIdRanges} instead. * {@hide} */ @Deprecated @@ -2076,7 +2076,7 @@ public final class SmsManager { * @see #enableCellBroadcastRange(int, int, int) * * @throws IllegalArgumentException if endMessageId < startMessageId - * @deprecated Use {@link TelephonyManager#setCellBroadcastRanges} instead. + * @deprecated Use {@link TelephonyManager#setCellBroadcastIdRanges} instead. * {@hide} */ @Deprecated @@ -3486,7 +3486,7 @@ public final class SmsManager { /** * Reset all cell broadcast ranges. Previously enabled ranges will become invalid after this. - * @deprecated Use {@link TelephonyManager#resetAllCellBroadcastRanges} instead + * @deprecated Use {@link TelephonyManager#setCellBroadcastIdRanges} with empty list instead * @hide */ @Deprecated diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java index 059f4c355437..0ad5ba0eaa47 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -17851,12 +17851,12 @@ public class TelephonyManager { /** @hide */ @Retention(RetentionPolicy.SOURCE) - @IntDef(prefix = {"CELLBROADCAST_RESULT_"}, value = { - CELLBROADCAST_RESULT_UNKNOWN, - CELLBROADCAST_RESULT_SUCCESS, - CELLBROADCAST_RESULT_UNSUPPORTED, - CELLBROADCAST_RESULT_FAIL_CONFIG, - CELLBROADCAST_RESULT_FAIL_ACTIVATION}) + @IntDef(prefix = {"CELL_BROADCAST_RESULT_"}, value = { + CELL_BROADCAST_RESULT_UNKNOWN, + CELL_BROADCAST_RESULT_SUCCESS, + CELL_BROADCAST_RESULT_UNSUPPORTED, + CELL_BROADCAST_RESULT_FAIL_CONFIG, + CELL_BROADCAST_RESULT_FAIL_ACTIVATION}) public @interface CellBroadcastResult {} /** @@ -17864,35 +17864,35 @@ public class TelephonyManager { * @hide */ @SystemApi - public static final int CELLBROADCAST_RESULT_UNKNOWN = -1; + public static final int CELL_BROADCAST_RESULT_UNKNOWN = -1; /** * The cell broadcast request is successful. * @hide */ @SystemApi - public static final int CELLBROADCAST_RESULT_SUCCESS = 0; + public static final int CELL_BROADCAST_RESULT_SUCCESS = 0; /** * The cell broadcast request is not supported. * @hide */ @SystemApi - public static final int CELLBROADCAST_RESULT_UNSUPPORTED = 1; + public static final int CELL_BROADCAST_RESULT_UNSUPPORTED = 1; /** * The cell broadcast request is failed due to the error to set config * @hide */ @SystemApi - public static final int CELLBROADCAST_RESULT_FAIL_CONFIG = 2; + public static final int CELL_BROADCAST_RESULT_FAIL_CONFIG = 2; /** * The cell broadcast request is failed due to the error to set activation * @hide */ @SystemApi - public static final int CELLBROADCAST_RESULT_FAIL_ACTIVATION = 3; + public static final int CELL_BROADCAST_RESULT_FAIL_ACTIVATION = 3; /** * Set reception of cell broadcast messages with the list of the given ranges diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt index 566ec9ab0620..64ed45307e8d 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt @@ -102,8 +102,8 @@ constructor( } /** - * Checks that the [ComponentNameMatcher.NAV_BAR] window is visible at the start and end of - * the transition + * Checks that the [ComponentNameMatcher.NAV_BAR] window is visible at the start and end of the + * transition * * Note: Phones only */ diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowFromFixedOrientationAppTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowFromFixedOrientationAppTest.kt index 197564a6039a..c2526d3fff58 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowFromFixedOrientationAppTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowFromFixedOrientationAppTest.kt @@ -18,7 +18,7 @@ package com.android.server.wm.flicker.ime import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.Presubmit -import android.platform.test.annotations.RequiresDevice +import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.BaseTest import com.android.server.wm.flicker.FlickerBuilder import com.android.server.wm.flicker.FlickerTest diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowToOverViewTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowToOverViewTest.kt index 209eb0c22b62..b05beba696e9 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowToOverViewTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/OpenImeWindowToOverViewTest.kt @@ -17,7 +17,7 @@ package com.android.server.wm.flicker.ime import android.platform.test.annotations.Presubmit -import android.platform.test.annotations.RequiresDevice +import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.BaseTest import com.android.server.wm.flicker.FlickerBuilder import com.android.server.wm.flicker.FlickerTest diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppAfterCameraTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppAfterCameraTest.kt index 14a6668fdc9a..4ca9d5fa90e3 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppAfterCameraTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppAfterCameraTest.kt @@ -16,7 +16,7 @@ package com.android.server.wm.flicker.launch -import android.platform.test.annotations.RequiresDevice +import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerBuilder import com.android.server.wm.flicker.FlickerTest import com.android.server.wm.flicker.FlickerTestFactory diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppAfterCameraTest_ShellTransit.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppAfterCameraTest_ShellTransit.kt index 99574ef832b6..a9f9204ded34 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppAfterCameraTest_ShellTransit.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppAfterCameraTest_ShellTransit.kt @@ -17,7 +17,7 @@ package com.android.server.wm.flicker.launch import android.platform.test.annotations.FlakyTest -import android.platform.test.annotations.RequiresDevice +import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerTest import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdFromIcon.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdFromIcon.kt index e0df5be9bac1..242f4576d808 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdFromIcon.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdFromIcon.kt @@ -17,7 +17,7 @@ package com.android.server.wm.flicker.launch import android.platform.test.annotations.FlakyTest -import android.platform.test.annotations.RequiresDevice +import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerBuilder import com.android.server.wm.flicker.FlickerTest import com.android.server.wm.flicker.FlickerTestFactory diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdTest.kt index 66af72ede90c..a4f09c000963 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppColdTest.kt @@ -18,7 +18,7 @@ package com.android.server.wm.flicker.launch import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Presubmit -import android.platform.test.annotations.RequiresDevice +import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerBuilder import com.android.server.wm.flicker.FlickerTest import com.android.server.wm.flicker.FlickerTestFactory diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationCold.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationCold.kt index 26898f8b3ae6..56d7d5e133de 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationCold.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationCold.kt @@ -19,7 +19,7 @@ package com.android.server.wm.flicker.launch import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.Presubmit -import android.platform.test.annotations.RequiresDevice +import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerBuilder import com.android.server.wm.flicker.FlickerTest import com.android.server.wm.flicker.FlickerTestFactory diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWarm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWarm.kt index c44ad83755f8..60b0f9b50a23 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWarm.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWarm.kt @@ -18,7 +18,7 @@ package com.android.server.wm.flicker.launch import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Presubmit -import android.platform.test.annotations.RequiresDevice +import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerBuilder import com.android.server.wm.flicker.FlickerTest import com.android.server.wm.flicker.FlickerTestFactory diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWithLockOverlayApp.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWithLockOverlayApp.kt index e3ffb45dbf42..52ca7a2e0612 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWithLockOverlayApp.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWithLockOverlayApp.kt @@ -19,7 +19,7 @@ package com.android.server.wm.flicker.launch import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.Presubmit -import android.platform.test.annotations.RequiresDevice +import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerBuilder import com.android.server.wm.flicker.FlickerTest import com.android.server.wm.flicker.FlickerTestFactory diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromNotificationCold.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromNotificationCold.kt index 240e90b9f019..6c833c4a5b62 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromNotificationCold.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromNotificationCold.kt @@ -18,7 +18,7 @@ package com.android.server.wm.flicker.launch import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.Presubmit -import android.platform.test.annotations.RequiresDevice +import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerBuilder import com.android.server.wm.flicker.FlickerTest import com.android.server.wm.flicker.FlickerTestFactory diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromNotificationWarm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromNotificationWarm.kt index 6388a5ae2259..d582931d882b 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromNotificationWarm.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromNotificationWarm.kt @@ -19,9 +19,9 @@ package com.android.server.wm.flicker.launch import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.Presubmit -import android.platform.test.annotations.RequiresDevice import android.view.WindowInsets import android.view.WindowManager +import androidx.test.filters.RequiresDevice import androidx.test.uiautomator.By import androidx.test.uiautomator.Until import com.android.server.wm.flicker.FlickerBuilder diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromOverviewTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromOverviewTest.kt index 9106835fb7b4..db4baa039856 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromOverviewTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromOverviewTest.kt @@ -18,7 +18,7 @@ package com.android.server.wm.flicker.launch import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Presubmit -import android.platform.test.annotations.RequiresDevice +import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerBuilder import com.android.server.wm.flicker.FlickerTest import com.android.server.wm.flicker.FlickerTestFactory diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppNonResizeableTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppNonResizeableTest.kt index 142c6888e776..7cfe87904dc3 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppNonResizeableTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppNonResizeableTest.kt @@ -19,7 +19,7 @@ package com.android.server.wm.flicker.launch import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.Presubmit -import android.platform.test.annotations.RequiresDevice +import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerTest import com.android.server.wm.flicker.FlickerTestFactory import com.android.server.wm.flicker.annotation.FlickerServiceCompatible diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppWarmTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppWarmTest.kt index 62d7cc099771..7a7990f4e36c 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppWarmTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppWarmTest.kt @@ -18,7 +18,7 @@ package com.android.server.wm.flicker.launch import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Presubmit -import android.platform.test.annotations.RequiresDevice +import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerBuilder import com.android.server.wm.flicker.FlickerTest import com.android.server.wm.flicker.FlickerTestFactory diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/TaskTransitionTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/TaskTransitionTest.kt index b064695554bb..31babb8479b6 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/TaskTransitionTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/TaskTransitionTest.kt @@ -18,6 +18,7 @@ package com.android.server.wm.flicker.launch import android.app.Instrumentation import android.app.WallpaperManager +import android.content.res.Resources import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Postsubmit import android.platform.test.annotations.Presubmit @@ -29,8 +30,8 @@ import com.android.server.wm.flicker.FlickerTestFactory import com.android.server.wm.flicker.helpers.NewTasksAppHelper import com.android.server.wm.flicker.helpers.SimpleAppHelper import com.android.server.wm.flicker.helpers.WindowUtils -import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled +import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory import com.android.server.wm.traces.common.ComponentNameMatcher import com.android.server.wm.traces.common.ComponentNameMatcher.Companion.DEFAULT_TASK_DISPLAY_AREA import com.android.server.wm.traces.common.ComponentNameMatcher.Companion.SPLASH_SCREEN @@ -64,9 +65,7 @@ import org.junit.runners.Parameterized class TaskTransitionTest(flicker: FlickerTest) : BaseTest(flicker) { private val launchNewTaskApp = NewTasksAppHelper(instrumentation) private val simpleApp = SimpleAppHelper(instrumentation) - private val wallpaper by lazy { - getWallpaperPackage(instrumentation) ?: error("Unable to obtain wallpaper") - } + private val wallpaper by lazy { getWallpaperPackage(instrumentation) } /** {@inheritDoc} */ override val transition: FlickerBuilder.() -> Unit = { @@ -143,8 +142,7 @@ class TaskTransitionTest(flicker: FlickerTest) : BaseTest(flicker) { val displayBounds = WindowUtils.getDisplayBounds(flicker.scenario.startRotation) flicker.assertLayers { - this - .invoke("LAUNCH_NEW_TASK_ACTIVITY coversExactly displayBounds") { + this.invoke("LAUNCH_NEW_TASK_ACTIVITY coversExactly displayBounds") { it.visibleRegion(launchNewTaskApp.componentMatcher).coversExactly(displayBounds) } .isInvisible(backgroundColorLayer) @@ -159,7 +157,7 @@ class TaskTransitionTest(flicker: FlickerTest) : BaseTest(flicker) { "SIMPLE_ACTIVITY's splashscreen coversExactly displayBounds", isOptional = true ) { - it.visibleRegion(ComponentSplashScreenMatcher( simpleApp.componentMatcher)) + it.visibleRegion(ComponentSplashScreenMatcher(simpleApp.componentMatcher)) .coversExactly(displayBounds) } .invoke("SIMPLE_ACTIVITY coversExactly displayBounds") { @@ -178,7 +176,8 @@ class TaskTransitionTest(flicker: FlickerTest) : BaseTest(flicker) { isOptional = true ) { it.visibleRegion( - ComponentSplashScreenMatcher(launchNewTaskApp.componentMatcher)) + ComponentSplashScreenMatcher(launchNewTaskApp.componentMatcher) + ) .coversExactly(displayBounds) } .invoke("LAUNCH_NEW_TASK_ACTIVITY coversExactly displayBounds") { @@ -215,10 +214,20 @@ class TaskTransitionTest(flicker: FlickerTest) : BaseTest(flicker) { override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd() companion object { - private fun getWallpaperPackage(instrumentation: Instrumentation): IComponentMatcher? { + private fun getWallpaperPackage(instrumentation: Instrumentation): IComponentMatcher { val wallpaperManager = WallpaperManager.getInstance(instrumentation.targetContext) return wallpaperManager.wallpaperInfo?.component?.toFlickerComponent() + ?: getStaticWallpaperPackage(instrumentation) + } + + private fun getStaticWallpaperPackage(instrumentation: Instrumentation): IComponentMatcher { + val resourceId = + Resources.getSystem() + .getIdentifier("image_wallpaper_component", "string", "android") + return ComponentNameMatcher.unflattenFromString( + instrumentation.targetContext.resources.getString(resourceId) + ) } @Parameterized.Parameters(name = "{0}") diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest.kt index b4a67eff75ee..be3b0bf15401 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest.kt @@ -17,7 +17,7 @@ package com.android.server.wm.flicker.quickswitch import android.platform.test.annotations.Presubmit -import android.platform.test.annotations.RequiresDevice +import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.BaseTest import com.android.server.wm.flicker.FlickerBuilder import com.android.server.wm.flicker.FlickerTest diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest_ShellTransit.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest_ShellTransit.kt index 6dc11b5de1f2..25d9753b363f 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest_ShellTransit.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest_ShellTransit.kt @@ -17,7 +17,7 @@ package com.android.server.wm.flicker.quickswitch import android.platform.test.annotations.FlakyTest -import android.platform.test.annotations.RequiresDevice +import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerTest import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest.kt index 593481cfc221..18d1d3ce701e 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest.kt @@ -17,7 +17,7 @@ package com.android.server.wm.flicker.quickswitch import android.platform.test.annotations.Presubmit -import android.platform.test.annotations.RequiresDevice +import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.BaseTest import com.android.server.wm.flicker.FlickerBuilder import com.android.server.wm.flicker.FlickerTest diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest_ShellTransit.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest_ShellTransit.kt index 5a78868a8a05..b40ecac2d19a 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest_ShellTransit.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest_ShellTransit.kt @@ -17,7 +17,7 @@ package com.android.server.wm.flicker.quickswitch import android.platform.test.annotations.FlakyTest -import android.platform.test.annotations.RequiresDevice +import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerTest import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTest.kt index 456eab141f93..df91754765ba 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTest.kt @@ -18,7 +18,7 @@ package com.android.server.wm.flicker.quickswitch import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Presubmit -import android.platform.test.annotations.RequiresDevice +import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.BaseTest import com.android.server.wm.flicker.FlickerBuilder import com.android.server.wm.flicker.FlickerTest diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTest.kt index 741ae51887a3..23edfb662693 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTest.kt @@ -19,8 +19,8 @@ package com.android.server.wm.flicker.rotation import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.IwTest import android.platform.test.annotations.Presubmit -import android.platform.test.annotations.RequiresDevice import android.view.WindowManager +import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.FlickerBuilder import com.android.server.wm.flicker.FlickerTest import com.android.server.wm.flicker.FlickerTestFactory @@ -253,7 +253,7 @@ open class SeamlessAppRotationTest(flicker: FlickerTest) : RotationTransition(fl * from [FlickerTestFactory.rotationTests], but adding a flag ( * [ActivityOptions.SeamlessRotation.EXTRA_STARVE_UI_THREAD]) to indicate if the app should * starve the UI thread of not - */ + */ @Parameterized.Parameters(name = "{0}") @JvmStatic fun getParams(): Collection<FlickerTest> { diff --git a/tests/FrameworkPerf/AndroidManifest.xml b/tests/FrameworkPerf/AndroidManifest.xml index 07e775aeb838..9696fc31469a 100644 --- a/tests/FrameworkPerf/AndroidManifest.xml +++ b/tests/FrameworkPerf/AndroidManifest.xml @@ -3,6 +3,16 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.frameworkperf"> <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL"/> + <uses-permission android:name="android.permission.MANAGE_OWN_CALLS"/> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION"/> + <uses-permission android:name="Manifest.permission.ACCESS_FINE_LOCATION"/> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE"/> + <uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/> + + <uses-permission android:name="android.permission.WAKE_LOCK"/> <uses-sdk android:minSdkVersion="5"/> diff --git a/tests/Input/src/com/android/test/input/InputDeviceTest.java b/tests/Input/src/com/android/test/input/InputDeviceTest.java index 4da530442c49..797e818285f9 100644 --- a/tests/Input/src/com/android/test/input/InputDeviceTest.java +++ b/tests/Input/src/com/android/test/input/InputDeviceTest.java @@ -18,7 +18,6 @@ package android.view; import static org.junit.Assert.assertEquals; -import android.hardware.input.InputDeviceCountryCode; import android.os.Parcel; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -55,7 +54,6 @@ public class InputDeviceTest { assertEquals(device.isExternal(), outDevice.isExternal()); assertEquals(device.getSources(), outDevice.getSources()); assertEquals(device.getKeyboardType(), outDevice.getKeyboardType()); - assertEquals(device.getCountryCode(), outDevice.getCountryCode()); assertEquals(device.getKeyboardLanguageTag(), outDevice.getKeyboardLanguageTag()); assertEquals(device.getKeyboardLayoutType(), outDevice.getKeyboardLayoutType()); assertEquals(device.getMotionRanges().size(), outDevice.getMotionRanges().size()); @@ -88,7 +86,6 @@ public class InputDeviceTest { .setHasButtonUnderPad(true) .setHasSensor(true) .setHasBattery(true) - .setCountryCode(InputDeviceCountryCode.INTERNATIONAL) .setKeyboardLanguageTag("en-US") .setKeyboardLayoutType("qwerty") .setSupportsUsi(true) diff --git a/tests/OneMedia/AndroidManifest.xml b/tests/OneMedia/AndroidManifest.xml index 7fc352405212..ddde6dbfefb0 100644 --- a/tests/OneMedia/AndroidManifest.xml +++ b/tests/OneMedia/AndroidManifest.xml @@ -7,6 +7,7 @@ <uses-sdk android:minSdkVersion="19"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/> <uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> @@ -27,7 +28,8 @@ </activity> <service android:name="com.android.onemedia.OnePlayerService" android:exported="true" - android:process="com.android.onemedia.service"/> + android:process="com.android.onemedia.service" + android:foregroundServiceType="mediaPlayback"/> </application> </manifest> diff --git a/tests/SurfaceViewBufferTests/src/com/android/test/SharedBufferModeScreenRecordTests.kt b/tests/SurfaceViewBufferTests/src/com/android/test/SharedBufferModeScreenRecordTests.kt index 996a1d3d79da..7378476554ec 100644 --- a/tests/SurfaceViewBufferTests/src/com/android/test/SharedBufferModeScreenRecordTests.kt +++ b/tests/SurfaceViewBufferTests/src/com/android/test/SharedBufferModeScreenRecordTests.kt @@ -18,7 +18,6 @@ package com.android.test import android.graphics.Color import android.graphics.Rect import android.os.SystemClock -import android.view.cts.surfacevalidator.PixelColor import junit.framework.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test @@ -53,7 +52,7 @@ class SharedBufferModeScreenRecordTests(useBlastAdapter: Boolean) : SystemClock.sleep(4000) } - val result = withScreenRecording(svBounds, PixelColor.RED) { + val result = withScreenRecording(svBounds, Color.RED) { it.mSurfaceProxy.drawBuffer(0, Color.RED) } val failRatio = 1.0f * result.failFrames / (result.failFrames + result.passFrames) diff --git a/tests/VectorDrawableTest/OWNERS b/tests/VectorDrawableTest/OWNERS new file mode 100644 index 000000000000..27e16681899e --- /dev/null +++ b/tests/VectorDrawableTest/OWNERS @@ -0,0 +1,3 @@ +# Bug component: 24939 + +include /graphics/java/android/graphics/OWNERS diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionHelperDetector.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionHelperDetector.kt index c1e47e906e22..268c565b65ff 100644 --- a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionHelperDetector.kt +++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionHelperDetector.kt @@ -25,12 +25,14 @@ import com.android.tools.lint.detector.api.JavaContext import com.android.tools.lint.detector.api.Scope import com.android.tools.lint.detector.api.Severity import com.android.tools.lint.detector.api.SourceCodeScanner +import com.google.android.lint.findCallExpression import com.intellij.psi.PsiElement import org.jetbrains.uast.UBlockExpression import org.jetbrains.uast.UDeclarationsExpression import org.jetbrains.uast.UElement import org.jetbrains.uast.UExpression import org.jetbrains.uast.UMethod +import org.jetbrains.uast.skipParenthesizedExprDown class EnforcePermissionHelperDetector : Detector(), SourceCodeScanner { override fun getApplicableUastTypes(): List<Class<out UElement?>> = @@ -43,34 +45,35 @@ class EnforcePermissionHelperDetector : Detector(), SourceCodeScanner { if (context.evaluator.isAbstract(node)) return if (!node.hasAnnotation(ANNOTATION_ENFORCE_PERMISSION)) return - val targetExpression = "super.${node.name}$HELPER_SUFFIX()" + val targetExpression = "${node.name}$HELPER_SUFFIX()" + val message = "Method must start with $targetExpression or super.${node.name}()" - val body = node.uastBody as? UBlockExpression - if (body == null) { - context.report( - ISSUE_ENFORCE_PERMISSION_HELPER, - context.getLocation(node), - "Method must start with $targetExpression", - ) - return - } + val firstExpression = (node.uastBody as? UBlockExpression) + ?.expressions?.firstOrNull() - val firstExpression = body.expressions.firstOrNull() if (firstExpression == null) { context.report( ISSUE_ENFORCE_PERMISSION_HELPER, context.getLocation(node), - "Method must start with $targetExpression", + message, ) return } - val firstExpressionSource = firstExpression.asSourceString() - .filterNot(Char::isWhitespace) + val firstExpressionSource = firstExpression.skipParenthesizedExprDown() + .asSourceString() + .filterNot(Char::isWhitespace) + + if (firstExpressionSource != targetExpression && + firstExpressionSource != "super.$targetExpression") { + // calling super.<methodName>() is also legal + val directSuper = context.evaluator.getSuperMethod(node) + val firstCall = findCallExpression(firstExpression)?.resolve() + if (directSuper != null && firstCall == directSuper) return - if (firstExpressionSource != targetExpression) { val locationTarget = getLocationTarget(firstExpression) val expressionLocation = context.getLocation(locationTarget) + val indent = " ".repeat(expressionLocation.start?.column ?: 0) val fix = fix() @@ -85,7 +88,7 @@ class EnforcePermissionHelperDetector : Detector(), SourceCodeScanner { context.report( ISSUE_ENFORCE_PERMISSION_HELPER, context.getLocation(node), - "Method must start with $targetExpression", + message, fix ) } @@ -99,7 +102,8 @@ class EnforcePermissionHelperDetector : Detector(), SourceCodeScanner { When @EnforcePermission is applied, the AIDL compiler generates a Stub method to do the permission check called yourMethodName$HELPER_SUFFIX. - You must call this method as the first expression in your implementation. + yourMethodName$HELPER_SUFFIX must be executed before any other operation. To do that, you can + either call it directly or indirectly via super.yourMethodName(). """ val ISSUE_ENFORCE_PERMISSION_HELPER: Issue = Issue.create( diff --git a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionHelperDetectorTest.kt b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionHelperDetectorTest.kt index 4799184b0e23..df7ebd7e1e7a 100644 --- a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionHelperDetectorTest.kt +++ b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionHelperDetectorTest.kt @@ -47,7 +47,7 @@ class EnforcePermissionHelperDetectorTest : LintDetectorTest() { .run() .expect( """ - src/Foo.java:5: Error: Method must start with super.test_enforcePermission() [MissingEnforcePermissionHelper] + src/Foo.java:5: Error: Method must start with test_enforcePermission() or super.test() [MissingEnforcePermissionHelper] @Override ^ 1 errors, 0 warnings @@ -85,7 +85,7 @@ class EnforcePermissionHelperDetectorTest : LintDetectorTest() { .run() .expect( """ - src/Foo.java:5: Error: Method must start with super.test_enforcePermission() [MissingEnforcePermissionHelper] + src/Foo.java:5: Error: Method must start with test_enforcePermission() or super.test() [MissingEnforcePermissionHelper] @Override ^ 1 errors, 0 warnings @@ -120,7 +120,7 @@ class EnforcePermissionHelperDetectorTest : LintDetectorTest() { .run() .expect( """ - src/Foo.java:5: Error: Method must start with super.test_enforcePermission() [MissingEnforcePermissionHelper] + src/Foo.java:5: Error: Method must start with test_enforcePermission() or super.test() [MissingEnforcePermissionHelper] @Override ^ 1 errors, 0 warnings @@ -150,6 +150,28 @@ class EnforcePermissionHelperDetectorTest : LintDetectorTest() { .expectClean() } + fun testHelperWithoutSuperPrefix_Okay() { + lint().files( + java( + """ + import android.content.Context; + import android.test.ITest; + public class Foo extends ITest.Stub { + private Context mContext; + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException { + test_enforcePermission(); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expectClean() + } + fun testInterfaceDefaultMethod_wouldStillReport() { lint().files( java( @@ -167,7 +189,7 @@ class EnforcePermissionHelperDetectorTest : LintDetectorTest() { .run() .expect( """ - src/IProtected.java:2: Error: Method must start with super.PermissionProtected_enforcePermission() [MissingEnforcePermissionHelper] + src/IProtected.java:2: Error: Method must start with super.PermissionProtected_enforcePermission() or super.PermissionProtected() [MissingEnforcePermissionHelper] @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE) ^ 1 errors, 0 warnings @@ -175,6 +197,216 @@ class EnforcePermissionHelperDetectorTest : LintDetectorTest() { ) } + fun testInheritance_callSuper_okay() { + lint().files( + java( + """ + package test; + import android.content.Context; + import android.test.ITest; + public class Foo extends ITest.Stub { + private Context mContext; + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException { + super.test_enforcePermission(); + } + } + """ + ).indented(), + java( + """ + package test; + import test.Foo; + public class Bar extends Foo { + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException { + super.test(); + } + } + """ + ).indented(), + java( + """ + package test; + import test.Bar; + public class Baz extends Bar { + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException { + super.test(); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expectClean() + } + + fun testInheritance_callHelper_okay() { + lint().files( + java( + """ + package test; + import android.content.Context; + import android.test.ITest; + public class Foo extends ITest.Stub { + private Context mContext; + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException { + super.test_enforcePermission(); + } + } + """ + ).indented(), + java( + """ + package test; + import test.Foo; + public class Bar extends Foo { + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException { + super.test(); + } + } + """ + ).indented(), + java( + """ + package test; + import test.Bar; + public class Baz extends Bar { + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException { + super.test_enforcePermission(); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expectClean() + } + + fun testInheritance_missingCallInChain_error() { + lint().files( + java( + """ + package test; + import android.content.Context; + import android.test.ITest; + public class Foo extends ITest.Stub { + private Context mContext; + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException { + super.test_enforcePermission(); + } + } + """ + ).indented(), + java( + """ + package test; + import test.Foo; + public class Bar extends Foo { + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException { + doStuff(); + } + } + """ + ).indented(), + java( + """ + package test; + import test.Bar; + public class Baz extends Bar { + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException { + super.test(); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/test/Bar.java:4: Error: Method must start with test_enforcePermission() or super.test() [MissingEnforcePermissionHelper] + @Override + ^ + 1 errors, 0 warnings + """ + ) + } + + fun testInheritance_missingCall_error() { + lint().files( + java( + """ + package test; + import android.content.Context; + import android.test.ITest; + public class Foo extends ITest.Stub { + private Context mContext; + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException { + super.test_enforcePermission(); + } + } + """ + ).indented(), + java( + """ + package test; + import test.Foo; + public class Bar extends Foo { + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException { + super.test(); + } + } + """ + ).indented(), + java( + """ + package test; + import test.Bar; + public class Baz extends Bar { + @Override + @android.annotation.EnforcePermission("android.Manifest.permission.READ_CONTACTS") + public void test() throws android.os.RemoteException { + doStuff(); + } + } + """ + ).indented(), + *stubs + ) + .run() + .expect( + """ + src/test/Baz.java:4: Error: Method must start with test_enforcePermission() or super.test() [MissingEnforcePermissionHelper] + @Override + ^ + 1 errors, 0 warnings + """ + ) + } + companion object { val stubs = arrayOf(aidlStub, contextStub, binderStub) } diff --git a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/Stubs.kt b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/Stubs.kt index 362ac61ff6e8..f6e58da78e66 100644 --- a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/Stubs.kt +++ b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/Stubs.kt @@ -7,7 +7,9 @@ val aidlStub: TestFile = java( """ package android.test; public interface ITest extends android.os.IInterface { - public static abstract class Stub extends android.os.Binder implements android.test.ITest {} + public static abstract class Stub extends android.os.Binder implements android.test.ITest { + protected void test_enforcePermission() throws SecurityException {} + } public void test() throws android.os.RemoteException; } """ |