diff options
382 files changed, 9526 insertions, 2405 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/apex/jobscheduler/service/java/com/android/server/tare/Agent.java b/apex/jobscheduler/service/java/com/android/server/tare/Agent.java index b84c8a41af1c..2b5920912718 100644 --- a/apex/jobscheduler/service/java/com/android/server/tare/Agent.java +++ b/apex/jobscheduler/service/java/com/android/server/tare/Agent.java @@ -164,8 +164,9 @@ class Agent { } @GuardedBy("mLock") - private boolean isAffordableLocked(long balance, long price, long ctp) { - return balance >= price && mScribe.getRemainingConsumableCakesLocked() >= ctp; + private boolean isAffordableLocked(long balance, long price, long stockLimitHonoringCtp) { + return balance >= price + && mScribe.getRemainingConsumableCakesLocked() >= stockLimitHonoringCtp; } @GuardedBy("mLock") @@ -303,7 +304,8 @@ class Agent { note.recalculateCosts(economicPolicy, userId, pkgName); final boolean isAffordable = isVip || isAffordableLocked(newBalance, - note.getCachedModifiedPrice(), note.getCtp()); + note.getCachedModifiedPrice(), + note.getStockLimitHonoringCtp()); if (note.isCurrentlyAffordable() != isAffordable) { note.setNewAffordability(isAffordable); mIrs.postAffordabilityChanged(userId, pkgName, note); @@ -339,7 +341,7 @@ class Agent { note.recalculateCosts(economicPolicy, userId, pkgName); final boolean isAffordable = isVip || isAffordableLocked(newBalance, - note.getCachedModifiedPrice(), note.getCtp()); + note.getCachedModifiedPrice(), note.getStockLimitHonoringCtp()); if (note.isCurrentlyAffordable() != isAffordable) { note.setNewAffordability(isAffordable); mIrs.postAffordabilityChanged(userId, pkgName, note); @@ -403,7 +405,8 @@ class Agent { note.recalculateCosts(economicPolicy, userId, pkgName); final boolean isAffordable = isVip || isAffordableLocked(newBalance, - note.getCachedModifiedPrice(), note.getCtp()); + note.getCachedModifiedPrice(), + note.getStockLimitHonoringCtp()); if (note.isCurrentlyAffordable() != isAffordable) { note.setNewAffordability(isAffordable); mIrs.postAffordabilityChanged(userId, pkgName, note); @@ -541,7 +544,7 @@ class Agent { final ActionAffordabilityNote note = actionAffordabilityNotes.valueAt(i); final boolean isAffordable = isVip || isAffordableLocked(newBalance, - note.getCachedModifiedPrice(), note.getCtp()); + note.getCachedModifiedPrice(), note.getStockLimitHonoringCtp()); if (note.isCurrentlyAffordable() != isAffordable) { note.setNewAffordability(isAffordable); mIrs.postAffordabilityChanged(userId, pkgName, note); @@ -882,7 +885,7 @@ class Agent { mUpperThreshold = (mUpperThreshold == Long.MIN_VALUE) ? price : Math.min(mUpperThreshold, price); } - final long ctp = note.getCtp(); + final long ctp = note.getStockLimitHonoringCtp(); if (ctp <= mRemainingConsumableCredits) { mCtpThreshold = Math.max(mCtpThreshold, ctp); } @@ -1119,7 +1122,7 @@ class Agent { note.recalculateCosts(economicPolicy, userId, pkgName); note.setNewAffordability(isVip || isAffordableLocked(getBalanceLocked(userId, pkgName), - note.getCachedModifiedPrice(), note.getCtp())); + note.getCachedModifiedPrice(), note.getStockLimitHonoringCtp())); mIrs.postAffordabilityChanged(userId, pkgName, note); // Update ongoing alarm scheduleBalanceCheckLocked(userId, pkgName); @@ -1146,7 +1149,7 @@ class Agent { static final class ActionAffordabilityNote { private final EconomyManagerInternal.ActionBill mActionBill; private final EconomyManagerInternal.AffordabilityChangeListener mListener; - private long mCtp; + private long mStockLimitHonoringCtp; private long mModifiedPrice; private boolean mIsAffordable; @@ -1185,29 +1188,34 @@ class Agent { return mModifiedPrice; } - private long getCtp() { - return mCtp; + /** Returns the cumulative CTP of actions in this note that respect the stock limit. */ + private long getStockLimitHonoringCtp() { + return mStockLimitHonoringCtp; } @VisibleForTesting void recalculateCosts(@NonNull EconomicPolicy economicPolicy, int userId, @NonNull String pkgName) { long modifiedPrice = 0; - long ctp = 0; + long stockLimitHonoringCtp = 0; final List<EconomyManagerInternal.AnticipatedAction> anticipatedActions = mActionBill.getAnticipatedActions(); for (int i = 0; i < anticipatedActions.size(); ++i) { final EconomyManagerInternal.AnticipatedAction aa = anticipatedActions.get(i); + final EconomicPolicy.Action action = economicPolicy.getAction(aa.actionId); final EconomicPolicy.Cost actionCost = economicPolicy.getCostOfAction(aa.actionId, userId, pkgName); modifiedPrice += actionCost.price * aa.numInstantaneousCalls + actionCost.price * (aa.ongoingDurationMs / 1000); - ctp += actionCost.costToProduce * aa.numInstantaneousCalls - + actionCost.costToProduce * (aa.ongoingDurationMs / 1000); + if (action.respectsStockLimit) { + stockLimitHonoringCtp += + actionCost.costToProduce * aa.numInstantaneousCalls + + actionCost.costToProduce * (aa.ongoingDurationMs / 1000); + } } mModifiedPrice = modifiedPrice; - mCtp = ctp; + mStockLimitHonoringCtp = stockLimitHonoringCtp; } boolean isCurrentlyAffordable() { @@ -1267,7 +1275,8 @@ class Agent { final ActionAffordabilityNote note = actionAffordabilityNotes.valueAt(i); final boolean isAffordable = isVip || isAffordableLocked( - newBalance, note.getCachedModifiedPrice(), note.getCtp()); + newBalance, note.getCachedModifiedPrice(), + note.getStockLimitHonoringCtp()); if (note.isCurrentlyAffordable() != isAffordable) { note.setNewAffordability(isAffordable); mIrs.postAffordabilityChanged(userId, pkgName, note); diff --git a/apex/jobscheduler/service/java/com/android/server/tare/AlarmManagerEconomicPolicy.java b/apex/jobscheduler/service/java/com/android/server/tare/AlarmManagerEconomicPolicy.java index 46338fa69eb0..a6d064c76044 100644 --- a/apex/jobscheduler/service/java/com/android/server/tare/AlarmManagerEconomicPolicy.java +++ b/apex/jobscheduler/service/java/com/android/server/tare/AlarmManagerEconomicPolicy.java @@ -262,12 +262,17 @@ public class AlarmManagerEconomicPolicy extends EconomicPolicy { KEY_AM_ACTION_ALARM_ALLOW_WHILE_IDLE_EXACT_WAKEUP_BASE_PRICE, DEFAULT_AM_ACTION_ALARM_ALLOW_WHILE_IDLE_EXACT_WAKEUP_BASE_PRICE_CAKES); + // Apps must hold the SCHEDULE_EXACT_ALARM or USE_EXACT_ALARMS permission in order to use + // exact alarms. Since the user has the option of granting/revoking the permission, we can + // be a little lenient on the action cost checks and only stop the action if the app has + // run out of credits, and not when the system has run out of stock. mActions.put(ACTION_ALARM_WAKEUP_EXACT_ALLOW_WHILE_IDLE, new Action(ACTION_ALARM_WAKEUP_EXACT_ALLOW_WHILE_IDLE, getConstantAsCake(mParser, properties, KEY_AM_ACTION_ALARM_ALLOW_WHILE_IDLE_EXACT_WAKEUP_CTP, DEFAULT_AM_ACTION_ALARM_ALLOW_WHILE_IDLE_EXACT_WAKEUP_CTP_CAKES), - exactAllowWhileIdleWakeupBasePrice)); + exactAllowWhileIdleWakeupBasePrice, + /* respectsStockLimit */ false)); mActions.put(ACTION_ALARM_WAKEUP_EXACT, new Action(ACTION_ALARM_WAKEUP_EXACT, getConstantAsCake(mParser, properties, @@ -275,7 +280,8 @@ public class AlarmManagerEconomicPolicy extends EconomicPolicy { DEFAULT_AM_ACTION_ALARM_EXACT_WAKEUP_CTP_CAKES), getConstantAsCake(mParser, properties, KEY_AM_ACTION_ALARM_EXACT_WAKEUP_BASE_PRICE, - DEFAULT_AM_ACTION_ALARM_EXACT_WAKEUP_BASE_PRICE_CAKES))); + DEFAULT_AM_ACTION_ALARM_EXACT_WAKEUP_BASE_PRICE_CAKES), + /* respectsStockLimit */ false)); final long inexactAllowWhileIdleWakeupBasePrice = getConstantAsCake(mParser, properties, @@ -287,7 +293,8 @@ public class AlarmManagerEconomicPolicy extends EconomicPolicy { getConstantAsCake(mParser, properties, KEY_AM_ACTION_ALARM_ALLOW_WHILE_IDLE_INEXACT_WAKEUP_CTP, DEFAULT_AM_ACTION_ALARM_ALLOW_WHILE_IDLE_INEXACT_WAKEUP_CTP_CAKES), - inexactAllowWhileIdleWakeupBasePrice)); + inexactAllowWhileIdleWakeupBasePrice, + /* respectsStockLimit */ false)); mActions.put(ACTION_ALARM_WAKEUP_INEXACT, new Action(ACTION_ALARM_WAKEUP_INEXACT, getConstantAsCake(mParser, properties, @@ -295,7 +302,8 @@ public class AlarmManagerEconomicPolicy extends EconomicPolicy { DEFAULT_AM_ACTION_ALARM_INEXACT_WAKEUP_CTP_CAKES), getConstantAsCake(mParser, properties, KEY_AM_ACTION_ALARM_INEXACT_WAKEUP_BASE_PRICE, - DEFAULT_AM_ACTION_ALARM_INEXACT_WAKEUP_BASE_PRICE_CAKES))); + DEFAULT_AM_ACTION_ALARM_INEXACT_WAKEUP_BASE_PRICE_CAKES), + /* respectsStockLimit */ false)); final long exactAllowWhileIdleNonWakeupBasePrice = getConstantAsCake(mParser, properties, KEY_AM_ACTION_ALARM_ALLOW_WHILE_IDLE_EXACT_NONWAKEUP_BASE_PRICE, @@ -305,7 +313,8 @@ public class AlarmManagerEconomicPolicy extends EconomicPolicy { getConstantAsCake(mParser, properties, KEY_AM_ACTION_ALARM_ALLOW_WHILE_IDLE_EXACT_NONWAKEUP_CTP, DEFAULT_AM_ACTION_ALARM_ALLOW_WHILE_IDLE_EXACT_NONWAKEUP_CTP_CAKES), - exactAllowWhileIdleNonWakeupBasePrice)); + exactAllowWhileIdleNonWakeupBasePrice, + /* respectsStockLimit */ false)); mActions.put(ACTION_ALARM_NONWAKEUP_EXACT, new Action(ACTION_ALARM_NONWAKEUP_EXACT, @@ -314,7 +323,8 @@ public class AlarmManagerEconomicPolicy extends EconomicPolicy { DEFAULT_AM_ACTION_ALARM_EXACT_NONWAKEUP_CTP_CAKES), getConstantAsCake(mParser, properties, KEY_AM_ACTION_ALARM_EXACT_NONWAKEUP_BASE_PRICE, - DEFAULT_AM_ACTION_ALARM_EXACT_NONWAKEUP_BASE_PRICE_CAKES))); + DEFAULT_AM_ACTION_ALARM_EXACT_NONWAKEUP_BASE_PRICE_CAKES), + /* respectsStockLimit */ false)); final long inexactAllowWhileIdleNonWakeupBasePrice = getConstantAsCake(mParser, properties, KEY_AM_ACTION_ALARM_ALLOW_WHILE_IDLE_INEXACT_NONWAKEUP_BASE_PRICE, @@ -342,7 +352,8 @@ public class AlarmManagerEconomicPolicy extends EconomicPolicy { DEFAULT_AM_ACTION_ALARM_ALARMCLOCK_CTP_CAKES), getConstantAsCake(mParser, properties, KEY_AM_ACTION_ALARM_ALARMCLOCK_BASE_PRICE, - DEFAULT_AM_ACTION_ALARM_ALARMCLOCK_BASE_PRICE_CAKES))); + DEFAULT_AM_ACTION_ALARM_ALARMCLOCK_BASE_PRICE_CAKES), + /* respectsStockLimit */ false)); mRewards.put(REWARD_TOP_ACTIVITY, new Reward(REWARD_TOP_ACTIVITY, getConstantAsCake(mParser, properties, diff --git a/apex/jobscheduler/service/java/com/android/server/tare/EconomicPolicy.java b/apex/jobscheduler/service/java/com/android/server/tare/EconomicPolicy.java index b52f6f11b3bf..a4043dd8ba78 100644 --- a/apex/jobscheduler/service/java/com/android/server/tare/EconomicPolicy.java +++ b/apex/jobscheduler/service/java/com/android/server/tare/EconomicPolicy.java @@ -149,11 +149,22 @@ public abstract class EconomicPolicy { * the action unless a modifier lowers the cost to produce. */ public final long basePrice; + /** + * Whether the remaining stock limit affects an app's ability to perform this action. + * If {@code false}, then the action can be performed, even if the cost is higher + * than the remaining stock. This does not affect checking against an app's balance. + */ + public final boolean respectsStockLimit; Action(int id, long costToProduce, long basePrice) { + this(id, costToProduce, basePrice, true); + } + + Action(int id, long costToProduce, long basePrice, boolean respectsStockLimit) { this.id = id; this.costToProduce = costToProduce; this.basePrice = basePrice; + this.respectsStockLimit = respectsStockLimit; } } 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/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceAidlImplTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceAidlImplTest.java index 9803474566fb..16c1499a2775 100644 --- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceAidlImplTest.java +++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceAidlImplTest.java @@ -20,6 +20,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.google.common.truth.Truth.assertWithMessage; +import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; @@ -124,6 +125,16 @@ public final class IRadioServiceAidlImplTest extends ExtendedRadioMockitoTestCas } @Test + public void openTuner_withNullCallbackForAidlImpl_fails() throws Exception { + IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, + () -> mAidlImpl.openTuner(/* moduleId= */ 0, mBandConfigMock, + /* withAudio= */ true, /* callback= */ null, TARGET_SDK_VERSION)); + + assertWithMessage("Exception for opening tuner with null callback") + .that(thrown).hasMessageThat().contains("Callback must not be null"); + } + + @Test public void addAnnouncementListener_forAidlImpl() { ICloseHandle closeHandle = mAidlImpl.addAnnouncementListener(ENABLE_TYPES, mListenerMock); diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceHidlImplTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceHidlImplTest.java index cfff47736240..164c9aff127a 100644 --- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceHidlImplTest.java +++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceHidlImplTest.java @@ -18,6 +18,7 @@ package com.android.server.broadcastradio; import static com.google.common.truth.Truth.assertWithMessage; +import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; @@ -121,6 +122,16 @@ public final class IRadioServiceHidlImplTest { } @Test + public void openTuner_withNullCallbackForHidlImpl_fails() throws Exception { + NullPointerException thrown = assertThrows(NullPointerException.class, + () -> mHidlImpl.openTuner(/* moduleId= */ 0, mBandConfigMock, + /* withAudio= */ true, /* callback= */ null, TARGET_SDK_VERSION)); + + assertWithMessage("Exception for opening tuner with null callback") + .that(thrown).hasMessageThat().contains("Callback must not be null"); + } + + @Test public void addAnnouncementListener_forHidlImpl() { when(mHal2Mock.hasAnyModules()).thenReturn(true); ICloseHandle closeHandle = mHidlImpl.addAnnouncementListener(ENABLE_TYPES, mListenerMock); diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/RadioServiceUserControllerTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/RadioServiceUserControllerTest.java index a2d8467d2a67..161ac2dc6aa0 100644 --- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/RadioServiceUserControllerTest.java +++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/RadioServiceUserControllerTest.java @@ -17,6 +17,7 @@ package com.android.server.broadcastradio; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doThrow; import static com.google.common.truth.Truth.assertWithMessage; @@ -78,4 +79,12 @@ public final class RadioServiceUserControllerTest extends ExtendedRadioMockitoTe assertWithMessage("System user") .that(RadioServiceUserController.isCurrentOrSystemUser()).isTrue(); } + + @Test + public void isCurrentUser_withActivityManagerFails_returnsFalse() { + doThrow(new RuntimeException()).when(() -> ActivityManager.getCurrentUser()); + + assertWithMessage("User when activity manager fails") + .that(RadioServiceUserController.isCurrentOrSystemUser()).isFalse(); + } } diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/AnnouncementAggregatorTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/AnnouncementAggregatorTest.java index 9a1af191fec4..0e0dbecb7df3 100644 --- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/AnnouncementAggregatorTest.java +++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/AnnouncementAggregatorTest.java @@ -18,9 +18,11 @@ package com.android.server.broadcastradio.aidl; import static com.google.common.truth.Truth.assertWithMessage; +import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -108,6 +110,33 @@ public final class AnnouncementAggregatorTest { } @Test + public void onListUpdated_afterClosed_notUpdated() throws Exception { + ArgumentCaptor<IAnnouncementListener> moduleWatcherCaptor = + ArgumentCaptor.forClass(IAnnouncementListener.class); + watchModules(/* moduleNumber= */ 1); + verify(mRadioModuleMocks[0]).addAnnouncementListener(moduleWatcherCaptor.capture(), any()); + mAnnouncementAggregator.close(); + + moduleWatcherCaptor.getValue().onListUpdated(Arrays.asList(mAnnouncementMocks[0])); + + verify(mListenerMock, never()).onListUpdated(any()); + } + + @Test + public void watchModule_afterClosed_throwsException() throws Exception { + watchModules(/* moduleNumber= */ 1); + mAnnouncementAggregator.close(); + + IllegalStateException thrown = assertThrows(IllegalStateException.class, + () -> mAnnouncementAggregator.watchModule(mRadioModuleMocks[0], + TEST_ENABLED_TYPES)); + + assertWithMessage("Exception for watching module after aggregator has been closed") + .that(thrown).hasMessageThat() + .contains("announcement aggregator has already been closed"); + } + + @Test public void close_withOneModuleWatcher_invokesCloseHandle() throws Exception { watchModules(/* moduleNumber= */ 1); diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/AnnouncementAggregatorHidlTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/AnnouncementAggregatorHidlTest.java index b68e65fb7674..5e99b28b62b1 100644 --- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/AnnouncementAggregatorHidlTest.java +++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/AnnouncementAggregatorHidlTest.java @@ -18,9 +18,11 @@ package com.android.server.broadcastradio.hal2; import static com.google.common.truth.Truth.assertWithMessage; +import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -109,6 +111,33 @@ public final class AnnouncementAggregatorHidlTest { } @Test + public void onListUpdated_afterClosed_notUpdated() throws Exception { + ArgumentCaptor<IAnnouncementListener> moduleWatcherCaptor = + ArgumentCaptor.forClass(IAnnouncementListener.class); + watchModules(/* moduleNumber= */ 1); + verify(mRadioModuleMocks[0]).addAnnouncementListener(any(), moduleWatcherCaptor.capture()); + mAnnouncementAggregator.close(); + + moduleWatcherCaptor.getValue().onListUpdated(Arrays.asList(mAnnouncementMocks[0])); + + verify(mListenerMock, never()).onListUpdated(any()); + } + + @Test + public void watchModule_afterClosed_throwsException() throws Exception { + watchModules(/* moduleNumber= */ 1); + mAnnouncementAggregator.close(); + + IllegalStateException thrown = assertThrows(IllegalStateException.class, + () -> mAnnouncementAggregator.watchModule(mRadioModuleMocks[0], + TEST_ENABLED_TYPES)); + + assertWithMessage("Exception for watching module after aggregator has been closed") + .that(thrown).hasMessageThat() + .contains("announcement aggregator has already been closed"); + } + + @Test public void close_withOneModuleWatcher_invokesCloseHandle() throws Exception { watchModules(/* moduleNumber= */ 1); diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/BroadcastRadioServiceHidlTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/BroadcastRadioServiceHidlTest.java index c9224bfbe1c7..acf698bce33d 100644 --- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/BroadcastRadioServiceHidlTest.java +++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/BroadcastRadioServiceHidlTest.java @@ -167,6 +167,18 @@ public final class BroadcastRadioServiceHidlTest extends ExtendedRadioMockitoTes } @Test + public void openSession_withoutAudio_fails() throws Exception { + createBroadcastRadioService(); + + IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, + () -> mBroadcastRadioService.openSession(FM_RADIO_MODULE_ID, + /* legacyConfig= */ null, /* withAudio= */ false, mTunerCallbackMock)); + + assertWithMessage("Exception for opening session without audio") + .that(thrown).hasMessageThat().contains("not supported"); + } + + @Test public void addAnnouncementListener_addsOnAllRadioModules() throws Exception { createBroadcastRadioService(); when(mAnnouncementListenerMock.asBinder()).thenReturn(mBinderMock); diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/TestUtils.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/TestUtils.java index 4eedd2fdb369..d4ca8d4c96d3 100644 --- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/TestUtils.java +++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/TestUtils.java @@ -25,7 +25,6 @@ import android.hardware.radio.RadioMetadata; import android.util.ArrayMap; import java.util.ArrayList; -import java.util.HashMap; final class TestUtils { @@ -43,19 +42,24 @@ final class TestUtils { /* dabFrequencyTable= */ null, /* vendorInfo= */ null); } - static RadioManager.ProgramInfo makeProgramInfo(ProgramSelector selector, int signalQuality) { + static RadioManager.ProgramInfo makeProgramInfo(ProgramSelector selector, + ProgramSelector.Identifier logicallyTunedTo, + ProgramSelector.Identifier physicallyTunedTo, int signalQuality) { return new RadioManager.ProgramInfo(selector, - selector.getPrimaryId(), selector.getPrimaryId(), /* relatedContents= */ null, + logicallyTunedTo, physicallyTunedTo, /* relatedContents= */ null, /* infoFlags= */ 0, signalQuality, new RadioMetadata.Builder().build(), new ArrayMap<>()); } + static RadioManager.ProgramInfo makeProgramInfo(ProgramSelector selector, int signalQuality) { + return makeProgramInfo(selector, selector.getPrimaryId(), selector.getPrimaryId(), + signalQuality); + } + static RadioManager.ProgramInfo makeProgramInfo(int programType, ProgramSelector.Identifier identifier, int signalQuality) { - // Note: If you set new fields, check if programInfoToHal() needs to be updated as well. - return new RadioManager.ProgramInfo(makeProgramSelector(programType, identifier), null, - null, null, 0, signalQuality, new RadioMetadata.Builder().build(), - new HashMap<String, String>()); + return makeProgramInfo(makeProgramSelector(programType, identifier), + /* logicallyTunedTo= */ null, /* physicallyTunedTo= */ null, signalQuality); } static ProgramSelector makeFmSelector(long freq) { diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/TunerSessionHidlTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/TunerSessionHidlTest.java index 6e54dcf3d451..ea9a8461ad92 100644 --- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/TunerSessionHidlTest.java +++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/TunerSessionHidlTest.java @@ -26,6 +26,7 @@ import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.timeout; @@ -45,6 +46,8 @@ import android.hardware.radio.ProgramList; import android.hardware.radio.ProgramSelector; import android.hardware.radio.RadioManager; import android.hardware.radio.RadioTuner; +import android.os.ParcelableException; +import android.os.RemoteException; import android.util.ArrayMap; import android.util.ArraySet; @@ -62,6 +65,7 @@ import org.mockito.verification.VerificationWithTimeout; import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import java.util.Map; /** @@ -74,10 +78,10 @@ public final class TunerSessionHidlTest extends ExtendedRadioMockitoTestCase { timeout(/* millis= */ 200); private static final int SIGNAL_QUALITY = 1; private static final long AM_FM_FREQUENCY_SPACING = 500; - private static final long[] AM_FM_FREQUENCY_LIST = {97500, 98100, 99100}; + private static final long[] AM_FM_FREQUENCY_LIST = {97_500, 98_100, 99_100}; private static final RadioManager.FmBandDescriptor FM_BAND_DESCRIPTOR = new RadioManager.FmBandDescriptor(RadioManager.REGION_ITU_1, RadioManager.BAND_FM, - /* lowerLimit= */ 87500, /* upperLimit= */ 108000, /* spacing= */ 100, + /* lowerLimit= */ 87_500, /* upperLimit= */ 108_000, /* spacing= */ 100, /* stereo= */ false, /* rds= */ false, /* ta= */ false, /* af= */ false, /* ea= */ false); private static final RadioManager.BandConfig FM_BAND_CONFIG = @@ -206,6 +210,17 @@ public final class TunerSessionHidlTest extends ExtendedRadioMockitoTestCase { } @Test + public void setConfiguration_forNonCurrentUser_doesNotInvokesCallback() throws Exception { + openAidlClients(/* numClients= */ 1); + doReturn(false).when(() -> RadioServiceUserController.isCurrentOrSystemUser()); + + mTunerSessions[0].setConfiguration(FM_BAND_CONFIG); + + verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT.times(0)) + .onConfigurationChanged(FM_BAND_CONFIG); + } + + @Test public void getConfiguration() throws Exception { openAidlClients(/* numClients= */ 1); mTunerSessions[0].setConfiguration(FM_BAND_CONFIG); @@ -344,7 +359,7 @@ public final class TunerSessionHidlTest extends ExtendedRadioMockitoTestCase { } @Test - public void tune_forCurrentUser_doesNotTune() throws Exception { + public void tune_forNonCurrentUser_doesNotTune() throws Exception { openAidlClients(/* numClients= */ 1); doReturn(false).when(() -> RadioServiceUserController.isCurrentOrSystemUser()); ProgramSelector initialSel = TestUtils.makeFmSelector(AM_FM_FREQUENCY_LIST[1]); @@ -358,6 +373,20 @@ public final class TunerSessionHidlTest extends ExtendedRadioMockitoTestCase { } @Test + public void tune_withHalHasUnknownError_fails() throws Exception { + openAidlClients(/* numClients= */ 1); + ProgramSelector sel = TestUtils.makeFmSelector(AM_FM_FREQUENCY_LIST[1]); + doAnswer(invocation -> Result.UNKNOWN_ERROR).when(mHalTunerSessionMock).tune(any()); + + ParcelableException thrown = assertThrows(ParcelableException.class, () -> { + mTunerSessions[0].tune(sel); + }); + + assertWithMessage("Exception for tuning when HAL has unknown error") + .that(thrown).hasMessageThat().contains(Result.toString(Result.UNKNOWN_ERROR)); + } + + @Test public void step_withDirectionUp() throws Exception { long initFreq = AM_FM_FREQUENCY_LIST[1]; ProgramSelector initialSel = TestUtils.makeFmSelector(initFreq); @@ -391,6 +420,34 @@ public final class TunerSessionHidlTest extends ExtendedRadioMockitoTestCase { } @Test + public void step_forNonCurrentUser_doesNotStep() throws Exception { + long initFreq = AM_FM_FREQUENCY_LIST[1]; + ProgramSelector initialSel = TestUtils.makeFmSelector(initFreq); + openAidlClients(/* numClients= */ 1); + mHalCurrentInfo = TestUtils.makeHalProgramInfo( + Convert.programSelectorToHal(initialSel), SIGNAL_QUALITY); + doReturn(false).when(() -> RadioServiceUserController.isCurrentOrSystemUser()); + + mTunerSessions[0].step(/* directionDown= */ true, /* skipSubChannel= */ false); + + verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT.times(0)) + .onCurrentProgramInfoChanged(any()); + } + + @Test + public void step_withHalInInvalidState_fails() throws Exception { + openAidlClients(/* numClients= */ 1); + doAnswer(invocation -> Result.INVALID_STATE).when(mHalTunerSessionMock).step(anyBoolean()); + + IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { + mTunerSessions[0].step(/* directionDown= */ true, /* skipSubChannel= */ false); + }); + + assertWithMessage("Exception for stepping when HAL is in invalid state") + .that(thrown).hasMessageThat().contains(Result.toString(Result.INVALID_STATE)); + } + + @Test public void seek_withDirectionUp() throws Exception { long initFreq = AM_FM_FREQUENCY_LIST[2]; ProgramSelector initialSel = TestUtils.makeFmSelector(initFreq); @@ -432,11 +489,44 @@ public final class TunerSessionHidlTest extends ExtendedRadioMockitoTestCase { Convert.programSelectorToHal(initialSel), SIGNAL_QUALITY); mTunerSessions[0].seek(/* directionDown= */ true, /* skipSubChannel= */ false); + verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT) .onCurrentProgramInfoChanged(seekUpInfo); } @Test + public void seek_forNonCurrentUser_doesNotSeek() throws Exception { + long initFreq = AM_FM_FREQUENCY_LIST[2]; + ProgramSelector initialSel = TestUtils.makeFmSelector(initFreq); + RadioManager.ProgramInfo seekUpInfo = TestUtils.makeProgramInfo( + TestUtils.makeFmSelector(getSeekFrequency(initFreq, /* seekDown= */ true)), + SIGNAL_QUALITY); + openAidlClients(/* numClients= */ 1); + mHalCurrentInfo = TestUtils.makeHalProgramInfo( + Convert.programSelectorToHal(initialSel), SIGNAL_QUALITY); + doReturn(false).when(() -> RadioServiceUserController.isCurrentOrSystemUser()); + + mTunerSessions[0].seek(/* directionDown= */ true, /* skipSubChannel= */ false); + + verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT.times(0)) + .onCurrentProgramInfoChanged(seekUpInfo); + } + + @Test + public void seek_withHalHasInternalError_fails() throws Exception { + openAidlClients(/* numClients= */ 1); + doAnswer(invocation -> Result.INTERNAL_ERROR).when(mHalTunerSessionMock) + .scan(anyBoolean(), anyBoolean()); + + ParcelableException thrown = assertThrows(ParcelableException.class, () -> { + mTunerSessions[0].seek(/* directionDown= */ true, /* skipSubChannel= */ false); + }); + + assertWithMessage("Exception for seeking when HAL has internal error") + .that(thrown).hasMessageThat().contains(Result.toString(Result.INTERNAL_ERROR)); + } + + @Test public void cancel() throws Exception { openAidlClients(/* numClients= */ 1); ProgramSelector initialSel = TestUtils.makeFmSelector(AM_FM_FREQUENCY_LIST[1]); @@ -460,6 +550,32 @@ public final class TunerSessionHidlTest extends ExtendedRadioMockitoTestCase { } @Test + public void cancel_forNonCurrentUser_doesNotCancel() throws Exception { + openAidlClients(/* numClients= */ 1); + ProgramSelector initialSel = TestUtils.makeFmSelector(AM_FM_FREQUENCY_LIST[1]); + mTunerSessions[0].tune(initialSel); + doReturn(false).when(() -> RadioServiceUserController.isCurrentOrSystemUser()); + + mTunerSessions[0].cancel(); + + verify(mHalTunerSessionMock, never()).cancel(); + } + + @Test + public void cancel_whenHalThrowsRemoteException_fails() throws Exception { + openAidlClients(/* numClients= */ 1); + String exceptionMessage = "HAL service died."; + doThrow(new RemoteException(exceptionMessage)).when(mHalTunerSessionMock).cancel(); + + RuntimeException thrown = assertThrows(RuntimeException.class, () -> { + mTunerSessions[0].cancel(); + }); + + assertWithMessage("Exception for canceling when HAL throws remote exception") + .that(thrown).hasMessageThat().contains(exceptionMessage); + } + + @Test public void getImage_withInvalidId_throwsIllegalArgumentException() throws Exception { openAidlClients(/* numClients= */ 1); int imageId = Constants.INVALID_IMAGE; @@ -483,6 +599,21 @@ public final class TunerSessionHidlTest extends ExtendedRadioMockitoTestCase { } @Test + public void getImage_whenHalThrowsException_fails() throws Exception { + openAidlClients(/* numClients= */ 1); + String exceptionMessage = "HAL service died."; + when(mBroadcastRadioMock.getImage(anyInt())) + .thenThrow(new RemoteException(exceptionMessage)); + + RuntimeException thrown = assertThrows(RuntimeException.class, () -> { + mTunerSessions[0].getImage(/* id= */ 1); + }); + + assertWithMessage("Exception for getting image when HAL throws remote exception") + .that(thrown).hasMessageThat().contains(exceptionMessage); + } + + @Test public void startBackgroundScan() throws Exception { openAidlClients(/* numClients= */ 1); @@ -492,6 +623,16 @@ public final class TunerSessionHidlTest extends ExtendedRadioMockitoTestCase { } @Test + public void startBackgroundScan_forNonCurrentUser_doesNotInvokesCallback() throws Exception { + openAidlClients(/* numClients= */ 1); + doReturn(false).when(() -> RadioServiceUserController.isCurrentOrSystemUser()); + + mTunerSessions[0].startBackgroundScan(); + + verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT.times(0)).onBackgroundScanComplete(); + } + + @Test public void stopProgramListUpdates() throws Exception { openAidlClients(/* numClients= */ 1); ProgramList.Filter aidlFilter = new ProgramList.Filter(new ArraySet<>(), new ArraySet<>(), @@ -504,6 +645,19 @@ public final class TunerSessionHidlTest extends ExtendedRadioMockitoTestCase { } @Test + public void stopProgramListUpdates_forNonCurrentUser_doesNotStopUpdates() throws Exception { + openAidlClients(/* numClients= */ 1); + ProgramList.Filter aidlFilter = new ProgramList.Filter(new ArraySet<>(), new ArraySet<>(), + /* includeCategories= */ true, /* excludeModifications= */ false); + mTunerSessions[0].startProgramListUpdates(aidlFilter); + doReturn(false).when(() -> RadioServiceUserController.isCurrentOrSystemUser()); + + mTunerSessions[0].stopProgramListUpdates(); + + verify(mHalTunerSessionMock, never()).stopProgramListUpdates(); + } + + @Test public void isConfigFlagSupported_withUnsupportedFlag_returnsFalse() throws Exception { openAidlClients(/* numClients= */ 1); int flag = UNSUPPORTED_CONFIG_FLAG; @@ -559,6 +713,18 @@ public final class TunerSessionHidlTest extends ExtendedRadioMockitoTestCase { } @Test + public void setConfigFlag_forNonCurrentUser_doesNotSetConfigFlag() throws Exception { + openAidlClients(/* numClients= */ 1); + int flag = UNSUPPORTED_CONFIG_FLAG + 1; + doReturn(false).when(() -> RadioServiceUserController.isCurrentOrSystemUser()); + + mTunerSessions[0].setConfigFlag(flag, /* value= */ true); + + verify(mHalTunerSessionMock, never()).setConfigFlag(flag, /* value= */ true); + } + + + @Test public void isConfigFlagSet_withUnsupportedFlag_throwsRuntimeException() throws Exception { openAidlClients(/* numClients= */ 1); @@ -568,7 +734,7 @@ public final class TunerSessionHidlTest extends ExtendedRadioMockitoTestCase { mTunerSessions[0].isConfigFlagSet(flag); }); - assertWithMessage("Exception for check if unsupported flag %s is set", flag) + assertWithMessage("Exception for checking if unsupported flag %s is set", flag) .that(thrown).hasMessageThat().contains("isConfigFlagSet: NOT_SUPPORTED"); } @@ -586,6 +752,20 @@ public final class TunerSessionHidlTest extends ExtendedRadioMockitoTestCase { } @Test + public void isConfigFlagSet_whenHalThrowsRemoteException_fails() throws Exception { + openAidlClients(/* numClients= */ 1); + int flag = UNSUPPORTED_CONFIG_FLAG + 1; + doThrow(new RemoteException()).when(mHalTunerSessionMock).isConfigFlagSet(anyInt(), any()); + + RuntimeException thrown = assertThrows(RuntimeException.class, () -> { + mTunerSessions[0].isConfigFlagSet(flag); + }); + + assertWithMessage("Exception for checking config flag when HAL throws remote exception") + .that(thrown).hasMessageThat().contains("Failed to check flag"); + } + + @Test public void setParameters_withMockParameters() throws Exception { openAidlClients(/* numClients= */ 1); Map<String, String> parametersSet = Map.of("mockParam1", "mockValue1", @@ -597,6 +777,35 @@ public final class TunerSessionHidlTest extends ExtendedRadioMockitoTestCase { } @Test + public void setParameters_forNonCurrentUser_doesNotSetParameters() throws Exception { + openAidlClients(/* numClients= */ 1); + Map<String, String> parametersSet = Map.of("mockParam1", "mockValue1", + "mockParam2", "mockValue2"); + doReturn(false).when(() -> RadioServiceUserController.isCurrentOrSystemUser()); + + mTunerSessions[0].setParameters(parametersSet); + + verify(mHalTunerSessionMock, never()).setParameters(any()); + } + + @Test + public void setParameters_whenHalThrowsRemoteException_fails() throws Exception { + openAidlClients(/* numClients= */ 1); + Map<String, String> parametersSet = Map.of("mockParam1", "mockValue1", + "mockParam2", "mockValue2"); + String exceptionMessage = "HAL service died."; + when(mHalTunerSessionMock.setParameters(any())) + .thenThrow(new RemoteException(exceptionMessage)); + + RuntimeException thrown = assertThrows(RuntimeException.class, () -> { + mTunerSessions[0].setParameters(parametersSet); + }); + + assertWithMessage("Exception for setting parameters when HAL throws remote exception") + .that(thrown).hasMessageThat().contains(exceptionMessage); + } + + @Test public void getParameters_withMockKeys() throws Exception { openAidlClients(/* numClients= */ 1); ArrayList<String> parameterKeys = new ArrayList<>(Arrays.asList("mockKey1", "mockKey2")); @@ -607,6 +816,22 @@ public final class TunerSessionHidlTest extends ExtendedRadioMockitoTestCase { } @Test + public void getParameters_whenServiceThrowsRemoteException_fails() throws Exception { + openAidlClients(/* numClients= */ 1); + List<String> parameterKeys = List.of("mockKey1", "mockKey2"); + String exceptionMessage = "HAL service died."; + when(mHalTunerSessionMock.getParameters(any())) + .thenThrow(new RemoteException(exceptionMessage)); + + RuntimeException thrown = assertThrows(RuntimeException.class, () -> { + mTunerSessions[0].getParameters(parameterKeys); + }); + + assertWithMessage("Exception for getting parameters when HAL throws remote exception") + .that(thrown).hasMessageThat().contains(exceptionMessage); + } + + @Test public void onConfigFlagUpdated_forTunerCallback() throws Exception { int numSessions = 3; openAidlClients(numSessions); 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/BackupRestoreConfirmation/res/values-b+sr+Latn/strings.xml b/packages/BackupRestoreConfirmation/res/values-b+sr+Latn/strings.xml index e7bdd2f84bc8..ab551201771c 100644 --- a/packages/BackupRestoreConfirmation/res/values-b+sr+Latn/strings.xml +++ b/packages/BackupRestoreConfirmation/res/values-b+sr+Latn/strings.xml @@ -16,23 +16,23 @@ <resources xmlns:android="http://schemas.android.com/apk/res/android" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> - <string name="backup_confirm_title" msgid="827563724209303345">"Резервна копије свих података"</string> - <string name="restore_confirm_title" msgid="5469365809567486602">"Потпуно враћање"</string> - <string name="backup_confirm_text" msgid="1878021282758896593">"Захтевана је потпуна резервна копија свих података на повезани рачунар. Да ли желите да дозволите то?\n\nАко нисте лично захтевали резервну копију, не дозвољавајте наставак радње."</string> - <string name="allow_backup_button_label" msgid="4217228747769644068">"Направи резервну копију мојих података"</string> - <string name="deny_backup_button_label" msgid="6009119115581097708">"Не прави резервне копије"</string> - <string name="restore_confirm_text" msgid="7499866728030461776">"Захтевано је потпуно враћање свих података са повезаног рачунара. Да ли желите да дозволите то?\n\nАко нисте лично захтевали враћање, не дозвољавајте наставак радње. Тиме ћете заменити све податке који су тренутно на уређају!"</string> - <string name="allow_restore_button_label" msgid="3081286752277127827">"Врати моје податке"</string> - <string name="deny_restore_button_label" msgid="1724367334453104378">"Не враћај"</string> - <string name="current_password_text" msgid="8268189555578298067">"Унесите тренутну лозинку резервне копије у наставку:"</string> - <string name="device_encryption_restore_text" msgid="1570864916855208992">"Унесите лозинку уређаја за шифровање у наставку."</string> - <string name="device_encryption_backup_text" msgid="5866590762672844664">"Унесите лозинку уређаја за шифровање. Ово ће се користити и за шифровање резервне архиве."</string> - <string name="backup_enc_password_text" msgid="4981585714795233099">"Унесите лозинку коју ћете користити за шифровање података потпуне резервне копије. Ако то поље оставите празно, користиће се тренутна лозинка резервне копије:"</string> - <string name="backup_enc_password_optional" msgid="1350137345907579306">"Ако желите да шифрујете податке потпуне резервне копије, унесите лозинку у наставку."</string> - <string name="restore_enc_password_text" msgid="6140898525580710823">"Ако су подаци за враћање шифровани, унесите лозинку у наставку:"</string> - <string name="toast_backup_started" msgid="550354281452756121">"Покретање прављења резервне копије..."</string> - <string name="toast_backup_ended" msgid="3818080769548726424">"Резервна копија је направљена"</string> - <string name="toast_restore_started" msgid="7881679218971277385">"Покретање враћања..."</string> - <string name="toast_restore_ended" msgid="1764041639199696132">"Враћање је завршено"</string> - <string name="toast_timeout" msgid="5276598587087626877">"Време за радњу је истекло"</string> + <string name="backup_confirm_title" msgid="827563724209303345">"Rezervna kopije svih podataka"</string> + <string name="restore_confirm_title" msgid="5469365809567486602">"Potpuno vraćanje"</string> + <string name="backup_confirm_text" msgid="1878021282758896593">"Zahtevana je potpuna rezervna kopija svih podataka na povezani računar. Da li želite da dozvolite to?\n\nAko niste lično zahtevali rezervnu kopiju, ne dozvoljavajte nastavak radnje."</string> + <string name="allow_backup_button_label" msgid="4217228747769644068">"Napravi rezervnu kopiju mojih podataka"</string> + <string name="deny_backup_button_label" msgid="6009119115581097708">"Ne pravi rezervne kopije"</string> + <string name="restore_confirm_text" msgid="7499866728030461776">"Zahtevano je potpuno vraćanje svih podataka sa povezanog računara. Da li želite da dozvolite to?\n\nAko niste lično zahtevali vraćanje, ne dozvoljavajte nastavak radnje. Time ćete zameniti sve podatke koji su trenutno na uređaju!"</string> + <string name="allow_restore_button_label" msgid="3081286752277127827">"Vrati moje podatke"</string> + <string name="deny_restore_button_label" msgid="1724367334453104378">"Ne vraćaj"</string> + <string name="current_password_text" msgid="8268189555578298067">"Unesite trenutnu lozinku rezervne kopije u nastavku:"</string> + <string name="device_encryption_restore_text" msgid="1570864916855208992">"Unesite lozinku uređaja za šifrovanje u nastavku."</string> + <string name="device_encryption_backup_text" msgid="5866590762672844664">"Unesite lozinku uređaja za šifrovanje. Ovo će se koristiti i za šifrovanje rezervne arhive."</string> + <string name="backup_enc_password_text" msgid="4981585714795233099">"Unesite lozinku koju ćete koristiti za šifrovanje podataka potpune rezervne kopije. Ako to polje ostavite prazno, koristiće se trenutna lozinka rezervne kopije:"</string> + <string name="backup_enc_password_optional" msgid="1350137345907579306">"Ako želite da šifrujete podatke potpune rezervne kopije, unesite lozinku u nastavku."</string> + <string name="restore_enc_password_text" msgid="6140898525580710823">"Ako su podaci za vraćanje šifrovani, unesite lozinku u nastavku:"</string> + <string name="toast_backup_started" msgid="550354281452756121">"Pokretanje pravljenja rezervne kopije..."</string> + <string name="toast_backup_ended" msgid="3818080769548726424">"Rezervna kopija je napravljena"</string> + <string name="toast_restore_started" msgid="7881679218971277385">"Pokretanje vraćanja..."</string> + <string name="toast_restore_ended" msgid="1764041639199696132">"Vraćanje je završeno"</string> + <string name="toast_timeout" msgid="5276598587087626877">"Vreme za radnju je isteklo"</string> </resources> diff --git a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt index 48aebec606d0..93f566ce783b 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt @@ -230,9 +230,15 @@ class CreateFlowUtils { // TODO: get from the actual service info val packageManager = context.packageManager return providerDataList?.map { + val componentName = ComponentName.unflattenFromString(it.providerFlattenedComponentName) + var packageName = componentName?.packageName + if (componentName == null) { + // TODO: Remove once test data is fixed + packageName = it.providerFlattenedComponentName + } val pkgInfo = packageManager - .getPackageInfo(it.providerFlattenedComponentName, - PackageManager.PackageInfoFlags.of(0)) + .getPackageInfo(packageName, + PackageManager.PackageInfoFlags.of(0)) DisabledProviderInfo( icon = pkgInfo.applicationInfo.loadIcon(packageManager)!!, name = it.providerFlattenedComponentName, 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/packages/VpnDialogs/res/values-b+sr+Latn/strings.xml b/packages/VpnDialogs/res/values-b+sr+Latn/strings.xml index eaa0aeffcb3e..3270744783e6 100644 --- a/packages/VpnDialogs/res/values-b+sr+Latn/strings.xml +++ b/packages/VpnDialogs/res/values-b+sr+Latn/strings.xml @@ -16,24 +16,24 @@ <resources xmlns:android="http://schemas.android.com/apk/res/android" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> - <string name="prompt" msgid="3183836924226407828">"Захтев за повезивање"</string> - <string name="warning" msgid="809658604548412033">"<xliff:g id="APP">%s</xliff:g> жели да подеси VPN везу која омогућава праћење саобраћаја на мрежи. Прихватите само ако верујете извору. <br /> <br /> <img src=vpn_icon /> се приказује у врху екрана када је VPN активан."</string> - <string name="warning" product="tv" msgid="5188957997628124947">"Апликација <xliff:g id="APP">%s</xliff:g> жели да подеси VPN везу која јој омогућава да прати мрежни саобраћај. Прихватите ово само ако имате поверења у извор. <br /> <br /> <img src=vpn_icon /> се приказује на екрану када је VPN активан."</string> - <string name="legacy_title" msgid="192936250066580964">"VPN је повезан"</string> - <string name="session" msgid="6470628549473641030">"Сесија:"</string> - <string name="duration" msgid="3584782459928719435">"Трајање:"</string> - <string name="data_transmitted" msgid="7988167672982199061">"Послато:"</string> - <string name="data_received" msgid="4062776929376067820">"Примљенo:"</string> - <string name="data_value_format" msgid="2192466557826897580">"<xliff:g id="NUMBER_0">%1$s</xliff:g> бајт(ов)а / <xliff:g id="NUMBER_1">%2$s</xliff:g> пакета"</string> - <string name="always_on_disconnected_title" msgid="1906740176262776166">"Повезивање са увек укљученим VPN-ом није успело"</string> - <string name="always_on_disconnected_message" msgid="555634519845992917">"Мрежа <xliff:g id="VPN_APP_0">%1$s</xliff:g> је подешена да буде увек повезана, али тренутно не може да успостави везу. Телефон ће користити јавну мрежу док се поново не повеже са <xliff:g id="VPN_APP_1">%1$s</xliff:g>."</string> - <string name="always_on_disconnected_message_lockdown" msgid="4232225539869452120">"Мрежа <xliff:g id="VPN_APP">%1$s</xliff:g> је подешена да буде увек повезана, али тренутно не може да успостави везу. Нећете имати везу док се VPN поново не повеже."</string> + <string name="prompt" msgid="3183836924226407828">"Zahtev za povezivanje"</string> + <string name="warning" msgid="809658604548412033">"<xliff:g id="APP">%s</xliff:g> želi da podesi VPN vezu koja omogućava praćenje saobraćaja na mreži. Prihvatite samo ako verujete izvoru. <br /> <br /> <img src=vpn_icon /> se prikazuje u vrhu ekrana kada je VPN aktivan."</string> + <string name="warning" product="tv" msgid="5188957997628124947">"Aplikacija <xliff:g id="APP">%s</xliff:g> želi da podesi VPN vezu koja joj omogućava da prati mrežni saobraćaj. Prihvatite ovo samo ako imate poverenja u izvor. <br /> <br /> <img src=vpn_icon /> se prikazuje na ekranu kada je VPN aktivan."</string> + <string name="legacy_title" msgid="192936250066580964">"VPN je povezan"</string> + <string name="session" msgid="6470628549473641030">"Sesija:"</string> + <string name="duration" msgid="3584782459928719435">"Trajanje:"</string> + <string name="data_transmitted" msgid="7988167672982199061">"Poslato:"</string> + <string name="data_received" msgid="4062776929376067820">"Primljeno:"</string> + <string name="data_value_format" msgid="2192466557826897580">"<xliff:g id="NUMBER_0">%1$s</xliff:g> bajt(ov)a / <xliff:g id="NUMBER_1">%2$s</xliff:g> paketa"</string> + <string name="always_on_disconnected_title" msgid="1906740176262776166">"Povezivanje sa uvek uključenim VPN-om nije uspelo"</string> + <string name="always_on_disconnected_message" msgid="555634519845992917">"Mreža <xliff:g id="VPN_APP_0">%1$s</xliff:g> je podešena da bude uvek povezana, ali trenutno ne može da uspostavi vezu. Telefon će koristiti javnu mrežu dok se ponovo ne poveže sa <xliff:g id="VPN_APP_1">%1$s</xliff:g>."</string> + <string name="always_on_disconnected_message_lockdown" msgid="4232225539869452120">"Mreža <xliff:g id="VPN_APP">%1$s</xliff:g> je podešena da bude uvek povezana, ali trenutno ne može da uspostavi vezu. Nećete imati vezu dok se VPN ponovo ne poveže."</string> <string name="always_on_disconnected_message_separator" msgid="3310614409322581371">" "</string> - <string name="always_on_disconnected_message_settings_link" msgid="6172280302829992412">"Промени подешавања VPN-а"</string> - <string name="configure" msgid="4905518375574791375">"Конфигуриши"</string> - <string name="disconnect" msgid="971412338304200056">"Прекини везу"</string> - <string name="open_app" msgid="3717639178595958667">"Отвори апликацију"</string> - <string name="dismiss" msgid="6192859333764711227">"Одбаци"</string> + <string name="always_on_disconnected_message_settings_link" msgid="6172280302829992412">"Promeni podešavanja VPN-a"</string> + <string name="configure" msgid="4905518375574791375">"Konfiguriši"</string> + <string name="disconnect" msgid="971412338304200056">"Prekini vezu"</string> + <string name="open_app" msgid="3717639178595958667">"Otvori aplikaciju"</string> + <string name="dismiss" msgid="6192859333764711227">"Odbaci"</string> <string name="sanitized_vpn_label_with_ellipsis" msgid="7014327474633422235">"<xliff:g id="SANITIZED_VPN_LABEL_WITH_ELLIPSIS_0">%1$s</xliff:g>… ( <xliff:g id="SANITIZED_VPN_LABEL_WITH_ELLIPSIS_1">%2$s</xliff:g>)"</string> <string name="sanitized_vpn_label" msgid="1877415015009794766">"<xliff:g id="SANITIZED_VPN_LABEL_0">%1$s</xliff:g> ( <xliff:g id="SANITIZED_VPN_LABEL_1">%2$s</xliff:g>)"</string> </resources> 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/app/GameManagerShellCommand.java b/services/core/java/com/android/server/app/GameManagerShellCommand.java index abab0e7ae3b9..00ff489ee0ff 100644 --- a/services/core/java/com/android/server/app/GameManagerShellCommand.java +++ b/services/core/java/com/android/server/app/GameManagerShellCommand.java @@ -93,14 +93,17 @@ public class GameManagerShellCommand extends ShellCommand { private int runListGameModes(PrintWriter pw) throws ServiceNotFoundException, RemoteException { final String packageName = getNextArgRequired(); + final int userId = ActivityManager.getCurrentUser(); final GameManagerService gameManagerService = (GameManagerService) ServiceManager.getService(Context.GAME_SERVICE); + final String currentMode = gameModeIntToString( + gameManagerService.getGameMode(packageName, userId)); final StringJoiner sj = new StringJoiner(","); - for (int mode : gameManagerService.getAvailableGameModes(packageName, - ActivityManager.getCurrentUser())) { + for (int mode : gameManagerService.getAvailableGameModes(packageName, userId)) { sj.add(gameModeIntToString(mode)); } - pw.println(packageName + " has available game modes: [" + sj + "]"); + pw.println(packageName + " current mode: " + currentMode + ", available game modes: [" + sj + + "]"); return 0; } @@ -360,7 +363,7 @@ public class GameManagerShellCommand extends ShellCommand { pw.println(" list-configs <PACKAGE_NAME>"); pw.println(" Lists the current intervention configs of an app."); pw.println(" list-modes <PACKAGE_NAME>"); - pw.println(" Lists the current available game modes of an app."); + pw.println(" Lists the current selected and available game modes of an app."); pw.println(" mode [--user <USER_ID>] [1|2|3|4|standard|performance|battery|custom] " + "<PACKAGE_NAME>"); pw.println(" Set app to run in the specified game mode, if supported."); 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/biometrics/sensors/InternalCleanupClient.java b/services/core/java/com/android/server/biometrics/sensors/InternalCleanupClient.java index ded9c8de4dba..cdf22aadbd8c 100644 --- a/services/core/java/com/android/server/biometrics/sensors/InternalCleanupClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/InternalCleanupClient.java @@ -113,7 +113,11 @@ public abstract class InternalCleanupClient<S extends BiometricAuthenticator.Ide @Override public void onClientFinished(@NonNull BaseClientMonitor clientMonitor, boolean success) { Slog.d(TAG, "Remove onClientFinished: " + clientMonitor + ", success: " + success); - mCallback.onClientFinished(InternalCleanupClient.this, success); + if (mUnknownHALTemplates.isEmpty()) { + mCallback.onClientFinished(InternalCleanupClient.this, success); + } else { + startCleanupUnknownHalTemplates(); + } } }; @@ -237,4 +241,9 @@ public abstract class InternalCleanupClient<S extends BiometricAuthenticator.Ide public RemovalClient<S, T> getCurrentRemoveClient() { return (RemovalClient<S, T>) mCurrentTask; } + + @VisibleForTesting + public ArrayList<UserTemplate> getUnknownHALTemplates() { + return mUnknownHALTemplates; + } } diff --git a/services/core/java/com/android/server/broadcastradio/hal2/AnnouncementAggregator.java b/services/core/java/com/android/server/broadcastradio/hal2/AnnouncementAggregator.java index 42e296f3e4ec..85c13aee37c9 100644 --- a/services/core/java/com/android/server/broadcastradio/hal2/AnnouncementAggregator.java +++ b/services/core/java/com/android/server/broadcastradio/hal2/AnnouncementAggregator.java @@ -101,7 +101,10 @@ public class AnnouncementAggregator extends ICloseHandle.Stub { public void watchModule(@NonNull RadioModule module, @NonNull int[] enabledTypes) { synchronized (mLock) { - if (mIsClosed) throw new IllegalStateException(); + if (mIsClosed) { + throw new IllegalStateException("Failed to watch module" + + "since announcement aggregator has already been closed"); + } ModuleWatcher watcher = new ModuleWatcher(); ICloseHandle closeHandle; diff --git a/services/core/java/com/android/server/broadcastradio/hal2/Convert.java b/services/core/java/com/android/server/broadcastradio/hal2/Convert.java index 98a450fa45e4..e6908b107d31 100644 --- a/services/core/java/com/android/server/broadcastradio/hal2/Convert.java +++ b/services/core/java/com/android/server/broadcastradio/hal2/Convert.java @@ -61,21 +61,20 @@ class Convert { } static void throwOnError(String action, int result) { + String errorString = action + ": " + Result.toString(result); switch (result) { case Result.OK: return; case Result.UNKNOWN_ERROR: - throw new ParcelableException(new RuntimeException(action + ": UNKNOWN_ERROR")); case Result.INTERNAL_ERROR: - throw new ParcelableException(new RuntimeException(action + ": INTERNAL_ERROR")); + case Result.TIMEOUT: + throw new ParcelableException(new RuntimeException(errorString)); case Result.INVALID_ARGUMENTS: - throw new IllegalArgumentException(action + ": INVALID_ARGUMENTS"); + throw new IllegalArgumentException(errorString); case Result.INVALID_STATE: - throw new IllegalStateException(action + ": INVALID_STATE"); + throw new IllegalStateException(errorString); case Result.NOT_SUPPORTED: - throw new UnsupportedOperationException(action + ": NOT_SUPPORTED"); - case Result.TIMEOUT: - throw new ParcelableException(new RuntimeException(action + ": TIMEOUT")); + throw new UnsupportedOperationException(errorString); default: throw new ParcelableException(new RuntimeException( action + ": unknown error (" + result + ")")); 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/credentials/java/com/android/server/credentials/CredentialManagerService.java b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java index d29f86e57248..9b2d87644123 100644 --- a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java +++ b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java @@ -247,6 +247,8 @@ public final class CredentialManagerService return cancelTransport; } + @SuppressWarnings("GuardedBy") // ErrorProne requires listEnabledProviders + // to be guarded by 'service.mLock', which is the same as mLock. @Override public ICancellationSignal listEnabledProviders(IListEnabledProvidersCallback callback) { Log.i(TAG, "listEnabledProviders"); @@ -256,7 +258,7 @@ public final class CredentialManagerService runForUser( (service) -> { enabledProviders.add( - service.getServiceInfo().getComponentName().flattenToString()); + service.getComponentName().flattenToString()); }); // Call the callback. diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java b/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java index c03d505bc90f..183f7437fa90 100644 --- a/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java +++ b/services/credentials/java/com/android/server/credentials/CredentialManagerServiceImpl.java @@ -53,6 +53,11 @@ public final class CredentialManagerServiceImpl extends } } + @GuardedBy("mLock") + public ComponentName getComponentName() { + return mInfo.getServiceInfo().getComponentName(); + } + @Override // from PerUserSystemService @GuardedBy("mLock") protected ServiceInfo newServiceInfoLocked(@NonNull ComponentName serviceComponent) 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/biometrics/sensors/fingerprint/aidl/FingerprintInternalCleanupClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalCleanupClientTest.java index 282c782e752a..f0d8616ece9f 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalCleanupClientTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintInternalCleanupClientTest.java @@ -137,6 +137,32 @@ public class FingerprintInternalCleanupClientTest { verify(mCallback).onClientFinished(eq(mClient), eq(true)); } + @Test + public void cleanupUnknownHalTemplatesAfterEnumerationWhenVirtualIsDisabled() { + mClient = createClient(); + + final List<Fingerprint> templates = List.of( + new Fingerprint("one", 1, 1), + new Fingerprint("two", 2, 1), + new Fingerprint("three", 3, 1) + ); + mClient.start(mCallback); + for (int i = templates.size() - 1; i >= 0; i--) { + mClient.getCurrentEnumerateClient().onEnumerationResult(templates.get(i), i); + } + // The first template is removed after enumeration + assertThat(mClient.getUnknownHALTemplates().size()).isEqualTo(2); + + // Simulate finishing the removal of the first template. + // |remaining| is 0 because one FingerprintRemovalClient is associated with only one + // biometrics ID. + mClient.getCurrentRemoveClient().onRemoved(templates.get(0), 0); + assertThat(mClient.getUnknownHALTemplates().size()).isEqualTo(1); + // Simulate finishing the removal of the second template. + mClient.getCurrentRemoveClient().onRemoved(templates.get(1), 0); + assertThat(mClient.getUnknownHALTemplates()).isEmpty(); + } + protected FingerprintInternalCleanupClient createClient() { final List<Fingerprint> enrollments = new ArrayList<>(); final Map<Integer, Long> authenticatorIds = new HashMap<>(); 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; } """ |