diff options
158 files changed, 3924 insertions, 1292 deletions
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java index d06596fa18aa..3fe83a64b5ec 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java @@ -382,6 +382,16 @@ public class JobSchedulerService extends com.android.server.SystemService * A mapping of which uids are currently in the foreground to their effective bias. */ final SparseIntArray mUidBiasOverride = new SparseIntArray(); + /** + * A cached mapping of uids to their current capabilities. + */ + @GuardedBy("mLock") + private final SparseIntArray mUidCapabilities = new SparseIntArray(); + /** + * A cached mapping of uids to their proc states. + */ + @GuardedBy("mLock") + private final SparseIntArray mUidProcStates = new SparseIntArray(); /** * Which uids are currently performing backups, so we shouldn't allow their jobs to run. @@ -1158,6 +1168,14 @@ public class JobSchedulerService extends com.android.server.SystemService mDebuggableApps.remove(pkgName); mConcurrencyManager.onAppRemovedLocked(pkgName, pkgUid); } + } else if (Intent.ACTION_UID_REMOVED.equals(action)) { + if (!intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) { + synchronized (mLock) { + mUidBiasOverride.delete(pkgUid); + mUidCapabilities.delete(pkgUid); + mUidProcStates.delete(pkgUid); + } + } } else if (Intent.ACTION_USER_ADDED.equals(action)) { final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0); synchronized (mLock) { @@ -1236,7 +1254,11 @@ public class JobSchedulerService extends com.android.server.SystemService final private IUidObserver mUidObserver = new IUidObserver.Stub() { @Override public void onUidStateChanged(int uid, int procState, long procStateSeq, int capability) { - mHandler.obtainMessage(MSG_UID_STATE_CHANGED, uid, procState).sendToTarget(); + final SomeArgs args = SomeArgs.obtain(); + args.argi1 = uid; + args.argi2 = procState; + args.argi3 = capability; + mHandler.obtainMessage(MSG_UID_STATE_CHANGED, args).sendToTarget(); } @Override public void onUidGone(int uid, boolean disabled) { @@ -1978,8 +2000,14 @@ public class JobSchedulerService extends com.android.server.SystemService } } - void updateUidState(int uid, int procState) { + void updateUidState(int uid, int procState, int capabilities) { + if (DEBUG) { + Slog.d(TAG, "UID " + uid + " proc state changed to " + + ActivityManager.procStateToString(procState) + + " with capabilities=" + ActivityManager.getCapabilitiesSummary(capabilities)); + } synchronized (mLock) { + mUidProcStates.put(uid, procState); final int prevBias = mUidBiasOverride.get(uid, JobInfo.BIAS_DEFAULT); if (procState == ActivityManager.PROCESS_STATE_TOP) { // Only use this if we are exactly the top app. All others can live @@ -1993,6 +2021,12 @@ public class JobSchedulerService extends com.android.server.SystemService } else { mUidBiasOverride.delete(uid); } + if (capabilities == ActivityManager.PROCESS_CAPABILITY_NONE + || procState == ActivityManager.PROCESS_STATE_NONEXISTENT) { + mUidCapabilities.delete(uid); + } else { + mUidCapabilities.put(uid, capabilities); + } final int newBias = mUidBiasOverride.get(uid, JobInfo.BIAS_DEFAULT); if (prevBias != newBias) { if (DEBUG) { @@ -2013,6 +2047,23 @@ public class JobSchedulerService extends com.android.server.SystemService } } + /** + * Return the current {@link ActivityManager#PROCESS_CAPABILITY_ALL capabilities} + * of the given UID. + */ + public int getUidCapabilities(int uid) { + synchronized (mLock) { + return mUidCapabilities.get(uid, ActivityManager.PROCESS_CAPABILITY_NONE); + } + } + + /** Return the current proc state of the given UID. */ + public int getUidProcState(int uid) { + synchronized (mLock) { + return mUidProcStates.get(uid, ActivityManager.PROCESS_STATE_UNKNOWN); + } + } + @Override public void onDeviceIdleStateChanged(boolean deviceIdle) { synchronized (mLock) { @@ -2276,6 +2327,9 @@ public class JobSchedulerService extends com.android.server.SystemService filter.addDataScheme("package"); getContext().registerReceiverAsUser( mBroadcastReceiver, UserHandle.ALL, filter, null, null); + final IntentFilter uidFilter = new IntentFilter(Intent.ACTION_UID_REMOVED); + getContext().registerReceiverAsUser( + mBroadcastReceiver, UserHandle.ALL, uidFilter, null, null); final IntentFilter userFilter = new IntentFilter(Intent.ACTION_USER_REMOVED); userFilter.addAction(Intent.ACTION_USER_ADDED); getContext().registerReceiverAsUser( @@ -2807,15 +2861,19 @@ public class JobSchedulerService extends com.android.server.SystemService break; case MSG_UID_STATE_CHANGED: { - final int uid = message.arg1; - final int procState = message.arg2; - updateUidState(uid, procState); + final SomeArgs args = (SomeArgs) message.obj; + final int uid = args.argi1; + final int procState = args.argi2; + final int capabilities = args.argi3; + updateUidState(uid, procState, capabilities); + args.recycle(); break; } case MSG_UID_GONE: { final int uid = message.arg1; final boolean disabled = message.arg2 != 0; - updateUidState(uid, ActivityManager.PROCESS_STATE_CACHED_EMPTY); + updateUidState(uid, ActivityManager.PROCESS_STATE_CACHED_EMPTY, + ActivityManager.PROCESS_CAPABILITY_NONE); if (disabled) { cancelJobsForUid(uid, /* includeSourceApp */ true, @@ -4891,6 +4949,25 @@ public class JobSchedulerService extends com.android.server.SystemService pw.decreaseIndent(); } + boolean procStatePrinted = false; + for (int i = 0; i < mUidProcStates.size(); i++) { + int uid = mUidProcStates.keyAt(i); + if (filterAppId == -1 || filterAppId == UserHandle.getAppId(uid)) { + if (!procStatePrinted) { + procStatePrinted = true; + pw.println(); + pw.println("Uid proc states:"); + pw.increaseIndent(); + } + pw.print(UserHandle.formatUid(uid)); + pw.print(": "); + pw.println(ActivityManager.procStateToString(mUidProcStates.valueAt(i))); + } + } + if (procStatePrinted) { + pw.decreaseIndent(); + } + boolean overridePrinted = false; for (int i = 0; i < mUidBiasOverride.size(); i++) { int uid = mUidBiasOverride.keyAt(i); @@ -4909,6 +4986,25 @@ public class JobSchedulerService extends com.android.server.SystemService pw.decreaseIndent(); } + boolean capabilitiesPrinted = false; + for (int i = 0; i < mUidCapabilities.size(); i++) { + int uid = mUidCapabilities.keyAt(i); + if (filterAppId == -1 || filterAppId == UserHandle.getAppId(uid)) { + if (!capabilitiesPrinted) { + capabilitiesPrinted = true; + pw.println(); + pw.println("Uid capabilities:"); + pw.increaseIndent(); + } + pw.print(UserHandle.formatUid(uid)); + pw.print(": "); + pw.println(ActivityManager.getCapabilitiesSummary(mUidCapabilities.valueAt(i))); + } + } + if (capabilitiesPrinted) { + pw.decreaseIndent(); + } + boolean uidMapPrinted = false; for (int i = 0; i < mUidToPackageCache.size(); ++i) { final int uid = mUidToPackageCache.keyAt(i); diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java index 1e2ef7755664..8355e9c6da99 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java @@ -393,23 +393,27 @@ public final class JobServiceContext implements ServiceConnection { .setFlags(Intent.FLAG_FROM_BACKGROUND); boolean binding = false; try { - final int bindFlags; + final Context.BindServiceFlags bindFlags; if (job.shouldTreatAsUserInitiatedJob()) { - // TODO (191785864, 261999509): add an appropriate flag so user-initiated jobs - // can bypass data saver - bindFlags = Context.BIND_AUTO_CREATE - | Context.BIND_ALMOST_PERCEPTIBLE - | Context.BIND_BYPASS_POWER_NETWORK_RESTRICTIONS - | Context.BIND_NOT_APP_COMPONENT_USAGE; + bindFlags = Context.BindServiceFlags.of( + Context.BIND_AUTO_CREATE + | Context.BIND_ALMOST_PERCEPTIBLE + | Context.BIND_BYPASS_POWER_NETWORK_RESTRICTIONS + | Context.BIND_BYPASS_USER_NETWORK_RESTRICTIONS + | Context.BIND_NOT_APP_COMPONENT_USAGE); } else if (job.shouldTreatAsExpeditedJob()) { - bindFlags = Context.BIND_AUTO_CREATE | Context.BIND_NOT_FOREGROUND - | Context.BIND_ALMOST_PERCEPTIBLE - | Context.BIND_BYPASS_POWER_NETWORK_RESTRICTIONS - | Context.BIND_NOT_APP_COMPONENT_USAGE; + bindFlags = Context.BindServiceFlags.of( + Context.BIND_AUTO_CREATE + | Context.BIND_NOT_FOREGROUND + | Context.BIND_ALMOST_PERCEPTIBLE + | Context.BIND_BYPASS_POWER_NETWORK_RESTRICTIONS + | Context.BIND_NOT_APP_COMPONENT_USAGE); } else { - bindFlags = Context.BIND_AUTO_CREATE | Context.BIND_NOT_FOREGROUND - | Context.BIND_NOT_PERCEPTIBLE - | Context.BIND_NOT_APP_COMPONENT_USAGE; + bindFlags = Context.BindServiceFlags.of( + Context.BIND_AUTO_CREATE + | Context.BIND_NOT_FOREGROUND + | Context.BIND_NOT_PERCEPTIBLE + | Context.BIND_NOT_APP_COMPONENT_USAGE); } binding = mContext.bindServiceAsUser(intent, this, bindFlags, UserHandle.of(job.getUserId())); diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java index 4c55dac626d5..5246f2bf838b 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java @@ -188,7 +188,7 @@ public final class BatteryController extends RestrictingController { mLastReportedStatsdStablePower = stablePower; } if (mLastReportedStatsdBatteryNotLow == null - || mLastReportedStatsdBatteryNotLow != stablePower) { + || mLastReportedStatsdBatteryNotLow != batteryNotLow) { logDeviceWideConstraintStateToStatsd(JobStatus.CONSTRAINT_BATTERY_NOT_LOW, batteryNotLow); mLastReportedStatsdBatteryNotLow = batteryNotLow; diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java index 3859d89c22cd..f6bdb9303a04 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/ConnectivityController.java @@ -18,15 +18,18 @@ package com.android.server.job.controllers; import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED; import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED; import static com.android.server.job.JobSchedulerService.RESTRICTED_INDEX; import static com.android.server.job.JobSchedulerService.sElapsedRealtimeClock; import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.ActivityManager; import android.app.job.JobInfo; import android.net.ConnectivityManager; import android.net.ConnectivityManager.NetworkCallback; +import android.net.INetworkPolicyListener; import android.net.Network; import android.net.NetworkCapabilities; import android.net.NetworkPolicyManager; @@ -47,6 +50,7 @@ import android.util.Log; import android.util.Pools; import android.util.Slog; import android.util.SparseArray; +import android.util.SparseBooleanArray; import android.util.TimeUtils; import android.util.proto.ProtoOutputStream; @@ -98,13 +102,12 @@ public final class ConnectivityController extends RestrictingController implemen ~(ConnectivityManager.BLOCKED_REASON_APP_STANDBY | ConnectivityManager.BLOCKED_REASON_BATTERY_SAVER | ConnectivityManager.BLOCKED_REASON_DOZE); - // TODO(261999509): allow bypassing data saver & user-restricted. However, when we allow a UI - // job to run while data saver restricts the app, we must ensure that we don't run regular - // jobs when we put a hole in the data saver wall for the UI job private static final int UNBYPASSABLE_UI_BLOCKED_REASONS = ~(ConnectivityManager.BLOCKED_REASON_APP_STANDBY | ConnectivityManager.BLOCKED_REASON_BATTERY_SAVER - | ConnectivityManager.BLOCKED_REASON_DOZE); + | ConnectivityManager.BLOCKED_REASON_DOZE + | ConnectivityManager.BLOCKED_METERED_REASON_DATA_SAVER + | ConnectivityManager.BLOCKED_METERED_REASON_USER_RESTRICTED); private static final int UNBYPASSABLE_FOREGROUND_BLOCKED_REASONS = ~(ConnectivityManager.BLOCKED_REASON_APP_STANDBY | ConnectivityManager.BLOCKED_REASON_BATTERY_SAVER @@ -113,6 +116,7 @@ public final class ConnectivityController extends RestrictingController implemen | ConnectivityManager.BLOCKED_METERED_REASON_USER_RESTRICTED); private final ConnectivityManager mConnManager; + private final NetworkPolicyManager mNetPolicyManager; private final NetworkPolicyManagerInternal mNetPolicyManagerInternal; private final FlexibilityController mFlexibilityController; @@ -241,6 +245,8 @@ public final class ConnectivityController extends RestrictingController implemen */ private final List<UidStats> mSortedStats = new ArrayList<>(); @GuardedBy("mLock") + private final SparseBooleanArray mBackgroundMeteredAllowed = new SparseBooleanArray(); + @GuardedBy("mLock") private long mLastCallbackAdjustmentTimeElapsed; @GuardedBy("mLock") private final SparseArray<CellSignalStrengthCallback> mSignalStrengths = new SparseArray<>(); @@ -250,6 +256,8 @@ public final class ConnectivityController extends RestrictingController implemen private static final int MSG_ADJUST_CALLBACKS = 0; private static final int MSG_UPDATE_ALL_TRACKED_JOBS = 1; + private static final int MSG_DATA_SAVER_TOGGLED = 2; + private static final int MSG_UID_POLICIES_CHANGED = 3; private final Handler mHandler; @@ -259,6 +267,7 @@ public final class ConnectivityController extends RestrictingController implemen mHandler = new CcHandler(AppSchedulingModuleThread.get().getLooper()); mConnManager = mContext.getSystemService(ConnectivityManager.class); + mNetPolicyManager = mContext.getSystemService(NetworkPolicyManager.class); mNetPolicyManagerInternal = LocalServices.getService(NetworkPolicyManagerInternal.class); mFlexibilityController = flexibilityController; @@ -266,6 +275,8 @@ public final class ConnectivityController extends RestrictingController implemen // network changes against the active network for each UID with jobs. final NetworkRequest request = new NetworkRequest.Builder().clearCapabilities().build(); mConnManager.registerNetworkCallback(request, mNetworkCallback); + + mNetPolicyManager.registerListener(mNetPolicyListener); } @GuardedBy("mLock") @@ -530,6 +541,7 @@ public final class ConnectivityController extends RestrictingController implemen // All packages in the UID have been removed. It's safe to remove things based on // UID alone. mTrackedJobs.delete(uid); + mBackgroundMeteredAllowed.delete(uid); UidStats uidStats = mUidStats.removeReturnOld(uid); unregisterDefaultNetworkCallbackLocked(uid, sElapsedRealtimeClock.millis()); mSortedStats.remove(uidStats); @@ -549,6 +561,12 @@ public final class ConnectivityController extends RestrictingController implemen mUidStats.removeAt(u); } } + for (int u = mBackgroundMeteredAllowed.size() - 1; u >= 0; --u) { + final int uid = mBackgroundMeteredAllowed.keyAt(u); + if (UserHandle.getUserId(uid) == userId) { + mBackgroundMeteredAllowed.removeAt(u); + } + } postAdjustCallbacks(); } @@ -666,6 +684,88 @@ public final class ConnectivityController extends RestrictingController implemen return false; } + private boolean isMeteredAllowed(@NonNull JobStatus jobStatus, + @NonNull NetworkCapabilities networkCapabilities) { + // Network isn't metered. Usage is allowed. The rest of this method doesn't apply. + if (networkCapabilities.hasCapability(NET_CAPABILITY_NOT_METERED) + || networkCapabilities.hasCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED)) { + return true; + } + + final int uid = jobStatus.getSourceUid(); + final int procState = mService.getUidProcState(uid); + final int capabilities = mService.getUidCapabilities(uid); + // Jobs don't raise the proc state to anything better than IMPORTANT_FOREGROUND. + // If the app is in a better state, see if it has the capability to use the metered network. + final boolean currentStateAllows = procState != ActivityManager.PROCESS_STATE_UNKNOWN + && procState < ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND + && NetworkPolicyManager.isProcStateAllowedWhileOnRestrictBackground( + procState, capabilities); + if (DEBUG) { + Slog.d(TAG, "UID " + uid + + " current state allows metered network=" + currentStateAllows + + " procState=" + ActivityManager.procStateToString(procState) + + " capabilities=" + ActivityManager.getCapabilitiesSummary(capabilities)); + } + if (currentStateAllows) { + return true; + } + + if ((jobStatus.getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) != 0) { + final int expectedProcState = ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE; + final int mergedCapabilities = capabilities + | NetworkPolicyManager.getDefaultProcessNetworkCapabilities(expectedProcState); + final boolean wouldBeAllowed = + NetworkPolicyManager.isProcStateAllowedWhileOnRestrictBackground( + expectedProcState, mergedCapabilities); + if (DEBUG) { + Slog.d(TAG, "UID " + uid + + " willBeForeground flag allows metered network=" + wouldBeAllowed + + " capabilities=" + + ActivityManager.getCapabilitiesSummary(mergedCapabilities)); + } + if (wouldBeAllowed) { + return true; + } + } + + if (jobStatus.shouldTreatAsUserInitiatedJob()) { + // Since the job is initiated by the user and will be visible to the user, it + // should be able to run on metered networks, similar to FGS. + // With user-initiated jobs, JobScheduler will request that the process + // run at IMPORTANT_FOREGROUND process state + // and get the USER_RESTRICTED_NETWORK process capability. + final int expectedProcState = ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND; + final int mergedCapabilities = capabilities + | ActivityManager.PROCESS_CAPABILITY_USER_RESTRICTED_NETWORK + | NetworkPolicyManager.getDefaultProcessNetworkCapabilities(expectedProcState); + final boolean wouldBeAllowed = + NetworkPolicyManager.isProcStateAllowedWhileOnRestrictBackground( + expectedProcState, mergedCapabilities); + if (DEBUG) { + Slog.d(TAG, "UID " + uid + + " UI job state allows metered network=" + wouldBeAllowed + + " capabilities=" + mergedCapabilities); + } + if (wouldBeAllowed) { + return true; + } + } + + if (mBackgroundMeteredAllowed.indexOfKey(uid) >= 0) { + return mBackgroundMeteredAllowed.get(uid); + } + + final boolean allowed = + mNetPolicyManager.getRestrictBackgroundStatus(uid) + != ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED; + if (DEBUG) { + Slog.d(TAG, "UID " + uid + " allowed in data saver=" + allowed); + } + mBackgroundMeteredAllowed.put(uid, allowed); + return allowed; + } + /** * Return the estimated amount of time this job will be transferring data, * based on the current network speed. @@ -859,6 +959,12 @@ public final class ConnectivityController extends RestrictingController implemen // First, are we insane? if (isInsane(jobStatus, network, capabilities, constants)) return false; + // User-initiated jobs might make NetworkPolicyManager open up network access for + // the whole UID. If network access is opened up just because of UI jobs, we want + // to make sure that non-UI jobs don't run during that time, + // so make sure the job can make use of the metered network at this time. + if (!isMeteredAllowed(jobStatus, capabilities)) return false; + // Second, is the network congested? if (isCongestionDelayed(jobStatus, network, capabilities, constants)) return false; @@ -1138,9 +1244,10 @@ public final class ConnectivityController extends RestrictingController implemen // but it doesn't yet satisfy the requested constraints and the old network // is still available and satisfies the constraints. Don't change the network // given to the job for now and let it keep running. We will re-evaluate when - // the capabilities or connection state of the either network change. + // the capabilities or connection state of either network change. if (DEBUG) { - Slog.i(TAG, "Not reassigning network for running job " + jobStatus); + Slog.i(TAG, "Not reassigning network from " + jobStatus.network + + " to " + network + " for running job " + jobStatus); } return false; } @@ -1389,6 +1496,26 @@ public final class ConnectivityController extends RestrictingController implemen } }; + private final INetworkPolicyListener mNetPolicyListener = new NetworkPolicyManager.Listener() { + @Override + public void onRestrictBackgroundChanged(boolean restrictBackground) { + if (DEBUG) { + Slog.v(TAG, "onRestrictBackgroundChanged: " + restrictBackground); + } + mHandler.obtainMessage(MSG_DATA_SAVER_TOGGLED).sendToTarget(); + } + + @Override + public void onUidPoliciesChanged(int uid, int uidPolicies) { + if (DEBUG) { + Slog.v(TAG, "onUidPoliciesChanged: " + uid); + } + mHandler.obtainMessage(MSG_UID_POLICIES_CHANGED, + uid, mNetPolicyManager.getRestrictBackgroundStatus(uid)) + .sendToTarget(); + } + }; + private class CcHandler extends Handler { CcHandler(Looper looper) { super(looper); @@ -1410,6 +1537,27 @@ public final class ConnectivityController extends RestrictingController implemen updateAllTrackedJobsLocked(allowThrottle); } break; + + case MSG_DATA_SAVER_TOGGLED: + removeMessages(MSG_DATA_SAVER_TOGGLED); + synchronized (mLock) { + mBackgroundMeteredAllowed.clear(); + updateTrackedJobsLocked(-1, null); + } + break; + + case MSG_UID_POLICIES_CHANGED: + final int uid = msg.arg1; + final boolean newAllowed = + msg.arg2 != ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED; + synchronized (mLock) { + final boolean oldAllowed = mBackgroundMeteredAllowed.get(uid); + if (oldAllowed != newAllowed) { + mBackgroundMeteredAllowed.put(uid, newAllowed); + updateTrackedJobsLocked(uid, null); + } + } + break; } } } diff --git a/core/api/current.txt b/core/api/current.txt index 45c9becdcfd4..80abd84733d7 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -64,8 +64,6 @@ package android { field public static final String BLUETOOTH_SCAN = "android.permission.BLUETOOTH_SCAN"; field public static final String BODY_SENSORS = "android.permission.BODY_SENSORS"; field public static final String BODY_SENSORS_BACKGROUND = "android.permission.BODY_SENSORS_BACKGROUND"; - field public static final String BODY_SENSORS_WRIST_TEMPERATURE = "android.permission.BODY_SENSORS_WRIST_TEMPERATURE"; - field public static final String BODY_SENSORS_WRIST_TEMPERATURE_BACKGROUND = "android.permission.BODY_SENSORS_WRIST_TEMPERATURE_BACKGROUND"; field public static final String BROADCAST_PACKAGE_REMOVED = "android.permission.BROADCAST_PACKAGE_REMOVED"; field public static final String BROADCAST_SMS = "android.permission.BROADCAST_SMS"; field public static final String BROADCAST_STICKY = "android.permission.BROADCAST_STICKY"; @@ -5024,7 +5022,6 @@ package android.app { field public static final String OPSTR_ADD_VOICEMAIL = "android:add_voicemail"; field public static final String OPSTR_ANSWER_PHONE_CALLS = "android:answer_phone_calls"; field public static final String OPSTR_BODY_SENSORS = "android:body_sensors"; - field public static final String OPSTR_BODY_SENSORS_WRIST_TEMPERATURE = "android:body_sensors_wrist_temperature"; field public static final String OPSTR_CALL_PHONE = "android:call_phone"; field public static final String OPSTR_CAMERA = "android:camera"; field public static final String OPSTR_COARSE_LOCATION = "android:coarse_location"; @@ -13029,7 +13026,7 @@ package android.content.pm { field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_CAMERA}, anyOf={android.Manifest.permission.CAMERA}, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_CAMERA = 64; // 0x40 field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE}, anyOf={android.Manifest.permission.BLUETOOTH_ADVERTISE, android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_SCAN, android.Manifest.permission.CHANGE_NETWORK_STATE, android.Manifest.permission.CHANGE_WIFI_STATE, android.Manifest.permission.CHANGE_WIFI_MULTICAST_STATE, android.Manifest.permission.NFC, android.Manifest.permission.TRANSMIT_IR, android.Manifest.permission.UWB_RANGING}, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE = 16; // 0x10 field @RequiresPermission(value=android.Manifest.permission.FOREGROUND_SERVICE_DATA_SYNC, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_DATA_SYNC = 1; // 0x1 - field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_HEALTH}, anyOf={android.Manifest.permission.ACTIVITY_RECOGNITION, android.Manifest.permission.BODY_SENSORS, android.Manifest.permission.BODY_SENSORS_WRIST_TEMPERATURE, android.Manifest.permission.HIGH_SAMPLING_RATE_SENSORS}) public static final int FOREGROUND_SERVICE_TYPE_HEALTH = 256; // 0x100 + field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_HEALTH}, anyOf={android.Manifest.permission.ACTIVITY_RECOGNITION, android.Manifest.permission.BODY_SENSORS, android.Manifest.permission.HIGH_SAMPLING_RATE_SENSORS}) public static final int FOREGROUND_SERVICE_TYPE_HEALTH = 256; // 0x100 field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_LOCATION}, anyOf={android.Manifest.permission.ACCESS_COARSE_LOCATION, android.Manifest.permission.ACCESS_FINE_LOCATION}, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_LOCATION = 8; // 0x8 field public static final int FOREGROUND_SERVICE_TYPE_MANIFEST = -1; // 0xffffffff field @RequiresPermission(value=android.Manifest.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK = 2; // 0x2 @@ -55066,7 +55063,6 @@ package android.view.autofill { public final class AutofillManager { method public void cancel(); - method public void clearAutofillRequestCallback(); method public void commit(); method public void disableAutofillServices(); method @Nullable public android.content.ComponentName getAutofillServiceComponentName(); @@ -55093,7 +55089,6 @@ package android.view.autofill { method public void registerCallback(@Nullable android.view.autofill.AutofillManager.AutofillCallback); method public void requestAutofill(@NonNull android.view.View); method public void requestAutofill(@NonNull android.view.View, int, @NonNull android.graphics.Rect); - method @RequiresPermission(android.Manifest.permission.PROVIDE_OWN_AUTOFILL_SUGGESTIONS) public void setAutofillRequestCallback(@NonNull java.util.concurrent.Executor, @NonNull android.view.autofill.AutofillRequestCallback); method public void setUserData(@Nullable android.service.autofill.UserData); method public boolean showAutofillDialog(@NonNull android.view.View); method public boolean showAutofillDialog(@NonNull android.view.View, int); @@ -55114,10 +55109,6 @@ package android.view.autofill { field public static final int EVENT_INPUT_UNAVAILABLE = 3; // 0x3 } - public interface AutofillRequestCallback { - method public void onFillRequest(@Nullable android.view.inputmethod.InlineSuggestionsRequest, @NonNull android.os.CancellationSignal, @NonNull android.service.autofill.FillCallback); - } - public final class AutofillValue implements android.os.Parcelable { method public int describeContents(); method public static android.view.autofill.AutofillValue forDate(long); @@ -55567,12 +55558,10 @@ package android.view.inputmethod { ctor public InlineSuggestionsRequest.Builder(@NonNull java.util.List<android.widget.inline.InlinePresentationSpec>); method @NonNull public android.view.inputmethod.InlineSuggestionsRequest.Builder addInlinePresentationSpecs(@NonNull android.widget.inline.InlinePresentationSpec); method @NonNull public android.view.inputmethod.InlineSuggestionsRequest build(); - method @NonNull public android.view.inputmethod.InlineSuggestionsRequest.Builder setClientSupported(boolean); method @NonNull public android.view.inputmethod.InlineSuggestionsRequest.Builder setExtras(@NonNull android.os.Bundle); method @NonNull public android.view.inputmethod.InlineSuggestionsRequest.Builder setInlinePresentationSpecs(@NonNull java.util.List<android.widget.inline.InlinePresentationSpec>); method @NonNull public android.view.inputmethod.InlineSuggestionsRequest.Builder setInlineTooltipPresentationSpec(@NonNull android.widget.inline.InlinePresentationSpec); method @NonNull public android.view.inputmethod.InlineSuggestionsRequest.Builder setMaxSuggestionCount(int); - method @NonNull public android.view.inputmethod.InlineSuggestionsRequest.Builder setServiceSupported(boolean); method @NonNull public android.view.inputmethod.InlineSuggestionsRequest.Builder setSupportedLocales(@NonNull android.os.LocaleList); } diff --git a/core/api/test-current.txt b/core/api/test-current.txt index ed36f1c3d99d..1346d0761b20 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -8,6 +8,8 @@ package android { field public static final String APPROVE_INCIDENT_REPORTS = "android.permission.APPROVE_INCIDENT_REPORTS"; field public static final String BACKGROUND_CAMERA = "android.permission.BACKGROUND_CAMERA"; field public static final String BIND_CELL_BROADCAST_SERVICE = "android.permission.BIND_CELL_BROADCAST_SERVICE"; + field public static final String BODY_SENSORS_WRIST_TEMPERATURE = "android.permission.BODY_SENSORS_WRIST_TEMPERATURE"; + field public static final String BODY_SENSORS_WRIST_TEMPERATURE_BACKGROUND = "android.permission.BODY_SENSORS_WRIST_TEMPERATURE_BACKGROUND"; field public static final String BRIGHTNESS_SLIDER_USAGE = "android.permission.BRIGHTNESS_SLIDER_USAGE"; field public static final String BROADCAST_CLOSE_SYSTEM_DIALOGS = "android.permission.BROADCAST_CLOSE_SYSTEM_DIALOGS"; field public static final String CHANGE_APP_IDLE_STATE = "android.permission.CHANGE_APP_IDLE_STATE"; @@ -150,8 +152,8 @@ package android.app { method @RequiresPermission(android.Manifest.permission.DUMP) public void waitForBroadcastIdle(); field public static final long LOCK_DOWN_CLOSE_SYSTEM_DIALOGS = 174664365L; // 0xa692aadL field public static final int PROCESS_CAPABILITY_ALL_IMPLICIT = 6; // 0x6 - field @Deprecated public static final int PROCESS_CAPABILITY_NETWORK = 8; // 0x8 field public static final int PROCESS_CAPABILITY_POWER_RESTRICTED_NETWORK = 8; // 0x8 + field public static final int PROCESS_CAPABILITY_USER_RESTRICTED_NETWORK = 32; // 0x20 field public static final int PROCESS_STATE_FOREGROUND_SERVICE = 4; // 0x4 field public static final int PROCESS_STATE_TOP = 2; // 0x2 field public static final int STOP_USER_ON_SWITCH_DEFAULT = -1; // 0xffffffff @@ -3520,12 +3522,18 @@ package android.view.autofill { } public final class AutofillManager { + method public void clearAutofillRequestCallback(); + method @RequiresPermission(android.Manifest.permission.PROVIDE_OWN_AUTOFILL_SUGGESTIONS) public void setAutofillRequestCallback(@NonNull java.util.concurrent.Executor, @NonNull android.view.autofill.AutofillRequestCallback); field public static final String ANY_HINT = "any"; field public static final int FLAG_SMART_SUGGESTION_OFF = 0; // 0x0 field public static final int FLAG_SMART_SUGGESTION_SYSTEM = 1; // 0x1 field public static final int MAX_TEMP_AUGMENTED_SERVICE_DURATION_MS = 120000; // 0x1d4c0 } + public interface AutofillRequestCallback { + method public void onFillRequest(@Nullable android.view.inputmethod.InlineSuggestionsRequest, @NonNull android.os.CancellationSignal, @NonNull android.service.autofill.FillCallback); + } + } package android.view.contentcapture { @@ -3649,6 +3657,11 @@ package android.view.inputmethod { method @NonNull public static android.view.inputmethod.InlineSuggestionInfo newInlineSuggestionInfo(@NonNull android.widget.inline.InlinePresentationSpec, @NonNull String, @Nullable String[], @NonNull String, boolean); } + public static final class InlineSuggestionsRequest.Builder { + method @NonNull public android.view.inputmethod.InlineSuggestionsRequest.Builder setClientSupported(boolean); + method @NonNull public android.view.inputmethod.InlineSuggestionsRequest.Builder setServiceSupported(boolean); + } + public final class InlineSuggestionsResponse implements android.os.Parcelable { method @NonNull public static android.view.inputmethod.InlineSuggestionsResponse newInlineSuggestionsResponse(@NonNull java.util.List<android.view.inputmethod.InlineSuggestion>); } diff --git a/core/api/test-lint-baseline.txt b/core/api/test-lint-baseline.txt index 4a972806b314..cf0264353159 100644 --- a/core/api/test-lint-baseline.txt +++ b/core/api/test-lint-baseline.txt @@ -183,6 +183,10 @@ MissingGetterMatchingBuilder: android.telecom.ConnectionRequest.Builder#setShoul android.telecom.ConnectionRequest does not declare a `shouldShowIncomingCallUi()` method matching method android.telecom.ConnectionRequest.Builder.setShouldShowIncomingCallUi(boolean) MissingGetterMatchingBuilder: android.view.Display.Mode.Builder#setResolution(int, int): android.view.Display.Mode does not declare a `getResolution()` method matching method android.view.Display.Mode.Builder.setResolution(int,int) +MissingGetterMatchingBuilder: android.view.inputmethod.InlineSuggestionsRequest.Builder#setClientSupported(boolean): + android.view.inputmethod.InlineSuggestionsRequest does not declare a `isClientSupported()` method matching method android.view.inputmethod.InlineSuggestionsRequest.Builder.setClientSupported(boolean) +MissingGetterMatchingBuilder: android.view.inputmethod.InlineSuggestionsRequest.Builder#setServiceSupported(boolean): + android.view.inputmethod.InlineSuggestionsRequest does not declare a `isServiceSupported()` method matching method android.view.inputmethod.InlineSuggestionsRequest.Builder.setServiceSupported(boolean) MissingNullability: android.app.Activity#onMovedToDisplay(int, android.content.res.Configuration) parameter #1: diff --git a/core/java/android/animation/ValueAnimator.java b/core/java/android/animation/ValueAnimator.java index 5d69f8b80799..ead238f75ba4 100644 --- a/core/java/android/animation/ValueAnimator.java +++ b/core/java/android/animation/ValueAnimator.java @@ -1118,10 +1118,6 @@ public class ValueAnimator extends Animator implements AnimationHandler.Animatio if (Looper.myLooper() == null) { throw new AndroidRuntimeException("Animators may only be run on Looper threads"); } - if (playBackwards == mResumed && mSelfPulse == !mSuppressSelfPulseRequested && mStarted) { - // already started - return; - } mReversing = playBackwards; mSelfPulse = !mSuppressSelfPulseRequested; // Special case: reversing from seek-to-0 should act as if not seeked at all. diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index 21097079540d..813e32a81983 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -794,6 +794,7 @@ public class ActivityManager { PROCESS_CAPABILITY_FOREGROUND_MICROPHONE, PROCESS_CAPABILITY_POWER_RESTRICTED_NETWORK, PROCESS_CAPABILITY_BFSL, + PROCESS_CAPABILITY_USER_RESTRICTED_NETWORK, }) @Retention(RetentionPolicy.SOURCE) public @interface ProcessCapability {} @@ -915,14 +916,6 @@ public class ActivityManager { /** @hide Process can access network despite any power saving restrictions */ @TestApi public static final int PROCESS_CAPABILITY_POWER_RESTRICTED_NETWORK = 1 << 3; - /** - * @hide - * @deprecated Use {@link #PROCESS_CAPABILITY_POWER_RESTRICTED_NETWORK} instead. - */ - @TestApi - @Deprecated - public static final int PROCESS_CAPABILITY_NETWORK = - PROCESS_CAPABILITY_POWER_RESTRICTED_NETWORK; /** * Flag used to indicate whether an app is allowed to start a foreground service from the @@ -944,6 +937,13 @@ public class ActivityManager { public static final int PROCESS_CAPABILITY_BFSL = 1 << 4; /** + * @hide + * Process can access network at a high enough proc state despite any user restrictions. + */ + @TestApi + public static final int PROCESS_CAPABILITY_USER_RESTRICTED_NETWORK = 1 << 5; + + /** * @hide all capabilities, the ORing of all flags in {@link ProcessCapability}. * * Don't expose it as TestApi -- we may add new capabilities any time, which could @@ -953,7 +953,8 @@ public class ActivityManager { | PROCESS_CAPABILITY_FOREGROUND_CAMERA | PROCESS_CAPABILITY_FOREGROUND_MICROPHONE | PROCESS_CAPABILITY_POWER_RESTRICTED_NETWORK - | PROCESS_CAPABILITY_BFSL; + | PROCESS_CAPABILITY_BFSL + | PROCESS_CAPABILITY_USER_RESTRICTED_NETWORK; /** * All implicit capabilities. There are capabilities that process automatically have. @@ -973,6 +974,7 @@ public class ActivityManager { pw.print((caps & PROCESS_CAPABILITY_FOREGROUND_MICROPHONE) != 0 ? 'M' : '-'); pw.print((caps & PROCESS_CAPABILITY_POWER_RESTRICTED_NETWORK) != 0 ? 'N' : '-'); pw.print((caps & PROCESS_CAPABILITY_BFSL) != 0 ? 'F' : '-'); + pw.print((caps & PROCESS_CAPABILITY_USER_RESTRICTED_NETWORK) != 0 ? 'U' : '-'); } /** @hide */ @@ -982,6 +984,7 @@ public class ActivityManager { sb.append((caps & PROCESS_CAPABILITY_FOREGROUND_MICROPHONE) != 0 ? 'M' : '-'); sb.append((caps & PROCESS_CAPABILITY_POWER_RESTRICTED_NETWORK) != 0 ? 'N' : '-'); sb.append((caps & PROCESS_CAPABILITY_BFSL) != 0 ? 'F' : '-'); + sb.append((caps & PROCESS_CAPABILITY_USER_RESTRICTED_NETWORK) != 0 ? 'U' : '-'); } /** diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java index feb9b4f2664d..d73f0cca9a4e 100644 --- a/core/java/android/app/ActivityOptions.java +++ b/core/java/android/app/ActivityOptions.java @@ -2539,7 +2539,8 @@ public class ActivityOptions extends ComponentOptions { public String toString() { return "ActivityOptions(" + hashCode() + "), mPackageName=" + mPackageName + ", mAnimationType=" + mAnimationType + ", mStartX=" + mStartX + ", mStartY=" - + mStartY + ", mWidth=" + mWidth + ", mHeight=" + mHeight; + + mStartY + ", mWidth=" + mWidth + ", mHeight=" + mHeight + ", mLaunchDisplayId=" + + mLaunchDisplayId; } /** diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index 265b56418d4b..b48a8fb73832 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -2188,7 +2188,10 @@ public class AppOpsManager { public static final String OPSTR_CAPTURE_CONSENTLESS_BUGREPORT_ON_USERDEBUG_BUILD = "android:capture_consentless_bugreport_on_userdebug_build"; - /** Access to wrist temperature body sensors. */ + /** + * Access to wrist temperature body sensors. + * @hide + */ public static final String OPSTR_BODY_SENSORS_WRIST_TEMPERATURE = "android:body_sensors_wrist_temperature"; diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 3b2ea785988e..2b73afcd740f 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -322,6 +322,7 @@ public abstract class Context { BIND_EXTERNAL_SERVICE_LONG, // Make sure no flag uses the sign bit (most significant bit) of the long integer, // to avoid future confusion. + BIND_BYPASS_USER_NETWORK_RESTRICTIONS, }) @Retention(RetentionPolicy.SOURCE) public @interface BindServiceFlagsLongBits {} @@ -688,6 +689,16 @@ public abstract class Context { public static final long BIND_EXTERNAL_SERVICE_LONG = 1L << 62; /** + * Flag for {@link #bindService}: allow the process hosting the target service to gain + * {@link ActivityManager#PROCESS_CAPABILITY_USER_RESTRICTED_NETWORK}, which allows it be able + * to access network regardless of any user restrictions. + * + * @hide + */ + public static final long BIND_BYPASS_USER_NETWORK_RESTRICTIONS = 0x1_0000_0000L; + + + /** * These bind flags reduce the strength of the binding such that we shouldn't * consider it as pulling the process up to the level of the one that is bound to it. * @hide diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index df8da246c976..58b0571653f1 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -11356,12 +11356,15 @@ public class Intent implements Parcelable, Cloneable { @Override public String toString() { StringBuilder b = new StringBuilder(128); + toString(b); + return b.toString(); + } + /** @hide */ + public void toString(@NonNull StringBuilder b) { b.append("Intent { "); toShortString(b, true, true, true, false); b.append(" }"); - - return b.toString(); } /** @hide */ diff --git a/core/java/android/content/pm/ServiceInfo.java b/core/java/android/content/pm/ServiceInfo.java index 1a3c3d97634c..7e0954a55560 100644 --- a/core/java/android/content/pm/ServiceInfo.java +++ b/core/java/android/content/pm/ServiceInfo.java @@ -332,7 +332,6 @@ public class ServiceInfo extends ComponentInfo * permissions: * {@link android.Manifest.permission#ACTIVITY_RECOGNITION}, * {@link android.Manifest.permission#BODY_SENSORS}, - * {@link android.Manifest.permission#BODY_SENSORS_WRIST_TEMPERATURE}, * {@link android.Manifest.permission#HIGH_SAMPLING_RATE_SENSORS}. */ @RequiresPermission( @@ -342,7 +341,6 @@ public class ServiceInfo extends ComponentInfo anyOf = { Manifest.permission.ACTIVITY_RECOGNITION, Manifest.permission.BODY_SENSORS, - Manifest.permission.BODY_SENSORS_WRIST_TEMPERATURE, Manifest.permission.HIGH_SAMPLING_RATE_SENSORS, } ) diff --git a/core/java/android/hardware/Camera.java b/core/java/android/hardware/Camera.java index 81d6ba93cfe9..ccc39b6080d7 100644 --- a/core/java/android/hardware/Camera.java +++ b/core/java/android/hardware/Camera.java @@ -51,6 +51,7 @@ import android.util.Log; import android.view.Surface; import android.view.SurfaceHolder; +import com.android.internal.R; import com.android.internal.annotations.GuardedBy; import com.android.internal.app.IAppOpsCallback; import com.android.internal.app.IAppOpsService; @@ -486,8 +487,22 @@ public class Camera { boolean overrideToPortrait = CameraManager.shouldOverrideToPortrait( ActivityThread.currentApplication().getApplicationContext()); + boolean forceSlowJpegMode = shouldForceSlowJpegMode(); return native_setup(new WeakReference<Camera>(this), cameraId, - ActivityThread.currentOpPackageName(), overrideToPortrait); + ActivityThread.currentOpPackageName(), overrideToPortrait, forceSlowJpegMode); + } + + private boolean shouldForceSlowJpegMode() { + Context applicationContext = ActivityThread.currentApplication().getApplicationContext(); + String[] slowJpegPackageNames = applicationContext.getResources().getStringArray( + R.array.config_forceSlowJpegModeList); + String callingPackageName = applicationContext.getPackageName(); + for (String packageName : slowJpegPackageNames) { + if (TextUtils.equals(packageName, callingPackageName)) { + return true; + } + } + return false; } /** used by Camera#open, Camera#open(int) */ @@ -558,7 +573,7 @@ public class Camera { @UnsupportedAppUsage private native int native_setup(Object cameraThis, int cameraId, String packageName, - boolean overrideToPortrait); + boolean overrideToPortrait, boolean forceSlowJpegMode); private native final void native_release(); diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java index 144b1de148b4..73dd50945289 100644 --- a/core/java/android/hardware/camera2/CameraManager.java +++ b/core/java/android/hardware/camera2/CameraManager.java @@ -152,21 +152,8 @@ public final class CameraManager { mContext.checkSelfPermission(CAMERA_OPEN_CLOSE_LISTENER_PERMISSION) == PackageManager.PERMISSION_GRANTED; } - - mFoldStateListener = new FoldStateListener(context); - try { - context.getSystemService(DeviceStateManager.class).registerCallback( - new HandlerExecutor(CameraManagerGlobal.get().getDeviceStateHandler()), - mFoldStateListener); - } catch (IllegalStateException e) { - Log.v(TAG, "Failed to register device state listener!"); - Log.v(TAG, "Device state dependent characteristics updates will not be functional!"); - mFoldStateListener = null; - } } - private FoldStateListener mFoldStateListener; - /** * @hide */ @@ -228,12 +215,7 @@ public final class CameraManager { * @hide */ public void registerDeviceStateListener(@NonNull CameraCharacteristics chars) { - synchronized (mLock) { - DeviceStateListener listener = chars.getDeviceStateListener(); - if (mFoldStateListener != null) { - mFoldStateListener.addDeviceStateListener(listener); - } - } + CameraManagerGlobal.get().registerDeviceStateListener(chars, mContext); } /** @@ -627,6 +609,21 @@ public final class CameraManager { } /** + * <p>Query the capabilities of a camera device. These capabilities are + * immutable for a given camera.</p> + * + * <p>The value of {@link CameraCharacteristics.SENSOR_ORIENTATION} will change for landscape + * cameras depending on whether overrideToPortrait is enabled. If enabled, these cameras will + * appear to be portrait orientation instead, provided that the override is supported by the + * camera device. Only devices that can be opened by {@link #openCamera} will report a changed + * {@link CameraCharacteristics.SENSOR_ORIENTATION}.</p> + * + * @param cameraId The id of the camera device to query. This could be either a standalone + * camera ID which can be directly opened by {@link #openCamera}, or a physical camera ID that + * can only used as part of a logical multi-camera. + * @param overrideToPortrait Whether to apply the landscape to portrait override. + * @return The properties of the given camera + * * @hide */ @TestApi @@ -1781,6 +1778,7 @@ public final class CameraManager { private HandlerThread mDeviceStateHandlerThread; private Handler mDeviceStateHandler; + private FoldStateListener mFoldStateListener; // Singleton, don't allow construction private CameraManagerGlobal() { } @@ -1795,7 +1793,8 @@ public final class CameraManager { return gCameraManager; } - public Handler getDeviceStateHandler() { + public void registerDeviceStateListener(@NonNull CameraCharacteristics chars, + @NonNull Context ctx) { synchronized(mLock) { if (mDeviceStateHandlerThread == null) { mDeviceStateHandlerThread = new HandlerThread(TAG); @@ -1803,7 +1802,20 @@ public final class CameraManager { mDeviceStateHandler = new Handler(mDeviceStateHandlerThread.getLooper()); } - return mDeviceStateHandler; + if (mFoldStateListener == null) { + mFoldStateListener = new FoldStateListener(ctx); + try { + ctx.getSystemService(DeviceStateManager.class).registerCallback( + new HandlerExecutor(mDeviceStateHandler), mFoldStateListener); + } catch (IllegalStateException e) { + Log.v(TAG, "Failed to register device state listener!"); + Log.v(TAG, "Device state dependent characteristics updates will not be" + + "functional!"); + return; + } + } + + mFoldStateListener.addDeviceStateListener(chars.getDeviceStateListener()); } } diff --git a/core/java/android/net/NetworkPolicyManager.java b/core/java/android/net/NetworkPolicyManager.java index 104a8b2095d8..efed6883a114 100644 --- a/core/java/android/net/NetworkPolicyManager.java +++ b/core/java/android/net/NetworkPolicyManager.java @@ -780,11 +780,11 @@ public class NetworkPolicyManager { case ActivityManager.PROCESS_STATE_PERSISTENT: case ActivityManager.PROCESS_STATE_PERSISTENT_UI: case ActivityManager.PROCESS_STATE_TOP: - return ActivityManager.PROCESS_CAPABILITY_ALL; case ActivityManager.PROCESS_STATE_BOUND_TOP: case ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE: case ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE: - return ActivityManager.PROCESS_CAPABILITY_POWER_RESTRICTED_NETWORK; + return ActivityManager.PROCESS_CAPABILITY_POWER_RESTRICTED_NETWORK + | ActivityManager.PROCESS_CAPABILITY_USER_RESTRICTED_NETWORK; default: return ActivityManager.PROCESS_CAPABILITY_NONE; } @@ -826,13 +826,18 @@ public class NetworkPolicyManager { if (uidState == null) { return false; } - return isProcStateAllowedWhileOnRestrictBackground(uidState.procState); + return isProcStateAllowedWhileOnRestrictBackground(uidState.procState, uidState.capability); } /** @hide */ - public static boolean isProcStateAllowedWhileOnRestrictBackground(int procState) { - // Data saver and bg policy restrictions will only take procstate into account. - return procState <= FOREGROUND_THRESHOLD_STATE; + public static boolean isProcStateAllowedWhileOnRestrictBackground(int procState, + @ProcessCapability int capabilities) { + return procState <= FOREGROUND_THRESHOLD_STATE + // This is meant to be a user-initiated job, and therefore gets similar network + // access to FGS. + || (procState <= ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND + && (capabilities + & ActivityManager.PROCESS_CAPABILITY_USER_RESTRICTED_NETWORK) != 0); } /** @hide */ diff --git a/core/java/android/util/FeatureFlagUtils.java b/core/java/android/util/FeatureFlagUtils.java index bc514b091409..c3295088ef11 100644 --- a/core/java/android/util/FeatureFlagUtils.java +++ b/core/java/android/util/FeatureFlagUtils.java @@ -234,8 +234,8 @@ public class FeatureFlagUtils { DEFAULT_FLAGS.put(SETTINGS_AUDIO_ROUTING, "true"); DEFAULT_FLAGS.put(SETTINGS_FLASH_NOTIFICATIONS, "true"); DEFAULT_FLAGS.put(SETTINGS_SHOW_UDFPS_ENROLL_IN_SETTINGS, "true"); - DEFAULT_FLAGS.put(SETTINGS_ENABLE_LOCKSCREEN_TRANSFER_API, "false"); - DEFAULT_FLAGS.put(SETTINGS_REMOTE_DEVICE_CREDENTIAL_VALIDATION, "false"); + DEFAULT_FLAGS.put(SETTINGS_ENABLE_LOCKSCREEN_TRANSFER_API, "true"); + DEFAULT_FLAGS.put(SETTINGS_REMOTE_DEVICE_CREDENTIAL_VALIDATION, "true"); } private static final Set<String> PERSISTENT_FLAGS; diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index 46ae3ea21890..f5e4da86bfea 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -66,6 +66,7 @@ import android.view.animation.AnimationUtils; import android.view.animation.LayoutAnimationController; import android.view.animation.Transformation; import android.view.autofill.AutofillId; +import android.view.autofill.AutofillManager; import android.view.autofill.Helper; import android.view.inspector.InspectableProperty; import android.view.inspector.InspectableProperty.EnumEntry; @@ -3709,6 +3710,20 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager return children; } + private AutofillManager getAutofillManager() { + return mContext.getSystemService(AutofillManager.class); + } + + private boolean shouldIncludeAllChildrenViewWithAutofillTypeNotNone(AutofillManager afm) { + if (afm == null) return false; + return afm.shouldIncludeAllChildrenViewsWithAutofillTypeNotNoneInAssistStructure(); + } + + private boolean shouldIncludeAllChildrenViews(AutofillManager afm){ + if (afm == null) return false; + return afm.shouldIncludeAllChildrenViewInAssistStructure(); + } + /** @hide */ private void populateChildrenForAutofill(ArrayList<View> list, @AutofillFlags int flags) { final int childrenCount = mChildrenCount; @@ -3718,6 +3733,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager final ArrayList<View> preorderedList = buildOrderedChildList(); final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled(); + final AutofillManager afm = getAutofillManager(); for (int i = 0; i < childrenCount; i++) { final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder); final View child = (preorderedList == null) @@ -3725,7 +3741,10 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager if ((flags & AUTOFILL_FLAG_INCLUDE_NOT_IMPORTANT_VIEWS) != 0 || child.isImportantForAutofill() || (child.isMatchingAutofillableHeuristics() - && !child.isActivityDeniedForAutofillForUnimportantView())) { + && !child.isActivityDeniedForAutofillForUnimportantView()) + || (shouldIncludeAllChildrenViewWithAutofillTypeNotNone(afm) + && child.getAutofillType() != AUTOFILL_TYPE_NONE) + || shouldIncludeAllChildrenViews(afm)){ list.add(child); } else if (child instanceof ViewGroup) { ((ViewGroup) child).populateChildrenForAutofill(list, flags); diff --git a/core/java/android/view/autofill/AutofillFeatureFlags.java b/core/java/android/view/autofill/AutofillFeatureFlags.java index e51eff42fed5..4aa612c526fe 100644 --- a/core/java/android/view/autofill/AutofillFeatureFlags.java +++ b/core/java/android/view/autofill/AutofillFeatureFlags.java @@ -193,6 +193,24 @@ public class AutofillFeatureFlags { public static final String DEVICE_CONFIG_SHOULD_ENABLE_AUTOFILL_ON_ALL_VIEW_TYPES = "should_enable_autofill_on_all_view_types"; + /** + * Whether include all autofill type not none views in assist structure + * + * @hide + */ + public static final String + DEVICE_CONFIG_INCLUDE_ALL_AUTOFILL_TYPE_NOT_NONE_VIEWS_IN_ASSIST_STRUCTURE = + "include_all_autofill_type_not_none_views_in_assist_structure"; + + /** + * Whether include all views in assist structure + * + * @hide + */ + public static final String + DEVICE_CONFIG_INCLUDE_ALL_VIEWS_IN_ASSIST_STRUCTURE = + "include_all_views_in_assist_structure"; + // END AUTOFILL FOR ALL APPS FLAGS // @@ -398,6 +416,28 @@ public class AutofillFeatureFlags { DeviceConfig.NAMESPACE_AUTOFILL, DEVICE_CONFIG_PACKAGE_AND_ACTIVITY_ALLOWLIST_FOR_TRIGGERING_FILL_REQUEST, ""); } + /** + * Whether include all views that have autofill type not none in assist structure. + * + * @hide + */ + public static boolean shouldIncludeAllViewsAutofillTypeNotNoneInAssistStructrue() { + return DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_AUTOFILL, + DEVICE_CONFIG_INCLUDE_ALL_AUTOFILL_TYPE_NOT_NONE_VIEWS_IN_ASSIST_STRUCTURE, false); + } + + /** + * Whether include all views in assist structure. + * + * @hide + */ + public static boolean shouldIncludeAllChildrenViewInAssistStructure() { + return DeviceConfig.getBoolean( + DeviceConfig.NAMESPACE_AUTOFILL, + DEVICE_CONFIG_INCLUDE_ALL_VIEWS_IN_ASSIST_STRUCTURE, false); + } + // START AUTOFILL PCC CLASSIFICATION FUNCTIONS diff --git a/core/java/android/view/autofill/AutofillManager.java b/core/java/android/view/autofill/AutofillManager.java index cc8ab1072083..801b13a2c69c 100644 --- a/core/java/android/view/autofill/AutofillManager.java +++ b/core/java/android/view/autofill/AutofillManager.java @@ -707,6 +707,12 @@ public final class AutofillManager { // An allowed activity set read from device config private Set<String> mAllowedActivitySet = new ArraySet<>(); + // Indicate whether should include all view with autofill type not none in assist structure + private boolean mShouldIncludeAllViewsWithAutofillTypeNotNoneInAssistStructure; + + // Indicate whether should include all view in assist structure + private boolean mShouldIncludeAllChildrenViewInAssistStructure; + // Indicates whether called the showAutofillDialog() method. private boolean mShowAutofillDialogCalled = false; @@ -913,6 +919,12 @@ public final class AutofillManager { mAllowedActivitySet = getDeniedOrAllowedActivitySetFromString( allowlistString, packageName); } + + mShouldIncludeAllViewsWithAutofillTypeNotNoneInAssistStructure + = AutofillFeatureFlags.shouldIncludeAllViewsAutofillTypeNotNoneInAssistStructrue(); + + mShouldIncludeAllChildrenViewInAssistStructure + = AutofillFeatureFlags.shouldIncludeAllChildrenViewInAssistStructure(); } /** @@ -963,6 +975,20 @@ public final class AutofillManager { } /** + * @hide + */ + public boolean shouldIncludeAllChildrenViewsWithAutofillTypeNotNoneInAssistStructure() { + return mShouldIncludeAllViewsWithAutofillTypeNotNoneInAssistStructure; + } + + /** + * @hide + */ + public boolean shouldIncludeAllChildrenViewInAssistStructure() { + return mShouldIncludeAllChildrenViewInAssistStructure; + } + + /** * Get the denied or allowed activitiy names under specified package from the list string and * set it in fields accordingly * @@ -2296,7 +2322,10 @@ public final class AutofillManager { * * @param executor specifies the thread upon which the callbacks will be invoked. * @param callback which handles autofill request to provide client's suggestions. + * + * @hide */ + @TestApi @RequiresPermission(PROVIDE_OWN_AUTOFILL_SUGGESTIONS) public void setAutofillRequestCallback(@NonNull @CallbackExecutor Executor executor, @NonNull AutofillRequestCallback callback) { @@ -2313,7 +2342,10 @@ public final class AutofillManager { /** * clears the client's suggestions callback for autofill. + * + * @hide */ + @TestApi public void clearAutofillRequestCallback() { synchronized (mLock) { mRequestCallbackExecutor = null; diff --git a/core/java/android/view/autofill/AutofillRequestCallback.java b/core/java/android/view/autofill/AutofillRequestCallback.java index e632a5849471..10a088b4ebfa 100644 --- a/core/java/android/view/autofill/AutofillRequestCallback.java +++ b/core/java/android/view/autofill/AutofillRequestCallback.java @@ -18,6 +18,7 @@ package android.view.autofill; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.TestApi; import android.os.CancellationSignal; import android.service.autofill.FillCallback; import android.view.inputmethod.InlineSuggestionsRequest; @@ -55,7 +56,10 @@ import android.view.inputmethod.InlineSuggestionsRequest; * * <P>IMPORTANT: This should not be used for displaying anything other than input suggestions, or * the keyboard may choose to block your app from the inline strip. + * + * @hide */ +@TestApi public interface AutofillRequestCallback { /** * Called by the Android system to decide if a screen can be autofilled by the callback. diff --git a/core/java/android/view/inputmethod/InlineSuggestionsRequest.java b/core/java/android/view/inputmethod/InlineSuggestionsRequest.java index 70279cc8e845..77a2b5b877be 100644 --- a/core/java/android/view/inputmethod/InlineSuggestionsRequest.java +++ b/core/java/android/view/inputmethod/InlineSuggestionsRequest.java @@ -18,6 +18,7 @@ package android.view.inputmethod; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.TestApi; import android.app.ActivityThread; import android.app.compat.CompatChanges; import android.compat.annotation.ChangeId; @@ -700,7 +701,10 @@ public final class InlineSuggestionsRequest implements Parcelable { * provides the input view. * * Note: The default value is {@code true}. + * + * @hide */ + @TestApi @DataClass.Generated.Member public @NonNull Builder setServiceSupported(boolean value) { checkNotUsed(); @@ -714,7 +718,10 @@ public final class InlineSuggestionsRequest implements Parcelable { * input view. * * Note: The default value is {@code true}. + * + * @hide */ + @TestApi @DataClass.Generated.Member public @NonNull Builder setClientSupported(boolean value) { checkNotUsed(); diff --git a/core/java/com/android/internal/os/TimeoutRecord.java b/core/java/com/android/internal/os/TimeoutRecord.java index 2f6091bc3266..a0e29347d07f 100644 --- a/core/java/com/android/internal/os/TimeoutRecord.java +++ b/core/java/com/android/internal/os/TimeoutRecord.java @@ -18,6 +18,8 @@ package com.android.internal.os; import android.annotation.IntDef; import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.ComponentName; import android.content.Intent; import android.os.SystemClock; @@ -98,18 +100,41 @@ public class TimeoutRecord { /** Record for a broadcast receiver timeout. */ @NonNull + public static TimeoutRecord forBroadcastReceiver(@NonNull Intent intent, + @Nullable String packageName, @Nullable String className) { + final Intent logIntent; + if (packageName != null) { + if (className != null) { + logIntent = new Intent(intent); + logIntent.setComponent(new ComponentName(packageName, className)); + } else { + logIntent = new Intent(intent); + logIntent.setPackage(packageName); + } + } else { + logIntent = intent; + } + return forBroadcastReceiver(logIntent); + } + + /** Record for a broadcast receiver timeout. */ + @NonNull public static TimeoutRecord forBroadcastReceiver(@NonNull Intent intent) { - String reason = "Broadcast of " + intent.toString(); - return TimeoutRecord.endingNow(TimeoutKind.BROADCAST_RECEIVER, reason); + final StringBuilder reason = new StringBuilder("Broadcast of "); + intent.toString(reason); + return TimeoutRecord.endingNow(TimeoutKind.BROADCAST_RECEIVER, reason.toString()); } /** Record for a broadcast receiver timeout. */ @NonNull public static TimeoutRecord forBroadcastReceiver(@NonNull Intent intent, long timeoutDurationMs) { - String reason = "Broadcast of " + intent.toString() + ", waited " + timeoutDurationMs - + "ms"; - return TimeoutRecord.endingNow(TimeoutKind.BROADCAST_RECEIVER, reason); + final StringBuilder reason = new StringBuilder("Broadcast of "); + intent.toString(reason); + reason.append(", waited "); + reason.append(timeoutDurationMs); + reason.append("ms"); + return TimeoutRecord.endingNow(TimeoutKind.BROADCAST_RECEIVER, reason.toString()); } /** Record for an input dispatch no focused window timeout */ diff --git a/core/jni/android_hardware_Camera.cpp b/core/jni/android_hardware_Camera.cpp index a8abe50a9755..2a670e865ced 100644 --- a/core/jni/android_hardware_Camera.cpp +++ b/core/jni/android_hardware_Camera.cpp @@ -556,7 +556,8 @@ static void android_hardware_Camera_getCameraInfo(JNIEnv *env, jobject thiz, jin // connect to camera service static jint android_hardware_Camera_native_setup(JNIEnv *env, jobject thiz, jobject weak_this, jint cameraId, jstring clientPackageName, - jboolean overrideToPortrait) { + jboolean overrideToPortrait, + jboolean forceSlowJpegMode) { // Convert jstring to String16 const char16_t *rawClientName = reinterpret_cast<const char16_t*>( env->GetStringChars(clientPackageName, NULL)); @@ -568,7 +569,7 @@ static jint android_hardware_Camera_native_setup(JNIEnv *env, jobject thiz, jobj int targetSdkVersion = android_get_application_target_sdk_version(); sp<Camera> camera = Camera::connect(cameraId, clientName, Camera::USE_CALLING_UID, Camera::USE_CALLING_PID, - targetSdkVersion, overrideToPortrait); + targetSdkVersion, overrideToPortrait, forceSlowJpegMode); if (camera == NULL) { return -EACCES; } @@ -1054,7 +1055,7 @@ static const JNINativeMethod camMethods[] = { {"getNumberOfCameras", "()I", (void *)android_hardware_Camera_getNumberOfCameras}, {"_getCameraInfo", "(IZLandroid/hardware/Camera$CameraInfo;)V", (void *)android_hardware_Camera_getCameraInfo}, - {"native_setup", "(Ljava/lang/Object;ILjava/lang/String;Z)I", + {"native_setup", "(Ljava/lang/Object;ILjava/lang/String;ZZ)I", (void *)android_hardware_Camera_native_setup}, {"native_release", "()V", (void *)android_hardware_Camera_release}, {"setPreviewSurface", "(Landroid/view/Surface;)V", diff --git a/core/proto/android/server/windowmanagerservice.proto b/core/proto/android/server/windowmanagerservice.proto index d1f7b63b57ea..e6c8557a8c50 100644 --- a/core/proto/android/server/windowmanagerservice.proto +++ b/core/proto/android/server/windowmanagerservice.proto @@ -390,6 +390,7 @@ message ActivityRecordProto { optional bool enable_recents_screenshot = 35; optional int32 last_drop_input_mode = 36; optional int32 override_orientation = 37 [(.android.typedef) = "android.content.pm.ActivityInfo.ScreenOrientation"]; + optional bool should_send_compat_fake_focus = 38; } /* represents WindowToken */ diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 81b1f21e286d..78d39236b392 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -1746,9 +1746,11 @@ android:protectionLevel="dangerous" android:permissionFlags="hardRestricted" /> - <!-- Allows an application to access wrist temperature data from the watch sensors. + <!-- @TestApi Allows an application to access wrist temperature data from the watch sensors. <p class="note"><strong>Note: </strong> This permission is for Wear OS only. - <p>Protection level: dangerous --> + <p>Protection level: dangerous + @hide + --> <permission android:name="android.permission.BODY_SENSORS_WRIST_TEMPERATURE" android:permissionGroup="android.permission-group.UNDEFINED" android:label="@string/permlab_bodySensorsWristTemperature" @@ -1756,7 +1758,7 @@ android:backgroundPermission="android.permission.BODY_SENSORS_WRIST_TEMPERATURE_BACKGROUND" android:protectionLevel="dangerous" /> - <!-- Allows an application to access wrist temperature data from the watch sensors. + <!-- @TestApi Allows an application to access wrist temperature data from the watch sensors. If you're requesting this permission, you must also request {@link #BODY_SENSORS_WRIST_TEMPERATURE}. Requesting this permission by itself doesn't give you wrist temperature body sensors access. @@ -1766,6 +1768,7 @@ <p> This is a hard restricted permission which cannot be held by an app until the installer on record allowlists the permission. For more details see {@link android.content.pm.PackageInstaller.SessionParams#setWhitelistedRestrictedPermissions(Set)}. + @hide --> <permission android:name="android.permission.BODY_SENSORS_WRIST_TEMPERATURE_BACKGROUND" android:permissionGroup="android.permission-group.UNDEFINED" diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 239747a37ec6..1a85f4cad9e0 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -5223,6 +5223,11 @@ of known compatibility issues. --> <string-array name="config_highRefreshRateBlacklist"></string-array> + <!-- The list of packages to force slowJpegMode for Apps using Camera API1 --> + <string-array name="config_forceSlowJpegModeList" translatable="false"> + <!-- Add packages here --> + </string-array> + <!-- Whether or not to hide the navigation bar when the soft keyboard is visible in order to create additional screen real estate outside beyond the keyboard. Note that the user needs to have a confirmed way to dismiss the keyboard when desired. --> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index a09cee797878..79f3dcd8c1ed 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -4196,6 +4196,7 @@ <java-symbol type="string" name="config_factoryResetPackage" /> <java-symbol type="array" name="config_highRefreshRateBlacklist" /> + <java-symbol type="array" name="config_forceSlowJpegModeList" /> <java-symbol type="layout" name="chooser_dialog" /> <java-symbol type="layout" name="chooser_dialog_item" /> diff --git a/graphics/java/android/graphics/ImageDecoder.java b/graphics/java/android/graphics/ImageDecoder.java index 51f99ec637da..0b29973507d2 100644 --- a/graphics/java/android/graphics/ImageDecoder.java +++ b/graphics/java/android/graphics/ImageDecoder.java @@ -38,8 +38,11 @@ import android.graphics.drawable.AnimatedImageDrawable; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.NinePatchDrawable; +import android.media.MediaCodecInfo; +import android.media.MediaCodecList; import android.net.Uri; import android.os.Build; +import android.os.SystemProperties; import android.os.Trace; import android.system.ErrnoException; import android.system.Os; @@ -928,6 +931,8 @@ public final class ImageDecoder implements AutoCloseable { case "image/x-pentax-pef": case "image/x-samsung-srw": return true; + case "image/avif": + return isP010SupportedForAV1(); default: return false; } @@ -2063,6 +2068,53 @@ public final class ImageDecoder implements AutoCloseable { return decodeBitmapImpl(src, null); } + private static boolean sIsP010SupportedForAV1 = false; + private static boolean sIsP010SupportedForAV1Initialized = false; + private static final Object sIsP010SupportedForAV1Lock = new Object(); + + /** + * Checks if the device supports decoding 10-bit AV1. + */ + @SuppressWarnings("AndroidFrameworkCompatChange") // This is not an app-visible API. + private static boolean isP010SupportedForAV1() { + synchronized (sIsP010SupportedForAV1Lock) { + if (sIsP010SupportedForAV1Initialized) { + return sIsP010SupportedForAV1; + } + + sIsP010SupportedForAV1Initialized = true; + + if (hasHardwareDecoder("video/av01")) { + sIsP010SupportedForAV1 = true; + return true; + } + + sIsP010SupportedForAV1 = Build.VERSION.DEVICE_INITIAL_SDK_INT + >= Build.VERSION_CODES.S; + return sIsP010SupportedForAV1; + } + } + + /** + * Checks if the device has hardware decoder for the target mime type. + */ + private static boolean hasHardwareDecoder(String mime) { + final MediaCodecList sMCL = new MediaCodecList(MediaCodecList.REGULAR_CODECS); + for (MediaCodecInfo info : sMCL.getCodecInfos()) { + if (info.isEncoder() == false && info.isHardwareAccelerated()) { + try { + if (info.getCapabilitiesForType(mime) != null) { + return true; + } + } catch (IllegalArgumentException e) { + // mime is not supported + return false; + } + } + } + return false; + } + /** * Private method called by JNI. */ 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 20b6ae29939c..53d39d9fa28e 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -710,24 +710,16 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { @NonNull SplitAttributes splitAttributes) { final Configuration taskConfiguration = taskProperties.getConfiguration(); final FoldingFeature foldingFeature = getFoldingFeature(taskProperties); - final SplitType splitType = computeSplitType(splitAttributes, taskConfiguration, - foldingFeature); - final SplitAttributes computedSplitAttributes = new SplitAttributes.Builder() - .setSplitType(splitType) - .setLayoutDirection(splitAttributes.getLayoutDirection()) - .build(); - if (!shouldShowSplit(computedSplitAttributes)) { + if (!shouldShowSplit(splitAttributes)) { return new Rect(); } final Rect bounds; switch (position) { case POSITION_START: - bounds = getPrimaryBounds(taskConfiguration, computedSplitAttributes, - foldingFeature); + bounds = getPrimaryBounds(taskConfiguration, splitAttributes, foldingFeature); break; case POSITION_END: - bounds = getSecondaryBounds(taskConfiguration, computedSplitAttributes, - foldingFeature); + bounds = getSecondaryBounds(taskConfiguration, splitAttributes, foldingFeature); break; case POSITION_FILL: default: @@ -742,63 +734,76 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { @NonNull private Rect getPrimaryBounds(@NonNull Configuration taskConfiguration, @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) { - if (!shouldShowSplit(splitAttributes)) { + final SplitAttributes computedSplitAttributes = updateSplitAttributesType(splitAttributes, + computeSplitType(splitAttributes, taskConfiguration, foldingFeature)); + if (!shouldShowSplit(computedSplitAttributes)) { return new Rect(); } - switch (splitAttributes.getLayoutDirection()) { + switch (computedSplitAttributes.getLayoutDirection()) { case SplitAttributes.LayoutDirection.LEFT_TO_RIGHT: { - return getLeftContainerBounds(taskConfiguration, splitAttributes, foldingFeature); + return getLeftContainerBounds(taskConfiguration, computedSplitAttributes, + foldingFeature); } case SplitAttributes.LayoutDirection.RIGHT_TO_LEFT: { - return getRightContainerBounds(taskConfiguration, splitAttributes, foldingFeature); + return getRightContainerBounds(taskConfiguration, computedSplitAttributes, + foldingFeature); } case SplitAttributes.LayoutDirection.LOCALE: { final boolean isLtr = taskConfiguration.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR; return isLtr - ? getLeftContainerBounds(taskConfiguration, splitAttributes, foldingFeature) - : getRightContainerBounds(taskConfiguration, splitAttributes, + ? getLeftContainerBounds(taskConfiguration, computedSplitAttributes, + foldingFeature) + : getRightContainerBounds(taskConfiguration, computedSplitAttributes, foldingFeature); } case SplitAttributes.LayoutDirection.TOP_TO_BOTTOM: { - return getTopContainerBounds(taskConfiguration, splitAttributes, foldingFeature); + return getTopContainerBounds(taskConfiguration, computedSplitAttributes, + foldingFeature); } case SplitAttributes.LayoutDirection.BOTTOM_TO_TOP: { - return getBottomContainerBounds(taskConfiguration, splitAttributes, foldingFeature); + return getBottomContainerBounds(taskConfiguration, computedSplitAttributes, + foldingFeature); } default: throw new IllegalArgumentException("Unknown layout direction:" - + splitAttributes.getLayoutDirection()); + + computedSplitAttributes.getLayoutDirection()); } } @NonNull private Rect getSecondaryBounds(@NonNull Configuration taskConfiguration, @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) { - if (!shouldShowSplit(splitAttributes)) { + final SplitAttributes computedSplitAttributes = updateSplitAttributesType(splitAttributes, + computeSplitType(splitAttributes, taskConfiguration, foldingFeature)); + if (!shouldShowSplit(computedSplitAttributes)) { return new Rect(); } - switch (splitAttributes.getLayoutDirection()) { + switch (computedSplitAttributes.getLayoutDirection()) { case SplitAttributes.LayoutDirection.LEFT_TO_RIGHT: { - return getRightContainerBounds(taskConfiguration, splitAttributes, foldingFeature); + return getRightContainerBounds(taskConfiguration, computedSplitAttributes, + foldingFeature); } case SplitAttributes.LayoutDirection.RIGHT_TO_LEFT: { - return getLeftContainerBounds(taskConfiguration, splitAttributes, foldingFeature); + return getLeftContainerBounds(taskConfiguration, computedSplitAttributes, + foldingFeature); } case SplitAttributes.LayoutDirection.LOCALE: { final boolean isLtr = taskConfiguration.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR; return isLtr - ? getRightContainerBounds(taskConfiguration, splitAttributes, + ? getRightContainerBounds(taskConfiguration, computedSplitAttributes, foldingFeature) - : getLeftContainerBounds(taskConfiguration, splitAttributes, + : getLeftContainerBounds(taskConfiguration, computedSplitAttributes, foldingFeature); } case SplitAttributes.LayoutDirection.TOP_TO_BOTTOM: { - return getBottomContainerBounds(taskConfiguration, splitAttributes, foldingFeature); + return getBottomContainerBounds(taskConfiguration, computedSplitAttributes, + foldingFeature); } case SplitAttributes.LayoutDirection.BOTTOM_TO_TOP: { - return getTopContainerBounds(taskConfiguration, splitAttributes, foldingFeature); + return getTopContainerBounds(taskConfiguration, computedSplitAttributes, + foldingFeature); } default: throw new IllegalArgumentException("Unknown layout direction:" @@ -806,6 +811,19 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { } } + /** + * Returns the {@link SplitAttributes} that update the {@link SplitType} to + * {@code splitTypeToUpdate}. + */ + private static SplitAttributes updateSplitAttributesType( + @NonNull SplitAttributes splitAttributes, @NonNull SplitType splitTypeToUpdate) { + return new SplitAttributes.Builder() + .setSplitType(splitTypeToUpdate) + .setLayoutDirection(splitAttributes.getLayoutDirection()) + .setAnimationBackgroundColor(splitAttributes.getAnimationBackgroundColor()) + .build(); + } + @NonNull private Rect getLeftContainerBounds(@NonNull Configuration taskConfiguration, @NonNull SplitAttributes splitAttributes, @Nullable FoldingFeature foldingFeature) { @@ -898,7 +916,8 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { } @Nullable - private FoldingFeature getFoldingFeature(@NonNull TaskProperties taskProperties) { + @VisibleForTesting + FoldingFeature getFoldingFeature(@NonNull TaskProperties taskProperties) { final int displayId = taskProperties.getDisplayId(); final WindowConfiguration windowConfiguration = taskProperties.getConfiguration() .windowConfiguration; 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 8dd13846ab3d..6981d9d7ebb8 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 @@ -730,6 +730,27 @@ public class SplitPresenterTest { } @Test + public void testComputeSplitAttributesOnHingeSplitTypeOnDeviceWithoutFoldingFeature() { + final SplitAttributes hingeSplitAttrs = new SplitAttributes.Builder() + .setSplitType(new SplitAttributes.SplitType.HingeSplitType( + SplitAttributes.SplitType.RatioSplitType.splitEqually())) + .build(); + final SplitPairRule splitPairRule = createSplitPairRuleBuilder( + activityPair -> true, + activityIntentPair -> true, + windowMetrics -> windowMetrics.getBounds().equals(TASK_BOUNDS)) + .setFinishSecondaryWithPrimary(DEFAULT_FINISH_SECONDARY_WITH_PRIMARY) + .setFinishPrimaryWithSecondary(DEFAULT_FINISH_PRIMARY_WITH_SECONDARY) + .setDefaultSplitAttributes(hingeSplitAttrs) + .build(); + final TaskContainer.TaskProperties taskProperties = getTaskProperties(); + doReturn(null).when(mPresenter).getFoldingFeature(any()); + + assertEquals(hingeSplitAttrs, mPresenter.computeSplitAttributes(taskProperties, + splitPairRule, hingeSplitAttrs, null /* minDimensionsPair */)); + } + + @Test public void testGetTaskWindowMetrics() { final Configuration taskConfig = new Configuration(); taskConfig.windowConfiguration.setBounds(TASK_BOUNDS); diff --git a/libs/WindowManager/Shell/res/layout/tv_pip_menu_background.xml b/libs/WindowManager/Shell/res/layout/tv_pip_menu_background.xml index 5af40200d240..bd48ad2cef44 100644 --- a/libs/WindowManager/Shell/res/layout/tv_pip_menu_background.xml +++ b/libs/WindowManager/Shell/res/layout/tv_pip_menu_background.xml @@ -19,10 +19,11 @@ android:layout_width="match_parent" android:layout_height="match_parent"> <View + android:id="@+id/background_view" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_margin="@dimen/pip_menu_outer_space_frame" android:background="@drawable/tv_pip_menu_background" - android:elevation="@dimen/pip_menu_elevation"/> + android:elevation="@dimen/pip_menu_elevation_no_menu"/> </FrameLayout> diff --git a/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml b/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml index 0b61d7a85d9e..adbf65648dd1 100644 --- a/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml +++ b/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml @@ -36,7 +36,9 @@ <dimen name="pip_menu_arrow_size">24dp</dimen> <dimen name="pip_menu_arrow_elevation">5dp</dimen> - <dimen name="pip_menu_elevation">1dp</dimen> + <dimen name="pip_menu_elevation_no_menu">1dp</dimen> + <dimen name="pip_menu_elevation_move_menu">7dp</dimen> + <dimen name="pip_menu_elevation_all_actions_menu">4dp</dimen> <dimen name="pip_menu_edu_text_view_height">24dp</dimen> <dimen name="pip_menu_edu_text_home_icon">9sp</dimen> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index 4b4b1af3662d..3dbb745f0c6c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -88,7 +88,6 @@ import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.statusbar.IStatusBarService; import com.android.wm.shell.ShellTaskOrganizer; -import com.android.wm.shell.TaskViewTransitions; import com.android.wm.shell.WindowManagerShellWrapper; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.ExternalInterfaceBinder; @@ -110,6 +109,8 @@ import com.android.wm.shell.sysui.ConfigurationChangeListener; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.taskview.TaskView; +import com.android.wm.shell.taskview.TaskViewTransitions; import java.io.PrintWriter; import java.util.ArrayList; @@ -1706,7 +1707,7 @@ public class BubbleController implements ConfigurationChangeListener, /** * Whether an intent is properly configured to display in a - * {@link com.android.wm.shell.TaskView}. + * {@link TaskView}. * * Keep checks in sync with BubbleExtractor#canLaunchInTaskView. Typically * that should filter out any invalid bubbles, but should protect SysUI side just in case. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java index 9ccd6ebc51e2..a317c449621b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java @@ -67,10 +67,10 @@ import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.policy.ScreenDecorationsUtils; import com.android.wm.shell.R; -import com.android.wm.shell.TaskView; -import com.android.wm.shell.TaskViewTaskController; import com.android.wm.shell.common.AlphaOptimizedButton; import com.android.wm.shell.common.TriangleShape; +import com.android.wm.shell.taskview.TaskView; +import com.android.wm.shell.taskview.TaskViewTaskController; import java.io.PrintWriter; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java index 2a3162931648..7a5815994dd0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java @@ -34,10 +34,10 @@ import android.view.View; import androidx.annotation.Nullable; -import com.android.wm.shell.TaskView; -import com.android.wm.shell.TaskViewTaskController; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.annotations.ShellMainThread; +import com.android.wm.shell.taskview.TaskView; +import com.android.wm.shell.taskview.TaskViewTaskController; /** * Handles creating and updating the {@link TaskView} associated with a {@link Bubble}. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index 57b5b8f24fad..9808c591592f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -32,9 +32,6 @@ import com.android.wm.shell.R; import com.android.wm.shell.RootDisplayAreaOrganizer; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; -import com.android.wm.shell.TaskViewFactory; -import com.android.wm.shell.TaskViewFactoryController; -import com.android.wm.shell.TaskViewTransitions; import com.android.wm.shell.WindowManagerShellWrapper; import com.android.wm.shell.activityembedding.ActivityEmbeddingController; import com.android.wm.shell.back.BackAnimation; @@ -93,6 +90,9 @@ import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.sysui.ShellInterface; +import com.android.wm.shell.taskview.TaskViewFactory; +import com.android.wm.shell.taskview.TaskViewFactoryController; +import com.android.wm.shell.taskview.TaskViewTransitions; import com.android.wm.shell.transition.ShellTransitions; import com.android.wm.shell.transition.Transitions; import com.android.wm.shell.unfold.ShellUnfoldProgressProvider; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index f8743ed23aaa..e2cd7a0d1d77 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -30,7 +30,6 @@ import com.android.internal.statusbar.IStatusBarService; import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; -import com.android.wm.shell.TaskViewTransitions; import com.android.wm.shell.WindowManagerShellWrapper; import com.android.wm.shell.bubbles.BubbleController; import com.android.wm.shell.bubbles.BubbleData; @@ -89,6 +88,7 @@ import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.taskview.TaskViewTransitions; import com.android.wm.shell.transition.DefaultMixedHandler; import com.android.wm.shell.transition.Transitions; import com.android.wm.shell.unfold.ShellUnfoldProgressProvider; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBackgroundView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBackgroundView.java new file mode 100644 index 000000000000..0221db836dda --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBackgroundView.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.pip.tv; + +import static com.android.wm.shell.pip.tv.TvPipMenuController.MODE_ALL_ACTIONS_MENU; +import static com.android.wm.shell.pip.tv.TvPipMenuController.MODE_MOVE_MENU; +import static com.android.wm.shell.pip.tv.TvPipMenuController.MODE_NO_MENU; + +import android.content.Context; +import android.content.res.Resources; +import android.view.View; +import android.view.animation.Interpolator; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; + +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.R; +import com.android.wm.shell.protolog.ShellProtoLogGroup; + +/** + * This view is part of the Tv PiP menu. It is drawn behind the PiP surface and serves as a + * background behind the PiP content. If the PiP content is translucent, this view is visible + * behind it. + * It is also used to draw the shadow behind the Tv PiP menu. The shadow intensity is determined + * by the menu mode that the Tv PiP menu is in. See {@link TvPipMenuController.TvPipMenuMode}. + */ +class TvPipBackgroundView extends FrameLayout { + private static final String TAG = "TvPipBackgroundView"; + + private final View mBackgroundView; + private final int mElevationNoMenu; + private final int mElevationMoveMenu; + private final int mElevationAllActionsMenu; + private final int mPipMenuFadeAnimationDuration; + + private @TvPipMenuController.TvPipMenuMode int mCurrentMenuMode = MODE_NO_MENU; + + TvPipBackgroundView(@NonNull Context context) { + super(context, null, 0, 0); + inflate(context, R.layout.tv_pip_menu_background, this); + + mBackgroundView = findViewById(R.id.background_view); + + final Resources res = mContext.getResources(); + mElevationNoMenu = res.getDimensionPixelSize(R.dimen.pip_menu_elevation_no_menu); + mElevationMoveMenu = res.getDimensionPixelSize(R.dimen.pip_menu_elevation_move_menu); + mElevationAllActionsMenu = + res.getDimensionPixelSize(R.dimen.pip_menu_elevation_all_actions_menu); + mPipMenuFadeAnimationDuration = + res.getInteger(R.integer.tv_window_menu_fade_animation_duration); + } + + void transitionToMenuMode(@TvPipMenuController.TvPipMenuMode int pipMenuMode) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: transitionToMenuMode(), old menu mode = %s, new menu mode = %s", + TAG, TvPipMenuController.getMenuModeString(mCurrentMenuMode), + TvPipMenuController.getMenuModeString(pipMenuMode)); + + if (mCurrentMenuMode == pipMenuMode) return; + + int elevation = mElevationNoMenu; + Interpolator interpolator = TvPipInterpolators.ENTER; + switch(pipMenuMode) { + case MODE_NO_MENU: + elevation = mElevationNoMenu; + interpolator = TvPipInterpolators.EXIT; + break; + case MODE_MOVE_MENU: + elevation = mElevationMoveMenu; + break; + case MODE_ALL_ACTIONS_MENU: + elevation = mElevationAllActionsMenu; + if (mCurrentMenuMode == MODE_MOVE_MENU) { + interpolator = TvPipInterpolators.EXIT; + } + break; + default: + throw new IllegalArgumentException( + "Unknown TV PiP menu mode: " + pipMenuMode); + } + + mBackgroundView.animate() + .translationZ(elevation) + .setInterpolator(interpolator) + .setDuration(mPipMenuFadeAnimationDuration) + .start(); + + mCurrentMenuMode = pipMenuMode; + } + +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java index 2c6ca1af62a6..be1f800b9d2e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java @@ -27,7 +27,6 @@ import android.content.IntentFilter; import android.graphics.Insets; import android.graphics.Rect; import android.os.Handler; -import android.view.LayoutInflater; import android.view.SurfaceControl; import android.view.View; import android.view.ViewRootImpl; @@ -61,7 +60,7 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis private Delegate mDelegate; private SurfaceControl mLeash; private TvPipMenuView mPipMenuView; - private View mPipBackgroundView; + private TvPipBackgroundView mPipBackgroundView; private boolean mMenuIsFocused; @TvPipMenuMode @@ -178,12 +177,16 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis } private void attachPipBackgroundView() { - mPipBackgroundView = LayoutInflater.from(mContext) - .inflate(R.layout.tv_pip_menu_background, null); + mPipBackgroundView = createTvPipBackgroundView(); setUpViewSurfaceZOrder(mPipBackgroundView, -1); addPipMenuViewToSystemWindows(mPipBackgroundView, BACKGROUND_WINDOW_TITLE); } + @VisibleForTesting + TvPipBackgroundView createTvPipBackgroundView() { + return new TvPipBackgroundView(mContext); + } + private void setUpViewSurfaceZOrder(View v, int zOrderRelativeToPip) { v.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { @Override @@ -415,10 +418,11 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis } private void updateUiOnNewMenuModeRequest(boolean resetMenu) { - if (mPipMenuView == null) return; + if (mPipMenuView == null || mPipBackgroundView == null) return; mPipMenuView.setPipGravity(mTvPipBoundsState.getTvPipGravity()); mPipMenuView.transitionToMenuMode(mCurrentMenuMode, resetMenu); + mPipBackgroundView.transitionToMenuMode(mCurrentMenuMode); grantPipMenuFocus(mCurrentMenuMode != MODE_NO_MENU); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java index 7a6aec718006..e4d8c32eb5c8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskView.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell; +package com.android.wm.shell.taskview; import android.annotation.NonNull; import android.annotation.Nullable; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewBase.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewBase.java index 3d0a8fd83819..5fdb60d2d342 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewBase.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewBase.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell; +package com.android.wm.shell.taskview; import android.app.ActivityManager; import android.graphics.Rect; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewFactory.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewFactory.java index a29e7a085a21..a7e4b0119480 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewFactory.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewFactory.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell; +package com.android.wm.shell.taskview; import android.annotation.UiContext; import android.content.Context; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewFactoryController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewFactoryController.java index 735d9bce2059..7eed5883043d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewFactoryController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewFactoryController.java @@ -14,11 +14,12 @@ * limitations under the License. */ -package com.android.wm.shell; +package com.android.wm.shell.taskview; import android.annotation.UiContext; import android.content.Context; +import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.annotations.ExternalThread; @@ -51,6 +52,9 @@ public class TaskViewFactoryController { mTaskViewTransitions = null; } + /** + * @return the underlying {@link TaskViewFactory}. + */ public TaskViewFactory asTaskViewFactory() { return mImpl; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewTaskController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java index 080b171f4d40..646d55e4581c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewTaskController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTaskController.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell; +package com.android.wm.shell.taskview; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; @@ -35,6 +35,7 @@ import android.view.SurfaceControl; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; +import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.SyncTransactionQueue; import java.io.PrintWriter; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java index 306d6196c553..9b995c5dc621 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/TaskViewTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell; +package com.android.wm.shell.taskview; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_BACK; 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 6478fe723027..c45e3fc4e0c2 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 @@ -26,6 +26,7 @@ import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Point; import android.graphics.PointF; +import android.graphics.drawable.Drawable; import android.os.Handler; import android.util.Log; import android.view.Choreographer; @@ -38,6 +39,7 @@ import android.widget.ImageView; import android.widget.TextView; import android.window.WindowContainerTransaction; +import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; @@ -81,6 +83,9 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin private final int mHandleMenuCornerRadiusId = R.dimen.caption_menu_corner_radius; private PointF mHandleMenuPosition = new PointF(); + private Drawable mAppIcon; + private CharSequence mAppName; + DesktopModeWindowDecoration( Context context, DisplayController displayController, @@ -95,6 +100,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mHandler = handler; mChoreographer = choreographer; mSyncQueue = syncQueue; + + loadAppInfo(); } @Override @@ -185,7 +192,9 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mWindowDecorViewHolder = new DesktopModeAppControlsWindowDecorationViewHolder( mResult.mRootView, mOnCaptionTouchListener, - mOnCaptionButtonClickListener + mOnCaptionButtonClickListener, + mAppName, + mAppIcon ); } else { throw new IllegalArgumentException("Unexpected layout resource id"); @@ -243,22 +252,24 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin final ImageView appIcon = menu.findViewById(R.id.application_icon); final TextView appName = menu.findViewById(R.id.application_name); - loadAppInfo(appName, appIcon); + appIcon.setImageDrawable(mAppIcon); + appName.setText(mAppName); } boolean isHandleMenuActive() { return mHandleMenu != null; } - private void loadAppInfo(TextView appNameTextView, ImageView appIconImageView) { + private void loadAppInfo() { String packageName = mTaskInfo.realActivity.getPackageName(); PackageManager pm = mContext.getApplicationContext().getPackageManager(); try { - // TODO(b/268363572): Use IconProvider or BaseIconCache to set drawable/name. + IconProvider provider = new IconProvider(mContext); + mAppIcon = provider.getIcon(pm.getActivityInfo(mTaskInfo.baseActivity, + PackageManager.ComponentInfoFlags.of(0))); ApplicationInfo applicationInfo = pm.getApplicationInfo(packageName, PackageManager.ApplicationInfoFlags.of(0)); - appNameTextView.setText(pm.getApplicationLabel(applicationInfo)); - appIconImageView.setImageDrawable(pm.getApplicationIcon(applicationInfo)); + mAppName = pm.getApplicationLabel(applicationInfo); } catch (PackageManager.NameNotFoundException e) { Log.w(TAG, "Package not found: " + packageName, e); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt index 95b5051cb81d..78cfcbd27ed6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/DesktopModeAppControlsWindowDecorationViewHolder.kt @@ -1,10 +1,9 @@ package com.android.wm.shell.windowdecor.viewholder import android.app.ActivityManager.RunningTaskInfo -import android.content.pm.PackageManager import android.content.res.ColorStateList +import android.graphics.drawable.Drawable import android.graphics.drawable.GradientDrawable -import android.util.Log import android.view.View import android.widget.ImageButton import android.widget.ImageView @@ -19,7 +18,9 @@ import com.android.wm.shell.R internal class DesktopModeAppControlsWindowDecorationViewHolder( rootView: View, onCaptionTouchListener: View.OnTouchListener, - onCaptionButtonClickListener: View.OnClickListener + onCaptionButtonClickListener: View.OnClickListener, + appName: CharSequence, + appIcon: Drawable ) : DesktopModeWindowDecorationViewHolder(rootView) { private val captionView: View = rootView.findViewById(R.id.desktop_mode_caption) @@ -35,10 +36,11 @@ internal class DesktopModeAppControlsWindowDecorationViewHolder( captionHandle.setOnTouchListener(onCaptionTouchListener) openMenuButton.setOnClickListener(onCaptionButtonClickListener) closeWindowButton.setOnClickListener(onCaptionButtonClickListener) + appNameTextView.text = appName + appIconImageView.setImageDrawable(appIcon) } override fun bindData(taskInfo: RunningTaskInfo) { - bindAppInfo(taskInfo) val captionDrawable = captionView.background as GradientDrawable captionDrawable.setColor(taskInfo.taskDescription.statusBarColor) @@ -50,20 +52,6 @@ internal class DesktopModeAppControlsWindowDecorationViewHolder( appNameTextView.setTextColor(getCaptionAppNameTextColor(taskInfo)) } - private fun bindAppInfo(taskInfo: RunningTaskInfo) { - val packageName: String = taskInfo.realActivity.packageName - val pm: PackageManager = context.applicationContext.packageManager - try { - // TODO(b/268363572): Use IconProvider or BaseIconCache to set drawable/name. - val applicationInfo = pm.getApplicationInfo(packageName, - PackageManager.ApplicationInfoFlags.of(0)) - appNameTextView.text = pm.getApplicationLabel(applicationInfo) - appIconImageView.setImageDrawable(pm.getApplicationIcon(applicationInfo)) - } catch (e: PackageManager.NameNotFoundException) { - Log.w(TAG, "Package not found: $packageName", e) - } - } - private fun getCaptionAppNameTextColor(taskInfo: RunningTaskInfo): Int { return if (shouldUseLightCaptionColors(taskInfo)) { context.getColor(R.color.desktop_mode_caption_app_name_light) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipMenuControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipMenuControllerTest.java index 7c6037c96360..3a08d32bc430 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipMenuControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipMenuControllerTest.java @@ -57,6 +57,8 @@ public class TvPipMenuControllerTest extends ShellTestCase { private TvPipActionsProvider mMockActionsProvider; @Mock private TvPipMenuView mMockTvPipMenuView; + @Mock + private TvPipBackgroundView mMockTvPipBackgroundView; private TvPipMenuController mTvPipMenuController; @@ -173,6 +175,7 @@ public class TvPipMenuControllerTest extends ShellTestCase { assertMenuIsInAllActionsMode(); verify(mMockDelegate, times(2)).onInMoveModeChanged(); verify(mMockTvPipMenuView).transitionToMenuMode(eq(MODE_ALL_ACTIONS_MENU), eq(false)); + verify(mMockTvPipBackgroundView, times(2)).transitionToMenuMode(eq(MODE_ALL_ACTIONS_MENU)); } @Test @@ -215,6 +218,7 @@ public class TvPipMenuControllerTest extends ShellTestCase { assertMenuIsInAllActionsMode(); verify(mMockDelegate, times(2)).onInMoveModeChanged(); verify(mMockTvPipMenuView).transitionToMenuMode(eq(MODE_ALL_ACTIONS_MENU), eq(false)); + verify(mMockTvPipBackgroundView, times(2)).transitionToMenuMode(eq(MODE_ALL_ACTIONS_MENU)); pressBackAndAssertMenuClosed(); } @@ -262,12 +266,14 @@ public class TvPipMenuControllerTest extends ShellTestCase { assertMenuIsInMoveMode(); verify(mMockDelegate).onInMoveModeChanged(); verify(mMockTvPipMenuView).transitionToMenuMode(eq(MODE_MOVE_MENU), eq(false)); + verify(mMockTvPipBackgroundView).transitionToMenuMode(eq(MODE_MOVE_MENU)); } private void showAndAssertAllActionsMenu() { mTvPipMenuController.showMenu(); assertMenuIsInAllActionsMode(); verify(mMockTvPipMenuView).transitionToMenuMode(eq(MODE_ALL_ACTIONS_MENU), eq(true)); + verify(mMockTvPipBackgroundView).transitionToMenuMode(eq(MODE_ALL_ACTIONS_MENU)); } private void closeMenuAndAssertMenuClosed() { @@ -284,6 +290,7 @@ public class TvPipMenuControllerTest extends ShellTestCase { assertMenuIsOpen(false); verify(mMockDelegate).onMenuClosed(); verify(mMockTvPipMenuView).transitionToMenuMode(eq(MODE_NO_MENU), eq(false)); + verify(mMockTvPipBackgroundView).transitionToMenuMode(eq(MODE_NO_MENU)); } private void assertMenuIsOpen(boolean open) { @@ -320,5 +327,10 @@ public class TvPipMenuControllerTest extends ShellTestCase { TvPipMenuView createTvPipMenuView() { return mMockTvPipMenuView; } + + @Override + TvPipBackgroundView createTvPipBackgroundView() { + return mMockTvPipBackgroundView; + } } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TaskViewTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java index 62bfd17cefba..b6d7ff3cd5cf 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/TaskViewTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell; +package com.android.wm.shell.taskview; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; @@ -53,6 +53,8 @@ import android.window.WindowContainerTransaction; import androidx.test.filters.SmallTest; +import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.HandlerExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.SyncTransactionQueue.TransactionRunnable; diff --git a/libs/dream/lowlight/Android.bp b/libs/dream/lowlight/Android.bp index 5b5b0f07cabd..e4d2e022cd76 100644 --- a/libs/dream/lowlight/Android.bp +++ b/libs/dream/lowlight/Android.bp @@ -25,6 +25,7 @@ filegroup { name: "low_light_dream_lib-sources", srcs: [ "src/**/*.java", + "src/**/*.kt", ], path: "src", } @@ -37,10 +38,15 @@ android_library { resource_dirs: [ "res", ], + libs: [ + "kotlin-annotations", + ], static_libs: [ "androidx.arch.core_core-runtime", "dagger2", "jsr330", + "kotlinx-coroutines-android", + "kotlinx-coroutines-core", ], manifest: "AndroidManifest.xml", plugins: ["dagger2-compiler"], diff --git a/libs/dream/lowlight/res/values/config.xml b/libs/dream/lowlight/res/values/config.xml index 70fe0738a6f4..78fefbf41141 100644 --- a/libs/dream/lowlight/res/values/config.xml +++ b/libs/dream/lowlight/res/values/config.xml @@ -17,4 +17,7 @@ <resources> <!-- The dream component used when the device is low light environment. --> <string translatable="false" name="config_lowLightDreamComponent"/> + <!-- The max number of milliseconds to wait for the low light transition before setting + the system dream component --> + <integer name="config_lowLightTransitionTimeoutMs">2000</integer> </resources> diff --git a/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.java b/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.java deleted file mode 100644 index 3125f088c72b..000000000000 --- a/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.java +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.dream.lowlight; - -import static com.android.dream.lowlight.dagger.LowLightDreamModule.LOW_LIGHT_DREAM_COMPONENT; - -import android.annotation.IntDef; -import android.annotation.RequiresPermission; -import android.app.DreamManager; -import android.content.ComponentName; -import android.util.Log; - -import androidx.annotation.Nullable; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -import javax.inject.Inject; -import javax.inject.Named; - -/** - * Maintains the ambient light mode of the environment the device is in, and sets a low light dream - * component, if present, as the system dream when the ambient light mode is low light. - * - * @hide - */ -public final class LowLightDreamManager { - private static final String TAG = "LowLightDreamManager"; - private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); - - /** - * @hide - */ - @Retention(RetentionPolicy.SOURCE) - @IntDef(prefix = { "AMBIENT_LIGHT_MODE_" }, value = { - AMBIENT_LIGHT_MODE_UNKNOWN, - AMBIENT_LIGHT_MODE_REGULAR, - AMBIENT_LIGHT_MODE_LOW_LIGHT - }) - public @interface AmbientLightMode {} - - /** - * Constant for ambient light mode being unknown. - * @hide - */ - public static final int AMBIENT_LIGHT_MODE_UNKNOWN = 0; - - /** - * Constant for ambient light mode being regular / bright. - * @hide - */ - public static final int AMBIENT_LIGHT_MODE_REGULAR = 1; - - /** - * Constant for ambient light mode being low light / dim. - * @hide - */ - public static final int AMBIENT_LIGHT_MODE_LOW_LIGHT = 2; - - private final DreamManager mDreamManager; - private final LowLightTransitionCoordinator mLowLightTransitionCoordinator; - - @Nullable - private final ComponentName mLowLightDreamComponent; - - private int mAmbientLightMode = AMBIENT_LIGHT_MODE_UNKNOWN; - - @Inject - public LowLightDreamManager( - DreamManager dreamManager, - LowLightTransitionCoordinator lowLightTransitionCoordinator, - @Named(LOW_LIGHT_DREAM_COMPONENT) @Nullable ComponentName lowLightDreamComponent) { - mDreamManager = dreamManager; - mLowLightTransitionCoordinator = lowLightTransitionCoordinator; - mLowLightDreamComponent = lowLightDreamComponent; - } - - /** - * Sets the current ambient light mode. - * @hide - */ - @RequiresPermission(android.Manifest.permission.WRITE_DREAM_STATE) - public void setAmbientLightMode(@AmbientLightMode int ambientLightMode) { - if (mLowLightDreamComponent == null) { - if (DEBUG) { - Log.d(TAG, "ignore ambient light mode change because low light dream component " - + "is empty"); - } - return; - } - - if (mAmbientLightMode == ambientLightMode) { - return; - } - - if (DEBUG) { - Log.d(TAG, "ambient light mode changed from " + mAmbientLightMode + " to " - + ambientLightMode); - } - - mAmbientLightMode = ambientLightMode; - - boolean shouldEnterLowLight = mAmbientLightMode == AMBIENT_LIGHT_MODE_LOW_LIGHT; - mLowLightTransitionCoordinator.notifyBeforeLowLightTransition(shouldEnterLowLight, - () -> mDreamManager.setSystemDreamComponent( - shouldEnterLowLight ? mLowLightDreamComponent : null)); - } -} diff --git a/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.kt b/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.kt new file mode 100644 index 000000000000..96bfb78eff0d --- /dev/null +++ b/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.kt @@ -0,0 +1,138 @@ +/* + * 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.dream.lowlight + +import android.Manifest +import android.annotation.IntDef +import android.annotation.RequiresPermission +import android.app.DreamManager +import android.content.ComponentName +import android.util.Log +import com.android.dream.lowlight.dagger.LowLightDreamModule +import com.android.dream.lowlight.dagger.qualifiers.Application +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.launch +import javax.inject.Inject +import javax.inject.Named +import kotlin.time.DurationUnit +import kotlin.time.toDuration + +/** + * Maintains the ambient light mode of the environment the device is in, and sets a low light dream + * component, if present, as the system dream when the ambient light mode is low light. + * + * @hide + */ +class LowLightDreamManager @Inject constructor( + @Application private val coroutineScope: CoroutineScope, + private val dreamManager: DreamManager, + private val lowLightTransitionCoordinator: LowLightTransitionCoordinator, + @param:Named(LowLightDreamModule.LOW_LIGHT_DREAM_COMPONENT) + private val lowLightDreamComponent: ComponentName?, + @param:Named(LowLightDreamModule.LOW_LIGHT_TRANSITION_TIMEOUT_MS) + private val lowLightTransitionTimeoutMs: Long +) { + /** + * @hide + */ + @Retention(AnnotationRetention.SOURCE) + @IntDef( + prefix = ["AMBIENT_LIGHT_MODE_"], + value = [ + AMBIENT_LIGHT_MODE_UNKNOWN, + AMBIENT_LIGHT_MODE_REGULAR, + AMBIENT_LIGHT_MODE_LOW_LIGHT + ] + ) + annotation class AmbientLightMode + + private var mTransitionJob: Job? = null + private var mAmbientLightMode = AMBIENT_LIGHT_MODE_UNKNOWN + private val mLowLightTransitionTimeout = + lowLightTransitionTimeoutMs.toDuration(DurationUnit.MILLISECONDS) + + /** + * Sets the current ambient light mode. + * + * @hide + */ + @RequiresPermission(Manifest.permission.WRITE_DREAM_STATE) + fun setAmbientLightMode(@AmbientLightMode ambientLightMode: Int) { + if (lowLightDreamComponent == null) { + if (DEBUG) { + Log.d( + TAG, + "ignore ambient light mode change because low light dream component is empty" + ) + } + return + } + if (mAmbientLightMode == ambientLightMode) { + return + } + if (DEBUG) { + Log.d( + TAG, "ambient light mode changed from $mAmbientLightMode to $ambientLightMode" + ) + } + mAmbientLightMode = ambientLightMode + val shouldEnterLowLight = mAmbientLightMode == AMBIENT_LIGHT_MODE_LOW_LIGHT + + // Cancel any previous transitions + mTransitionJob?.cancel() + mTransitionJob = coroutineScope.launch { + try { + lowLightTransitionCoordinator.waitForLowLightTransitionAnimation( + timeout = mLowLightTransitionTimeout, + entering = shouldEnterLowLight + ) + } catch (ex: TimeoutCancellationException) { + Log.e(TAG, "timed out while waiting for low light animation", ex) + } + dreamManager.setSystemDreamComponent( + if (shouldEnterLowLight) lowLightDreamComponent else null + ) + } + } + + companion object { + private const val TAG = "LowLightDreamManager" + private val DEBUG = Log.isLoggable(TAG, Log.DEBUG) + + /** + * Constant for ambient light mode being unknown. + * + * @hide + */ + const val AMBIENT_LIGHT_MODE_UNKNOWN = 0 + + /** + * Constant for ambient light mode being regular / bright. + * + * @hide + */ + const val AMBIENT_LIGHT_MODE_REGULAR = 1 + + /** + * Constant for ambient light mode being low light / dim. + * + * @hide + */ + const val AMBIENT_LIGHT_MODE_LOW_LIGHT = 2 + } +} diff --git a/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightTransitionCoordinator.java b/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightTransitionCoordinator.java deleted file mode 100644 index 874a2d5af75e..000000000000 --- a/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightTransitionCoordinator.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * 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.dream.lowlight; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.annotation.Nullable; - -import javax.inject.Inject; -import javax.inject.Singleton; - -/** - * Helper class that allows listening and running animations before entering or exiting low light. - */ -@Singleton -public class LowLightTransitionCoordinator { - /** - * Listener that is notified before low light entry. - */ - public interface LowLightEnterListener { - /** - * Callback that is notified before the device enters low light. - * - * @return an optional animator that will be waited upon before entering low light. - */ - Animator onBeforeEnterLowLight(); - } - - /** - * Listener that is notified before low light exit. - */ - public interface LowLightExitListener { - /** - * Callback that is notified before the device exits low light. - * - * @return an optional animator that will be waited upon before exiting low light. - */ - Animator onBeforeExitLowLight(); - } - - private LowLightEnterListener mLowLightEnterListener; - private LowLightExitListener mLowLightExitListener; - - @Inject - public LowLightTransitionCoordinator() { - } - - /** - * Sets the listener for the low light enter event. - * - * Only one listener can be set at a time. This method will overwrite any previously set - * listener. Null can be used to unset the listener. - */ - public void setLowLightEnterListener(@Nullable LowLightEnterListener lowLightEnterListener) { - mLowLightEnterListener = lowLightEnterListener; - } - - /** - * Sets the listener for the low light exit event. - * - * Only one listener can be set at a time. This method will overwrite any previously set - * listener. Null can be used to unset the listener. - */ - public void setLowLightExitListener(@Nullable LowLightExitListener lowLightExitListener) { - mLowLightExitListener = lowLightExitListener; - } - - /** - * Notifies listeners that the device is about to enter or exit low light. - * - * @param entering true if listeners should be notified before entering low light, false if this - * is notifying before exiting. - * @param callback callback that will be run after listeners complete. - */ - void notifyBeforeLowLightTransition(boolean entering, Runnable callback) { - Animator animator = null; - - if (entering && mLowLightEnterListener != null) { - animator = mLowLightEnterListener.onBeforeEnterLowLight(); - } else if (!entering && mLowLightExitListener != null) { - animator = mLowLightExitListener.onBeforeExitLowLight(); - } - - // If the listener returned an animator to indicate it was running an animation, run the - // callback after the animation completes, otherwise call the callback directly. - if (animator != null) { - animator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animator) { - callback.run(); - } - }); - } else { - callback.run(); - } - } -} diff --git a/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightTransitionCoordinator.kt b/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightTransitionCoordinator.kt new file mode 100644 index 000000000000..26efb55fa560 --- /dev/null +++ b/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightTransitionCoordinator.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.dream.lowlight + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import com.android.dream.lowlight.util.suspendCoroutineWithTimeout +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.coroutines.resume +import kotlin.time.Duration + +/** + * Helper class that allows listening and running animations before entering or exiting low light. + */ +@Singleton +class LowLightTransitionCoordinator @Inject constructor() { + /** + * Listener that is notified before low light entry. + */ + interface LowLightEnterListener { + /** + * Callback that is notified before the device enters low light. + * + * @return an optional animator that will be waited upon before entering low light. + */ + fun onBeforeEnterLowLight(): Animator? + } + + /** + * Listener that is notified before low light exit. + */ + interface LowLightExitListener { + /** + * Callback that is notified before the device exits low light. + * + * @return an optional animator that will be waited upon before exiting low light. + */ + fun onBeforeExitLowLight(): Animator? + } + + private var mLowLightEnterListener: LowLightEnterListener? = null + private var mLowLightExitListener: LowLightExitListener? = null + + /** + * Sets the listener for the low light enter event. + * + * Only one listener can be set at a time. This method will overwrite any previously set + * listener. Null can be used to unset the listener. + */ + fun setLowLightEnterListener(lowLightEnterListener: LowLightEnterListener?) { + mLowLightEnterListener = lowLightEnterListener + } + + /** + * Sets the listener for the low light exit event. + * + * Only one listener can be set at a time. This method will overwrite any previously set + * listener. Null can be used to unset the listener. + */ + fun setLowLightExitListener(lowLightExitListener: LowLightExitListener?) { + mLowLightExitListener = lowLightExitListener + } + + /** + * Notifies listeners that the device is about to enter or exit low light, and waits for the + * animation to complete. If this function is cancelled, the animation is also cancelled. + * + * @param timeout the maximum duration to wait for the transition animation. If the animation + * does not complete within this time period, a + * @param entering true if listeners should be notified before entering low light, false if this + * is notifying before exiting. + */ + suspend fun waitForLowLightTransitionAnimation(timeout: Duration, entering: Boolean) = + suspendCoroutineWithTimeout(timeout) { continuation -> + var animator: Animator? = null + if (entering && mLowLightEnterListener != null) { + animator = mLowLightEnterListener!!.onBeforeEnterLowLight() + } else if (!entering && mLowLightExitListener != null) { + animator = mLowLightExitListener!!.onBeforeExitLowLight() + } + + if (animator == null) { + continuation.resume(Unit) + return@suspendCoroutineWithTimeout + } + + // If the listener returned an animator to indicate it was running an animation, run the + // callback after the animation completes, otherwise call the callback directly. + val listener = object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animator: Animator) { + continuation.resume(Unit) + } + + override fun onAnimationCancel(animation: Animator) { + continuation.cancel() + } + } + animator.addListener(listener) + continuation.invokeOnCancellation { + animator.removeListener(listener) + animator.cancel() + } + } +} diff --git a/libs/dream/lowlight/src/com/android/dream/lowlight/dagger/LowLightDreamModule.java b/libs/dream/lowlight/src/com/android/dream/lowlight/dagger/LowLightDreamModule.java deleted file mode 100644 index c183a04cb2f9..000000000000 --- a/libs/dream/lowlight/src/com/android/dream/lowlight/dagger/LowLightDreamModule.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.dream.lowlight.dagger; - -import android.app.DreamManager; -import android.content.ComponentName; -import android.content.Context; - -import androidx.annotation.Nullable; - -import com.android.dream.lowlight.R; - -import javax.inject.Named; - -import dagger.Module; -import dagger.Provides; - -/** - * Dagger module for low light dream. - * - * @hide - */ -@Module -public interface LowLightDreamModule { - String LOW_LIGHT_DREAM_COMPONENT = "low_light_dream_component"; - - /** - * Provides dream manager. - */ - @Provides - static DreamManager providesDreamManager(Context context) { - return context.getSystemService(DreamManager.class); - } - - /** - * Provides the component name of the low light dream, or null if not configured. - */ - @Provides - @Named(LOW_LIGHT_DREAM_COMPONENT) - @Nullable - static ComponentName providesLowLightDreamComponent(Context context) { - final String lowLightDreamComponent = context.getResources().getString( - R.string.config_lowLightDreamComponent); - return lowLightDreamComponent.isEmpty() ? null - : ComponentName.unflattenFromString(lowLightDreamComponent); - } -} diff --git a/libs/dream/lowlight/src/com/android/dream/lowlight/dagger/LowLightDreamModule.kt b/libs/dream/lowlight/src/com/android/dream/lowlight/dagger/LowLightDreamModule.kt new file mode 100644 index 000000000000..dd274bd9d509 --- /dev/null +++ b/libs/dream/lowlight/src/com/android/dream/lowlight/dagger/LowLightDreamModule.kt @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.dream.lowlight.dagger + +import android.app.DreamManager +import android.content.ComponentName +import android.content.Context +import com.android.dream.lowlight.R +import com.android.dream.lowlight.dagger.qualifiers.Application +import com.android.dream.lowlight.dagger.qualifiers.Main +import dagger.Module +import dagger.Provides +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import javax.inject.Named + +/** + * Dagger module for low light dream. + * + * @hide + */ +@Module +object LowLightDreamModule { + /** + * Provides dream manager. + */ + @Provides + fun providesDreamManager(context: Context): DreamManager { + return requireNotNull(context.getSystemService(DreamManager::class.java)) + } + + /** + * Provides the component name of the low light dream, or null if not configured. + */ + @Provides + @Named(LOW_LIGHT_DREAM_COMPONENT) + fun providesLowLightDreamComponent(context: Context): ComponentName? { + val lowLightDreamComponent = context.resources.getString( + R.string.config_lowLightDreamComponent + ) + return if (lowLightDreamComponent.isEmpty()) { + null + } else { + ComponentName.unflattenFromString(lowLightDreamComponent) + } + } + + @Provides + @Named(LOW_LIGHT_TRANSITION_TIMEOUT_MS) + fun providesLowLightTransitionTimeout(context: Context): Long { + return context.resources.getInteger(R.integer.config_lowLightTransitionTimeoutMs).toLong() + } + + @Provides + @Main + fun providesMainDispatcher(): CoroutineDispatcher { + return Dispatchers.Main.immediate + } + + @Provides + @Application + fun providesApplicationScope(@Main dispatcher: CoroutineDispatcher): CoroutineScope { + return CoroutineScope(dispatcher) + } + + const val LOW_LIGHT_DREAM_COMPONENT = "low_light_dream_component" + const val LOW_LIGHT_TRANSITION_TIMEOUT_MS = "low_light_transition_timeout" +} diff --git a/libs/dream/lowlight/src/com/android/dream/lowlight/dagger/qualifiers/Application.kt b/libs/dream/lowlight/src/com/android/dream/lowlight/dagger/qualifiers/Application.kt new file mode 100644 index 000000000000..541fe4017e6e --- /dev/null +++ b/libs/dream/lowlight/src/com/android/dream/lowlight/dagger/qualifiers/Application.kt @@ -0,0 +1,9 @@ +package com.android.dream.lowlight.dagger.qualifiers + +import android.content.Context +import javax.inject.Qualifier + +/** + * Used to qualify a context as [Context.getApplicationContext] + */ +@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class Application diff --git a/libs/dream/lowlight/src/com/android/dream/lowlight/dagger/qualifiers/Main.kt b/libs/dream/lowlight/src/com/android/dream/lowlight/dagger/qualifiers/Main.kt new file mode 100644 index 000000000000..ccd0710bdc60 --- /dev/null +++ b/libs/dream/lowlight/src/com/android/dream/lowlight/dagger/qualifiers/Main.kt @@ -0,0 +1,8 @@ +package com.android.dream.lowlight.dagger.qualifiers + +import javax.inject.Qualifier + +/** + * Used to qualify code running on the main thread. + */ +@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class Main diff --git a/libs/dream/lowlight/src/com/android/dream/lowlight/util/KotlinUtils.kt b/libs/dream/lowlight/src/com/android/dream/lowlight/util/KotlinUtils.kt new file mode 100644 index 000000000000..ff675ccfaffb --- /dev/null +++ b/libs/dream/lowlight/src/com/android/dream/lowlight/util/KotlinUtils.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dream.lowlight.util + +import kotlinx.coroutines.CancellableContinuation +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withTimeout +import kotlin.time.Duration + +suspend inline fun <T> suspendCoroutineWithTimeout( + timeout: Duration, + crossinline block: (CancellableContinuation<T>) -> Unit +) = withTimeout(timeout) { + suspendCancellableCoroutine(block = block) +} diff --git a/libs/dream/lowlight/tests/Android.bp b/libs/dream/lowlight/tests/Android.bp index bd6f05eabac5..2d79090cd7d4 100644 --- a/libs/dream/lowlight/tests/Android.bp +++ b/libs/dream/lowlight/tests/Android.bp @@ -20,6 +20,7 @@ android_test { name: "LowLightDreamTests", srcs: [ "**/*.java", + "**/*.kt", ], static_libs: [ "LowLightDreamLib", @@ -28,6 +29,7 @@ android_test { "androidx.test.ext.junit", "frameworks-base-testutils", "junit", + "kotlinx_coroutines_test", "mockito-target-extended-minus-junit4", "platform-test-annotations", "testables", diff --git a/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightDreamManagerTest.java b/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightDreamManagerTest.java deleted file mode 100644 index 4b95d8c84bac..000000000000 --- a/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightDreamManagerTest.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.dream.lowlight; - -import static com.android.dream.lowlight.LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT; -import static com.android.dream.lowlight.LowLightDreamManager.AMBIENT_LIGHT_MODE_REGULAR; -import static com.android.dream.lowlight.LowLightDreamManager.AMBIENT_LIGHT_MODE_UNKNOWN; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.clearInvocations; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; - -import android.app.DreamManager; -import android.content.ComponentName; -import android.testing.AndroidTestingRunner; - -import androidx.test.filters.SmallTest; - -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) -public class LowLightDreamManagerTest { - @Mock - private DreamManager mDreamManager; - - @Mock - private LowLightTransitionCoordinator mTransitionCoordinator; - - @Mock - private ComponentName mDreamComponent; - - LowLightDreamManager mLowLightDreamManager; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - - // Automatically run any provided Runnable to mTransitionCoordinator to simplify testing. - doAnswer(invocation -> { - ((Runnable) invocation.getArgument(1)).run(); - return null; - }).when(mTransitionCoordinator).notifyBeforeLowLightTransition(anyBoolean(), - any(Runnable.class)); - - mLowLightDreamManager = new LowLightDreamManager(mDreamManager, mTransitionCoordinator, - mDreamComponent); - } - - @Test - public void setAmbientLightMode_lowLight_setSystemDream() { - mLowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_LOW_LIGHT); - - verify(mTransitionCoordinator).notifyBeforeLowLightTransition(eq(true), any()); - verify(mDreamManager).setSystemDreamComponent(mDreamComponent); - } - - @Test - public void setAmbientLightMode_regularLight_clearSystemDream() { - mLowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_REGULAR); - - verify(mTransitionCoordinator).notifyBeforeLowLightTransition(eq(false), any()); - verify(mDreamManager).setSystemDreamComponent(null); - } - - @Test - public void setAmbientLightMode_defaultUnknownMode_clearSystemDream() { - // Set to low light first. - mLowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_LOW_LIGHT); - clearInvocations(mDreamManager); - - // Return to default unknown mode. - mLowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_UNKNOWN); - - verify(mDreamManager).setSystemDreamComponent(null); - } - - @Test - public void setAmbientLightMode_dreamComponentNotSet_doNothing() { - final LowLightDreamManager lowLightDreamManager = new LowLightDreamManager(mDreamManager, - mTransitionCoordinator, null /*dream component*/); - - lowLightDreamManager.setAmbientLightMode(AMBIENT_LIGHT_MODE_LOW_LIGHT); - - verify(mDreamManager, never()).setSystemDreamComponent(any()); - } -} diff --git a/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightDreamManagerTest.kt b/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightDreamManagerTest.kt new file mode 100644 index 000000000000..2a886bc31788 --- /dev/null +++ b/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightDreamManagerTest.kt @@ -0,0 +1,169 @@ +/* + * 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.dream.lowlight + +import android.animation.Animator +import android.app.DreamManager +import android.content.ComponentName +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.clearInvocations +import org.mockito.Mockito.never +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations +import src.com.android.dream.lowlight.utils.any +import src.com.android.dream.lowlight.utils.withArgCaptor + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidTestingRunner::class) +class LowLightDreamManagerTest { + @Mock + private lateinit var mDreamManager: DreamManager + @Mock + private lateinit var mEnterAnimator: Animator + @Mock + private lateinit var mExitAnimator: Animator + + private lateinit var mTransitionCoordinator: LowLightTransitionCoordinator + private lateinit var mLowLightDreamManager: LowLightDreamManager + private lateinit var testScope: TestScope + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + testScope = TestScope(StandardTestDispatcher()) + + mTransitionCoordinator = LowLightTransitionCoordinator() + mTransitionCoordinator.setLowLightEnterListener( + object : LowLightTransitionCoordinator.LowLightEnterListener { + override fun onBeforeEnterLowLight() = mEnterAnimator + }) + mTransitionCoordinator.setLowLightExitListener( + object : LowLightTransitionCoordinator.LowLightExitListener { + override fun onBeforeExitLowLight() = mExitAnimator + }) + + mLowLightDreamManager = LowLightDreamManager( + coroutineScope = testScope, + dreamManager = mDreamManager, + lowLightTransitionCoordinator = mTransitionCoordinator, + lowLightDreamComponent = DREAM_COMPONENT, + lowLightTransitionTimeoutMs = LOW_LIGHT_TIMEOUT_MS + ) + } + + @Test + fun setAmbientLightMode_lowLight_setSystemDream() = testScope.runTest { + mLowLightDreamManager.setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT) + runCurrent() + verify(mDreamManager, never()).setSystemDreamComponent(DREAM_COMPONENT) + completeEnterAnimations() + runCurrent() + verify(mDreamManager).setSystemDreamComponent(DREAM_COMPONENT) + } + + @Test + fun setAmbientLightMode_regularLight_clearSystemDream() = testScope.runTest { + mLowLightDreamManager.setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_REGULAR) + runCurrent() + verify(mDreamManager, never()).setSystemDreamComponent(null) + completeExitAnimations() + runCurrent() + verify(mDreamManager).setSystemDreamComponent(null) + } + + @Test + fun setAmbientLightMode_defaultUnknownMode_clearSystemDream() = testScope.runTest { + // Set to low light first. + mLowLightDreamManager.setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT) + runCurrent() + completeEnterAnimations() + runCurrent() + verify(mDreamManager).setSystemDreamComponent(DREAM_COMPONENT) + clearInvocations(mDreamManager) + + // Return to default unknown mode. + mLowLightDreamManager.setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_UNKNOWN) + runCurrent() + completeExitAnimations() + runCurrent() + verify(mDreamManager).setSystemDreamComponent(null) + } + + @Test + fun setAmbientLightMode_dreamComponentNotSet_doNothing() = testScope.runTest { + val lowLightDreamManager = LowLightDreamManager( + coroutineScope = testScope, + dreamManager = mDreamManager, + lowLightTransitionCoordinator = mTransitionCoordinator, + lowLightDreamComponent = null, + lowLightTransitionTimeoutMs = LOW_LIGHT_TIMEOUT_MS + ) + lowLightDreamManager.setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT) + runCurrent() + verify(mEnterAnimator, never()).addListener(any()) + verify(mDreamManager, never()).setSystemDreamComponent(any()) + } + + @Test + fun setAmbientLightMode_multipleTimesBeforeAnimationEnds_cancelsPrevious() = testScope.runTest { + mLowLightDreamManager.setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT) + runCurrent() + // If we reset the light mode back to regular before the previous animation finishes, it + // should be ignored. + mLowLightDreamManager.setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_REGULAR) + runCurrent() + completeEnterAnimations() + completeExitAnimations() + runCurrent() + verify(mDreamManager, times(1)).setSystemDreamComponent(null) + } + + @Test + fun setAmbientLightMode_animatorNeverFinishes_timesOut() = testScope.runTest { + mLowLightDreamManager.setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT) + advanceTimeBy(delayTimeMillis = LOW_LIGHT_TIMEOUT_MS + 1) + // Animation never finishes, but we should still set the system dream + verify(mDreamManager).setSystemDreamComponent(DREAM_COMPONENT) + } + + private fun completeEnterAnimations() { + val listener = withArgCaptor { verify(mEnterAnimator).addListener(capture()) } + listener.onAnimationEnd(mEnterAnimator) + } + + private fun completeExitAnimations() { + val listener = withArgCaptor { verify(mExitAnimator).addListener(capture()) } + listener.onAnimationEnd(mExitAnimator) + } + + companion object { + private val DREAM_COMPONENT = ComponentName("test_package", "test_dream") + private const val LOW_LIGHT_TIMEOUT_MS: Long = 1000 + } +}
\ No newline at end of file diff --git a/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightTransitionCoordinatorTest.java b/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightTransitionCoordinatorTest.java deleted file mode 100644 index 81e1e33d6220..000000000000 --- a/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightTransitionCoordinatorTest.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * 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.dream.lowlight; - -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyZeroInteractions; -import static org.mockito.Mockito.when; - -import android.animation.Animator; -import android.testing.AndroidTestingRunner; - -import androidx.test.filters.SmallTest; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -@SmallTest -@RunWith(AndroidTestingRunner.class) -public class LowLightTransitionCoordinatorTest { - @Mock - private LowLightTransitionCoordinator.LowLightEnterListener mEnterListener; - - @Mock - private LowLightTransitionCoordinator.LowLightExitListener mExitListener; - - @Mock - private Animator mAnimator; - - @Captor - private ArgumentCaptor<Animator.AnimatorListener> mAnimatorListenerCaptor; - - @Mock - private Runnable mRunnable; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - } - - @Test - public void onEnterCalledOnListeners() { - LowLightTransitionCoordinator coordinator = new LowLightTransitionCoordinator(); - - coordinator.setLowLightEnterListener(mEnterListener); - - coordinator.notifyBeforeLowLightTransition(true, mRunnable); - - verify(mEnterListener).onBeforeEnterLowLight(); - verify(mRunnable).run(); - } - - @Test - public void onExitCalledOnListeners() { - LowLightTransitionCoordinator coordinator = new LowLightTransitionCoordinator(); - - coordinator.setLowLightExitListener(mExitListener); - - coordinator.notifyBeforeLowLightTransition(false, mRunnable); - - verify(mExitListener).onBeforeExitLowLight(); - verify(mRunnable).run(); - } - - @Test - public void listenerNotCalledAfterRemoval() { - LowLightTransitionCoordinator coordinator = new LowLightTransitionCoordinator(); - - coordinator.setLowLightEnterListener(mEnterListener); - coordinator.setLowLightEnterListener(null); - - coordinator.notifyBeforeLowLightTransition(true, mRunnable); - - verifyZeroInteractions(mEnterListener); - verify(mRunnable).run(); - } - - @Test - public void runnableCalledAfterAnimationEnds() { - when(mEnterListener.onBeforeEnterLowLight()).thenReturn(mAnimator); - - LowLightTransitionCoordinator coordinator = new LowLightTransitionCoordinator(); - coordinator.setLowLightEnterListener(mEnterListener); - - coordinator.notifyBeforeLowLightTransition(true, mRunnable); - - // Animator listener is added and the runnable is not run yet. - verify(mAnimator).addListener(mAnimatorListenerCaptor.capture()); - verifyZeroInteractions(mRunnable); - - // Runnable is run once the animation ends. - mAnimatorListenerCaptor.getValue().onAnimationEnd(null); - verify(mRunnable).run(); - } -} diff --git a/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightTransitionCoordinatorTest.kt b/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightTransitionCoordinatorTest.kt new file mode 100644 index 000000000000..4c526a6ac69d --- /dev/null +++ b/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightTransitionCoordinatorTest.kt @@ -0,0 +1,184 @@ +/* + * 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.dream.lowlight + +import android.animation.Animator +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.dream.lowlight.LowLightTransitionCoordinator.LowLightEnterListener +import com.android.dream.lowlight.LowLightTransitionCoordinator.LowLightExitListener +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Captor +import org.mockito.Mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations +import src.com.android.dream.lowlight.utils.whenever +import kotlin.time.DurationUnit +import kotlin.time.toDuration + +@SmallTest +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidTestingRunner::class) +class LowLightTransitionCoordinatorTest { + @Mock + private lateinit var mEnterListener: LowLightEnterListener + + @Mock + private lateinit var mExitListener: LowLightExitListener + + @Mock + private lateinit var mAnimator: Animator + + @Captor + private lateinit var mAnimatorListenerCaptor: ArgumentCaptor<Animator.AnimatorListener> + + private lateinit var testScope: TestScope + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + testScope = TestScope(StandardTestDispatcher()) + } + + @Test + fun onEnterCalledOnListeners() = testScope.runTest { + val coordinator = LowLightTransitionCoordinator() + coordinator.setLowLightEnterListener(mEnterListener) + val job = launch { + coordinator.waitForLowLightTransitionAnimation(timeout = TIMEOUT, entering = true) + } + runCurrent() + verify(mEnterListener).onBeforeEnterLowLight() + assertThat(job.isCompleted).isTrue() + } + + @Test + fun onExitCalledOnListeners() = testScope.runTest { + val coordinator = LowLightTransitionCoordinator() + coordinator.setLowLightExitListener(mExitListener) + val job = launch { + coordinator.waitForLowLightTransitionAnimation(timeout = TIMEOUT, entering = false) + } + runCurrent() + verify(mExitListener).onBeforeExitLowLight() + assertThat(job.isCompleted).isTrue() + } + + @Test + fun listenerNotCalledAfterRemoval() = testScope.runTest { + val coordinator = LowLightTransitionCoordinator() + coordinator.setLowLightEnterListener(mEnterListener) + coordinator.setLowLightEnterListener(null) + val job = launch { + coordinator.waitForLowLightTransitionAnimation(timeout = TIMEOUT, entering = true) + } + runCurrent() + verify(mEnterListener, never()).onBeforeEnterLowLight() + assertThat(job.isCompleted).isTrue() + } + + @Test + fun waitsForAnimationToEnd() = testScope.runTest { + whenever(mEnterListener.onBeforeEnterLowLight()).thenReturn(mAnimator) + val coordinator = LowLightTransitionCoordinator() + coordinator.setLowLightEnterListener(mEnterListener) + val job = launch { + coordinator.waitForLowLightTransitionAnimation(timeout = TIMEOUT, entering = true) + } + runCurrent() + // Animator listener is added and the runnable is not run yet. + verify(mAnimator).addListener(mAnimatorListenerCaptor.capture()) + assertThat(job.isCompleted).isFalse() + + // Runnable is run once the animation ends. + mAnimatorListenerCaptor.value.onAnimationEnd(mAnimator) + runCurrent() + assertThat(job.isCompleted).isTrue() + assertThat(job.isCancelled).isFalse() + } + + @Test + fun waitsForTimeoutIfAnimationNeverEnds() = testScope.runTest { + whenever(mEnterListener.onBeforeEnterLowLight()).thenReturn(mAnimator) + val coordinator = LowLightTransitionCoordinator() + coordinator.setLowLightEnterListener(mEnterListener) + val job = launch { + coordinator.waitForLowLightTransitionAnimation(timeout = TIMEOUT, entering = true) + } + runCurrent() + assertThat(job.isCancelled).isFalse() + advanceTimeBy(delayTimeMillis = TIMEOUT.inWholeMilliseconds + 1) + // If animator doesn't complete within the timeout, we should cancel ourselves. + assertThat(job.isCancelled).isTrue() + } + + @Test + fun shouldCancelIfAnimationIsCancelled() = testScope.runTest { + whenever(mEnterListener.onBeforeEnterLowLight()).thenReturn(mAnimator) + val coordinator = LowLightTransitionCoordinator() + coordinator.setLowLightEnterListener(mEnterListener) + val job = launch { + coordinator.waitForLowLightTransitionAnimation(timeout = TIMEOUT, entering = true) + } + runCurrent() + // Animator listener is added and the runnable is not run yet. + verify(mAnimator).addListener(mAnimatorListenerCaptor.capture()) + assertThat(job.isCompleted).isFalse() + assertThat(job.isCancelled).isFalse() + + // Runnable is run once the animation ends. + mAnimatorListenerCaptor.value.onAnimationCancel(mAnimator) + runCurrent() + assertThat(job.isCompleted).isTrue() + assertThat(job.isCancelled).isTrue() + } + + @Test + fun shouldCancelAnimatorWhenJobCancelled() = testScope.runTest { + whenever(mEnterListener.onBeforeEnterLowLight()).thenReturn(mAnimator) + val coordinator = LowLightTransitionCoordinator() + coordinator.setLowLightEnterListener(mEnterListener) + val job = launch { + coordinator.waitForLowLightTransitionAnimation(timeout = TIMEOUT, entering = true) + } + runCurrent() + // Animator listener is added and the runnable is not run yet. + verify(mAnimator).addListener(mAnimatorListenerCaptor.capture()) + verify(mAnimator, never()).cancel() + assertThat(job.isCompleted).isFalse() + + job.cancel() + // We should have removed the listener and cancelled the animator + verify(mAnimator).removeListener(mAnimatorListenerCaptor.value) + verify(mAnimator).cancel() + } + + companion object { + private val TIMEOUT = 1.toDuration(DurationUnit.SECONDS) + } +} diff --git a/libs/dream/lowlight/tests/src/com/android/dream/lowlight/utils/KotlinMockitoHelpers.kt b/libs/dream/lowlight/tests/src/com/android/dream/lowlight/utils/KotlinMockitoHelpers.kt new file mode 100644 index 000000000000..e5ec26ca4b41 --- /dev/null +++ b/libs/dream/lowlight/tests/src/com/android/dream/lowlight/utils/KotlinMockitoHelpers.kt @@ -0,0 +1,125 @@ +package src.com.android.dream.lowlight.utils + +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatcher +import org.mockito.Mockito +import org.mockito.Mockito.`when` +import org.mockito.stubbing.OngoingStubbing +import org.mockito.stubbing.Stubber + +/** + * Returns Mockito.eq() as nullable type to avoid java.lang.IllegalStateException when + * null is returned. + * + * Generic T is nullable because implicitly bounded by Any?. + */ +fun <T> eq(obj: T): T = Mockito.eq<T>(obj) + +/** + * Returns Mockito.any() as nullable type to avoid java.lang.IllegalStateException when + * null is returned. + * + * Generic T is nullable because implicitly bounded by Any?. + */ +fun <T> any(type: Class<T>): T = Mockito.any<T>(type) +inline fun <reified T> any(): T = any(T::class.java) + +/** + * Returns Mockito.argThat() as nullable type to avoid java.lang.IllegalStateException when + * null is returned. + * + * Generic T is nullable because implicitly bounded by Any?. + */ +fun <T> argThat(matcher: ArgumentMatcher<T>): T = Mockito.argThat(matcher) + +/** + * Kotlin type-inferred version of Mockito.nullable() + */ +inline fun <reified T> nullable(): T? = Mockito.nullable(T::class.java) + +/** + * Returns ArgumentCaptor.capture() as nullable type to avoid java.lang.IllegalStateException + * when null is returned. + * + * Generic T is nullable because implicitly bounded by Any?. + */ +fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture() + +/** + * Helper function for creating an argumentCaptor in kotlin. + * + * Generic T is nullable because implicitly bounded by Any?. + */ +inline fun <reified T : Any> argumentCaptor(): ArgumentCaptor<T> = + ArgumentCaptor.forClass(T::class.java) + +/** + * Helper function for creating new mocks, without the need to pass in a [Class] instance. + * + * Generic T is nullable because implicitly bounded by Any?. + * + * @param apply builder function to simplify stub configuration by improving type inference. + */ +inline fun <reified T : Any> mock(apply: T.() -> Unit = {}): T = Mockito.mock(T::class.java) + .apply(apply) + +/** + * Helper function for stubbing methods without the need to use backticks. + * + * @see Mockito.when + */ +fun <T> whenever(methodCall: T): OngoingStubbing<T> = `when`(methodCall) +fun <T> Stubber.whenever(mock: T): T = `when`(mock) + +/** + * A kotlin implemented wrapper of [ArgumentCaptor] which prevents the following exception when + * kotlin tests are mocking kotlin objects and the methods take non-null parameters: + * + * java.lang.NullPointerException: capture() must not be null + */ +class KotlinArgumentCaptor<T> constructor(clazz: Class<T>) { + private val wrapped: ArgumentCaptor<T> = ArgumentCaptor.forClass(clazz) + fun capture(): T = wrapped.capture() + val value: T + get() = wrapped.value + val allValues: List<T> + get() = wrapped.allValues +} + +/** + * Helper function for creating an argumentCaptor in kotlin. + * + * Generic T is nullable because implicitly bounded by Any?. + */ +inline fun <reified T : Any> kotlinArgumentCaptor(): KotlinArgumentCaptor<T> = + KotlinArgumentCaptor(T::class.java) + +/** + * Helper function for creating and using a single-use ArgumentCaptor in kotlin. + * + * val captor = argumentCaptor<Foo>() + * verify(...).someMethod(captor.capture()) + * val captured = captor.value + * + * becomes: + * + * val captured = withArgCaptor<Foo> { verify(...).someMethod(capture()) } + * + * NOTE: this uses the KotlinArgumentCaptor to avoid the NullPointerException. + */ +inline fun <reified T : Any> withArgCaptor(block: KotlinArgumentCaptor<T>.() -> Unit): T = + kotlinArgumentCaptor<T>().apply { block() }.value + +/** + * Variant of [withArgCaptor] for capturing multiple arguments. + * + * val captor = argumentCaptor<Foo>() + * verify(...).someMethod(captor.capture()) + * val captured: List<Foo> = captor.allValues + * + * becomes: + * + * val capturedList = captureMany<Foo> { verify(...).someMethod(capture()) } + */ +inline fun <reified T : Any> captureMany(block: KotlinArgumentCaptor<T>.() -> Unit): List<T> = + kotlinArgumentCaptor<T>().apply{ block() }.allValues diff --git a/media/java/android/media/projection/IMediaProjection.aidl b/media/java/android/media/projection/IMediaProjection.aidl index 2bdd5c8bc977..5f7d636fdd1e 100644 --- a/media/java/android/media/projection/IMediaProjection.aidl +++ b/media/java/android/media/projection/IMediaProjection.aidl @@ -23,22 +23,32 @@ import android.os.IBinder; interface IMediaProjection { void start(IMediaProjectionCallback callback); void stop(); + boolean canProjectAudio(); boolean canProjectVideo(); boolean canProjectSecureVideo(); + + @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest" + + ".permission.MANAGE_MEDIA_PROJECTION)") int applyVirtualDisplayFlags(int flags); + void registerCallback(IMediaProjectionCallback callback); + void unregisterCallback(IMediaProjectionCallback callback); /** * Returns the {@link android.os.IBinder} identifying the task to record, or {@code null} if * there is none. */ + @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest" + + ".permission.MANAGE_MEDIA_PROJECTION)") IBinder getLaunchCookie(); /** * Updates the {@link android.os.IBinder} identifying the task to record, or {@code null} if * there is none. */ + @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest" + + ".permission.MANAGE_MEDIA_PROJECTION)") void setLaunchCookie(in IBinder launchCookie); } diff --git a/media/java/android/media/projection/IMediaProjectionManager.aidl b/media/java/android/media/projection/IMediaProjectionManager.aidl index c259f9ad9cf9..c97265d4939d 100644 --- a/media/java/android/media/projection/IMediaProjectionManager.aidl +++ b/media/java/android/media/projection/IMediaProjectionManager.aidl @@ -28,11 +28,20 @@ interface IMediaProjectionManager { @UnsupportedAppUsage boolean hasProjectionPermission(int uid, String packageName); + /** + * Returns a new {@link IMediaProjection} instance associated with the given package. + */ @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest" + ".permission.MANAGE_MEDIA_PROJECTION)") IMediaProjection createProjection(int uid, String packageName, int type, boolean permanentGrant); + /** + * Returns {@code true} if the given {@link IMediaProjection} corresponds to the current + * projection, or {@code false} otherwise. + */ + @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest" + + ".permission.MANAGE_MEDIA_PROJECTION)") boolean isCurrentProjection(IMediaProjection projection); @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest" @@ -67,6 +76,8 @@ interface IMediaProjectionManager { * @param incomingSession the nullable incoming content recording session * @param projection the non-null projection the session describes */ + @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest" + + ".permission.MANAGE_MEDIA_PROJECTION)") void setContentRecordingSession(in ContentRecordingSession incomingSession, in IMediaProjection projection); } diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraBinderTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraBinderTest.java index 2b7bcbee79fd..cc7a7d5bb9dc 100644 --- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraBinderTest.java +++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraBinderTest.java @@ -161,7 +161,8 @@ public class CameraBinderTest extends AndroidTestCase { ICameraService.USE_CALLING_UID, ICameraService.USE_CALLING_PID, getContext().getApplicationInfo().targetSdkVersion, - /*overrideToPortrait*/false); + /*overrideToPortrait*/false, + /*forceSlowJpegMode*/false); assertNotNull(String.format("Camera %s was null", cameraId), cameraUser); Log.v(TAG, String.format("Camera %s connected", cameraId)); diff --git a/packages/CredentialManager/res/values/strings.xml b/packages/CredentialManager/res/values/strings.xml index 1e0c2cdb6d99..0498a15269ce 100644 --- a/packages/CredentialManager/res/values/strings.xml +++ b/packages/CredentialManager/res/values/strings.xml @@ -105,6 +105,8 @@ <string name="get_dialog_title_choose_sign_in_for">Choose a saved sign-in for <xliff:g id="app_name" example="YouTube">%1$s</xliff:g></string> <!-- This appears as the title of the dialog asking for user to make a choice from various previously saved credentials to sign in to the app. [CHAR LIMIT=200] --> <string name="get_dialog_title_choose_option_for">Choose an option for <xliff:g id="app_name" example="YouTube">%1$s</xliff:g>?</string> + <!-- This appears as the title of the dialog asking user to use a previously saved credentials to sign in to the app. [CHAR LIMIT=200] --> + <string name="get_dialog_title_use_info_on">Use this info on <xliff:g id="app_name" example="YouTube">%1$s</xliff:g>?</string> <!-- This is a label for a button that links the user to different sign-in methods . [CHAR LIMIT=80] --> <string name="get_dialog_use_saved_passkey_for">Sign in another way</string> <!-- This is a label for a button that links the user to different sign-in methods. [CHAR LIMIT=80] --> diff --git a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt index b3d3b6dc66d0..783cf3b47344 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt @@ -102,7 +102,7 @@ private fun getServiceLabelAndIcon( ).toString() providerIcon = pkgInfo.applicationInfo.loadIcon(pm) } catch (e: PackageManager.NameNotFoundException) { - Log.e(Constants.LOG_TAG, "Provider info not found", e) + Log.e(Constants.LOG_TAG, "Provider package info not found", e) } } else { try { @@ -113,7 +113,23 @@ private fun getServiceLabelAndIcon( ).toString() providerIcon = si.loadIcon(pm) } catch (e: PackageManager.NameNotFoundException) { - Log.e(Constants.LOG_TAG, "Provider info not found", e) + Log.e(Constants.LOG_TAG, "Provider service info not found", e) + // Added for mdoc use case where the provider may not need to register a service and + // instead only relies on the registration api. + try { + val pkgInfo = pm.getPackageInfo( + component.packageName, + PackageManager.PackageInfoFlags.of(0) + ) + providerLabel = + pkgInfo.applicationInfo.loadSafeLabel( + pm, 0f, + TextUtils.SAFE_STRING_FLAG_FIRST_LINE or TextUtils.SAFE_STRING_FLAG_TRIM + ).toString() + providerIcon = pkgInfo.applicationInfo.loadIcon(pm) + } catch (e: PackageManager.NameNotFoundException) { + Log.e(Constants.LOG_TAG, "Provider package info not found", e) + } } } return if (providerLabel == null || providerIcon == null) { diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetGenericCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetGenericCredentialComponents.kt index ba48f2b0fc1b..57fefbe577b4 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetGenericCredentialComponents.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetGenericCredentialComponents.kt @@ -34,7 +34,9 @@ import com.android.credentialmanager.CredentialSelectorViewModel import com.android.credentialmanager.R import com.android.credentialmanager.common.BaseEntry import com.android.credentialmanager.common.ProviderActivityState +import com.android.credentialmanager.common.ui.ConfirmButton import com.android.credentialmanager.common.ui.CredentialContainerCard +import com.android.credentialmanager.common.ui.CtaButtonRow import com.android.credentialmanager.common.ui.HeadlineIcon import com.android.credentialmanager.common.ui.HeadlineText import com.android.credentialmanager.common.ui.LargeLabelTextOnSurfaceVariant @@ -62,6 +64,7 @@ fun GetGenericCredentialScreen( providerDisplayInfo = getCredentialUiState.providerDisplayInfo, providerInfoList = getCredentialUiState.providerInfoList, onEntrySelected = viewModel::getFlowOnEntrySelected, + onConfirm = viewModel::getFlowOnConfirmEntrySelected, onLog = { viewModel.logUiEvent(it) }, ) viewModel.uiMetrics.log(GetCredentialEvent @@ -93,10 +96,13 @@ fun PrimarySelectionCardGeneric( providerDisplayInfo: ProviderDisplayInfo, providerInfoList: List<ProviderInfo>, onEntrySelected: (BaseEntry) -> Unit, + onConfirm: () -> Unit, onLog: @Composable (UiEventLogger.UiEventEnum) -> Unit, ) { val sortedUserNameToCredentialEntryList = providerDisplayInfo.sortedUserNameToCredentialEntryList + val totalEntriesCount = sortedUserNameToCredentialEntryList + .flatMap { it.sortedCredentialEntryList }.size SheetContainerCard { // When only one provider (not counting the remote-only provider) exists, display that // provider's icon + name up top. @@ -125,7 +131,11 @@ fun PrimarySelectionCardGeneric( item { HeadlineText( text = stringResource( - R.string.get_dialog_title_choose_option_for, + if (totalEntriesCount == 1) { + R.string.get_dialog_title_use_info_on + } else { + R.string.get_dialog_title_choose_option_for + }, requestDisplayInfo.appName ), ) @@ -134,7 +144,6 @@ fun PrimarySelectionCardGeneric( item { CredentialContainerCard { Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { - // Show max 4 entries in this primary page sortedUserNameToCredentialEntryList.forEach { // TODO(b/275375861): fallback UI merges entries by account names. // Need a strategy to be able to show all entries. @@ -147,6 +156,19 @@ fun PrimarySelectionCardGeneric( } } } + item { Divider(thickness = 24.dp, color = Color.Transparent) } + item { + if (totalEntriesCount == 1) { + CtaButtonRow( + rightButton = { + ConfirmButton( + stringResource(R.string.get_dialog_button_label_continue), + onClick = onConfirm + ) + } + ) + } + } } onLog(GetCredentialEvent.CREDMAN_GET_CRED_PRIMARY_SELECTION_CARD) } diff --git a/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java b/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java index 78b78101e64e..964e4b24d130 100644 --- a/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java +++ b/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java @@ -119,29 +119,15 @@ public class RestrictedLockUtilsInternal extends RestrictedLockUtils { } final int restrictionSource = enforcingUsers.get(0).getUserRestrictionSource(); - final int adminUserId = enforcingUsers.get(0).getUserHandle().getIdentifier(); - if (restrictionSource == UserManager.RESTRICTION_SOURCE_PROFILE_OWNER) { - // Check if it is a profile owner of the user under consideration. - if (adminUserId == userId) { - return getProfileOwner(context, userRestriction, adminUserId); - } else { - // Check if it is a profile owner of a managed profile of the current user. - // Otherwise it is in a separate user and we return a default EnforcedAdmin. - final UserInfo parentUser = um.getProfileParent(adminUserId); - return (parentUser != null && parentUser.id == userId) - ? getProfileOwner(context, userRestriction, adminUserId) - : EnforcedAdmin.createDefaultEnforcedAdminWithRestriction(userRestriction); - } - } else if (restrictionSource == UserManager.RESTRICTION_SOURCE_DEVICE_OWNER) { - // When the restriction is enforced by device owner, return the device owner admin only - // if the admin is for the {@param userId} otherwise return a default EnforcedAdmin. - return adminUserId == userId - ? getDeviceOwner(context, userRestriction) - : EnforcedAdmin.createDefaultEnforcedAdminWithRestriction(userRestriction); + if (restrictionSource == UserManager.RESTRICTION_SOURCE_SYSTEM) { + return null; } - // If the restriction is enforced by system then return null. - return null; + final EnforcedAdmin admin = getProfileOrDeviceOwner(context, userHandle); + if (admin != null) { + return admin; + } + return EnforcedAdmin.createDefaultEnforcedAdminWithRestriction(userRestriction); } public static boolean hasBaseUserRestriction(Context context, diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java index 48d449dd7daa..5e8f3a18cbc0 100644 --- a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java +++ b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java @@ -483,7 +483,10 @@ public class ApplicationsState { public AppEntry getEntry(String packageName, int userId) { if (DEBUG_LOCKING) Log.v(TAG, "getEntry about to acquire lock..."); synchronized (mEntriesMap) { - AppEntry entry = mEntriesMap.get(userId).get(packageName); + AppEntry entry = null; + if (mEntriesMap.contains(userId)) { + entry = mEntriesMap.get(userId).get(packageName); + } if (entry == null) { ApplicationInfo info = getAppInfoLocked(packageName, userId); if (info == null) { diff --git a/packages/SettingsProvider/res/values/defaults.xml b/packages/SettingsProvider/res/values/defaults.xml index 6312913d1403..59cd7a051fad 100644 --- a/packages/SettingsProvider/res/values/defaults.xml +++ b/packages/SettingsProvider/res/values/defaults.xml @@ -25,6 +25,8 @@ <!-- Comma-separated list of bluetooth, wifi, and cell. --> <string name="def_airplane_mode_radios" translatable="false">cell,bluetooth,uwb,wifi,wimax</string> <string name="airplane_mode_toggleable_radios" translatable="false">bluetooth,wifi</string> + <string name="def_satellite_mode_radios" translatable="false"></string> + <integer name="def_satellite_mode_enabled" translatable="false">0</integer> <string name="def_bluetooth_disabled_profiles" translatable="false">0</string> <bool name="def_auto_time">true</bool> <bool name="def_auto_time_zone">true</bool> diff --git a/packages/SettingsProvider/src/com/android/providers/settings/DatabaseHelper.java b/packages/SettingsProvider/src/com/android/providers/settings/DatabaseHelper.java index 74774167caa0..ed5654d4f259 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/DatabaseHelper.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/DatabaseHelper.java @@ -2413,6 +2413,12 @@ class DatabaseHelper extends SQLiteOpenHelper { loadStringSetting(stmt, Settings.Global.AIRPLANE_MODE_RADIOS, R.string.def_airplane_mode_radios); + loadStringSetting(stmt, Global.SATELLITE_MODE_RADIOS, + R.string.def_satellite_mode_radios); + + loadIntegerSetting(stmt, Global.SATELLITE_MODE_ENABLED, + R.integer.def_satellite_mode_enabled); + loadStringSetting(stmt, Settings.Global.AIRPLANE_MODE_TOGGLEABLE_RADIOS, R.string.airplane_mode_toggleable_radios); diff --git a/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt b/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt index db88b593e432..204bac88bc0d 100644 --- a/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt +++ b/packages/SystemUI/monet/src/com/android/systemui/monet/ColorScheme.kt @@ -23,6 +23,8 @@ import com.android.internal.graphics.ColorUtils import com.android.internal.graphics.cam.Cam import com.android.internal.graphics.cam.CamUtils import kotlin.math.absoluteValue +import kotlin.math.max +import kotlin.math.min import kotlin.math.roundToInt const val TAG = "ColorScheme" @@ -35,12 +37,12 @@ internal interface Hue { fun get(sourceColor: Cam): Double /** - * Given a hue, and a mapping of hues to hue rotations, find which hues in the mapping the - * hue fall betweens, and use the hue rotation of the lower hue. + * Given a hue, and a mapping of hues to hue rotations, find which hues in the mapping the hue + * fall betweens, and use the hue rotation of the lower hue. * * @param sourceHue hue of source color - * @param hueAndRotations list of pairs, where the first item in a pair is a hue, and the - * second item in the pair is a hue rotation that should be applied + * @param hueAndRotations list of pairs, where the first item in a pair is a hue, and the second + * item in the pair is a hue rotation that should be applied */ fun getHueRotation(sourceHue: Float, hueAndRotations: List<Pair<Int, Int>>): Double { val sanitizedSourceHue = (if (sourceHue < 0 || sourceHue >= 360) 0 else sourceHue).toFloat() @@ -48,8 +50,9 @@ internal interface Hue { val thisHue = hueAndRotations[i].first.toFloat() val nextHue = hueAndRotations[i + 1].first.toFloat() if (thisHue <= sanitizedSourceHue && sanitizedSourceHue < nextHue) { - return ColorScheme.wrapDegreesDouble(sanitizedSourceHue.toDouble() + - hueAndRotations[i].second) + return ColorScheme.wrapDegreesDouble( + sanitizedSourceHue.toDouble() + hueAndRotations[i].second + ) } } @@ -78,8 +81,18 @@ internal class HueSubtract(val amountDegrees: Double) : Hue { } internal class HueVibrantSecondary() : Hue { - val hueToRotations = listOf(Pair(0, 18), Pair(41, 15), Pair(61, 10), Pair(101, 12), - Pair(131, 15), Pair(181, 18), Pair(251, 15), Pair(301, 12), Pair(360, 12)) + val hueToRotations = + listOf( + Pair(0, 18), + Pair(41, 15), + Pair(61, 10), + Pair(101, 12), + Pair(131, 15), + Pair(181, 18), + Pair(251, 15), + Pair(301, 12), + Pair(360, 12) + ) override fun get(sourceColor: Cam): Double { return getHueRotation(sourceColor.hue, hueToRotations) @@ -87,8 +100,18 @@ internal class HueVibrantSecondary() : Hue { } internal class HueVibrantTertiary() : Hue { - val hueToRotations = listOf(Pair(0, 35), Pair(41, 30), Pair(61, 20), Pair(101, 25), - Pair(131, 30), Pair(181, 35), Pair(251, 30), Pair(301, 25), Pair(360, 25)) + val hueToRotations = + listOf( + Pair(0, 35), + Pair(41, 30), + Pair(61, 20), + Pair(101, 25), + Pair(131, 30), + Pair(181, 35), + Pair(251, 30), + Pair(301, 25), + Pair(360, 25) + ) override fun get(sourceColor: Cam): Double { return getHueRotation(sourceColor.hue, hueToRotations) @@ -96,8 +119,18 @@ internal class HueVibrantTertiary() : Hue { } internal class HueExpressiveSecondary() : Hue { - val hueToRotations = listOf(Pair(0, 45), Pair(21, 95), Pair(51, 45), Pair(121, 20), - Pair(151, 45), Pair(191, 90), Pair(271, 45), Pair(321, 45), Pair(360, 45)) + val hueToRotations = + listOf( + Pair(0, 45), + Pair(21, 95), + Pair(51, 45), + Pair(121, 20), + Pair(151, 45), + Pair(191, 90), + Pair(271, 45), + Pair(321, 45), + Pair(360, 45) + ) override fun get(sourceColor: Cam): Double { return getHueRotation(sourceColor.hue, hueToRotations) @@ -105,8 +138,18 @@ internal class HueExpressiveSecondary() : Hue { } internal class HueExpressiveTertiary() : Hue { - val hueToRotations = listOf(Pair(0, 120), Pair(21, 120), Pair(51, 20), Pair(121, 45), - Pair(151, 20), Pair(191, 15), Pair(271, 20), Pair(321, 120), Pair(360, 120)) + val hueToRotations = + listOf( + Pair(0, 120), + Pair(21, 120), + Pair(51, 20), + Pair(121, 45), + Pair(151, 20), + Pair(191, 15), + Pair(271, 20), + Pair(321, 120), + Pair(360, 120) + ) override fun get(sourceColor: Cam): Double { return getHueRotation(sourceColor.hue, hueToRotations) @@ -115,13 +158,18 @@ internal class HueExpressiveTertiary() : Hue { internal interface Chroma { fun get(sourceColor: Cam): Double + + companion object { + val MAX_VALUE = 120.0 + val MIN_VALUE = 0.0 + } } internal class ChromaMaxOut : Chroma { override fun get(sourceColor: Cam): Double { // Intentionally high. Gamut mapping from impossible HCT to sRGB will ensure that // the maximum chroma is reached, even if lower than this constant. - return 130.0 + return Chroma.MAX_VALUE + 10.0 } } @@ -131,6 +179,23 @@ internal class ChromaMultiple(val multiple: Double) : Chroma { } } +internal class ChromaAdd(val amount: Double) : Chroma { + override fun get(sourceColor: Cam): Double { + return sourceColor.chroma + amount + } +} + +internal class ChromaBound( + val baseChroma: Chroma, + val minVal: Double, + val maxVal: Double, +) : Chroma { + override fun get(sourceColor: Cam): Double { + val result = baseChroma.get(sourceColor) + return min(max(result, minVal), maxVal) + } +} + internal class ChromaConstant(val chroma: Double) : Chroma { override fun get(sourceColor: Cam): Double { return chroma @@ -149,109 +214,173 @@ internal class TonalSpec(val hue: Hue = HueSource(), val chroma: Chroma) { val chroma = chroma.get(sourceColor) return Shades.of(hue.toFloat(), chroma.toFloat()).toList() } + + fun getAtTone(sourceColor: Cam, tone: Float): Int { + val hue = hue.get(sourceColor) + val chroma = chroma.get(sourceColor) + return ColorUtils.CAMToColor(hue.toFloat(), chroma.toFloat(), (1000f - tone) / 10f) + } } internal class CoreSpec( - val a1: TonalSpec, - val a2: TonalSpec, - val a3: TonalSpec, - val n1: TonalSpec, - val n2: TonalSpec + val a1: TonalSpec, + val a2: TonalSpec, + val a3: TonalSpec, + val n1: TonalSpec, + val n2: TonalSpec ) enum class Style(internal val coreSpec: CoreSpec) { - SPRITZ(CoreSpec( + SPRITZ( + CoreSpec( a1 = TonalSpec(HueSource(), ChromaConstant(12.0)), a2 = TonalSpec(HueSource(), ChromaConstant(8.0)), a3 = TonalSpec(HueSource(), ChromaConstant(16.0)), n1 = TonalSpec(HueSource(), ChromaConstant(2.0)), n2 = TonalSpec(HueSource(), ChromaConstant(2.0)) - )), - TONAL_SPOT(CoreSpec( + ) + ), + TONAL_SPOT( + CoreSpec( a1 = TonalSpec(HueSource(), ChromaConstant(36.0)), a2 = TonalSpec(HueSource(), ChromaConstant(16.0)), a3 = TonalSpec(HueAdd(60.0), ChromaConstant(24.0)), n1 = TonalSpec(HueSource(), ChromaConstant(6.0)), n2 = TonalSpec(HueSource(), ChromaConstant(8.0)) - )), - VIBRANT(CoreSpec( + ) + ), + VIBRANT( + CoreSpec( a1 = TonalSpec(HueSource(), ChromaMaxOut()), a2 = TonalSpec(HueVibrantSecondary(), ChromaConstant(24.0)), a3 = TonalSpec(HueVibrantTertiary(), ChromaConstant(32.0)), n1 = TonalSpec(HueSource(), ChromaConstant(10.0)), n2 = TonalSpec(HueSource(), ChromaConstant(12.0)) - )), - EXPRESSIVE(CoreSpec( + ) + ), + EXPRESSIVE( + CoreSpec( a1 = TonalSpec(HueAdd(240.0), ChromaConstant(40.0)), a2 = TonalSpec(HueExpressiveSecondary(), ChromaConstant(24.0)), a3 = TonalSpec(HueExpressiveTertiary(), ChromaConstant(32.0)), n1 = TonalSpec(HueAdd(15.0), ChromaConstant(8.0)), n2 = TonalSpec(HueAdd(15.0), ChromaConstant(12.0)) - )), - RAINBOW(CoreSpec( + ) + ), + RAINBOW( + CoreSpec( a1 = TonalSpec(HueSource(), ChromaConstant(48.0)), a2 = TonalSpec(HueSource(), ChromaConstant(16.0)), a3 = TonalSpec(HueAdd(60.0), ChromaConstant(24.0)), n1 = TonalSpec(HueSource(), ChromaConstant(0.0)), n2 = TonalSpec(HueSource(), ChromaConstant(0.0)) - )), - FRUIT_SALAD(CoreSpec( + ) + ), + FRUIT_SALAD( + CoreSpec( a1 = TonalSpec(HueSubtract(50.0), ChromaConstant(48.0)), a2 = TonalSpec(HueSubtract(50.0), ChromaConstant(36.0)), a3 = TonalSpec(HueSource(), ChromaConstant(36.0)), n1 = TonalSpec(HueSource(), ChromaConstant(10.0)), n2 = TonalSpec(HueSource(), ChromaConstant(16.0)) - )), - CONTENT(CoreSpec( + ) + ), + CONTENT( + CoreSpec( a1 = TonalSpec(HueSource(), ChromaSource()), a2 = TonalSpec(HueSource(), ChromaMultiple(0.33)), a3 = TonalSpec(HueSource(), ChromaMultiple(0.66)), n1 = TonalSpec(HueSource(), ChromaMultiple(0.0833)), n2 = TonalSpec(HueSource(), ChromaMultiple(0.1666)) - )), - MONOCHROMATIC(CoreSpec( + ) + ), + MONOCHROMATIC( + CoreSpec( a1 = TonalSpec(HueSource(), ChromaConstant(.0)), a2 = TonalSpec(HueSource(), ChromaConstant(.0)), a3 = TonalSpec(HueSource(), ChromaConstant(.0)), n1 = TonalSpec(HueSource(), ChromaConstant(.0)), n2 = TonalSpec(HueSource(), ChromaConstant(.0)) - )), + ) + ), + CLOCK( + CoreSpec( + a1 = TonalSpec(HueSource(), ChromaBound(ChromaSource(), 20.0, Chroma.MAX_VALUE)), + a2 = TonalSpec(HueAdd(10.0), ChromaBound(ChromaMultiple(0.85), 17.0, 40.0)), + a3 = TonalSpec(HueAdd(20.0), ChromaBound(ChromaAdd(20.0), 50.0, Chroma.MAX_VALUE)), + + // Not Used + n1 = TonalSpec(HueSource(), ChromaConstant(0.0)), + n2 = TonalSpec(HueSource(), ChromaConstant(0.0)) + ) + ), + CLOCK_VIBRANT( + CoreSpec( + a1 = TonalSpec(HueSource(), ChromaBound(ChromaSource(), 30.0, Chroma.MAX_VALUE)), + a2 = TonalSpec(HueAdd(20.0), ChromaBound(ChromaSource(), 30.0, Chroma.MAX_VALUE)), + a3 = TonalSpec(HueAdd(60.0), ChromaBound(ChromaSource(), 30.0, Chroma.MAX_VALUE)), + + // Not Used + n1 = TonalSpec(HueSource(), ChromaConstant(0.0)), + n2 = TonalSpec(HueSource(), ChromaConstant(0.0)) + ) + ) } -class TonalPalette { - val shadeKeys = listOf(10, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000) - val allShades: List<Int> - val allShadesMapped: Map<Int, Int> +class TonalPalette +internal constructor( + private val spec: TonalSpec, + seedColor: Int, +) { + val seedCam: Cam = Cam.fromInt(seedColor) + val allShades: List<Int> = spec.shades(seedCam) + val allShadesMapped: Map<Int, Int> = SHADE_KEYS.zip(allShades).toMap() val baseColor: Int - internal constructor(spec: TonalSpec, seedColor: Int) { - val seedCam = Cam.fromInt(seedColor) - allShades = spec.shades(seedCam) - allShadesMapped = shadeKeys.zip(allShades).toMap() - + init { val h = spec.hue.get(seedCam).toFloat() val c = spec.chroma.get(seedCam).toFloat() baseColor = ColorUtils.CAMToColor(h, c, CamUtils.lstarFromInt(seedColor)) } - val s10: Int get() = this.allShades[0] - val s50: Int get() = this.allShades[1] - val s100: Int get() = this.allShades[2] - val s200: Int get() = this.allShades[3] - val s300: Int get() = this.allShades[4] - val s400: Int get() = this.allShades[5] - val s500: Int get() = this.allShades[6] - val s600: Int get() = this.allShades[7] - val s700: Int get() = this.allShades[8] - val s800: Int get() = this.allShades[9] - val s900: Int get() = this.allShades[10] - val s1000: Int get() = this.allShades[11] + // Dynamically computed tones across the full range from 0 to 1000 + fun getAtTone(tone: Float) = spec.getAtTone(seedCam, tone) + + // Predefined & precomputed tones + val s10: Int + get() = this.allShades[0] + val s50: Int + get() = this.allShades[1] + val s100: Int + get() = this.allShades[2] + val s200: Int + get() = this.allShades[3] + val s300: Int + get() = this.allShades[4] + val s400: Int + get() = this.allShades[5] + val s500: Int + get() = this.allShades[6] + val s600: Int + get() = this.allShades[7] + val s700: Int + get() = this.allShades[8] + val s800: Int + get() = this.allShades[9] + val s900: Int + get() = this.allShades[10] + val s1000: Int + get() = this.allShades[11] + + companion object { + val SHADE_KEYS = listOf(10, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000) + } } class ColorScheme( - @ColorInt val seed: Int, - val darkTheme: Boolean, - val style: Style = Style.TONAL_SPOT + @ColorInt val seed: Int, + val darkTheme: Boolean, + val style: Style = Style.TONAL_SPOT ) { val accent1: TonalPalette @@ -260,16 +389,14 @@ class ColorScheme( val neutral1: TonalPalette val neutral2: TonalPalette - constructor(@ColorInt seed: Int, darkTheme: Boolean) : - this(seed, darkTheme, Style.TONAL_SPOT) + constructor(@ColorInt seed: Int, darkTheme: Boolean) : this(seed, darkTheme, Style.TONAL_SPOT) @JvmOverloads constructor( - wallpaperColors: WallpaperColors, - darkTheme: Boolean, - style: Style = Style.TONAL_SPOT - ) : - this(getSeedColor(wallpaperColors, style != Style.CONTENT), darkTheme, style) + wallpaperColors: WallpaperColors, + darkTheme: Boolean, + style: Style = Style.TONAL_SPOT + ) : this(getSeedColor(wallpaperColors, style != Style.CONTENT), darkTheme, style) val allHues: List<TonalPalette> get() { @@ -301,13 +428,14 @@ class ColorScheme( init { val proposedSeedCam = Cam.fromInt(seed) - val seedArgb = if (seed == Color.TRANSPARENT) { - GOOGLE_BLUE - } else if (style != Style.CONTENT && proposedSeedCam.chroma < 5) { - GOOGLE_BLUE - } else { - seed - } + val seedArgb = + if (seed == Color.TRANSPARENT) { + GOOGLE_BLUE + } else if (style != Style.CONTENT && proposedSeedCam.chroma < 5) { + GOOGLE_BLUE + } else { + seed + } accent1 = TonalPalette(style.coreSpec.a1, seedArgb) accent2 = TonalPalette(style.coreSpec.a2, seedArgb) @@ -316,19 +444,23 @@ class ColorScheme( neutral2 = TonalPalette(style.coreSpec.n2, seedArgb) } - val shadeCount get() = this.accent1.allShades.size + val shadeCount + get() = this.accent1.allShades.size + + val seedTone: Float + get() = 1000f - CamUtils.lstarFromInt(seed) * 10f override fun toString(): String { return "ColorScheme {\n" + - " seed color: ${stringForColor(seed)}\n" + - " style: $style\n" + - " palettes: \n" + - " ${humanReadable("PRIMARY", accent1.allShades)}\n" + - " ${humanReadable("SECONDARY", accent2.allShades)}\n" + - " ${humanReadable("TERTIARY", accent3.allShades)}\n" + - " ${humanReadable("NEUTRAL", neutral1.allShades)}\n" + - " ${humanReadable("NEUTRAL VARIANT", neutral2.allShades)}\n" + - "}" + " seed color: ${stringForColor(seed)}\n" + + " style: $style\n" + + " palettes: \n" + + " ${humanReadable("PRIMARY", accent1.allShades)}\n" + + " ${humanReadable("SECONDARY", accent2.allShades)}\n" + + " ${humanReadable("TERTIARY", accent3.allShades)}\n" + + " ${humanReadable("NEUTRAL", neutral1.allShades)}\n" + + " ${humanReadable("NEUTRAL VARIANT", neutral2.allShades)}\n" + + "}" } companion object { @@ -356,8 +488,8 @@ class ColorScheme( @JvmStatic @JvmOverloads fun getSeedColors(wallpaperColors: WallpaperColors, filter: Boolean = true): List<Int> { - val totalPopulation = wallpaperColors.allColors.values.reduce { a, b -> a + b } - .toDouble() + val totalPopulation = + wallpaperColors.allColors.values.reduce { a, b -> a + b }.toDouble() val totalPopulationMeaningless = (totalPopulation == 0.0) if (totalPopulationMeaningless) { // WallpaperColors with a population of 0 indicate the colors didn't come from @@ -365,51 +497,56 @@ class ColorScheme( // secondary/tertiary colors. // // In this case, the colors are usually from a Live Wallpaper. - val distinctColors = wallpaperColors.mainColors.map { - it.toArgb() - }.distinct().filter { - if (!filter) { - true - } else { - Cam.fromInt(it).chroma >= MIN_CHROMA - } - }.toList() + val distinctColors = + wallpaperColors.mainColors + .map { it.toArgb() } + .distinct() + .filter { + if (!filter) { + true + } else { + Cam.fromInt(it).chroma >= MIN_CHROMA + } + } + .toList() if (distinctColors.isEmpty()) { return listOf(GOOGLE_BLUE) } return distinctColors } - val intToProportion = wallpaperColors.allColors.mapValues { - it.value.toDouble() / totalPopulation - } + val intToProportion = + wallpaperColors.allColors.mapValues { it.value.toDouble() / totalPopulation } val intToCam = wallpaperColors.allColors.mapValues { Cam.fromInt(it.key) } // Get an array with 360 slots. A slot contains the percentage of colors with that hue. val hueProportions = huePopulations(intToCam, intToProportion, filter) // Map each color to the percentage of the image with its hue. - val intToHueProportion = wallpaperColors.allColors.mapValues { - val cam = intToCam[it.key]!! - val hue = cam.hue.roundToInt() - var proportion = 0.0 - for (i in hue - 15..hue + 15) { - proportion += hueProportions[wrapDegrees(i)] + val intToHueProportion = + wallpaperColors.allColors.mapValues { + val cam = intToCam[it.key]!! + val hue = cam.hue.roundToInt() + var proportion = 0.0 + for (i in hue - 15..hue + 15) { + proportion += hueProportions[wrapDegrees(i)] + } + proportion } - proportion - } // Remove any inappropriate seed colors. For example, low chroma colors look grayscale // raising their chroma will turn them to a much louder color that may not have been // in the image. - val filteredIntToCam = if (!filter) intToCam else (intToCam.filter { - val cam = it.value - val proportion = intToHueProportion[it.key]!! - cam.chroma >= MIN_CHROMA && - (totalPopulationMeaningless || proportion > 0.01) - }) + val filteredIntToCam = + if (!filter) intToCam + else + (intToCam.filter { + val cam = it.value + val proportion = intToHueProportion[it.key]!! + cam.chroma >= MIN_CHROMA && + (totalPopulationMeaningless || proportion > 0.01) + }) // Sort the colors by score, from high to low. - val intToScoreIntermediate = filteredIntToCam.mapValues { - score(it.value, intToHueProportion[it.key]!!) - } + val intToScoreIntermediate = + filteredIntToCam.mapValues { score(it.value, intToHueProportion[it.key]!!) } val intToScore = intToScoreIntermediate.entries.toMutableList() intToScore.sortByDescending { it.value } @@ -423,11 +560,12 @@ class ColorScheme( seeds.clear() for (entry in intToScore) { val int = entry.key - val existingSeedNearby = seeds.find { - val hueA = intToCam[int]!!.hue - val hueB = intToCam[it]!!.hue - hueDiff(hueA, hueB) < i - } != null + val existingSeedNearby = + seeds.find { + val hueA = intToCam[int]!!.hue + val hueB = intToCam[it]!!.hue + hueDiff(hueA, hueB) < i + } != null if (existingSeedNearby) { continue } @@ -489,22 +627,22 @@ class ColorScheme( } private fun humanReadable(paletteName: String, colors: List<Int>): String { - return "$paletteName\n" + colors.map { - stringForColor(it) - }.joinToString(separator = "\n") { it } + return "$paletteName\n" + + colors.map { stringForColor(it) }.joinToString(separator = "\n") { it } } private fun score(cam: Cam, proportion: Double): Double { val proportionScore = 0.7 * 100.0 * proportion - val chromaScore = if (cam.chroma < ACCENT1_CHROMA) 0.1 * (cam.chroma - ACCENT1_CHROMA) - else 0.3 * (cam.chroma - ACCENT1_CHROMA) + val chromaScore = + if (cam.chroma < ACCENT1_CHROMA) 0.1 * (cam.chroma - ACCENT1_CHROMA) + else 0.3 * (cam.chroma - ACCENT1_CHROMA) return chromaScore + proportionScore } private fun huePopulations( - camByColor: Map<Int, Cam>, - populationByColor: Map<Int, Double>, - filter: Boolean = true + camByColor: Map<Int, Cam>, + populationByColor: Map<Int, Double>, + filter: Boolean = true ): List<Double> { val huePopulation = List(size = 360, init = { 0.0 }).toMutableList() diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt index 0080517a722b..c279053e6daf 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt @@ -109,6 +109,9 @@ interface ClockEvents { /** Call whenever the locale changes */ fun onLocaleChanged(locale: Locale) {} + val isReactiveToTone + get() = true + /** Call whenever the color palette should update */ fun onColorPaletteChanged(resources: Resources) {} @@ -164,8 +167,12 @@ interface ClockFaceEvents { val hasCustomWeatherDataDisplay: Boolean get() = false - /** Region Darkness specific to the clock face */ - fun onRegionDarknessChanged(isDark: Boolean) {} + /** + * Region Darkness specific to the clock face. + * - isRegionDark = dark theme -> clock should be light + * - !isRegionDark = light theme -> clock should be dark + */ + fun onRegionDarknessChanged(isRegionDark: Boolean) {} /** * Call whenever font settings change. Pass in a target font size in pixels. The specific clock diff --git a/packages/SystemUI/res/values/integers.xml b/packages/SystemUI/res/values/integers.xml index 8d4431520c75..befbfab7dbc3 100644 --- a/packages/SystemUI/res/values/integers.xml +++ b/packages/SystemUI/res/values/integers.xml @@ -37,4 +37,12 @@ <dimen name="percent_displacement_at_fade_out" format="float">0.1066</dimen> <integer name="qs_carrier_max_em">7</integer> + + <!-- Maximum number of notification icons shown on the Always on Display + (excluding overflow dot) --> + <integer name="max_notif_icons_on_aod">3</integer> + <!-- Maximum number of notification icons shown on the lockscreen (excluding overflow dot) --> + <integer name="max_notif_icons_on_lockscreen">3</integer> + <!-- Maximum number of notification icons shown in the status bar (excluding overflow dot) --> + <integer name="max_notif_static_icons">4</integer> </resources>
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardView.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardView.java index ee9081c7027d..178cda46cdda 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardView.java @@ -24,6 +24,7 @@ import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.content.Context; +import android.content.res.ColorStateList; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; import android.graphics.Rect; @@ -176,7 +177,9 @@ public class UdfpsKeyguardView extends UdfpsAnimationView { mTextColorPrimary = Utils.getColorAttrDefaultColor(mContext, android.R.attr.textColorPrimary); - mBgProtection.setImageDrawable(getContext().getDrawable(R.drawable.fingerprint_bg)); + final int backgroundColor = Utils.getColorAttrDefaultColor(getContext(), + com.android.internal.R.attr.colorSurface); + mBgProtection.setImageTintList(ColorStateList.valueOf(backgroundColor)); mLockScreenFp.invalidate(); // updated with a valueCallback } diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt index 99a10a33ab0f..37138114c740 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlActionCoordinatorImpl.kt @@ -44,7 +44,7 @@ import com.android.systemui.plugins.ActivityStarter import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.concurrency.DelayableExecutor -import com.android.wm.shell.TaskViewFactory +import com.android.wm.shell.taskview.TaskViewFactory import java.util.Optional import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt index c20af074c71e..d4ce9b699619 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt @@ -79,7 +79,7 @@ import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.asIndenting import com.android.systemui.util.concurrency.DelayableExecutor import com.android.systemui.util.indentIfPossible -import com.android.wm.shell.TaskViewFactory +import com.android.wm.shell.taskview.TaskViewFactory import dagger.Lazy import java.io.PrintWriter import java.text.Collator diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/DetailDialog.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/DetailDialog.kt index 3d9eee4e9feb..5d608c3e3f9e 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/DetailDialog.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/DetailDialog.kt @@ -36,7 +36,7 @@ import com.android.systemui.R import com.android.systemui.broadcast.BroadcastSender import com.android.systemui.plugins.ActivityStarter import com.android.systemui.statusbar.policy.KeyguardStateController -import com.android.wm.shell.TaskView +import com.android.wm.shell.taskview.TaskView /** * A dialog that provides an {@link TaskView}, allowing the application to provide @@ -44,13 +44,13 @@ import com.android.wm.shell.TaskView * The activity being launched is specified by {@link android.service.controls.Control#getAppIntent}. */ class DetailDialog( - val activityContext: Context, - val broadcastSender: BroadcastSender, - val taskView: TaskView, - val pendingIntent: PendingIntent, - val cvh: ControlViewHolder, - val keyguardStateController: KeyguardStateController, - val activityStarter: ActivityStarter + val activityContext: Context, + val broadcastSender: BroadcastSender, + val taskView: TaskView, + val pendingIntent: PendingIntent, + val cvh: ControlViewHolder, + val keyguardStateController: KeyguardStateController, + val activityStarter: ActivityStarter ) : Dialog( activityContext, R.style.Theme_SystemUI_Dialog_Control_DetailPanel diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/PanelTaskViewController.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/PanelTaskViewController.kt index 1f89c917186a..9a231814a813 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/PanelTaskViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/PanelTaskViewController.kt @@ -30,7 +30,7 @@ import android.graphics.drawable.shapes.RoundRectShape import android.os.Trace import com.android.systemui.R import com.android.systemui.util.boundsOnScreen -import com.android.wm.shell.TaskView +import com.android.wm.shell.taskview.TaskView import java.util.concurrent.Executor class PanelTaskViewController( diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java index 625a02801392..1a0fcea6ca87 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java @@ -38,7 +38,6 @@ import com.android.systemui.unfold.UnfoldLatencyTracker; import com.android.systemui.unfold.UnfoldTransitionProgressProvider; import com.android.systemui.unfold.progress.UnfoldTransitionProgressForwarder; import com.android.systemui.unfold.util.NaturalRotationUnfoldProgressProvider; -import com.android.wm.shell.TaskViewFactory; import com.android.wm.shell.back.BackAnimation; import com.android.wm.shell.bubbles.Bubbles; import com.android.wm.shell.desktopmode.DesktopMode; @@ -49,16 +48,17 @@ import com.android.wm.shell.recents.RecentTasks; import com.android.wm.shell.splitscreen.SplitScreen; import com.android.wm.shell.startingsurface.StartingSurface; import com.android.wm.shell.sysui.ShellInterface; +import com.android.wm.shell.taskview.TaskViewFactory; import com.android.wm.shell.transition.ShellTransitions; +import dagger.BindsInstance; +import dagger.Subcomponent; + import java.util.Map; import java.util.Optional; import javax.inject.Provider; -import dagger.BindsInstance; -import dagger.Subcomponent; - /** * An example Dagger Subcomponent for Core SysUI. * diff --git a/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java index d756f3a44655..17d2332a4dac 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java @@ -23,7 +23,6 @@ import androidx.annotation.Nullable; import com.android.systemui.SystemUIInitializerFactory; import com.android.systemui.tv.TvWMComponent; -import com.android.wm.shell.TaskViewFactory; import com.android.wm.shell.back.BackAnimation; import com.android.wm.shell.bubbles.Bubbles; import com.android.wm.shell.common.annotations.ShellMainThread; @@ -38,13 +37,14 @@ import com.android.wm.shell.recents.RecentTasks; import com.android.wm.shell.splitscreen.SplitScreen; import com.android.wm.shell.startingsurface.StartingSurface; import com.android.wm.shell.sysui.ShellInterface; +import com.android.wm.shell.taskview.TaskViewFactory; import com.android.wm.shell.transition.ShellTransitions; -import java.util.Optional; - import dagger.BindsInstance; import dagger.Subcomponent; +import java.util.Optional; + /** * Dagger Subcomponent for WindowManager. This class explicitly describes the interfaces exported * from the WM component into the SysUI component (in diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 79a51d6670c4..c7b4edb31b0c 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -635,7 +635,7 @@ object Flags { // TODO(b/269132640): Tracking Bug @JvmField val APP_PANELS_REMOVE_APPS_ALLOWED = - unreleasedFlag(2003, "app_panels_remove_apps_allowed", teamfood = false) + unreleasedFlag(2003, "app_panels_remove_apps_allowed", teamfood = true) // 2100 - Falsing Manager @JvmField val FALSING_FOR_LONG_TAPS = releasedFlag(2100, "falsing_for_long_taps") 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 77541e931e08..33f4e2e24322 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 @@ -21,8 +21,6 @@ import android.content.res.ColorStateList import android.hardware.biometrics.BiometricSourceType import android.os.Handler import android.os.Trace -import android.os.UserHandle -import android.os.UserManager import android.util.Log import android.view.View import com.android.keyguard.KeyguardConstants @@ -106,10 +104,9 @@ constructor( val panelExpansionAmount: Flow<Float> = repository.panelExpansionAmount /** 0f = bouncer fully hidden. 1f = bouncer fully visible. */ val bouncerExpansion: Flow<Float> = - combine( - repository.panelExpansionAmount, - repository.primaryBouncerShow - ) { panelExpansion, primaryBouncerIsShowing -> + combine(repository.panelExpansionAmount, repository.primaryBouncerShow) { + panelExpansion, + primaryBouncerIsShowing -> if (primaryBouncerIsShowing) { 1f - panelExpansion } else { @@ -195,6 +192,7 @@ constructor( dismissCallbackRegistry.notifyDismissCancelled() } + repository.setPrimaryStartDisappearAnimation(null) falsingCollector.onBouncerHidden() keyguardStateController.notifyPrimaryBouncerShowing(false /* showing */) cancelShowRunnable() @@ -306,11 +304,8 @@ constructor( runnable.run() return } - val finishRunnable = Runnable { - runnable.run() - repository.setPrimaryStartDisappearAnimation(null) - } - repository.setPrimaryStartDisappearAnimation(finishRunnable) + + repository.setPrimaryStartDisappearAnimation(runnable) } /** Determine whether to show the side fps animation. */ diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSHost.java index 9ece72d2ca7f..6be74a0b5646 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSHost.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSHost.java @@ -40,13 +40,12 @@ public interface QSHost extends PanelInteractor { /** * Returns the default QS tiles for the context. - * @param context the context to obtain the resources from + * @param res the resources to use to determine the default tiles * @return a list of specs of the default tiles */ - static List<String> getDefaultSpecs(Context context) { + static List<String> getDefaultSpecs(Resources res) { final ArrayList<String> tiles = new ArrayList(); - final Resources res = context.getResources(); final String defaultTileList = res.getString(R.string.quick_settings_tiles_default); tiles.addAll(Arrays.asList(defaultTileList.split(","))); diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java index 0ead97976ad9..8bbdeeda356c 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java @@ -600,7 +600,7 @@ public class QSTileHost implements QSHost, Tunable, PluginListener<QSFactory>, P if (tile.isEmpty()) continue; if (tile.equals("default")) { if (!addedDefault) { - List<String> defaultSpecs = QSHost.getDefaultSpecs(context); + List<String> defaultSpecs = QSHost.getDefaultSpecs(context.getResources()); for (String spec : defaultSpecs) { if (!addedSpecs.contains(spec)) { tiles.add(spec); diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizerController.java b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizerController.java index a319fb8d8756..4002ac3aa120 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizerController.java +++ b/packages/SystemUI/src/com/android/systemui/qs/customize/QSCustomizerController.java @@ -175,7 +175,7 @@ public class QSCustomizerController extends ViewController<QSCustomizer> { private void reset() { - mTileAdapter.resetTileSpecs(QSHost.getDefaultSpecs(getContext())); + mTileAdapter.resetTileSpecs(QSHost.getDefaultSpecs(getContext().getResources())); } public boolean isCustomizing() { diff --git a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java index cfe93132c044..dffe7fd5f818 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java +++ b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java @@ -29,6 +29,7 @@ import com.android.systemui.qs.AutoAddTracker; import com.android.systemui.qs.QSHost; import com.android.systemui.qs.ReduceBrightColorsController; import com.android.systemui.qs.external.QSExternalModule; +import com.android.systemui.qs.pipeline.dagger.QSPipelineModule; import com.android.systemui.qs.tileimpl.QSTileImpl; import com.android.systemui.statusbar.phone.AutoTileManager; import com.android.systemui.statusbar.phone.ManagedProfileController; @@ -40,14 +41,14 @@ import com.android.systemui.statusbar.policy.SafetyController; import com.android.systemui.statusbar.policy.WalletController; import com.android.systemui.util.settings.SecureSettings; -import java.util.Map; - -import javax.inject.Named; - import dagger.Module; import dagger.Provides; import dagger.multibindings.Multibinds; +import java.util.Map; + +import javax.inject.Named; + /** * Module for QS dependencies */ @@ -56,7 +57,8 @@ import dagger.multibindings.Multibinds; MediaModule.class, QSExternalModule.class, QSFlagsModule.class, - QSHostModule.class + QSHostModule.class, + QSPipelineModule.class, } ) public interface QSModule { diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSPipelineModule.kt new file mode 100644 index 000000000000..00f0a67dbe22 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSPipelineModule.kt @@ -0,0 +1,57 @@ +/* + * 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.qs.pipeline.dagger + +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.log.LogBufferFactory +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository +import com.android.systemui.qs.pipeline.data.repository.TileSpecSettingsRepository +import com.android.systemui.qs.pipeline.prototyping.PrototypeCoreStartable +import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +abstract class QSPipelineModule { + + /** Implementation for [TileSpecRepository] */ + @Binds + abstract fun provideTileSpecRepository(impl: TileSpecSettingsRepository): TileSpecRepository + + @Binds + @IntoMap + @ClassKey(PrototypeCoreStartable::class) + abstract fun providePrototypeCoreStartable(startable: PrototypeCoreStartable): CoreStartable + + companion object { + /** + * Provides a logging buffer for all logs related to the new Quick Settings pipeline to log + * the list of current tiles. + */ + @Provides + @SysUISingleton + @QSTileListLog + fun provideQSTileListLogBuffer(factory: LogBufferFactory): LogBuffer { + return factory.create(QSPipelineLogger.TILE_LIST_TAG, maxSize = 700, systrace = false) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSTileListLog.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSTileListLog.kt new file mode 100644 index 000000000000..ad8bfeabc676 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/QSTileListLog.kt @@ -0,0 +1,23 @@ +/* + * 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.qs.pipeline.dagger + +import java.lang.annotation.Retention +import java.lang.annotation.RetentionPolicy +import javax.inject.Qualifier + +/** A {@link LogBuffer} for the new QS Pipeline for logging changes to the set of current tiles. */ +@Qualifier @MustBeDocumented @Retention(RetentionPolicy.RUNTIME) annotation class QSTileListLog diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt new file mode 100644 index 000000000000..d254e1b3d0d7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/TileSpecRepository.kt @@ -0,0 +1,191 @@ +/* + * 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.qs.pipeline.data.repository + +import android.annotation.UserIdInt +import android.content.res.Resources +import android.database.ContentObserver +import android.provider.Settings +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.qs.QSHost +import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger +import com.android.systemui.util.settings.SecureSettings +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.withContext + +/** Repository that tracks the current tiles. */ +interface TileSpecRepository { + + /** + * Returns a flow of the current list of [TileSpec] for a given [userId]. + * + * Tiles will never be [TileSpec.Invalid] in the list and it will never be empty. + */ + fun tilesSpecs(@UserIdInt userId: Int): Flow<List<TileSpec>> + + /** + * Adds a [tile] for a given [userId] at [position]. Using [POSITION_AT_END] will add the tile + * at the end of the list. + * + * Passing [TileSpec.Invalid] is a noop. + */ + suspend fun addTile(@UserIdInt userId: Int, tile: TileSpec, position: Int = POSITION_AT_END) + + /** + * Removes a [tile] for a given [userId]. + * + * Passing [TileSpec.Invalid] or a non present tile is a noop. + */ + suspend fun removeTile(@UserIdInt userId: Int, tile: TileSpec) + + /** + * Sets the list of current [tiles] for a given [userId]. + * + * [TileSpec.Invalid] will be ignored, and an effectively empty list will not be stored. + */ + suspend fun setTiles(@UserIdInt userId: Int, tiles: List<TileSpec>) + + companion object { + /** Position to indicate the end of the list */ + const val POSITION_AT_END = -1 + } +} + +/** + * Implementation of [TileSpecRepository] that persist the values of tiles in + * [Settings.Secure.QS_TILES]. + * + * All operations against [Settings] will be performed in a background thread. + */ +@SysUISingleton +class TileSpecSettingsRepository +@Inject +constructor( + private val secureSettings: SecureSettings, + @Main private val resources: Resources, + private val logger: QSPipelineLogger, + @Background private val backgroundDispatcher: CoroutineDispatcher, +) : TileSpecRepository { + override fun tilesSpecs(userId: Int): Flow<List<TileSpec>> { + return conflatedCallbackFlow { + val observer = + object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + trySend(Unit) + } + } + + secureSettings.registerContentObserverForUser(SETTING, observer, userId) + + awaitClose { secureSettings.unregisterContentObserver(observer) } + } + .onStart { emit(Unit) } + .map { secureSettings.getStringForUser(SETTING, userId) ?: "" } + .onEach { logger.logTilesChangedInSettings(it, userId) } + .map { parseTileSpecs(it, userId) } + .flowOn(backgroundDispatcher) + } + + override suspend fun addTile(userId: Int, tile: TileSpec, position: Int) { + if (tile == TileSpec.Invalid) { + return + } + val tilesList = loadTiles(userId).toMutableList() + if (tile !in tilesList) { + if (position < 0) { + tilesList.add(tile) + } else { + tilesList.add(position, tile) + } + storeTiles(userId, tilesList) + } + } + + override suspend fun removeTile(userId: Int, tile: TileSpec) { + if (tile == TileSpec.Invalid) { + return + } + val tilesList = loadTiles(userId).toMutableList() + if (tilesList.remove(tile)) { + storeTiles(userId, tilesList.toList()) + } + } + + override suspend fun setTiles(userId: Int, tiles: List<TileSpec>) { + val filtered = tiles.filter { it != TileSpec.Invalid } + if (filtered.isNotEmpty()) { + storeTiles(userId, filtered) + } + } + + private suspend fun loadTiles(@UserIdInt forUser: Int): List<TileSpec> { + return withContext(backgroundDispatcher) { + (secureSettings.getStringForUser(SETTING, forUser) ?: "") + .split(DELIMITER) + .map(TileSpec::create) + .filter { it !is TileSpec.Invalid } + } + } + + private suspend fun storeTiles(@UserIdInt forUser: Int, tiles: List<TileSpec>) { + val toStore = + tiles + .filter { it !is TileSpec.Invalid } + .joinToString(DELIMITER, transform = TileSpec::spec) + withContext(backgroundDispatcher) { + secureSettings.putStringForUser( + SETTING, + toStore, + null, + false, + forUser, + true, + ) + } + } + + private fun parseTileSpecs(tilesFromSettings: String, user: Int): List<TileSpec> { + val fromSettings = + tilesFromSettings.split(DELIMITER).map(TileSpec::create).filter { + it != TileSpec.Invalid + } + return if (fromSettings.isNotEmpty()) { + fromSettings.also { logger.logParsedTiles(it, false, user) } + } else { + QSHost.getDefaultSpecs(resources) + .map(TileSpec::create) + .filter { it != TileSpec.Invalid } + .also { logger.logParsedTiles(it, true, user) } + } + } + + companion object { + private const val SETTING = Settings.Secure.QS_TILES + private const val DELIMITER = "," + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/prototyping/PrototypeCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/prototyping/PrototypeCoreStartable.kt new file mode 100644 index 000000000000..69d8248a11f5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/prototyping/PrototypeCoreStartable.kt @@ -0,0 +1,109 @@ +/* + * 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.qs.pipeline.prototyping + +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository +import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.statusbar.commandline.Command +import com.android.systemui.statusbar.commandline.CommandRegistry +import com.android.systemui.user.data.repository.UserRepository +import java.io.PrintWriter +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.launch + +/** + * Class for observing results while prototyping. + * + * The flows do their own logging, so we just need to make sure that they collect. + * + * This will be torn down together with the last of the new pipeline flags remaining here. + */ +// TODO(b/270385608) +@SysUISingleton +class PrototypeCoreStartable +@Inject +constructor( + private val tileSpecRepository: TileSpecRepository, + private val userRepository: UserRepository, + private val featureFlags: FeatureFlags, + @Application private val scope: CoroutineScope, + private val commandRegistry: CommandRegistry, +) : CoreStartable { + + @OptIn(ExperimentalCoroutinesApi::class) + override fun start() { + if (featureFlags.isEnabled(Flags.QS_PIPELINE_NEW_HOST)) { + scope.launch { + userRepository.selectedUserInfo + .flatMapLatest { user -> tileSpecRepository.tilesSpecs(user.id) } + .collect {} + } + commandRegistry.registerCommand(COMMAND, ::CommandExecutor) + } + } + + private inner class CommandExecutor : Command { + override fun execute(pw: PrintWriter, args: List<String>) { + if (args.size < 2) { + pw.println("Error: needs at least two arguments") + return + } + val spec = TileSpec.create(args[1]) + if (spec == TileSpec.Invalid) { + pw.println("Error: Invalid tile spec ${args[1]}") + } + if (args[0] == "add") { + performAdd(args, spec) + pw.println("Requested tile added") + } else if (args[0] == "remove") { + performRemove(args, spec) + pw.println("Requested tile removed") + } else { + pw.println("Error: unknown command") + } + } + + private fun performAdd(args: List<String>, spec: TileSpec) { + val position = args.getOrNull(2)?.toInt() ?: TileSpecRepository.POSITION_AT_END + val user = args.getOrNull(3)?.toInt() ?: userRepository.getSelectedUserInfo().id + scope.launch { tileSpecRepository.addTile(user, spec, position) } + } + + private fun performRemove(args: List<String>, spec: TileSpec) { + val user = args.getOrNull(2)?.toInt() ?: userRepository.getSelectedUserInfo().id + scope.launch { tileSpecRepository.removeTile(user, spec) } + } + + override fun help(pw: PrintWriter) { + pw.println("Usage: adb shell cmd statusbar $COMMAND:") + pw.println(" add <spec> [position] [user]") + pw.println(" remove <spec> [user]") + } + } + + companion object { + private const val COMMAND = "qs-pipeline" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TileSpec.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TileSpec.kt new file mode 100644 index 000000000000..c691c2f668ad --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/TileSpec.kt @@ -0,0 +1,85 @@ +/* + * 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.qs.pipeline.shared + +import android.content.ComponentName +import android.text.TextUtils +import com.android.systemui.qs.external.CustomTile + +/** + * Container for the spec that identifies a tile. + * + * A tile's [spec] is one of two options: + * * `custom(<componentName>)`: A [ComponentName] surrounded by [CustomTile.PREFIX] and terminated + * by `)`, represents a tile provided by an app, corresponding to a `TileService`. + * * a string not starting with [CustomTile.PREFIX], representing a tile provided by SystemUI. + */ +sealed class TileSpec private constructor(open val spec: String) { + + /** Represents a spec that couldn't be parsed into a valid type of tile. */ + object Invalid : TileSpec("") { + override fun toString(): String { + return "TileSpec.INVALID" + } + } + + /** Container for the spec of a tile provided by SystemUI. */ + data class PlatformTileSpec + internal constructor( + override val spec: String, + ) : TileSpec(spec) + + /** + * Container for the spec of a tile provided by an app. + * + * [componentName] indicates the associated `TileService`. + */ + data class CustomTileSpec + internal constructor( + override val spec: String, + val componentName: ComponentName, + ) : TileSpec(spec) + + companion object { + /** Create a [TileSpec] from the string [spec]. */ + fun create(spec: String): TileSpec { + return if (TextUtils.isEmpty(spec)) { + Invalid + } else if (!spec.isCustomTileSpec) { + PlatformTileSpec(spec) + } else { + spec.componentName?.let { CustomTileSpec(spec, it) } ?: Invalid + } + } + + private val String.isCustomTileSpec: Boolean + get() = startsWith(CustomTile.PREFIX) + + private val String.componentName: ComponentName? + get() = + if (!isCustomTileSpec) { + null + } else { + if (endsWith(")")) { + val extracted = substring(CustomTile.PREFIX.length, length - 1) + ComponentName.unflattenFromString(extracted) + } else { + null + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLogger.kt new file mode 100644 index 000000000000..200f7431e906 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/shared/logging/QSPipelineLogger.kt @@ -0,0 +1,76 @@ +/* + * 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.qs.pipeline.shared.logging + +import android.annotation.UserIdInt +import com.android.systemui.plugins.log.LogBuffer +import com.android.systemui.plugins.log.LogLevel +import com.android.systemui.qs.pipeline.dagger.QSTileListLog +import com.android.systemui.qs.pipeline.shared.TileSpec +import javax.inject.Inject + +/** + * Logger for the new pipeline. + * + * This may log to different buffers depending of the function of the log. + */ +class QSPipelineLogger +@Inject +constructor( + @QSTileListLog private val tileListLogBuffer: LogBuffer, +) { + + companion object { + const val TILE_LIST_TAG = "QSTileListLog" + } + + /** + * Log the tiles that are parsed in the repo. This is effectively what is surfaces in the flow. + * + * [usesDefault] indicates if the default tiles were used (due to the setting being empty or + * invalid). + */ + fun logParsedTiles(tiles: List<TileSpec>, usesDefault: Boolean, user: Int) { + tileListLogBuffer.log( + TILE_LIST_TAG, + LogLevel.DEBUG, + { + str1 = tiles.toString() + bool1 = usesDefault + int1 = user + }, + { "Parsed tiles (default=$bool1, user=$int1): $str1" } + ) + } + + /** + * Logs when the tiles change in Settings. + * + * This could be caused by SystemUI, or restore. + */ + fun logTilesChangedInSettings(newTiles: String, @UserIdInt user: Int) { + tileListLogBuffer.log( + TILE_LIST_TAG, + LogLevel.VERBOSE, + { + str1 = newTiles + int1 = user + }, + { "Tiles changed in settings for user $int1: $str1" } + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java index 80eea81a541d..1b83397b1afb 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java +++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java @@ -788,7 +788,8 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis private void disconnectFromLauncherService(String disconnectReason) { Log.d(TAG_OPS, "disconnectFromLauncherService bound?: " + mBound + - " currentProxy: " + mOverviewProxy + " disconnectReason: " + disconnectReason); + " currentProxy: " + mOverviewProxy + " disconnectReason: " + disconnectReason, + new Throwable()); if (mBound) { // Always unbind the service (ie. if called through onNullBinding or onBindingDied) mContext.unbindService(mOverviewServiceConnection); diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java index efd79d737f71..3227ef47f733 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java @@ -190,9 +190,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> { } catch (Exception e) { // IOException/UnsupportedOperationException may be thrown if external storage is // not mounted - if (DEBUG_STORAGE) { - Log.d(TAG, "Failed to store screenshot", e); - } + Log.d(TAG, "Failed to store screenshot", e); mParams.clearImage(); mImageData.reset(); mQuickShareData.reset(); diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java index b2ae4a021f2c..d51a97f2ac52 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java @@ -1119,9 +1119,7 @@ public class ScreenshotController { /** Reset screenshot view and then call onCompleteRunnable */ private void finishDismiss() { - if (DEBUG_DISMISS) { - Log.d(TAG, "finishDismiss"); - } + Log.d(TAG, "finishDismiss"); if (mLastScrollCaptureRequest != null) { mLastScrollCaptureRequest.cancel(true); mLastScrollCaptureRequest = null; diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java index 7ac0fd50ea33..f3d2828072be 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java @@ -253,6 +253,7 @@ public class TakeScreenshotService extends Service { Consumer<Uri> uriConsumer, RequestCallback callback) { mUiEventLogger.log(ScreenshotEvent.getScreenshotSource(screenshot.getSource()), 0, screenshot.getPackageNameString()); + Log.d(TAG, "Screenshot request: " + screenshot); mScreenshot.handleScreenshot(screenshot, uriConsumer, callback); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java index c9f31bad74c0..8aeefeeac211 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java @@ -307,7 +307,7 @@ public class NotifCollection implements Dumpable, PipelineDumpable { } entriesToLocallyDismiss.add(entry); - if (!isCanceled(entry)) { + if (!entry.isCanceled()) { // send message to system server if this notification hasn't already been cancelled mBgExecutor.execute(() -> { try { @@ -387,7 +387,7 @@ public class NotifCollection implements Dumpable, PipelineDumpable { entry.setDismissState(DISMISSED); mLogger.logNotifDismissed(entry); - if (isCanceled(entry)) { + if (entry.isCanceled()) { canceledEntries.add(entry); } else { // Mark any children as dismissed as system server will auto-dismiss them as well @@ -396,7 +396,7 @@ public class NotifCollection implements Dumpable, PipelineDumpable { if (shouldAutoDismissChildren(otherEntry, entry.getSbn().getGroupKey())) { otherEntry.setDismissState(PARENT_DISMISSED); mLogger.logChildDismissed(otherEntry); - if (isCanceled(otherEntry)) { + if (otherEntry.isCanceled()) { canceledEntries.add(otherEntry); } } @@ -523,7 +523,7 @@ public class NotifCollection implements Dumpable, PipelineDumpable { + logKey(entry))); } - if (!isCanceled(entry)) { + if (!entry.isCanceled()) { throw mEulogizer.record( new IllegalStateException("Cannot remove notification " + logKey(entry) + ": has not been marked for removal")); @@ -587,7 +587,7 @@ public class NotifCollection implements Dumpable, PipelineDumpable { private void applyRanking(@NonNull RankingMap rankingMap) { ArrayMap<String, NotificationEntry> currentEntriesWithoutRankings = null; for (NotificationEntry entry : mNotificationSet.values()) { - if (!isCanceled(entry)) { + if (!entry.isCanceled()) { // TODO: (b/148791039) We should crash if we are ever handed a ranking with // incomplete entries. Right now, there's a race condition in NotificationListener @@ -815,15 +815,6 @@ public class NotifCollection implements Dumpable, PipelineDumpable { return ranking; } - /** - * True if the notification has been canceled by system server. Usually, such notifications are - * immediately removed from the collection, but can sometimes stick around due to lifetime - * extenders. - */ - private boolean isCanceled(NotificationEntry entry) { - return entry.mCancellationReason != REASON_NOT_CANCELED; - } - private boolean cannotBeLifetimeExtended(NotificationEntry entry) { final boolean locallyDismissedByUser = entry.getDismissState() != NOT_DISMISSED; final boolean systemServerReportedUserCancel = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java index 3399f9df7fd5..f7790e861e27 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java @@ -321,6 +321,15 @@ public final class NotificationEntry extends ListEntry { mDismissState = requireNonNull(dismissState); } + /** + * True if the notification has been canceled by system server. Usually, such notifications are + * immediately removed from the collection, but can sometimes stick around due to lifetime + * extenders. + */ + public boolean isCanceled() { + return mCancellationReason != REASON_NOT_CANCELED; + } + @Nullable public NotifFilter getExcludingFilter() { return getAttachState().getExcludingFilter(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProvider.kt index 0d9a654fa485..058545689c01 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProvider.kt @@ -46,7 +46,7 @@ class NotifUiAdjustmentProvider @Inject constructor( private val userTracker: UserTracker ) { private val dirtyListeners = ListenerSet<Runnable>() - private var isSnoozeEnabled = false + private var isSnoozeSettingsEnabled = false /** * Update the snooze enabled value on user switch @@ -95,7 +95,7 @@ class NotifUiAdjustmentProvider @Inject constructor( } private fun updateSnoozeEnabled() { - isSnoozeEnabled = + isSnoozeSettingsEnabled = secureSettings.getIntForUser(SHOW_NOTIFICATION_SNOOZE, 0, UserHandle.USER_CURRENT) == 1 } @@ -118,7 +118,7 @@ class NotifUiAdjustmentProvider @Inject constructor( smartActions = entry.ranking.smartActions, smartReplies = entry.ranking.smartReplies, isConversation = entry.ranking.isConversation, - isSnoozeEnabled = isSnoozeEnabled, + isSnoozeEnabled = isSnoozeSettingsEnabled && !entry.isCanceled, isMinimized = isEntryMinimized(entry), needsRedaction = lockscreenUserManager.needsRedaction(entry), ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java index 4ee2de11abdf..006a029de8e0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java @@ -136,11 +136,13 @@ public class NotificationIconContainer extends ViewGroup { } }.setDuration(CONTENT_FADE_DURATION); - private static final int MAX_ICONS_ON_AOD = 3; + /* Maximum number of icons on AOD when also showing overflow dot. */ + private int mMaxIconsOnAod; /* Maximum number of icons in short shelf on lockscreen when also showing overflow dot. */ - public static final int MAX_ICONS_ON_LOCKSCREEN = 3; - public static final int MAX_STATIC_ICONS = 4; + private int mMaxIconsOnLockscreen; + /* Maximum number of icons in the status bar when also showing overflow dot. */ + private int mMaxStaticIcons; private boolean mIsStaticLayout = true; private final HashMap<View, IconState> mIconStates = new HashMap<>(); @@ -174,14 +176,19 @@ public class NotificationIconContainer extends ViewGroup { public NotificationIconContainer(Context context, AttributeSet attrs) { super(context, attrs); - initDimens(); + initResources(); setWillNotDraw(!(DEBUG || DEBUG_OVERFLOW)); } - private void initDimens() { + private void initResources() { + mMaxIconsOnAod = getResources().getInteger(R.integer.max_notif_icons_on_aod); + mMaxIconsOnLockscreen = getResources().getInteger(R.integer.max_notif_icons_on_lockscreen); + mMaxStaticIcons = getResources().getInteger(R.integer.max_notif_static_icons); + mDotPadding = getResources().getDimensionPixelSize(R.dimen.overflow_icon_dot_padding); mStaticDotRadius = getResources().getDimensionPixelSize(R.dimen.overflow_dot_radius); mStaticDotDiameter = 2 * mStaticDotRadius; + final Context themedContext = new ContextThemeWrapper(getContext(), com.android.internal.R.style.Theme_DeviceDefault_DayNight); mThemedTextColorPrimary = Utils.getColorAttr(themedContext, @@ -225,7 +232,7 @@ public class NotificationIconContainer extends ViewGroup { @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); - initDimens(); + initResources(); } @Override @@ -424,7 +431,7 @@ public class NotificationIconContainer extends ViewGroup { return 0f; } final float contentWidth = - mIconSize * MathUtils.min(numIcons, MAX_ICONS_ON_LOCKSCREEN + 1); + mIconSize * MathUtils.min(numIcons, mMaxIconsOnLockscreen + 1); return getActualPaddingStart() + contentWidth + getActualPaddingEnd(); @@ -539,8 +546,8 @@ public class NotificationIconContainer extends ViewGroup { } private int getMaxVisibleIcons(int childCount) { - return mOnLockScreen ? MAX_ICONS_ON_AOD : - mIsStaticLayout ? MAX_STATIC_ICONS : childCount; + return mOnLockScreen ? mMaxIconsOnAod : + mIsStaticLayout ? mMaxStaticIcons : childCount; } private float getLayoutEnd() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlActionCoordinatorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlActionCoordinatorImplTest.kt index 16fb50c15af6..38372a33f1e6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlActionCoordinatorImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlActionCoordinatorImplTest.kt @@ -29,7 +29,7 @@ import com.android.systemui.plugins.ActivityStarter import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.concurrency.DelayableExecutor -import com.android.wm.shell.TaskViewFactory +import com.android.wm.shell.taskview.TaskViewFactory import org.junit.Before import org.junit.Test import org.junit.runner.RunWith diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt index 845848071f54..605dc3f2e90a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/ControlsUiControllerImplTest.kt @@ -59,8 +59,8 @@ import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock -import com.android.wm.shell.TaskView -import com.android.wm.shell.TaskViewFactory +import com.android.wm.shell.taskview.TaskView +import com.android.wm.shell.taskview.TaskViewFactory import com.google.common.truth.Truth.assertThat import java.util.Optional import java.util.function.Consumer diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/DetailDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/DetailDialogTest.kt index 6a6a65a601fb..c3506e80966b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/DetailDialogTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/DetailDialogTest.kt @@ -24,7 +24,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastSender import com.android.systemui.plugins.ActivityStarter import com.android.systemui.statusbar.policy.KeyguardStateController -import com.android.wm.shell.TaskView +import com.android.wm.shell.taskview.TaskView import org.junit.Before import org.junit.Test import org.junit.runner.RunWith diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/PanelTaskViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/PanelTaskViewControllerTest.kt index 9df7992f979f..f7c8ccaf731a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/ui/PanelTaskViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/controls/ui/PanelTaskViewControllerTest.kt @@ -36,7 +36,7 @@ import com.android.systemui.util.mockito.capture import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock -import com.android.wm.shell.TaskView +import com.android.wm.shell.taskview.TaskView import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorTest.kt index bdc33f45c717..4c8a0a51bcdf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorTest.kt @@ -131,6 +131,7 @@ class PrimaryBouncerInteractorTest : SysuiTestCase() { verify(repository).setPrimaryShowingSoon(false) verify(repository).setPrimaryShow(false) verify(mPrimaryBouncerCallbackInteractor).dispatchVisibilityChanged(View.INVISIBLE) + verify(repository).setPrimaryStartDisappearAnimation(null) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/lifecycle/RepeatWhenAttachedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/RepeatWhenAttachedTest.kt index 91a6de6ae4c0..ea11f01ed580 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/lifecycle/RepeatWhenAttachedTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/RepeatWhenAttachedTest.kt @@ -17,7 +17,6 @@ package com.android.systemui.lifecycle -import android.testing.TestableLooper.RunWithLooper import android.view.View import android.view.ViewTreeObserver import androidx.lifecycle.Lifecycle @@ -28,8 +27,16 @@ import com.android.systemui.util.Assert import com.android.systemui.util.mockito.argumentCaptor import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.DisposableHandle -import kotlinx.coroutines.test.runBlockingTest +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test @@ -41,9 +48,9 @@ import org.mockito.Mockito.verify import org.mockito.Mockito.`when` as whenever import org.mockito.junit.MockitoJUnit +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(JUnit4::class) -@RunWithLooper class RepeatWhenAttachedTest : SysuiTestCase() { @JvmField @Rule val mockito = MockitoJUnit.rule() @@ -54,9 +61,13 @@ class RepeatWhenAttachedTest : SysuiTestCase() { private lateinit var block: Block private lateinit var attachListeners: MutableList<View.OnAttachStateChangeListener> + private lateinit var testScope: TestScope @Before fun setUp() { + val testDispatcher = StandardTestDispatcher() + testScope = TestScope(testDispatcher) + Dispatchers.setMain(testDispatcher) Assert.setTestThread(Thread.currentThread()) whenever(view.viewTreeObserver).thenReturn(viewTreeObserver) whenever(view.windowVisibility).thenReturn(View.GONE) @@ -71,186 +82,218 @@ class RepeatWhenAttachedTest : SysuiTestCase() { block = Block() } - @Test(expected = IllegalStateException::class) - fun `repeatWhenAttached - enforces main thread`() = runBlockingTest { - Assert.setTestThread(null) - - repeatWhenAttached() + @After + fun tearDown() { + Dispatchers.resetMain() } @Test(expected = IllegalStateException::class) - fun `repeatWhenAttached - dispose enforces main thread`() = runBlockingTest { - val disposableHandle = repeatWhenAttached() - Assert.setTestThread(null) + fun `repeatWhenAttached - enforces main thread`() = + testScope.runTest { + Assert.setTestThread(null) - disposableHandle.dispose() - } - - @Test - fun `repeatWhenAttached - view starts detached - runs block when attached`() = runBlockingTest { - whenever(view.isAttachedToWindow).thenReturn(false) - repeatWhenAttached() - assertThat(block.invocationCount).isEqualTo(0) + repeatWhenAttached() + } - whenever(view.isAttachedToWindow).thenReturn(true) - attachListeners.last().onViewAttachedToWindow(view) + @Test(expected = IllegalStateException::class) + fun `repeatWhenAttached - dispose enforces main thread`() = + testScope.runTest { + val disposableHandle = repeatWhenAttached() + Assert.setTestThread(null) - assertThat(block.invocationCount).isEqualTo(1) - assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.CREATED) - } + disposableHandle.dispose() + } @Test - fun `repeatWhenAttached - view already attached - immediately runs block`() = runBlockingTest { - whenever(view.isAttachedToWindow).thenReturn(true) - - repeatWhenAttached() - - assertThat(block.invocationCount).isEqualTo(1) - assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.CREATED) - } + fun `repeatWhenAttached - view starts detached - runs block when attached`() = + testScope.runTest { + whenever(view.isAttachedToWindow).thenReturn(false) + repeatWhenAttached() + assertThat(block.invocationCount).isEqualTo(0) + + whenever(view.isAttachedToWindow).thenReturn(true) + attachListeners.last().onViewAttachedToWindow(view) + + runCurrent() + assertThat(block.invocationCount).isEqualTo(1) + assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.CREATED) + } @Test - fun `repeatWhenAttached - starts visible without focus - STARTED`() = runBlockingTest { - whenever(view.isAttachedToWindow).thenReturn(true) - whenever(view.windowVisibility).thenReturn(View.VISIBLE) + fun `repeatWhenAttached - view already attached - immediately runs block`() = + testScope.runTest { + whenever(view.isAttachedToWindow).thenReturn(true) - repeatWhenAttached() + repeatWhenAttached() - assertThat(block.invocationCount).isEqualTo(1) - assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.STARTED) - } + runCurrent() + assertThat(block.invocationCount).isEqualTo(1) + assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.CREATED) + } @Test - fun `repeatWhenAttached - starts with focus but invisible - CREATED`() = runBlockingTest { - whenever(view.isAttachedToWindow).thenReturn(true) - whenever(view.hasWindowFocus()).thenReturn(true) + fun `repeatWhenAttached - starts visible without focus - STARTED`() = + testScope.runTest { + whenever(view.isAttachedToWindow).thenReturn(true) + whenever(view.windowVisibility).thenReturn(View.VISIBLE) - repeatWhenAttached() + repeatWhenAttached() - assertThat(block.invocationCount).isEqualTo(1) - assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.CREATED) - } + runCurrent() + assertThat(block.invocationCount).isEqualTo(1) + assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.STARTED) + } @Test - fun `repeatWhenAttached - starts visible and with focus - RESUMED`() = runBlockingTest { - whenever(view.isAttachedToWindow).thenReturn(true) - whenever(view.windowVisibility).thenReturn(View.VISIBLE) - whenever(view.hasWindowFocus()).thenReturn(true) + fun `repeatWhenAttached - starts with focus but invisible - CREATED`() = + testScope.runTest { + whenever(view.isAttachedToWindow).thenReturn(true) + whenever(view.hasWindowFocus()).thenReturn(true) - repeatWhenAttached() + repeatWhenAttached() - assertThat(block.invocationCount).isEqualTo(1) - assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.RESUMED) - } + runCurrent() + assertThat(block.invocationCount).isEqualTo(1) + assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.CREATED) + } @Test - fun `repeatWhenAttached - becomes visible without focus - STARTED`() = runBlockingTest { - whenever(view.isAttachedToWindow).thenReturn(true) - repeatWhenAttached() - val listenerCaptor = argumentCaptor<ViewTreeObserver.OnWindowVisibilityChangeListener>() - verify(viewTreeObserver).addOnWindowVisibilityChangeListener(listenerCaptor.capture()) + fun `repeatWhenAttached - starts visible and with focus - RESUMED`() = + testScope.runTest { + whenever(view.isAttachedToWindow).thenReturn(true) + whenever(view.windowVisibility).thenReturn(View.VISIBLE) + whenever(view.hasWindowFocus()).thenReturn(true) - whenever(view.windowVisibility).thenReturn(View.VISIBLE) - listenerCaptor.value.onWindowVisibilityChanged(View.VISIBLE) + repeatWhenAttached() - assertThat(block.invocationCount).isEqualTo(1) - assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.STARTED) - } + runCurrent() + assertThat(block.invocationCount).isEqualTo(1) + assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.RESUMED) + } @Test - fun `repeatWhenAttached - gains focus but invisible - CREATED`() = runBlockingTest { - whenever(view.isAttachedToWindow).thenReturn(true) - repeatWhenAttached() - val listenerCaptor = argumentCaptor<ViewTreeObserver.OnWindowFocusChangeListener>() - verify(viewTreeObserver).addOnWindowFocusChangeListener(listenerCaptor.capture()) - - whenever(view.hasWindowFocus()).thenReturn(true) - listenerCaptor.value.onWindowFocusChanged(true) + fun `repeatWhenAttached - becomes visible without focus - STARTED`() = + testScope.runTest { + whenever(view.isAttachedToWindow).thenReturn(true) + repeatWhenAttached() + val listenerCaptor = argumentCaptor<ViewTreeObserver.OnWindowVisibilityChangeListener>() + verify(viewTreeObserver).addOnWindowVisibilityChangeListener(listenerCaptor.capture()) + + whenever(view.windowVisibility).thenReturn(View.VISIBLE) + listenerCaptor.value.onWindowVisibilityChanged(View.VISIBLE) + + runCurrent() + assertThat(block.invocationCount).isEqualTo(1) + assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.STARTED) + } - assertThat(block.invocationCount).isEqualTo(1) - assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.CREATED) - } + @Test + fun `repeatWhenAttached - gains focus but invisible - CREATED`() = + testScope.runTest { + whenever(view.isAttachedToWindow).thenReturn(true) + repeatWhenAttached() + val listenerCaptor = argumentCaptor<ViewTreeObserver.OnWindowFocusChangeListener>() + verify(viewTreeObserver).addOnWindowFocusChangeListener(listenerCaptor.capture()) + + whenever(view.hasWindowFocus()).thenReturn(true) + listenerCaptor.value.onWindowFocusChanged(true) + + runCurrent() + assertThat(block.invocationCount).isEqualTo(1) + assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.CREATED) + } @Test - fun `repeatWhenAttached - becomes visible and gains focus - RESUMED`() = runBlockingTest { - whenever(view.isAttachedToWindow).thenReturn(true) - repeatWhenAttached() - val visibleCaptor = argumentCaptor<ViewTreeObserver.OnWindowVisibilityChangeListener>() - verify(viewTreeObserver).addOnWindowVisibilityChangeListener(visibleCaptor.capture()) - val focusCaptor = argumentCaptor<ViewTreeObserver.OnWindowFocusChangeListener>() - verify(viewTreeObserver).addOnWindowFocusChangeListener(focusCaptor.capture()) - - whenever(view.windowVisibility).thenReturn(View.VISIBLE) - visibleCaptor.value.onWindowVisibilityChanged(View.VISIBLE) - whenever(view.hasWindowFocus()).thenReturn(true) - focusCaptor.value.onWindowFocusChanged(true) - - assertThat(block.invocationCount).isEqualTo(1) - assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.RESUMED) - } + fun `repeatWhenAttached - becomes visible and gains focus - RESUMED`() = + testScope.runTest { + whenever(view.isAttachedToWindow).thenReturn(true) + repeatWhenAttached() + val visibleCaptor = argumentCaptor<ViewTreeObserver.OnWindowVisibilityChangeListener>() + verify(viewTreeObserver).addOnWindowVisibilityChangeListener(visibleCaptor.capture()) + val focusCaptor = argumentCaptor<ViewTreeObserver.OnWindowFocusChangeListener>() + verify(viewTreeObserver).addOnWindowFocusChangeListener(focusCaptor.capture()) + + whenever(view.windowVisibility).thenReturn(View.VISIBLE) + visibleCaptor.value.onWindowVisibilityChanged(View.VISIBLE) + whenever(view.hasWindowFocus()).thenReturn(true) + focusCaptor.value.onWindowFocusChanged(true) + + runCurrent() + assertThat(block.invocationCount).isEqualTo(1) + assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.RESUMED) + } @Test - fun `repeatWhenAttached - view gets detached - destroys the lifecycle`() = runBlockingTest { - whenever(view.isAttachedToWindow).thenReturn(true) - repeatWhenAttached() + fun `repeatWhenAttached - view gets detached - destroys the lifecycle`() = + testScope.runTest { + whenever(view.isAttachedToWindow).thenReturn(true) + repeatWhenAttached() - whenever(view.isAttachedToWindow).thenReturn(false) - attachListeners.last().onViewDetachedFromWindow(view) + whenever(view.isAttachedToWindow).thenReturn(false) + attachListeners.last().onViewDetachedFromWindow(view) - assertThat(block.invocationCount).isEqualTo(1) - assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.DESTROYED) - } + runCurrent() + assertThat(block.invocationCount).isEqualTo(1) + assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.DESTROYED) + } @Test - fun `repeatWhenAttached - view gets reattached - recreates a lifecycle`() = runBlockingTest { - whenever(view.isAttachedToWindow).thenReturn(true) - repeatWhenAttached() - whenever(view.isAttachedToWindow).thenReturn(false) - attachListeners.last().onViewDetachedFromWindow(view) - - whenever(view.isAttachedToWindow).thenReturn(true) - attachListeners.last().onViewAttachedToWindow(view) - - assertThat(block.invocationCount).isEqualTo(2) - assertThat(block.invocations[0].lifecycleState).isEqualTo(Lifecycle.State.DESTROYED) - assertThat(block.invocations[1].lifecycleState).isEqualTo(Lifecycle.State.CREATED) - } + fun `repeatWhenAttached - view gets reattached - recreates a lifecycle`() = + testScope.runTest { + whenever(view.isAttachedToWindow).thenReturn(true) + repeatWhenAttached() + whenever(view.isAttachedToWindow).thenReturn(false) + attachListeners.last().onViewDetachedFromWindow(view) + + whenever(view.isAttachedToWindow).thenReturn(true) + attachListeners.last().onViewAttachedToWindow(view) + + runCurrent() + assertThat(block.invocationCount).isEqualTo(2) + assertThat(block.invocations[0].lifecycleState).isEqualTo(Lifecycle.State.DESTROYED) + assertThat(block.invocations[1].lifecycleState).isEqualTo(Lifecycle.State.CREATED) + } @Test - fun `repeatWhenAttached - dispose attached`() = runBlockingTest { - whenever(view.isAttachedToWindow).thenReturn(true) - val handle = repeatWhenAttached() + fun `repeatWhenAttached - dispose attached`() = + testScope.runTest { + whenever(view.isAttachedToWindow).thenReturn(true) + val handle = repeatWhenAttached() - handle.dispose() + handle.dispose() - assertThat(attachListeners).isEmpty() - assertThat(block.invocationCount).isEqualTo(1) - assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.DESTROYED) - } + runCurrent() + assertThat(attachListeners).isEmpty() + assertThat(block.invocationCount).isEqualTo(1) + assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.DESTROYED) + } @Test - fun `repeatWhenAttached - dispose never attached`() = runBlockingTest { - whenever(view.isAttachedToWindow).thenReturn(false) - val handle = repeatWhenAttached() + fun `repeatWhenAttached - dispose never attached`() = + testScope.runTest { + whenever(view.isAttachedToWindow).thenReturn(false) + val handle = repeatWhenAttached() - handle.dispose() + handle.dispose() - assertThat(attachListeners).isEmpty() - assertThat(block.invocationCount).isEqualTo(0) - } + assertThat(attachListeners).isEmpty() + assertThat(block.invocationCount).isEqualTo(0) + } @Test - fun `repeatWhenAttached - dispose previously attached now detached`() = runBlockingTest { - whenever(view.isAttachedToWindow).thenReturn(true) - val handle = repeatWhenAttached() - attachListeners.last().onViewDetachedFromWindow(view) - - handle.dispose() - - assertThat(attachListeners).isEmpty() - assertThat(block.invocationCount).isEqualTo(1) - assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.DESTROYED) - } + fun `repeatWhenAttached - dispose previously attached now detached`() = + testScope.runTest { + whenever(view.isAttachedToWindow).thenReturn(true) + val handle = repeatWhenAttached() + attachListeners.last().onViewDetachedFromWindow(view) + + handle.dispose() + + runCurrent() + assertThat(attachListeners).isEmpty() + assertThat(block.invocationCount).isEqualTo(1) + assertThat(block.latestLifecycleState).isEqualTo(Lifecycle.State.DESTROYED) + } private fun CoroutineScope.repeatWhenAttached(): DisposableHandle { return view.repeatWhenAttached( diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt new file mode 100644 index 000000000000..c03849b35f54 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/data/repository/TileSpecSettingsRepositoryTest.kt @@ -0,0 +1,341 @@ +/* + * 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.qs.pipeline.data.repository + +import android.provider.Settings +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.qs.QSHost +import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.pipeline.shared.logging.QSPipelineLogger +import com.android.systemui.util.settings.FakeSettings +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@OptIn(ExperimentalCoroutinesApi::class) +class TileSpecSettingsRepositoryTest : SysuiTestCase() { + + private lateinit var secureSettings: FakeSettings + + @Mock private lateinit var logger: QSPipelineLogger + + private val testDispatcher = StandardTestDispatcher() + private val testScope = TestScope(testDispatcher) + + private lateinit var underTest: TileSpecSettingsRepository + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + + secureSettings = FakeSettings() + + with(context.orCreateTestableResources) { + addOverride(R.string.quick_settings_tiles_default, DEFAULT_TILES) + } + + underTest = + TileSpecSettingsRepository( + secureSettings, + context.resources, + logger, + testDispatcher, + ) + } + + @Test + fun emptySetting_usesDefaultValue() = + testScope.runTest { + val tiles by collectLastValue(underTest.tilesSpecs(0)) + assertThat(tiles).isEqualTo(getDefaultTileSpecs()) + } + + @Test + fun changeInSettings_changesValue() = + testScope.runTest { + val tiles by collectLastValue(underTest.tilesSpecs(0)) + + storeTilesForUser("a", 0) + assertThat(tiles).isEqualTo(listOf(TileSpec.create("a"))) + + storeTilesForUser("a,custom(b/c)", 0) + assertThat(tiles) + .isEqualTo(listOf(TileSpec.create("a"), TileSpec.create("custom(b/c)"))) + } + + @Test + fun tilesForCorrectUsers() = + testScope.runTest { + val tilesFromUser0 by collectLastValue(underTest.tilesSpecs(0)) + val tilesFromUser1 by collectLastValue(underTest.tilesSpecs(1)) + + val user0Tiles = "a" + val user1Tiles = "custom(b/c)" + storeTilesForUser(user0Tiles, 0) + storeTilesForUser(user1Tiles, 1) + + assertThat(tilesFromUser0).isEqualTo(user0Tiles.toTileSpecs()) + assertThat(tilesFromUser1).isEqualTo(user1Tiles.toTileSpecs()) + } + + @Test + fun invalidTilesAreNotPresent() = + testScope.runTest { + val tiles by collectLastValue(underTest.tilesSpecs(0)) + + val specs = "d,custom(bad)" + storeTilesForUser(specs, 0) + + assertThat(tiles).isEqualTo(specs.toTileSpecs().filter { it != TileSpec.Invalid }) + } + + @Test + fun noValidTiles_defaultSet() = + testScope.runTest { + val tiles by collectLastValue(underTest.tilesSpecs(0)) + + storeTilesForUser("custom(bad),custom()", 0) + + assertThat(tiles).isEqualTo(getDefaultTileSpecs()) + } + + @Test + fun addTileAtEnd() = + testScope.runTest { + val tiles by collectLastValue(underTest.tilesSpecs(0)) + + storeTilesForUser("a", 0) + + underTest.addTile(userId = 0, TileSpec.create("b")) + + val expected = "a,b" + assertThat(loadTilesForUser(0)).isEqualTo(expected) + assertThat(tiles).isEqualTo(expected.toTileSpecs()) + } + + @Test + fun addTileAtPosition() = + testScope.runTest { + val tiles by collectLastValue(underTest.tilesSpecs(0)) + + storeTilesForUser("a,custom(b/c)", 0) + + underTest.addTile(userId = 0, TileSpec.create("d"), position = 1) + + val expected = "a,d,custom(b/c)" + assertThat(loadTilesForUser(0)).isEqualTo(expected) + assertThat(tiles).isEqualTo(expected.toTileSpecs()) + } + + @Test + fun addInvalidTile_noop() = + testScope.runTest { + val tiles by collectLastValue(underTest.tilesSpecs(0)) + + val specs = "a,custom(b/c)" + storeTilesForUser(specs, 0) + + underTest.addTile(userId = 0, TileSpec.Invalid) + + assertThat(loadTilesForUser(0)).isEqualTo(specs) + assertThat(tiles).isEqualTo(specs.toTileSpecs()) + } + + @Test + fun addTileForOtherUser_addedInThatUser() = + testScope.runTest { + val tilesUser0 by collectLastValue(underTest.tilesSpecs(0)) + val tilesUser1 by collectLastValue(underTest.tilesSpecs(1)) + + storeTilesForUser("a", 0) + storeTilesForUser("b", 1) + + underTest.addTile(userId = 1, TileSpec.create("c")) + + assertThat(loadTilesForUser(0)).isEqualTo("a") + assertThat(tilesUser0).isEqualTo("a".toTileSpecs()) + assertThat(loadTilesForUser(1)).isEqualTo("b,c") + assertThat(tilesUser1).isEqualTo("b,c".toTileSpecs()) + } + + @Test + fun removeTile() = + testScope.runTest { + val tiles by collectLastValue(underTest.tilesSpecs(0)) + + storeTilesForUser("a,b", 0) + + underTest.removeTile(userId = 0, TileSpec.create("a")) + + assertThat(loadTilesForUser(0)).isEqualTo("b") + assertThat(tiles).isEqualTo("b".toTileSpecs()) + } + + @Test + fun removeTileNotThere_noop() = + testScope.runTest { + val tiles by collectLastValue(underTest.tilesSpecs(0)) + + val specs = "a,b" + storeTilesForUser(specs, 0) + + underTest.removeTile(userId = 0, TileSpec.create("c")) + + assertThat(loadTilesForUser(0)).isEqualTo(specs) + assertThat(tiles).isEqualTo(specs.toTileSpecs()) + } + + @Test + fun removeInvalidTile_noop() = + testScope.runTest { + val tiles by collectLastValue(underTest.tilesSpecs(0)) + + val specs = "a,b" + storeTilesForUser(specs, 0) + + underTest.removeTile(userId = 0, TileSpec.Invalid) + + assertThat(loadTilesForUser(0)).isEqualTo(specs) + assertThat(tiles).isEqualTo(specs.toTileSpecs()) + } + + @Test + fun removeTileFromSecondaryUser_removedOnlyInCorrectUser() = + testScope.runTest { + val user0Tiles by collectLastValue(underTest.tilesSpecs(0)) + val user1Tiles by collectLastValue(underTest.tilesSpecs(1)) + + val specs = "a,b" + storeTilesForUser(specs, 0) + storeTilesForUser(specs, 1) + + underTest.removeTile(userId = 1, TileSpec.create("a")) + + assertThat(loadTilesForUser(0)).isEqualTo(specs) + assertThat(user0Tiles).isEqualTo(specs.toTileSpecs()) + assertThat(loadTilesForUser(1)).isEqualTo("b") + assertThat(user1Tiles).isEqualTo("b".toTileSpecs()) + } + + @Test + fun changeTiles() = + testScope.runTest { + val tiles by collectLastValue(underTest.tilesSpecs(0)) + + val specs = "a,custom(b/c)" + + underTest.setTiles(userId = 0, specs.toTileSpecs()) + + assertThat(loadTilesForUser(0)).isEqualTo(specs) + assertThat(tiles).isEqualTo(specs.toTileSpecs()) + } + + @Test + fun changeTiles_ignoresInvalid() = + testScope.runTest { + val tiles by collectLastValue(underTest.tilesSpecs(0)) + + val specs = "a,custom(b/c)" + + underTest.setTiles(userId = 0, listOf(TileSpec.Invalid) + specs.toTileSpecs()) + + assertThat(loadTilesForUser(0)).isEqualTo(specs) + assertThat(tiles).isEqualTo(specs.toTileSpecs()) + } + + @Test + fun changeTiles_empty_noChanges() = + testScope.runTest { + val tiles by collectLastValue(underTest.tilesSpecs(0)) + + underTest.setTiles(userId = 0, emptyList()) + + assertThat(loadTilesForUser(0)).isNull() + assertThat(tiles).isEqualTo(getDefaultTileSpecs()) + } + + @Test + fun changeTiles_forCorrectUser() = + testScope.runTest { + val user0Tiles by collectLastValue(underTest.tilesSpecs(0)) + val user1Tiles by collectLastValue(underTest.tilesSpecs(1)) + + val specs = "a" + storeTilesForUser(specs, 0) + storeTilesForUser(specs, 1) + + underTest.setTiles(userId = 1, "b".toTileSpecs()) + + assertThat(loadTilesForUser(0)).isEqualTo("a") + assertThat(user0Tiles).isEqualTo(specs.toTileSpecs()) + + assertThat(loadTilesForUser(1)).isEqualTo("b") + assertThat(user1Tiles).isEqualTo("b".toTileSpecs()) + } + + @Test + fun multipleConcurrentRemovals_bothRemoved() = + testScope.runTest { + val tiles by collectLastValue(underTest.tilesSpecs(0)) + + val specs = "a,b,c" + storeTilesForUser(specs, 0) + + coroutineScope { + underTest.removeTile(userId = 0, TileSpec.create("c")) + underTest.removeTile(userId = 0, TileSpec.create("a")) + } + + assertThat(loadTilesForUser(0)).isEqualTo("b") + assertThat(tiles).isEqualTo("b".toTileSpecs()) + } + + private fun getDefaultTileSpecs(): List<TileSpec> { + return QSHost.getDefaultSpecs(context.resources).map(TileSpec::create) + } + + private fun storeTilesForUser(specs: String, forUser: Int) { + secureSettings.putStringForUser(SETTING, specs, forUser) + } + + private fun loadTilesForUser(forUser: Int): String? { + return secureSettings.getStringForUser(SETTING, forUser) + } + + companion object { + private const val DEFAULT_TILES = "a,b,c" + private const val SETTING = Settings.Secure.QS_TILES + + private fun String.toTileSpecs(): List<TileSpec> { + return split(",").map(TileSpec::create) + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/shared/TileSpecTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/shared/TileSpecTest.kt new file mode 100644 index 000000000000..d880172c1ba6 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/pipeline/shared/TileSpecTest.kt @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.pipeline.shared + +import android.content.ComponentName +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class TileSpecTest : SysuiTestCase() { + + @Test + fun platformTile() { + val spec = "spec" + + val tileSpec = TileSpec.create(spec) + + assertThat(tileSpec is TileSpec.PlatformTileSpec).isTrue() + assertThat(tileSpec.spec).isEqualTo(spec) + } + + @Test + fun customTile() { + val componentName = ComponentName("test_pkg", "test_cls") + val spec = CUSTOM_TILE_PREFIX + componentName.flattenToString() + ")" + + val tileSpec = TileSpec.create(spec) + + assertThat(tileSpec is TileSpec.CustomTileSpec).isTrue() + assertThat(tileSpec.spec).isEqualTo(spec) + assertThat((tileSpec as TileSpec.CustomTileSpec).componentName).isEqualTo(componentName) + } + + @Test + fun emptyCustomTile_invalid() { + val spec = CUSTOM_TILE_PREFIX + ")" + + val tileSpec = TileSpec.create(spec) + + assertThat(tileSpec).isEqualTo(TileSpec.Invalid) + } + + @Test + fun invalidCustomTileSpec_invalid() { + val spec = CUSTOM_TILE_PREFIX + "invalid)" + + val tileSpec = TileSpec.create(spec) + + assertThat(tileSpec).isEqualTo(TileSpec.Invalid) + } + + @Test + fun customTileNotEndsWithParenthesis_invalid() { + val componentName = ComponentName("test_pkg", "test_cls") + val spec = CUSTOM_TILE_PREFIX + componentName.flattenToString() + + val tileSpec = TileSpec.create(spec) + + assertThat(tileSpec).isEqualTo(TileSpec.Invalid) + } + + @Test + fun emptySpec_invalid() { + assertThat(TileSpec.create("")).isEqualTo(TileSpec.Invalid) + } + + companion object { + private const val CUSTOM_TILE_PREFIX = "custom(" + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java index 9b6d29310d0b..b5e77e0fb693 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java @@ -16,14 +16,18 @@ package com.android.systemui.statusbar.notification.collection.coordinator; +import static android.provider.Settings.Secure.SHOW_NOTIFICATION_SNOOZE; + import static com.android.systemui.statusbar.notification.collection.GroupEntry.ROOT_ENTRY; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -31,6 +35,7 @@ import static org.mockito.Mockito.when; import static java.util.Objects.requireNonNull; +import android.database.ContentObserver; import android.os.Handler; import android.os.RemoteException; import android.testing.AndroidTestingRunner; @@ -295,6 +300,42 @@ public class PreparationCoordinatorTest extends SysuiTestCase { } @Test + public void testEntryCancellationWillRebindViews() { + // Configure NotifUiAdjustmentProvider to set up SHOW_NOTIFICATION_SNOOZE value + mEntry = spy(mEntry); + mAdjustmentProvider.addDirtyListener(mock(Runnable.class)); + when(mSecureSettings.getIntForUser(eq(SHOW_NOTIFICATION_SNOOZE), anyInt(), anyInt())) + .thenReturn(1); + ArgumentCaptor<ContentObserver> contentObserverCaptor = ArgumentCaptor.forClass( + ContentObserver.class); + verify(mSecureSettings).registerContentObserverForUser(eq(SHOW_NOTIFICATION_SNOOZE), + contentObserverCaptor.capture(), anyInt()); + ContentObserver contentObserver = contentObserverCaptor.getValue(); + contentObserver.onChange(false); + + // GIVEN an inflated notification + mCollectionListener.onEntryInit(mEntry); + mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry)); + verify(mNotifInflater).inflateViews(eq(mEntry), any(), any()); + mNotifInflater.invokeInflateCallbackForEntry(mEntry); + + // Verify that snooze is initially enabled: from Settings & notification is not cancelled + assertTrue(mAdjustmentProvider.calculateAdjustment(mEntry).isSnoozeEnabled()); + + // WHEN notification is cancelled, rebind views because snooze enabled value changes + when(mEntry.isCanceled()).thenReturn(true); + mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry)); + + assertFalse(mAdjustmentProvider.calculateAdjustment(mEntry).isSnoozeEnabled()); + + // THEN we rebind it + verify(mNotifInflater).rebindViews(eq(mEntry), any(), any()); + + // THEN we do not filter it because it's not the first inflation. + assertFalse(mUninflatedFilter.shouldFilterOut(mEntry, 0)); + } + + @Test public void testDoesntFilterInflatedNotifs() { // GIVEN an inflated notification mCollectionListener.onEntryInit(mEntry); diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java index 652052736bed..28bdca97552d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java @@ -125,7 +125,6 @@ import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.policy.ZenModeController; import com.android.wm.shell.ShellTaskOrganizer; -import com.android.wm.shell.TaskViewTransitions; import com.android.wm.shell.WindowManagerShellWrapper; import com.android.wm.shell.bubbles.Bubble; import com.android.wm.shell.bubbles.BubbleBadgeIconFactory; @@ -148,6 +147,7 @@ import com.android.wm.shell.onehanded.OneHandedController; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.taskview.TaskViewTransitions; import org.junit.After; import org.junit.Before; diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java index 317928516c03..c3bb7716d9a9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java @@ -25,7 +25,6 @@ import android.view.WindowManager; import com.android.internal.statusbar.IStatusBarService; import com.android.wm.shell.ShellTaskOrganizer; -import com.android.wm.shell.TaskViewTransitions; import com.android.wm.shell.WindowManagerShellWrapper; import com.android.wm.shell.bubbles.BubbleController; import com.android.wm.shell.bubbles.BubbleData; @@ -42,6 +41,7 @@ import com.android.wm.shell.onehanded.OneHandedController; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.taskview.TaskViewTransitions; import java.util.Optional; diff --git a/services/core/java/com/android/server/am/BroadcastFilter.java b/services/core/java/com/android/server/am/BroadcastFilter.java index a92723ee7036..749427730ca0 100644 --- a/services/core/java/com/android/server/am/BroadcastFilter.java +++ b/services/core/java/com/android/server/am/BroadcastFilter.java @@ -16,6 +16,7 @@ package com.android.server.am; +import android.annotation.Nullable; import android.content.IntentFilter; import android.util.PrintWriterPrinter; import android.util.Printer; @@ -55,6 +56,16 @@ final class BroadcastFilter extends IntentFilter { exported = _exported; } + public @Nullable String getReceiverClassName() { + if (receiverId != null) { + final int index = receiverId.lastIndexOf('@'); + if (index > 0) { + return receiverId.substring(0, index); + } + } + return null; + } + @NeverCompile public void dumpDebug(ProtoOutputStream proto, long fieldId) { long token = proto.start(fieldId); diff --git a/services/core/java/com/android/server/am/BroadcastProcessQueue.java b/services/core/java/com/android/server/am/BroadcastProcessQueue.java index 0cdd4e9041e9..056e17a5ef3c 100644 --- a/services/core/java/com/android/server/am/BroadcastProcessQueue.java +++ b/services/core/java/com/android/server/am/BroadcastProcessQueue.java @@ -393,6 +393,10 @@ class BroadcastProcessQueue { setProcessInstrumented(false); setProcessPersistent(false); } + + // Since we may have just changed our PID, invalidate cached strings + mCachedToString = null; + mCachedToShortString = null; } /** @@ -1128,16 +1132,16 @@ class BroadcastProcessQueue { @Override public String toString() { if (mCachedToString == null) { - mCachedToString = "BroadcastProcessQueue{" - + Integer.toHexString(System.identityHashCode(this)) - + " " + processName + "/" + UserHandle.formatUid(uid) + "}"; + mCachedToString = "BroadcastProcessQueue{" + toShortString() + "}"; } return mCachedToString; } public String toShortString() { if (mCachedToShortString == null) { - mCachedToShortString = processName + "/" + UserHandle.formatUid(uid); + mCachedToShortString = Integer.toHexString(System.identityHashCode(this)) + + " " + ((app != null) ? app.getPid() : "?") + ":" + processName + "/" + + UserHandle.formatUid(uid); } return mCachedToShortString; } diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java index 1f0b1628aa22..93173abeb106 100644 --- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java +++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java @@ -32,6 +32,7 @@ import static com.android.server.am.BroadcastProcessQueue.insertIntoRunnableList import static com.android.server.am.BroadcastProcessQueue.reasonToString; import static com.android.server.am.BroadcastProcessQueue.removeFromRunnableList; import static com.android.server.am.BroadcastRecord.deliveryStateToString; +import static com.android.server.am.BroadcastRecord.getReceiverClassName; import static com.android.server.am.BroadcastRecord.getReceiverPackageName; import static com.android.server.am.BroadcastRecord.getReceiverProcessName; import static com.android.server.am.BroadcastRecord.getReceiverUid; @@ -1040,7 +1041,10 @@ class BroadcastQueueModernImpl extends BroadcastQueue { if (deliveryState == BroadcastRecord.DELIVERY_TIMEOUT) { r.anrCount++; if (app != null && !app.isDebugging()) { - mService.appNotResponding(queue.app, TimeoutRecord.forBroadcastReceiver(r.intent)); + final String packageName = getReceiverPackageName(receiver); + final String className = getReceiverClassName(receiver); + mService.appNotResponding(queue.app, + TimeoutRecord.forBroadcastReceiver(r.intent, packageName, className)); } } else { mLocalHandler.removeMessages(MSG_DELIVERY_TIMEOUT_SOFT, queue); diff --git a/services/core/java/com/android/server/am/BroadcastRecord.java b/services/core/java/com/android/server/am/BroadcastRecord.java index e6ef3b468d2d..c368290386a0 100644 --- a/services/core/java/com/android/server/am/BroadcastRecord.java +++ b/services/core/java/com/android/server/am/BroadcastRecord.java @@ -832,6 +832,14 @@ final class BroadcastRecord extends Binder { } } + static @Nullable String getReceiverClassName(@NonNull Object receiver) { + if (receiver instanceof BroadcastFilter) { + return ((BroadcastFilter) receiver).getReceiverClassName(); + } else /* if (receiver instanceof ResolveInfo) */ { + return ((ResolveInfo) receiver).activityInfo.name; + } + } + static int getReceiverPriority(@NonNull Object receiver) { if (receiver instanceof BroadcastFilter) { return ((BroadcastFilter) receiver).getPriority(); @@ -1068,9 +1076,7 @@ final class BroadcastRecord extends Binder { if (label == null) { label = intent.toString(); } - mCachedToString = "BroadcastRecord{" - + Integer.toHexString(System.identityHashCode(this)) - + " u" + userId + " " + label + "}"; + mCachedToString = "BroadcastRecord{" + toShortString() + "}"; } return mCachedToString; } @@ -1082,7 +1088,7 @@ final class BroadcastRecord extends Binder { label = intent.toString(); } mCachedToShortString = Integer.toHexString(System.identityHashCode(this)) - + ":" + label + "/u" + userId; + + " " + label + "/u" + userId; } return mCachedToShortString; } diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java index 7a92434a4ba4..b9c9efee0ffd 100644 --- a/services/core/java/com/android/server/am/OomAdjuster.java +++ b/services/core/java/com/android/server/am/OomAdjuster.java @@ -24,6 +24,7 @@ import static android.app.ActivityManager.PROCESS_CAPABILITY_FOREGROUND_LOCATION import static android.app.ActivityManager.PROCESS_CAPABILITY_FOREGROUND_MICROPHONE; import static android.app.ActivityManager.PROCESS_CAPABILITY_NONE; import static android.app.ActivityManager.PROCESS_CAPABILITY_POWER_RESTRICTED_NETWORK; +import static android.app.ActivityManager.PROCESS_CAPABILITY_USER_RESTRICTED_NETWORK; import static android.app.ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE; import static android.app.ActivityManager.PROCESS_STATE_BOUND_TOP; import static android.app.ActivityManager.PROCESS_STATE_CACHED_ACTIVITY; @@ -2273,6 +2274,15 @@ public class OomAdjuster { capability |= PROCESS_CAPABILITY_POWER_RESTRICTED_NETWORK; } } + if ((cstate.getCurCapability() + & PROCESS_CAPABILITY_USER_RESTRICTED_NETWORK) != 0) { + if (clientProcState <= PROCESS_STATE_IMPORTANT_FOREGROUND) { + // This is used to grant network access to User Initiated Jobs. + if (cr.hasFlag(Context.BIND_BYPASS_USER_NETWORK_RESTRICTIONS)) { + capability |= PROCESS_CAPABILITY_USER_RESTRICTED_NETWORK; + } + } + } if (shouldSkipDueToCycle(app, cstate, procState, adj, cycleReEval)) { continue; diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java index 4e401b258550..b26a1705b309 100644 --- a/services/core/java/com/android/server/am/ProcessList.java +++ b/services/core/java/com/android/server/am/ProcessList.java @@ -4900,12 +4900,14 @@ public final class ProcessList { final boolean isAllowed = isProcStateAllowedWhileIdleOrPowerSaveMode(uidRec.getCurProcState(), uidRec.getCurCapability()) - || isProcStateAllowedWhileOnRestrictBackground(uidRec.getCurProcState()); + || isProcStateAllowedWhileOnRestrictBackground(uidRec.getCurProcState(), + uidRec.getCurCapability()); // Denotes whether uid's process state was previously allowed network access. final boolean wasAllowed = isProcStateAllowedWhileIdleOrPowerSaveMode(uidRec.getSetProcState(), uidRec.getSetCapability()) - || isProcStateAllowedWhileOnRestrictBackground(uidRec.getSetProcState()); + || isProcStateAllowedWhileOnRestrictBackground(uidRec.getSetProcState(), + uidRec.getSetCapability()); // When the uid is coming to foreground, AMS should inform the app thread that it should // block for the network rules to get updated before launching an activity. diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 5893f1efc505..3780620ca0d3 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -11602,6 +11602,7 @@ public class AudioService extends IAudioService.Stub return false; } + final long token = Binder.clearCallingIdentity(); try { if (!projectionService.isCurrentProjection(projection)) { Log.w(TAG, "App passed invalid MediaProjection token"); @@ -11611,6 +11612,8 @@ public class AudioService extends IAudioService.Stub Log.e(TAG, "Can't call .isCurrentProjection() on IMediaProjectionManager" + projectionService.asBinder(), e); return false; + } finally { + Binder.restoreCallingIdentity(token); } try { diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java index 01ffc7e29ac0..128ef0b2a802 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java @@ -82,7 +82,9 @@ import com.android.internal.widget.LockPatternUtils; import com.android.server.SystemService; import com.android.server.biometrics.Utils; import com.android.server.biometrics.log.BiometricContext; +import com.android.server.biometrics.sensors.BaseClientMonitor; import com.android.server.biometrics.sensors.BiometricStateCallback; +import com.android.server.biometrics.sensors.ClientMonitorCallback; import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter; import com.android.server.biometrics.sensors.LockoutResetDispatcher; import com.android.server.biometrics.sensors.LockoutTracker; @@ -97,7 +99,9 @@ import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.function.Supplier; @@ -1138,12 +1142,28 @@ public class FingerprintService extends SystemService { if (Utils.isVirtualEnabled(getContext())) { Slog.i(TAG, "Sync virtual enrollments"); final int userId = ActivityManager.getCurrentUser(); + final CountDownLatch latch = new CountDownLatch(mRegistry.getProviders().size()); for (ServiceProvider provider : mRegistry.getProviders()) { for (FingerprintSensorPropertiesInternal props : provider.getSensorProperties()) { - provider.scheduleInternalCleanup(props.sensorId, userId, null /* callback */, - true /* favorHalEnrollments */); + provider.scheduleInternalCleanup(props.sensorId, userId, + new ClientMonitorCallback() { + @Override + public void onClientFinished( + @NonNull BaseClientMonitor clientMonitor, + boolean success) { + latch.countDown(); + if (!success) { + Slog.e(TAG, "Sync virtual enrollments failed"); + } + } + }, true /* favorHalEnrollments */); } } + try { + latch.await(3, TimeUnit.SECONDS); + } catch (Exception e) { + Slog.e(TAG, "Failed to wait for sync finishing", e); + } } } } diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index 745e40411bad..c3a4c2e389ac 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -1419,6 +1419,7 @@ public final class DisplayManagerService extends SystemService { } if (projection != null) { + final long firstToken = Binder.clearCallingIdentity(); try { if (!getProjectionService().isCurrentProjection(projection)) { throw new SecurityException("Cannot create VirtualDisplay with " @@ -1427,6 +1428,8 @@ public final class DisplayManagerService extends SystemService { flags = projection.applyVirtualDisplayFlags(flags); } catch (RemoteException e) { throw new SecurityException("unable to validate media projection or flags"); + } finally { + Binder.restoreCallingIdentity(firstToken); } } @@ -1494,7 +1497,7 @@ public final class DisplayManagerService extends SystemService { throw new SecurityException("Requires INTERNAL_SYSTEM_WINDOW permission"); } - final long token = Binder.clearCallingIdentity(); + final long secondToken = Binder.clearCallingIdentity(); try { final int displayId; synchronized (mSyncRoot) { @@ -1566,7 +1569,7 @@ public final class DisplayManagerService extends SystemService { return displayId; } finally { - Binder.restoreCallingIdentity(token); + Binder.restoreCallingIdentity(secondToken); } } diff --git a/services/core/java/com/android/server/display/WifiDisplayAdapter.java b/services/core/java/com/android/server/display/WifiDisplayAdapter.java index e8327018e144..e3d38e7a25e9 100644 --- a/services/core/java/com/android/server/display/WifiDisplayAdapter.java +++ b/services/core/java/com/android/server/display/WifiDisplayAdapter.java @@ -16,6 +16,7 @@ package com.android.server.display; +import android.app.BroadcastOptions; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -421,6 +422,7 @@ final class WifiDisplayAdapter extends DisplayAdapter { // Runs on the handler. private void handleSendStatusChangeBroadcast() { final Intent intent; + final BroadcastOptions options; synchronized (getSyncRoot()) { if (!mPendingStatusChangeBroadcast) { return; @@ -431,10 +433,13 @@ final class WifiDisplayAdapter extends DisplayAdapter { intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); intent.putExtra(DisplayManager.EXTRA_WIFI_DISPLAY_STATUS, getWifiDisplayStatusLocked()); + + options = BroadcastOptions.makeBasic(); + options.setDeliveryGroupPolicy(BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT); } // Send protected broadcast about wifi display status to registered receivers. - getContext().sendBroadcastAsUser(intent, UserHandle.ALL); + getContext().sendBroadcastAsUser(intent, UserHandle.ALL, null, options.toBundle()); } private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java index c0deb3f8274b..805ff6611c29 100644 --- a/services/core/java/com/android/server/hdmi/HdmiControlService.java +++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java @@ -3774,11 +3774,12 @@ public class HdmiControlService extends SystemService { } try { record.mListener.onReceived(srcAddress, destAddress, params, hasVendorId); + return true; } catch (RemoteException e) { Slog.e(TAG, "Failed to notify vendor command reception", e); } } - return true; + return false; } } diff --git a/services/core/java/com/android/server/inputmethod/HandwritingModeController.java b/services/core/java/com/android/server/inputmethod/HandwritingModeController.java index d54354715d03..bb1a445b52e9 100644 --- a/services/core/java/com/android/server/inputmethod/HandwritingModeController.java +++ b/services/core/java/com/android/server/inputmethod/HandwritingModeController.java @@ -24,8 +24,8 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.UiThread; -import android.os.Handler; import android.hardware.input.InputManagerGlobal; +import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.text.TextUtils; @@ -165,7 +165,11 @@ final class HandwritingModeController { @NonNull String delegatePackageName, @NonNull String delegatorPackageName) { mDelegatePackageName = delegatePackageName; mDelegatorPackageName = delegatorPackageName; - mHandwritingBuffer.ensureCapacity(getHandwritingBufferSize()); + if (mHandwritingBuffer == null) { + mHandwritingBuffer = new ArrayList<>(getHandwritingBufferSize()); + } else { + mHandwritingBuffer.ensureCapacity(getHandwritingBufferSize()); + } scheduleHandwritingDelegationTimeout(); } diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java index b75b7d4daacd..48acc7c2ba8d 100644 --- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java +++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java @@ -16,6 +16,7 @@ package com.android.server.media.projection; +import static android.Manifest.permission.MANAGE_MEDIA_PROJECTION; import static android.app.ActivityManagerInternal.MEDIA_PROJECTION_TOKEN_EVENT_CREATED; import static android.app.ActivityManagerInternal.MEDIA_PROJECTION_TOKEN_EVENT_DESTROYED; @@ -282,7 +283,7 @@ public final class MediaProjectionManagerService extends SystemService @Override // Binder call public IMediaProjection createProjection(int uid, String packageName, int type, boolean isPermanentGrant) { - if (mContext.checkCallingPermission(Manifest.permission.MANAGE_MEDIA_PROJECTION) + if (mContext.checkCallingPermission(MANAGE_MEDIA_PROJECTION) != PackageManager.PERMISSION_GRANTED) { throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to grant " + "projection permission"); @@ -314,16 +315,21 @@ public final class MediaProjectionManagerService extends SystemService @Override // Binder call public boolean isCurrentProjection(IMediaProjection projection) { + if (mContext.checkCallingOrSelfPermission(MANAGE_MEDIA_PROJECTION) + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to check " + + "if the given projection is current."); + } return MediaProjectionManagerService.this.isCurrentProjection( projection == null ? null : projection.asBinder()); } @Override // Binder call public MediaProjectionInfo getActiveProjectionInfo() { - if (mContext.checkCallingPermission(Manifest.permission.MANAGE_MEDIA_PROJECTION) + if (mContext.checkCallingPermission(MANAGE_MEDIA_PROJECTION) != PackageManager.PERMISSION_GRANTED) { - throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to add " - + "projection callbacks"); + throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to get " + + "active projection info"); } final long token = Binder.clearCallingIdentity(); try { @@ -335,10 +341,10 @@ public final class MediaProjectionManagerService extends SystemService @Override // Binder call public void stopActiveProjection() { - if (mContext.checkCallingOrSelfPermission(Manifest.permission.MANAGE_MEDIA_PROJECTION) + if (mContext.checkCallingOrSelfPermission(MANAGE_MEDIA_PROJECTION) != PackageManager.PERMISSION_GRANTED) { - throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to add " - + "projection callbacks"); + throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to stop " + + "the active projection"); } final long token = Binder.clearCallingIdentity(); try { @@ -352,7 +358,7 @@ public final class MediaProjectionManagerService extends SystemService @Override // Binder call public void notifyActiveProjectionCapturedContentResized(int width, int height) { - if (mContext.checkCallingOrSelfPermission(Manifest.permission.MANAGE_MEDIA_PROJECTION) + if (mContext.checkCallingOrSelfPermission(MANAGE_MEDIA_PROJECTION) != PackageManager.PERMISSION_GRANTED) { throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to notify " + "on captured content resize"); @@ -372,10 +378,10 @@ public final class MediaProjectionManagerService extends SystemService @Override public void notifyActiveProjectionCapturedContentVisibilityChanged(boolean isVisible) { - if (mContext.checkCallingOrSelfPermission(Manifest.permission.MANAGE_MEDIA_PROJECTION) + if (mContext.checkCallingOrSelfPermission(MANAGE_MEDIA_PROJECTION) != PackageManager.PERMISSION_GRANTED) { throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to notify " - + "on captured content resize"); + + "on captured content visibility changed"); } if (!isCurrentProjection(mProjectionGrant)) { return; @@ -392,7 +398,7 @@ public final class MediaProjectionManagerService extends SystemService @Override //Binder call public void addCallback(final IMediaProjectionWatcherCallback callback) { - if (mContext.checkCallingPermission(Manifest.permission.MANAGE_MEDIA_PROJECTION) + if (mContext.checkCallingPermission(MANAGE_MEDIA_PROJECTION) != PackageManager.PERMISSION_GRANTED) { throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to add " + "projection callbacks"); @@ -407,7 +413,7 @@ public final class MediaProjectionManagerService extends SystemService @Override public void removeCallback(IMediaProjectionWatcherCallback callback) { - if (mContext.checkCallingPermission(Manifest.permission.MANAGE_MEDIA_PROJECTION) + if (mContext.checkCallingPermission(MANAGE_MEDIA_PROJECTION) != PackageManager.PERMISSION_GRANTED) { throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to remove " + "projection callbacks"); @@ -512,6 +518,11 @@ public final class MediaProjectionManagerService extends SystemService @Override // Binder call public int applyVirtualDisplayFlags(int flags) { + if (mContext.checkCallingOrSelfPermission(MANAGE_MEDIA_PROJECTION) + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION to apply virtual " + + "display flags."); + } if (mType == MediaProjectionManager.TYPE_SCREEN_CAPTURE) { flags &= ~DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY; flags |= DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR @@ -660,11 +671,21 @@ public final class MediaProjectionManagerService extends SystemService @Override // Binder call public void setLaunchCookie(IBinder launchCookie) { + if (mContext.checkCallingOrSelfPermission(MANAGE_MEDIA_PROJECTION) + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION to set launch " + + "cookie."); + } mLaunchCookie = launchCookie; } @Override // Binder call public IBinder getLaunchCookie() { + if (mContext.checkCallingOrSelfPermission(MANAGE_MEDIA_PROJECTION) + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION to get launch " + + "cookie."); + } return mLaunchCookie; } diff --git a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java index 92be0943c9f4..4c36b910e77b 100644 --- a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java +++ b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java @@ -5550,7 +5550,8 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { // Do this without the lock held. handleUidChanged() and handleUidGone() are // called from the handler, so there's no multi-threading issue. if (updated) { - updateNetworkStats(uid, isProcStateAllowedWhileOnRestrictBackground(procState)); + updateNetworkStats(uid, + isProcStateAllowedWhileOnRestrictBackground(procState, capability)); } } finally { Trace.traceEnd(Trace.TRACE_TAG_NETWORK); diff --git a/services/core/java/com/android/server/notification/BubbleExtractor.java b/services/core/java/com/android/server/notification/BubbleExtractor.java index a561390ac7e4..d3dea0d96812 100644 --- a/services/core/java/com/android/server/notification/BubbleExtractor.java +++ b/services/core/java/com/android/server/notification/BubbleExtractor.java @@ -182,7 +182,7 @@ public class BubbleExtractor implements NotificationSignalExtractor { /** * Whether an intent is properly configured to display in an {@link - * com.android.wm.shell.TaskView} for bubbling. + * TaskView} for bubbling. * * @param context the context to use. * @param pendingIntent the pending intent of the bubble. diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index ee2e4589e1aa..54f87d004b5c 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -590,7 +590,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { private int mDoubleTapOnHomeBehavior; // Whether to lock the device after the next app transition has finished. - private boolean mLockAfterAppTransitionFinished; + boolean mLockAfterAppTransitionFinished; // Allowed theater mode wake actions private boolean mAllowTheaterModeWakeFromKey; diff --git a/services/core/java/com/android/server/stats/pull/ProcfsMemoryUtil.java b/services/core/java/com/android/server/stats/pull/ProcfsMemoryUtil.java index f653e4b26438..6cb6dc07f8b8 100644 --- a/services/core/java/com/android/server/stats/pull/ProcfsMemoryUtil.java +++ b/services/core/java/com/android/server/stats/pull/ProcfsMemoryUtil.java @@ -28,7 +28,8 @@ public final class ProcfsMemoryUtil { "VmHWM:", "VmRSS:", "RssAnon:", - "VmSwap:" + "RssShmem:", + "VmSwap:", }; private static final String[] VMSTAT_KEYS = new String[] { "oom_kill" @@ -38,7 +39,7 @@ public final class ProcfsMemoryUtil { /** * Reads memory stats of a process from procfs. Returns values of the VmHWM, VmRss, AnonRSS, - * VmSwap fields in /proc/pid/status in kilobytes or null if not available. + * VmSwap, RssShmem fields in /proc/pid/status in kilobytes or null if not available. */ @Nullable public static MemorySnapshot readMemorySnapshotFromProcfs(int pid) { @@ -46,8 +47,9 @@ public final class ProcfsMemoryUtil { output[0] = -1; output[3] = -1; output[4] = -1; + output[5] = -1; Process.readProcLines("/proc/" + pid + "/status", STATUS_KEYS, output); - if (output[0] == -1 || output[3] == -1 || output[4] == -1) { + if (output[0] == -1 || output[3] == -1 || output[4] == -1 || output[5] == -1) { // Could not open or parse file. return null; } @@ -56,7 +58,8 @@ public final class ProcfsMemoryUtil { snapshot.rssHighWaterMarkInKilobytes = (int) output[1]; snapshot.rssInKilobytes = (int) output[2]; snapshot.anonRssInKilobytes = (int) output[3]; - snapshot.swapInKilobytes = (int) output[4]; + snapshot.rssShmemKilobytes = (int) output[4]; + snapshot.swapInKilobytes = (int) output[5]; return snapshot; } @@ -101,6 +104,7 @@ public final class ProcfsMemoryUtil { public int rssInKilobytes; public int anonRssInKilobytes; public int swapInKilobytes; + public int rssShmemKilobytes; } /** Reads and parses selected entries of /proc/vmstat. */ diff --git a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java index f8a4b04180c3..b2f48d9e3d8c 100644 --- a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java +++ b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java @@ -2290,7 +2290,8 @@ public class StatsPullAtomService extends SystemService { managedProcess.processName, managedProcess.pid, managedProcess.oomScore, snapshot.rssInKilobytes, snapshot.anonRssInKilobytes, snapshot.swapInKilobytes, snapshot.anonRssInKilobytes + snapshot.swapInKilobytes, - gpuMemPerPid.get(managedProcess.pid), managedProcess.hasForegroundServices)); + gpuMemPerPid.get(managedProcess.pid), managedProcess.hasForegroundServices, + snapshot.rssShmemKilobytes)); } // Complement the data with native system processes. Given these measurements can be taken // in response to LMKs happening, we want to first collect the managed app stats (to @@ -2309,7 +2310,8 @@ public class StatsPullAtomService extends SystemService { -1001 /*Placeholder for native processes, OOM_SCORE_ADJ_MIN - 1.*/, snapshot.rssInKilobytes, snapshot.anonRssInKilobytes, snapshot.swapInKilobytes, snapshot.anonRssInKilobytes + snapshot.swapInKilobytes, - gpuMemPerPid.get(pid), false /* has_foreground_services */)); + gpuMemPerPid.get(pid), false /* has_foreground_services */, + snapshot.rssShmemKilobytes)); } return StatsManager.PULL_SUCCESS; } diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java index f14a432f73ae..ff1c28ad1973 100644 --- a/services/core/java/com/android/server/wm/ActivityClientController.java +++ b/services/core/java/com/android/server/wm/ActivityClientController.java @@ -778,13 +778,12 @@ class ActivityClientController extends IActivityClientController.Stub { && r.mTransitionController.inPlayingTransition(r) && !r.mTransitionController.isCollecting() ? r.mTransitionController.createTransition(TRANSIT_TO_BACK) : null; - if (transition != null) { - r.mTransitionController.requestStartTransition(transition, null /*startTask */, - null /* remoteTransition */, null /* displayChange */); - } final boolean changed = r != null && r.setOccludesParent(true); if (transition != null) { if (changed) { + r.mTransitionController.requestStartTransition(transition, + null /*startTask */, null /* remoteTransition */, + null /* displayChange */); r.mTransitionController.setReady(r.getDisplayContent()); } else { transition.abort(); @@ -818,13 +817,12 @@ class ActivityClientController extends IActivityClientController.Stub { final Transition transition = r.mTransitionController.inPlayingTransition(r) && !r.mTransitionController.isCollecting() ? r.mTransitionController.createTransition(TRANSIT_TO_FRONT) : null; - if (transition != null) { - r.mTransitionController.requestStartTransition(transition, null /*startTask */, - null /* remoteTransition */, null /* displayChange */); - } final boolean changed = r.setOccludesParent(false); if (transition != null) { if (changed) { + r.mTransitionController.requestStartTransition(transition, + null /*startTask */, null /* remoteTransition */, + null /* displayChange */); r.mTransitionController.setReady(r.getDisplayContent()); } else { transition.abort(); diff --git a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java index 80d1e1683de1..a757d90b75ba 100644 --- a/services/core/java/com/android/server/wm/ActivityMetricsLogger.java +++ b/services/core/java/com/android/server/wm/ActivityMetricsLogger.java @@ -1155,6 +1155,8 @@ class ActivityMetricsLogger { sb.setLength(0); sb.append("Displayed "); sb.append(info.launchedActivityShortComponentName); + sb.append(" for user "); + sb.append(info.userId); sb.append(": "); TimeUtils.formatDuration(info.windowsDrawnDelayMs, sb); Log.i(TAG, sb.toString()); diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 6944b2918d84..81dabfd48bf3 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -179,6 +179,7 @@ import static com.android.server.wm.ActivityRecordProto.PROC_ID; import static com.android.server.wm.ActivityRecordProto.PROVIDES_MAX_BOUNDS; import static com.android.server.wm.ActivityRecordProto.REPORTED_DRAWN; import static com.android.server.wm.ActivityRecordProto.REPORTED_VISIBLE; +import static com.android.server.wm.ActivityRecordProto.SHOULD_SEND_COMPAT_FAKE_FOCUS; import static com.android.server.wm.ActivityRecordProto.STARTING_DISPLAYED; import static com.android.server.wm.ActivityRecordProto.STARTING_MOVED; import static com.android.server.wm.ActivityRecordProto.STARTING_WINDOW; @@ -10224,6 +10225,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A proto.write(ENABLE_RECENTS_SCREENSHOT, mEnableRecentsScreenshot); proto.write(LAST_DROP_INPUT_MODE, mLastDropInputMode); proto.write(OVERRIDE_ORIENTATION, getOverrideOrientation()); + proto.write(SHOULD_SEND_COMPAT_FAKE_FOCUS, shouldSendCompatFakeFocus()); } @Override diff --git a/services/core/java/com/android/server/wm/AppTransitionController.java b/services/core/java/com/android/server/wm/AppTransitionController.java index 841d28b0231f..597c8bf45132 100644 --- a/services/core/java/com/android/server/wm/AppTransitionController.java +++ b/services/core/java/com/android/server/wm/AppTransitionController.java @@ -258,7 +258,7 @@ public class AppTransitionController { tmpCloseApps = new ArraySet<>(mDisplayContent.mClosingApps); if (mDisplayContent.mAtmService.mBackNavigationController .removeIfContainsBackAnimationTargets(tmpOpenApps, tmpCloseApps)) { - mDisplayContent.mAtmService.mBackNavigationController.clearBackAnimations(null); + mDisplayContent.mAtmService.mBackNavigationController.clearBackAnimations(); } } @@ -929,7 +929,7 @@ public class AppTransitionController { /** * Returns {@code true} if a given {@link WindowContainer} is an embedded Task in - * {@link com.android.wm.shell.TaskView}. + * {@link TaskView}. * * Note that this is a short term workaround to support Android Auto until it migrate to * ShellTransition. This should only be used by {@link #getAnimationTargets}. diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java index 0d1f2ce8d63f..587e7204f993 100644 --- a/services/core/java/com/android/server/wm/BackNavigationController.java +++ b/services/core/java/com/android/server/wm/BackNavigationController.java @@ -62,13 +62,12 @@ import com.android.server.wm.utils.InsetUtils; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Objects; -import java.util.function.Consumer; /** * Controller to handle actions related to the back gesture on the server side. */ class BackNavigationController { - private static final String TAG = "BackNavigationController"; + private static final String TAG = "CoreBackPreview"; private WindowManagerService mWindowManagerService; private boolean mBackAnimationInProgress; private @BackNavigationInfo.BackTargetType int mLastBackType; @@ -76,7 +75,13 @@ class BackNavigationController { private Runnable mPendingAnimation; private final NavigationMonitor mNavigationMonitor = new NavigationMonitor(); - AnimationHandler mAnimationHandler; + private AnimationHandler mAnimationHandler; + + /** + * The transition who match the back navigation targets, + * release animation after this transition finish. + */ + private Transition mWaitTransitionFinish; private final ArrayList<WindowContainer> mTmpOpenApps = new ArrayList<>(); private final ArrayList<WindowContainer> mTmpCloseApps = new ArrayList<>(); @@ -140,6 +145,11 @@ class BackNavigationController { BackNavigationInfo.Builder infoBuilder = new BackNavigationInfo.Builder(); synchronized (wmService.mGlobalLock) { + if (isMonitoringTransition()) { + Slog.w(TAG, "Previous animation hasn't finish, status: " + mAnimationHandler); + // Don't start any animation for it. + return null; + } WindowManagerInternal windowManagerInternal = LocalServices.getService(WindowManagerInternal.class); IBinder focusedWindowToken = windowManagerInternal.getFocusedWindowToken(); @@ -374,7 +384,7 @@ class BackNavigationController { } boolean isMonitoringTransition() { - return isWaitBackTransition() || mNavigationMonitor.isMonitoring(); + return mAnimationHandler.mComposed || mNavigationMonitor.isMonitorForRemote(); } private void scheduleAnimation(@NonNull AnimationHandler.ScheduleAnimationBuilder builder) { @@ -477,7 +487,7 @@ class BackNavigationController { return false; } - private static class NavigationMonitor { + private class NavigationMonitor { // The window which triggering the back navigation. private WindowState mNavigatingWindow; private RemoteCallback mObserver; @@ -487,15 +497,23 @@ class BackNavigationController { mObserver = observer; } - void stopMonitor() { - mNavigatingWindow = null; + void stopMonitorForRemote() { mObserver = null; } - boolean isMonitoring() { + void stopMonitorTransition() { + mNavigatingWindow = null; + } + + boolean isMonitorForRemote() { return mNavigatingWindow != null && mObserver != null; } + boolean isMonitorAnimationOrTransition() { + return mNavigatingWindow != null + && (mAnimationHandler.mComposed || mAnimationHandler.mWaitTransition); + } + /** * Notify focus window changed during back navigation. This will cancel the gesture for * scenarios like: a system window popup, or when an activity add a new window. @@ -506,7 +524,8 @@ class BackNavigationController { * a short time, but we should not cancel the navigation. */ private void onFocusWindowChanged(WindowState newFocus) { - if (!isMonitoring() || !atSameDisplay(newFocus)) { + if (!atSameDisplay(newFocus) + || !(isMonitorForRemote() || isMonitorAnimationOrTransition())) { return; } // Keep navigating if either new focus == navigating window or null. @@ -514,7 +533,13 @@ class BackNavigationController { && (newFocus.mActivityRecord == null || (newFocus.mActivityRecord == mNavigatingWindow.mActivityRecord))) { EventLogTags.writeWmBackNaviCanceled("focusWindowChanged"); - mObserver.sendResult(null /* result */); + if (isMonitorForRemote()) { + mObserver.sendResult(null /* result */); + } + if (isMonitorAnimationOrTransition()) { + // transition won't happen, cancel internal status + clearBackAnimations(); + } } } @@ -523,7 +548,7 @@ class BackNavigationController { */ private void onTransitionReadyWhileNavigate(ArrayList<WindowContainer> opening, ArrayList<WindowContainer> closing) { - if (!isMonitoring()) { + if (!isMonitorForRemote() && !isMonitorAnimationOrTransition()) { return; } final ArrayList<WindowContainer> all = new ArrayList<>(opening); @@ -531,7 +556,12 @@ class BackNavigationController { for (WindowContainer app : all) { if (app.hasChild(mNavigatingWindow)) { EventLogTags.writeWmBackNaviCanceled("transitionHappens"); - mObserver.sendResult(null /* result */); + if (isMonitorForRemote()) { + mObserver.sendResult(null /* result */); + } + if (isMonitorAnimationOrTransition()) { + clearBackAnimations(); + } break; } } @@ -539,6 +569,9 @@ class BackNavigationController { } private boolean atSameDisplay(WindowState newFocus) { + if (mNavigatingWindow == null) { + return false; + } final int navigatingDisplayId = mNavigatingWindow.getDisplayId(); return newFocus == null || newFocus.getDisplayId() == navigatingDisplayId; } @@ -546,17 +579,15 @@ class BackNavigationController { // For shell transition /** - * Check whether the transition targets was animated by back gesture animation. - * Because the opening target could request to do other stuff at onResume, so it could become - * close target for a transition. So the condition here is - * The closing target should only exist in close list, but the opening target can be either in - * open or close list. - * @return {@code true} if the participants of this transition was animated by back gesture - * animations, and shouldn't join next transition. + * Check whether the transition targets was animated by back gesture animation. + * Because the opening target could request to do other stuff at onResume, so it could become + * close target for a transition. So the condition here is + * The closing target should only exist in close list, but the opening target can be either in + * open or close list. */ - boolean containsBackAnimationTargets(Transition transition) { + void onTransactionReady(Transition transition) { if (!isMonitoringTransition()) { - return false; + return; } final ArraySet<WindowContainer> targets = transition.mParticipants; for (int i = targets.size() - 1; i >= 0; --i) { @@ -576,33 +607,44 @@ class BackNavigationController { && mAnimationHandler.containsBackAnimationTargets(mTmpOpenApps, mTmpCloseApps); if (!matchAnimationTargets) { mNavigationMonitor.onTransitionReadyWhileNavigate(mTmpOpenApps, mTmpCloseApps); + } else { + if (mWaitTransitionFinish != null) { + Slog.e(TAG, "Gesture animation is applied on another transition?"); + } + mWaitTransitionFinish = transition; } mTmpOpenApps.clear(); mTmpCloseApps.clear(); - return matchAnimationTargets; } boolean isMonitorTransitionTarget(WindowContainer wc) { - if (!isWaitBackTransition()) { + if (!isWaitBackTransition() || mWaitTransitionFinish == null) { return false; } return mAnimationHandler.isTarget(wc, wc.isVisibleRequested() /* open */); } /** - * Cleanup animation, this can either happen when transition ready or finish. - * @param cleanupTransaction The transaction which the caller want to apply the internal - * cleanup together. + * Cleanup animation, this can either happen when legacy transition ready, or when the Shell + * transition finish. */ - void clearBackAnimations(SurfaceControl.Transaction cleanupTransaction) { - mAnimationHandler.clearBackAnimateTarget(cleanupTransaction); + void clearBackAnimations() { + mAnimationHandler.clearBackAnimateTarget(); + mNavigationMonitor.stopMonitorTransition(); + mWaitTransitionFinish = null; } - /** + /** + * Called when a transition finished. * Handle the pending animation when the running transition finished. * @param targets The final animation targets derived in transition. - */ - boolean handleDeferredBackAnimation(@NonNull ArrayList<Transition.ChangeInfo> targets) { + * @param finishedTransition The finished transition target. + */ + boolean onTransitionFinish(ArrayList<Transition.ChangeInfo> targets, + @NonNull Transition finishedTransition) { + if (finishedTransition == mWaitTransitionFinish) { + clearBackAnimations(); + } if (!mBackAnimationInProgress || mPendingAnimationBuilder == null) { return false; } @@ -660,7 +702,7 @@ class BackNavigationController { private boolean mComposed; private boolean mWaitTransition; private int mSwitchType = UNKNOWN; - private SurfaceControl.Transaction mFinishedTransaction; + // This will be set before transition happen, to know whether the real opening target // exactly match animating target. When target match, reparent the starting surface to // the opening target like starting window do. @@ -669,6 +711,7 @@ class BackNavigationController { // request one during animating. private int mRequestedStartingSurfaceTaskId; private SurfaceControl mStartingSurface; + private ActivityRecord mOpenActivity; AnimationHandler(WindowManagerService wms) { mWindowManagerService = wms; @@ -697,7 +740,8 @@ class BackNavigationController { return true; } - private void initiate(WindowContainer close, WindowContainer open) { + private void initiate(WindowContainer close, WindowContainer open, + ActivityRecord openActivity) { WindowContainer closeTarget; if (isActivitySwitch(close, open)) { mSwitchType = ACTIVITY_SWITCH; @@ -712,22 +756,26 @@ class BackNavigationController { mCloseAdaptor = createAdaptor(closeTarget, false /* isOpen */); mOpenAdaptor = createAdaptor(open, true /* isOpen */); - + mOpenActivity = openActivity; if (mCloseAdaptor.mAnimationTarget == null || mOpenAdaptor.mAnimationTarget == null) { Slog.w(TAG, "composeNewAnimations fail, skip"); - clearBackAnimateTarget(null /* cleanupTransaction */); + clearBackAnimateTarget(); } } - private boolean composeAnimations(@NonNull WindowContainer close, - @NonNull WindowContainer open) { - clearBackAnimateTarget(null /* cleanupTransaction */); - if (close == null || open == null) { + boolean composeAnimations(@NonNull WindowContainer close, @NonNull WindowContainer open, + ActivityRecord openActivity) { + if (mComposed || mWaitTransition) { + Slog.e(TAG, "Previous animation is running " + this); + return false; + } + clearBackAnimateTarget(); + if (close == null || open == null || openActivity == null) { Slog.e(TAG, "reset animation with null target close: " + close + " open: " + open); return false; } - initiate(close, open); + initiate(close, open, openActivity); if (mSwitchType == UNKNOWN) { return false; } @@ -791,24 +839,10 @@ class BackNavigationController { return false; } - boolean setFinishTransaction(SurfaceControl.Transaction finishTransaction) { - if (!mComposed) { - return false; - } - mFinishedTransaction = finishTransaction; - return true; - } - - void finishPresentAnimations(SurfaceControl.Transaction t) { + void finishPresentAnimations() { if (!mComposed) { return; } - final SurfaceControl.Transaction pt = t != null ? t - : mOpenAdaptor.mTarget.getPendingTransaction(); - if (mFinishedTransaction != null) { - pt.merge(mFinishedTransaction); - mFinishedTransaction = null; - } cleanUpWindowlessSurface(); if (mCloseAdaptor != null) { @@ -819,6 +853,9 @@ class BackNavigationController { mOpenAdaptor.mTarget.cancelAnimation(); mOpenAdaptor = null; } + if (mOpenActivity != null && mOpenActivity.mLaunchTaskBehind) { + restoreLaunchBehind(mOpenActivity); + } } private void cleanUpWindowlessSurface() { @@ -845,22 +882,14 @@ class BackNavigationController { } } - void clearBackAnimateTarget(SurfaceControl.Transaction cleanupTransaction) { - finishPresentAnimations(cleanupTransaction); + void clearBackAnimateTarget() { + finishPresentAnimations(); mComposed = false; mWaitTransition = false; mOpenTransitionTargetMatch = false; mRequestedStartingSurfaceTaskId = 0; mSwitchType = UNKNOWN; - if (mFinishedTransaction != null) { - Slog.w(TAG, "Clear back animation, found un-processed finished transaction"); - if (cleanupTransaction != null) { - cleanupTransaction.merge(mFinishedTransaction); - } else { - mFinishedTransaction.apply(); - } - mFinishedTransaction = null; - } + mOpenActivity = null; } // The close target must in close list @@ -876,9 +905,9 @@ class BackNavigationController { public String toString() { return "AnimationTargets{" + " openTarget= " - + mOpenAdaptor.mTarget + + (mOpenAdaptor != null ? mOpenAdaptor.mTarget : "null") + " closeTarget= " - + mCloseAdaptor.mTarget + + (mCloseAdaptor != null ? mCloseAdaptor.mTarget : "null") + " mSwitchType= " + mSwitchType + " mComposed= " @@ -1048,14 +1077,13 @@ class BackNavigationController { * @return If the preview strategy is launch behind, returns the Activity that has * launchBehind set, or null otherwise. */ - private ActivityRecord applyPreviewStrategy(WindowContainer open, + private void applyPreviewStrategy(WindowContainer open, ActivityRecord visibleOpenActivity) { if (isSupportWindowlessSurface() && mShowWindowlessSurface && !mIsLaunchBehind) { createStartingSurface(getSnapshot(open)); - return null; + return; } setLaunchBehind(visibleOpenActivity); - return visibleOpenActivity; } Runnable build() { @@ -1071,19 +1099,12 @@ class BackNavigationController { return null; } - if (!composeAnimations(mCloseTarget, mOpenTarget)) { + if (!composeAnimations(mCloseTarget, mOpenTarget, openActivity)) { return null; } - final ActivityRecord launchBehindActivity = - applyPreviewStrategy(mOpenTarget, openActivity); + applyPreviewStrategy(mOpenTarget, openActivity); - final IBackAnimationFinishedCallback callback = makeAnimationFinishedCallback( - launchBehindActivity != null ? triggerBack -> { - if (!triggerBack) { - restoreLaunchBehind(launchBehindActivity); - } - } : null, - mCloseTarget); + final IBackAnimationFinishedCallback callback = makeAnimationFinishedCallback(); final RemoteAnimationTarget[] targets = getAnimationTargets(); return () -> { @@ -1096,31 +1117,17 @@ class BackNavigationController { }; } - private IBackAnimationFinishedCallback makeAnimationFinishedCallback( - Consumer<Boolean> b, WindowContainer closeTarget) { + private IBackAnimationFinishedCallback makeAnimationFinishedCallback() { return new IBackAnimationFinishedCallback.Stub() { @Override public void onAnimationFinished(boolean triggerBack) { - final SurfaceControl.Transaction finishedTransaction = - new SurfaceControl.Transaction(); synchronized (mWindowManagerService.mGlobalLock) { - if (b != null) { - b.accept(triggerBack); - } - if (triggerBack) { - final SurfaceControl surfaceControl = - closeTarget.getSurfaceControl(); - if (surfaceControl != null && surfaceControl.isValid()) { - // Hide the close target surface when transition start. - finishedTransaction.hide(surfaceControl); - } - } - if (!setFinishTransaction(finishedTransaction)) { - finishedTransaction.apply(); + if (!mComposed) { + // animation was canceled + return; } if (!triggerBack) { - clearBackAnimateTarget( - null /* cleanupTransaction */); + clearBackAnimateTarget(); } else { mWaitTransition = true; } @@ -1180,6 +1187,14 @@ class BackNavigationController { } void startAnimation() { + if (!mBackAnimationInProgress) { + // gesture is already finished, do not start animation + if (mPendingAnimation != null) { + clearBackAnimations(); + mPendingAnimation = null; + } + return; + } if (mPendingAnimation != null) { mPendingAnimation.run(); mPendingAnimation = null; @@ -1192,7 +1207,7 @@ class BackNavigationController { ProtoLog.d(WM_DEBUG_BACK_PREVIEW, "onBackNavigationDone backType=%s, " + "triggerBack=%b", backType, triggerBack); - mNavigationMonitor.stopMonitor(); + mNavigationMonitor.stopMonitorForRemote(); mBackAnimationInProgress = false; mShowWallpaper = false; mPendingAnimationBuilder = null; diff --git a/services/core/java/com/android/server/wm/EmbeddedWindowController.java b/services/core/java/com/android/server/wm/EmbeddedWindowController.java index c3c727a1d879..052c09a0e0eb 100644 --- a/services/core/java/com/android/server/wm/EmbeddedWindowController.java +++ b/services/core/java/com/android/server/wm/EmbeddedWindowController.java @@ -326,7 +326,7 @@ class EmbeddedWindowController { @Override public boolean shouldControlIme() { - return false; + return mHostWindowState != null; } @Override @@ -336,6 +336,9 @@ class EmbeddedWindowController { @Override public InsetsControlTarget getImeControlTarget() { + if (mHostWindowState != null) { + return mHostWindowState.getImeControlTarget(); + } return mWmService.getDefaultDisplayContentLocked().mRemoteInsetsControlTarget; } @@ -346,7 +349,7 @@ class EmbeddedWindowController { @Override public ActivityRecord getActivityRecord() { - return null; + return mHostActivityRecord; } @Override diff --git a/services/core/java/com/android/server/wm/RecentTasks.java b/services/core/java/com/android/server/wm/RecentTasks.java index dda0d6c3c3f2..b38666522754 100644 --- a/services/core/java/com/android/server/wm/RecentTasks.java +++ b/services/core/java/com/android/server/wm/RecentTasks.java @@ -976,9 +976,10 @@ class RecentTasks { if (!task.mUserSetupComplete) { // Don't include task launched while user is not done setting-up. - if (DEBUG_RECENTS) { - Slog.d(TAG_RECENTS, "Skipping, user setup not complete: " + task); - } + + // NOTE: not guarding with DEBUG_RECENTS as it's not frequent enough to spam logcat, + // but is useful when running CTS. + Slog.d(TAG_RECENTS, "Skipping, user setup not complete: " + task); continue; } diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 5a4615ad9578..b7e2265e3a16 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -5636,8 +5636,6 @@ class Task extends TaskFragment { mWmService.mSyncEngine.queueSyncSet( () -> mTransitionController.moveToCollecting(transition), () -> { - mTransitionController.requestStartTransition(transition, tr, - null /* remoteTransition */, null /* displayChange */); // Need to check again since this happens later and the system might // be in a different state. if (!canMoveTaskToBack(tr)) { @@ -5646,6 +5644,8 @@ class Task extends TaskFragment { transition.abort(); return; } + mTransitionController.requestStartTransition(transition, tr, + null /* remoteTransition */, null /* displayChange */); moveTaskToBackInner(tr); }); } else { diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index 951a71d2ddb9..683767e474ef 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -1031,7 +1031,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { mTmpTransaction.apply(); // Handle back animation if it's already started. - mController.mAtm.mBackNavigationController.handleDeferredBackAnimation(mTargets); + mController.mAtm.mBackNavigationController.onTransitionFinish(mTargets, this); mController.mFinishingTransition = null; } @@ -1136,8 +1136,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { mFlags |= TRANSIT_FLAG_KEYGUARD_LOCKED; } // Check whether the participants were animated from back navigation. - final boolean markBackAnimated = mController.mAtm.mBackNavigationController - .containsBackAnimationTargets(this); + mController.mAtm.mBackNavigationController.onTransactionReady(this); // Resolve the animating targets from the participants. mTargets = calculateTargets(mParticipants, mChanges); final TransitionInfo info = calculateTransitionInfo(mType, mFlags, mTargets, transaction); @@ -1150,9 +1149,6 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { mTargetDisplays.add(dc); } - if (markBackAnimated) { - mController.mAtm.mBackNavigationController.clearBackAnimations(mStartTransaction); - } if (mOverrideOptions != null) { info.setAnimationOptions(mOverrideOptions); if (mOverrideOptions.getType() == ANIM_OPEN_CROSS_PROFILE_APPS) { diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 14c826d71af7..a7a90604f228 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -88,7 +88,6 @@ import static android.view.WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY; import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER; import static android.view.WindowManager.REMOVE_CONTENT_MODE_UNDEFINED; import static android.view.WindowManager.TRANSIT_NONE; -import static android.view.WindowManager.TRANSIT_TO_FRONT; import static android.view.WindowManager.fixScale; import static android.view.WindowManagerGlobal.ADD_OKAY; import static android.view.WindowManagerGlobal.RELAYOUT_RES_CANCEL_AND_REDRAW; @@ -8537,13 +8536,7 @@ public class WindowManagerService extends IWindowManager.Stub } } - // focus-transfer can re-order windows and thus potentially causes visible changes: - final Transition transition = mAtmService.getTransitionController() - .requestTransitionIfNeeded(TRANSIT_TO_FRONT, task); mAtmService.setFocusedTask(task.mTaskId, touchedActivity); - if (transition != null) { - transition.setReady(task, true /* ready */); - } } /** diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index b5a5d94486da..770e728ba935 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -22264,7 +22264,8 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { MANAGE_DEVICE_POLICY_INSTALL_UNKNOWN_SOURCES, MANAGE_DEVICE_POLICY_USERS, MANAGE_DEVICE_POLICY_SAFE_BOOT, - MANAGE_DEVICE_POLICY_TIME); + MANAGE_DEVICE_POLICY_TIME, + MANAGE_DEVICE_POLICY_LOCK_CREDENTIALS); private static final List<String> PROFILE_OWNER_OF_ORGANIZATION_OWNED_DEVICE_PERMISSIONS = List.of( MANAGE_DEVICE_POLICY_ACROSS_USERS, @@ -22370,7 +22371,8 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { MANAGE_DEVICE_POLICY_PACKAGE_STATE, MANAGE_DEVICE_POLICY_RESET_PASSWORD, MANAGE_DEVICE_POLICY_STATUS_BAR, - MANAGE_DEVICE_POLICY_APP_RESTRICTIONS); + MANAGE_DEVICE_POLICY_APP_RESTRICTIONS, + MANAGE_DEVICE_POLICY_SYSTEM_DIALOGS); private static final List<String> PROFILE_OWNER_PERMISSIONS = List.of( MANAGE_DEVICE_POLICY_ACROSS_USERS_SECURITY_CRITICAL, MANAGE_DEVICE_POLICY_ORGANIZATION_IDENTITY, @@ -22509,8 +22511,6 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { MANAGE_DEVICE_POLICY_ACROSS_USERS_FULL); CROSS_USER_PERMISSIONS.put(MANAGE_DEVICE_POLICY_LOCATION, MANAGE_DEVICE_POLICY_ACROSS_USERS_FULL); - CROSS_USER_PERMISSIONS.put(MANAGE_DEVICE_POLICY_LOCK_CREDENTIALS, - MANAGE_DEVICE_POLICY_ACROSS_USERS_FULL); CROSS_USER_PERMISSIONS.put(MANAGE_DEVICE_POLICY_MICROPHONE, MANAGE_DEVICE_POLICY_ACROSS_USERS); CROSS_USER_PERMISSIONS.put(MANAGE_DEVICE_POLICY_MOBILE_NETWORK, @@ -22665,6 +22665,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { hasPermissionOnTargetUser = hasPermission(CROSS_USER_PERMISSIONS.get(permission), callerPackageName); } + return hasPermissionOnOwnUser && hasPermissionOnTargetUser; } @@ -22705,7 +22706,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { } // Check the permission for the role-holder if (isCallerDevicePolicyManagementRoleHolder(caller)) { - return anyDpcHasPermission(permission, mContext.getUserId()); + return anyDpcHasPermission(permission, caller.getUserId()); } if (DELEGATE_SCOPES.containsKey(permission)) { return isCallerDelegate(caller, DELEGATE_SCOPES.get(permission)); diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java index 2d8fa1bcd8cd..2180a781e437 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/ConnectivityControllerTest.java @@ -21,11 +21,13 @@ import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED; import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED; import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED; import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED; import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED; import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; import static android.net.NetworkCapabilities.TRANSPORT_VPN; import static android.text.format.DateUtils.SECOND_IN_MILLIS; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.inOrder; @@ -50,6 +52,7 @@ import static org.mockito.Mockito.verify; import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.ActivityManager; import android.app.job.JobInfo; import android.content.ComponentName; import android.content.Context; @@ -81,6 +84,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; import java.time.Clock; import java.time.ZoneOffset; @@ -322,6 +326,109 @@ public class ConnectivityControllerTest { } @Test + public void testMeteredAllowed() throws Exception { + final JobInfo.Builder jobBuilder = createJob() + .setEstimatedNetworkBytes(DataUnit.MEBIBYTES.toBytes(1), + DataUnit.MEBIBYTES.toBytes(1)) + .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY); + final JobStatus job = spy(createJobStatus(jobBuilder)); + + final ConnectivityController controller = new ConnectivityController(mService, + mFlexibilityController); + + // Unmetered network is always "metered allowed" + { + final Network net = mock(Network.class); + final NetworkCapabilities caps = createCapabilitiesBuilder() + .addCapability(NET_CAPABILITY_NOT_CONGESTED) + .addCapability(NET_CAPABILITY_NOT_METERED) + .build(); + assertTrue(controller.isSatisfied(job, net, caps, mConstants)); + } + + // Temporarily unmetered network is always "metered allowed" + { + final Network net = mock(Network.class); + final NetworkCapabilities caps = createCapabilitiesBuilder() + .addCapability(NET_CAPABILITY_NOT_CONGESTED) + .addCapability(NET_CAPABILITY_TEMPORARILY_NOT_METERED) + .build(); + assertTrue(controller.isSatisfied(job, net, caps, mConstants)); + } + + // Respond with the default values in NetworkPolicyManager. If those ever change enough + // to cause these tests to fail, we would likely need to go and update + // ConnectivityController. + doAnswer( + (Answer<Integer>) invocationOnMock + -> NetworkPolicyManager.getDefaultProcessNetworkCapabilities( + invocationOnMock.getArgument(0))) + .when(mService).getUidCapabilities(anyInt()); + + // Foreground is always allowed for metered network + { + final Network net = mock(Network.class); + final NetworkCapabilities caps = createCapabilitiesBuilder() + .addCapability(NET_CAPABILITY_NOT_CONGESTED) + .build(); + + when(mService.getUidProcState(anyInt())) + .thenReturn(ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE); + assertTrue(controller.isSatisfied(job, net, caps, mConstants)); + + when(mService.getUidProcState(anyInt())) + .thenReturn(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE); + assertTrue(controller.isSatisfied(job, net, caps, mConstants)); + + when(mService.getUidProcState(anyInt())).thenReturn(ActivityManager.PROCESS_STATE_TOP); + assertTrue(controller.isSatisfied(job, net, caps, mConstants)); + + when(mService.getUidProcState(anyInt())).thenReturn(JobInfo.BIAS_DEFAULT); + when(job.getFlags()).thenReturn(JobInfo.FLAG_WILL_BE_FOREGROUND); + assertTrue(controller.isSatisfied(job, net, caps, mConstants)); + } + + when(mService.getUidProcState(anyInt())).thenReturn(ActivityManager.PROCESS_STATE_UNKNOWN); + when(job.getFlags()).thenReturn(0); + + // User initiated is always allowed for metered network + { + final Network net = mock(Network.class); + final NetworkCapabilities caps = createCapabilitiesBuilder() + .addCapability(NET_CAPABILITY_NOT_CONGESTED) + .build(); + when(job.shouldTreatAsUserInitiatedJob()).thenReturn(true); + assertTrue(controller.isSatisfied(job, net, caps, mConstants)); + } + + // Background non-user-initiated should follow the app's restricted state + { + final Network net = mock(Network.class); + final NetworkCapabilities caps = createCapabilitiesBuilder() + .addCapability(NET_CAPABILITY_NOT_CONGESTED) + .build(); + when(job.shouldTreatAsUserInitiatedJob()).thenReturn(false); + when(mNetPolicyManager.getRestrictBackgroundStatus(anyInt())) + .thenReturn(ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED); + assertTrue(controller.isSatisfied(job, net, caps, mConstants)); + // Test cache + when(mNetPolicyManager.getRestrictBackgroundStatus(anyInt())) + .thenReturn(ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED); + assertTrue(controller.isSatisfied(job, net, caps, mConstants)); + // Clear cache + controller.onAppRemovedLocked(job.getSourcePackageName(), job.getSourceUid()); + assertFalse(controller.isSatisfied(job, net, caps, mConstants)); + // Test cache + when(mNetPolicyManager.getRestrictBackgroundStatus(anyInt())) + .thenReturn(ConnectivityManager.RESTRICT_BACKGROUND_STATUS_WHITELISTED); + assertFalse(controller.isSatisfied(job, net, caps, mConstants)); + // Clear cache + controller.onAppRemovedLocked(job.getSourcePackageName(), job.getSourceUid()); + assertTrue(controller.isSatisfied(job, net, caps, mConstants)); + } + } + + @Test public void testStrongEnough_Cellular() { mConstants.CONN_UPDATE_ALL_JOBS_MIN_INTERVAL_MS = 0; diff --git a/services/tests/voiceinteractiontests/src/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareLoggingTest.java b/services/tests/voiceinteractiontests/src/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareLoggingTest.java index 8694094ce6ac..4d3c26f4973e 100644 --- a/services/tests/voiceinteractiontests/src/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareLoggingTest.java +++ b/services/tests/voiceinteractiontests/src/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareLoggingTest.java @@ -37,6 +37,7 @@ import android.os.BatteryStatsInternal; import android.os.Process; import android.os.RemoteException; +import androidx.test.filters.FlakyTest; import androidx.test.platform.app.InstrumentationRegistry; import com.android.internal.util.FakeLatencyTracker; @@ -93,10 +94,12 @@ public class SoundTriggerMiddlewareLoggingTest { } @Test + @FlakyTest(bugId = 275113847) public void testSetUpAndTearDown() { } @Test + @FlakyTest(bugId = 275113847) public void testOnPhraseRecognitionStartsLatencyTrackerWithSuccessfulPhraseIdTrigger() throws RemoteException { ArgumentCaptor<ISoundTriggerCallback> soundTriggerCallbackCaptor = ArgumentCaptor.forClass( @@ -112,6 +115,7 @@ public class SoundTriggerMiddlewareLoggingTest { } @Test + @FlakyTest(bugId = 275113847) public void testOnPhraseRecognitionRestartsActiveSession() throws RemoteException { ArgumentCaptor<ISoundTriggerCallback> soundTriggerCallbackCaptor = ArgumentCaptor.forClass( ISoundTriggerCallback.class); @@ -131,6 +135,7 @@ public class SoundTriggerMiddlewareLoggingTest { } @Test + @FlakyTest(bugId = 275113847) public void testOnPhraseRecognitionNeverStartsLatencyTrackerWithNonSuccessEvent() throws RemoteException { ArgumentCaptor<ISoundTriggerCallback> soundTriggerCallbackCaptor = ArgumentCaptor.forClass( @@ -147,6 +152,7 @@ public class SoundTriggerMiddlewareLoggingTest { } @Test + @FlakyTest(bugId = 275113847) public void testOnPhraseRecognitionNeverStartsLatencyTrackerWithNoKeyphraseId() throws RemoteException { ArgumentCaptor<ISoundTriggerCallback> soundTriggerCallbackCaptor = ArgumentCaptor.forClass( diff --git a/services/tests/wmtests/src/com/android/server/policy/PowerKeyGestureTests.java b/services/tests/wmtests/src/com/android/server/policy/PowerKeyGestureTests.java index 49af2c1dc681..5863e9d9243a 100644 --- a/services/tests/wmtests/src/com/android/server/policy/PowerKeyGestureTests.java +++ b/services/tests/wmtests/src/com/android/server/policy/PowerKeyGestureTests.java @@ -20,6 +20,7 @@ import static android.view.KeyEvent.KEYCODE_VOLUME_UP; import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_POWER_ASSISTANT; import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_POWER_GLOBAL_ACTIONS; +import static com.android.server.policy.PhoneWindowManager.SHORT_PRESS_POWER_DREAM_OR_SLEEP; import android.provider.Settings; import android.view.Display; @@ -49,6 +50,32 @@ public class PowerKeyGestureTests extends ShortcutKeyTestBase { } /** + * Power single press to start dreaming when so configured. + */ + @Test + public void testPowerSinglePressRequestsDream() { + mPhoneWindowManager.overrideShortPressOnPower(SHORT_PRESS_POWER_DREAM_OR_SLEEP); + mPhoneWindowManager.overrideCanStartDreaming(true); + sendKey(KEYCODE_POWER); + mPhoneWindowManager.assertDreamRequest(); + mPhoneWindowManager.assertLockedAfterAppTransitionFinished(); + } + + /** + * Power double-press to launch camera does not lock device when the single press behavior is to + * dream. + */ + @Test + public void testPowerDoublePressWillNotLockDevice() { + mPhoneWindowManager.overrideShortPressOnPower(SHORT_PRESS_POWER_DREAM_OR_SLEEP); + mPhoneWindowManager.overrideCanStartDreaming(false); + sendKey(KEYCODE_POWER); + sendKey(KEYCODE_POWER); + mPhoneWindowManager.assertCameraLaunch(); + mPhoneWindowManager.assertWillNotLockAfterAppTransitionFinished(); + } + + /** * Power double press to trigger camera. */ @Test diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java index b6939747a7b6..a2ee8a45433d 100644 --- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java +++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java @@ -84,6 +84,7 @@ import com.android.server.wm.ActivityTaskManagerInternal; import com.android.server.wm.DisplayPolicy; import com.android.server.wm.DisplayRotation; import com.android.server.wm.WindowManagerInternal; +import com.android.server.wm.WindowManagerInternal.AppTransitionListener; import junit.framework.Assert; @@ -289,6 +290,10 @@ class TestPhoneWindowManager { } } + void overrideShortPressOnPower(int behavior) { + mPhoneWindowManager.mShortPressOnPowerBehavior = behavior; + } + // Override assist perform function. void overrideLongPressOnPower(int behavior) { mPhoneWindowManager.mLongPressOnPowerBehavior = behavior; @@ -311,6 +316,10 @@ class TestPhoneWindowManager { } } + void overrideCanStartDreaming(boolean canDream) { + doReturn(canDream).when(mDreamManagerInternal).canStartDreaming(anyBoolean()); + } + void overrideDisplayState(int state) { doReturn(state).when(mDisplay).getState(); Mockito.reset(mPowerManager); @@ -374,6 +383,10 @@ class TestPhoneWindowManager { timeout(SHORTCUT_KEY_DELAY_MILLIS)).performAccessibilityShortcut(); } + void assertDreamRequest() { + verify(mDreamManagerInternal).requestDream(); + } + void assertPowerSleep() { waitForIdle(); verify(mPowerManager, @@ -454,4 +467,17 @@ class TestPhoneWindowManager { waitForIdle(); verify(mInputManagerInternal).toggleCapsLock(anyInt()); } + + void assertWillNotLockAfterAppTransitionFinished() { + Assert.assertFalse(mPhoneWindowManager.mLockAfterAppTransitionFinished); + } + + void assertLockedAfterAppTransitionFinished() { + ArgumentCaptor<AppTransitionListener> transitionCaptor = + ArgumentCaptor.forClass(AppTransitionListener.class); + verify(mWindowManagerInternal).registerAppTransitionListener( + transitionCaptor.capture()); + transitionCaptor.getValue().onAppTransitionFinishedLocked(any()); + verify(mPhoneWindowManager).lockNow(null); + } } diff --git a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java index d0628f12336c..17ae215c2930 100644 --- a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java @@ -89,11 +89,12 @@ public class BackNavigationControllerTests extends WindowTestsBase { @Before public void setUp() throws Exception { - mBackNavigationController = Mockito.spy(new BackNavigationController()); + final BackNavigationController original = new BackNavigationController(); + original.setWindowManager(mWm); + mBackNavigationController = Mockito.spy(original); LocalServices.removeServiceForTest(WindowManagerInternal.class); mWindowManagerInternal = mock(WindowManagerInternal.class); LocalServices.addService(WindowManagerInternal.class, mWindowManagerInternal); - mBackNavigationController.setWindowManager(mWm); mBackAnimationAdapter = mock(BackAnimationAdapter.class); mRootHomeTask = initHomeActivity(); } @@ -129,7 +130,9 @@ public class BackNavigationControllerTests extends WindowTestsBase { // verify if back animation would start. assertTrue("Animation scheduled", backNavigationInfo.isPrepareRemoteAnimation()); - // reset drawning status + // reset drawing status + backNavigationInfo.onBackNavigationFinished(false); + mBackNavigationController.clearBackAnimations(); topTask.forAllWindows(w -> { makeWindowVisibleAndDrawn(w); }, true); @@ -138,6 +141,8 @@ public class BackNavigationControllerTests extends WindowTestsBase { assertThat(typeToString(backNavigationInfo.getType())) .isEqualTo(typeToString(BackNavigationInfo.TYPE_CALLBACK)); + backNavigationInfo.onBackNavigationFinished(false); + mBackNavigationController.clearBackAnimations(); doReturn(true).when(recordA).canShowWhenLocked(); backNavigationInfo = startBackNavigation(); assertThat(typeToString(backNavigationInfo.getType())) @@ -194,6 +199,8 @@ public class BackNavigationControllerTests extends WindowTestsBase { assertTrue("Animation scheduled", backNavigationInfo.isPrepareRemoteAnimation()); // reset drawing status + backNavigationInfo.onBackNavigationFinished(false); + mBackNavigationController.clearBackAnimations(); testCase.recordFront.forAllWindows(w -> { makeWindowVisibleAndDrawn(w); }, true); @@ -202,6 +209,8 @@ public class BackNavigationControllerTests extends WindowTestsBase { assertThat(typeToString(backNavigationInfo.getType())) .isEqualTo(typeToString(BackNavigationInfo.TYPE_CALLBACK)); + backNavigationInfo.onBackNavigationFinished(false); + mBackNavigationController.clearBackAnimations(); doReturn(true).when(testCase.recordBack).canShowWhenLocked(); backNavigationInfo = startBackNavigation(); assertThat(typeToString(backNavigationInfo.getType())) @@ -240,6 +249,8 @@ public class BackNavigationControllerTests extends WindowTestsBase { assertThat(typeToString(backNavigationInfo.getType())) .isEqualTo(typeToString(BackNavigationInfo.TYPE_RETURN_TO_HOME)); + backNavigationInfo.onBackNavigationFinished(false); + mBackNavigationController.clearBackAnimations(); setupKeyguardOccluded(); backNavigationInfo = startBackNavigation(); assertThat(typeToString(backNavigationInfo.getType())) @@ -553,6 +564,7 @@ public class BackNavigationControllerTests extends WindowTestsBase { assertTrue(toHomeBuilder.mIsLaunchBehind); toHomeBuilder.build(); verify(animationHandler, never()).createStartingSurface(any()); + animationHandler.clearBackAnimateTarget(); // Back to ACTIVITY and TASK have the same logic, just with different target. final ActivityRecord topActivity = createActivityRecord(task); diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java index d2431f1cebf2..7abae1854025 100644 --- a/telephony/java/android/telephony/CarrierConfigManager.java +++ b/telephony/java/android/telephony/CarrierConfigManager.java @@ -5761,6 +5761,57 @@ public class CarrierConfigManager { public static final String KEY_CAPABILITY_TYPE_PRESENCE_UCE_INT_ARRAY = KEY_PREFIX + "capability_type_presence_uce_int_array"; + /** + * Specifies the policy for disabling NR SA mode. Default value is + *{@link #SA_DISABLE_POLICY_NONE}. + * The value set as below: + * <ul> + * <li>0: {@link #SA_DISABLE_POLICY_NONE }</li> + * <li>1: {@link #SA_DISABLE_POLICY_WFC_ESTABLISHED }</li> + * <li>2: {@link #SA_DISABLE_POLICY_WFC_ESTABLISHED_WHEN_VONR_DISABLED }</li> + * <li>3: {@link #SA_DISABLE_POLICY_VOWIFI_REGISTERED }</li> + * </ul> + * @hide + */ + public static final String KEY_SA_DISABLE_POLICY_INT = KEY_PREFIX + "sa_disable_policy_int"; + + /** @hide */ + @IntDef({ + SA_DISABLE_POLICY_NONE, + SA_DISABLE_POLICY_WFC_ESTABLISHED, + SA_DISABLE_POLICY_WFC_ESTABLISHED_WHEN_VONR_DISABLED, + SA_DISABLE_POLICY_VOWIFI_REGISTERED + }) + public @interface NrSaDisablePolicy {} + + /** + * Do not disables NR SA mode. + * @hide + */ + public static final int SA_DISABLE_POLICY_NONE = 0; + + /** + * Disables NR SA mode when VoWiFi call is established in order to improve the delay or + * voice mute when the handover from ePDG to NR is not supported in UE or network. + * @hide + */ + public static final int SA_DISABLE_POLICY_WFC_ESTABLISHED = 1; + + /** + * Disables NR SA mode when VoWiFi call is established when VoNR is disabled in order to + * improve the delay or voice mute when the handover from ePDG to NR is not supported + * in UE or network. + * @hide + */ + public static final int SA_DISABLE_POLICY_WFC_ESTABLISHED_WHEN_VONR_DISABLED = 2; + + /** + * Disables NR SA mode when IMS is registered over WiFi in order to improve the delay or + * voice mute when the handover from ePDG to NR is not supported in UE or network. + * @hide + */ + public static final int SA_DISABLE_POLICY_VOWIFI_REGISTERED = 3; + private Ims() {} private static PersistableBundle getDefaults() { @@ -5832,6 +5883,7 @@ public class CarrierConfigManager { defaults.putInt(KEY_REGISTRATION_RETRY_BASE_TIMER_MILLIS_INT, 30000); defaults.putInt(KEY_REGISTRATION_RETRY_MAX_TIMER_MILLIS_INT, 1800000); defaults.putInt(KEY_REGISTRATION_SUBSCRIBE_EXPIRY_TIMER_SEC_INT, 600000); + defaults.putInt(KEY_SA_DISABLE_POLICY_INT, SA_DISABLE_POLICY_NONE); defaults.putIntArray( KEY_IPSEC_AUTHENTICATION_ALGORITHMS_INT_ARRAY, diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java index 286e71c14726..78c61964edfd 100644 --- a/telephony/java/android/telephony/SubscriptionManager.java +++ b/telephony/java/android/telephony/SubscriptionManager.java @@ -3652,17 +3652,10 @@ public class SubscriptionManager { } /** - * Enables or disables a subscription. This is currently used in the settings page. It will - * fail and return false if operation is not supported or failed. + * Enable or disable a subscription. This method is same as + * {@link #setUiccApplicationsEnabled(int, boolean)}. * - * To disable an active subscription on a physical (non-Euicc) SIM, - * {@link #canDisablePhysicalSubscription} needs to be true. - * - * <p> - * Permissions android.Manifest.permission.MODIFY_PHONE_STATE is required - * - * @param subscriptionId Subscription to be enabled or disabled. It could be a eSIM or pSIM - * subscription. + * @param subscriptionId Subscription to be enabled or disabled. * @param enable whether user is turning it on or off. * * @return whether the operation is successful. @@ -3672,19 +3665,15 @@ public class SubscriptionManager { @SystemApi @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public boolean setSubscriptionEnabled(int subscriptionId, boolean enable) { - if (VDBG) { - logd("setSubscriptionActivated subId= " + subscriptionId + " enable " + enable); - } try { ISub iSub = TelephonyManager.getSubscriptionService(); if (iSub != null) { - return iSub.setSubscriptionEnabled(enable, subscriptionId); + iSub.setUiccApplicationsEnabled(enable, subscriptionId); } } catch (RemoteException ex) { - // ignore it + return false; } - - return false; + return true; } /** @@ -3707,11 +3696,7 @@ public class SubscriptionManager { logd("setUiccApplicationsEnabled subId= " + subscriptionId + " enable " + enabled); } try { - ISub iSub = ISub.Stub.asInterface( - TelephonyFrameworkInitializer - .getTelephonyServiceManager() - .getSubscriptionServiceRegisterer() - .get()); + ISub iSub = TelephonyManager.getSubscriptionService(); if (iSub != null) { iSub.setUiccApplicationsEnabled(enabled, subscriptionId); } @@ -3739,11 +3724,7 @@ public class SubscriptionManager { logd("canDisablePhysicalSubscription"); } try { - ISub iSub = ISub.Stub.asInterface( - TelephonyFrameworkInitializer - .getTelephonyServiceManager() - .getSubscriptionServiceRegisterer() - .get()); + ISub iSub = TelephonyManager.getSubscriptionService(); if (iSub != null) { return iSub.canDisablePhysicalSubscription(); } @@ -3867,10 +3848,15 @@ public class SubscriptionManager { } /** - * DO NOT USE. - * This API is designed for features that are not finished at this point. Do not call this API. + * Get the active subscription id by logical SIM slot index. + * + * @param slotIndex The logical SIM slot index. + * @return The active subscription id. + * + * @throws IllegalArgumentException if the provided slot index is invalid. + * @throws SecurityException if callers do not hold the required permission. + * * @hide - * TODO b/135547512: further clean up */ @SystemApi @RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE) diff --git a/telephony/java/com/android/internal/telephony/ISub.aidl b/telephony/java/com/android/internal/telephony/ISub.aidl index 632a6874b5f5..6a5380ddb36e 100644 --- a/telephony/java/com/android/internal/telephony/ISub.aidl +++ b/telephony/java/com/android/internal/telephony/ISub.aidl @@ -265,8 +265,6 @@ interface ISub { String getSubscriptionProperty(int subId, String propKey, String callingPackage, String callingFeatureId); - boolean setSubscriptionEnabled(boolean enable, int subId); - boolean isSubscriptionEnabled(int subId); int getEnabledSubscriptionId(int slotIndex); @@ -277,7 +275,7 @@ interface ISub { boolean canDisablePhysicalSubscription(); - int setUiccApplicationsEnabled(boolean enabled, int subscriptionId); + void setUiccApplicationsEnabled(boolean enabled, int subscriptionId); int setDeviceToDeviceStatusSharing(int sharing, int subId); diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnDismissPopupDialogTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnDismissPopupDialogTest.kt index d72f5288d4d5..6066d2e74209 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnDismissPopupDialogTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeOnDismissPopupDialogTest.kt @@ -66,7 +66,7 @@ open class CloseImeOnDismissPopupDialogTest(flicker: FlickerTest) : BaseTest(fli flicker.assertLayers { this.isVisible(ComponentNameMatcher.IME) .then() - .isVisible(ComponentNameMatcher.IME_SNAPSHOT) + .isVisible(ComponentNameMatcher.IME_SNAPSHOT, isOptional = true) .then() .isInvisible(ComponentNameMatcher.IME_SNAPSHOT, isOptional = true) .isInvisible(ComponentNameMatcher.IME) diff --git a/tests/Internal/src/com/android/internal/os/TimeoutRecordTest.java b/tests/Internal/src/com/android/internal/os/TimeoutRecordTest.java index 0f9663442740..7419ee1230d3 100644 --- a/tests/Internal/src/com/android/internal/os/TimeoutRecordTest.java +++ b/tests/Internal/src/com/android/internal/os/TimeoutRecordTest.java @@ -16,15 +16,15 @@ package com.android.internal.os; -import android.content.ComponentName; -import android.content.Intent; -import android.platform.test.annotations.Presubmit; - import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import android.content.ComponentName; +import android.content.Intent; +import android.platform.test.annotations.Presubmit; + import androidx.test.filters.SmallTest; import org.junit.Test; @@ -40,7 +40,7 @@ public class TimeoutRecordTest { @Test public void forBroadcastReceiver_returnsCorrectTimeoutRecord() { Intent intent = new Intent(Intent.ACTION_MAIN); - intent.setComponent(ComponentName.createRelative("com.example.app", "ExampleClass")); + intent.setComponent(new ComponentName("com.example.app", "com.example.app.ExampleClass")); TimeoutRecord record = TimeoutRecord.forBroadcastReceiver(intent); @@ -48,14 +48,28 @@ public class TimeoutRecordTest { assertEquals(record.mKind, TimeoutRecord.TimeoutKind.BROADCAST_RECEIVER); assertEquals(record.mReason, "Broadcast of Intent { act=android.intent.action.MAIN cmp=com.example" - + ".app/ExampleClass }"); + + ".app/.ExampleClass }"); + assertTrue(record.mEndTakenBeforeLocks); + } + + @Test + public void forBroadcastReceiver_withPackageAndClass_returnsCorrectTimeoutRecord() { + Intent intent = new Intent(Intent.ACTION_MAIN); + TimeoutRecord record = TimeoutRecord.forBroadcastReceiver(intent, + "com.example.app", "com.example.app.ExampleClass"); + + assertNotNull(record); + assertEquals(record.mKind, TimeoutRecord.TimeoutKind.BROADCAST_RECEIVER); + assertEquals(record.mReason, + "Broadcast of Intent { act=android.intent.action.MAIN cmp=com.example" + + ".app/.ExampleClass }"); assertTrue(record.mEndTakenBeforeLocks); } @Test public void forBroadcastReceiver_withTimeoutDurationMs_returnsCorrectTimeoutRecord() { Intent intent = new Intent(Intent.ACTION_MAIN); - intent.setComponent(ComponentName.createRelative("com.example.app", "ExampleClass")); + intent.setComponent(new ComponentName("com.example.app", "com.example.app.ExampleClass")); TimeoutRecord record = TimeoutRecord.forBroadcastReceiver(intent, 1000L); @@ -63,7 +77,7 @@ public class TimeoutRecordTest { assertEquals(record.mKind, TimeoutRecord.TimeoutKind.BROADCAST_RECEIVER); assertEquals(record.mReason, "Broadcast of Intent { act=android.intent.action.MAIN cmp=com.example" - + ".app/ExampleClass }, waited 1000ms"); + + ".app/.ExampleClass }, waited 1000ms"); assertTrue(record.mEndTakenBeforeLocks); } |