diff options
341 files changed, 17799 insertions, 10300 deletions
diff --git a/apct-tests/perftests/contentcapture/AndroidManifest.xml b/apct-tests/perftests/contentcapture/AndroidManifest.xml index 80957c78abf3..6b566afe284d 100644 --- a/apct-tests/perftests/contentcapture/AndroidManifest.xml +++ b/apct-tests/perftests/contentcapture/AndroidManifest.xml @@ -18,6 +18,8 @@ <application> <uses-library android:name="android.test.runner" /> + <activity android:name="android.perftests.utils.PerfTestActivity" + android:exported="true" /> <activity android:name="android.view.contentcapture.CustomTestActivity" android:exported="true"> </activity> diff --git a/apct-tests/perftests/contentcapture/src/android/view/contentcapture/AbstractContentCapturePerfTestCase.java b/apct-tests/perftests/contentcapture/src/android/view/contentcapture/AbstractContentCapturePerfTestCase.java index 9b853fed81d3..0ea2dafbb047 100644 --- a/apct-tests/perftests/contentcapture/src/android/view/contentcapture/AbstractContentCapturePerfTestCase.java +++ b/apct-tests/perftests/contentcapture/src/android/view/contentcapture/AbstractContentCapturePerfTestCase.java @@ -22,18 +22,20 @@ import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentat import static com.android.compatibility.common.util.ShellUtils.runShellCommand; +import android.app.Activity; import android.app.Application; +import android.app.Instrumentation; import android.content.ContentCaptureOptions; import android.content.Context; import android.content.Intent; import android.os.BatteryManager; import android.os.UserHandle; import android.perftests.utils.PerfStatusReporter; +import android.perftests.utils.PerfTestActivity; import android.provider.Settings; import android.util.Log; import androidx.annotation.NonNull; -import androidx.test.rule.ActivityTestRule; import com.android.compatibility.common.util.ActivitiesWatcher; import com.android.compatibility.common.util.ActivitiesWatcher.ActivityWatcher; @@ -53,18 +55,18 @@ import org.junit.runners.model.Statement; public abstract class AbstractContentCapturePerfTestCase { private static final String TAG = AbstractContentCapturePerfTestCase.class.getSimpleName(); - private static final long GENERIC_TIMEOUT_MS = 10_000; + protected static final long GENERIC_TIMEOUT_MS = 5_000; private static int sOriginalStayOnWhilePluggedIn; - private static Context sContext = getInstrumentation().getTargetContext(); + protected static final Instrumentation sInstrumentation = getInstrumentation(); + protected static final Context sContext = sInstrumentation.getTargetContext(); protected ActivitiesWatcher mActivitiesWatcher; - private MyContentCaptureService.ServiceWatcher mServiceWatcher; + /** A simple activity as the task root to reduce the noise of pause and animation time. */ + protected Activity mEntryActivity; - @Rule - public ActivityTestRule<CustomTestActivity> mActivityRule = - new ActivityTestRule<>(CustomTestActivity.class, false, false); + private MyContentCaptureService.ServiceWatcher mServiceWatcher; @Rule public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter(); @@ -220,6 +222,17 @@ public abstract class AbstractContentCapturePerfTestCase { } } + @Before + public void setUp() { + mEntryActivity = sInstrumentation.startActivitySync( + PerfTestActivity.createLaunchIntent(sInstrumentation.getContext())); + } + + @After + public void tearDown() { + mEntryActivity.finishAndRemoveTask(); + } + /** * Sets {@link MyContentCaptureService} as the service for the current user and waits until * its created, then add the perf test package into allow list. @@ -248,20 +261,24 @@ public abstract class AbstractContentCapturePerfTestCase { } /** - * Launch test activity with give layout and parameter + * Returns the intent which will launch CustomTestActivity. */ - protected CustomTestActivity launchActivity(int layoutId, int numViews) { - final Intent intent = new Intent(sContext, CustomTestActivity.class); + protected Intent getLaunchIntent(int layoutId, int numViews) { + final Intent intent = new Intent(sContext, CustomTestActivity.class) + // Use NEW_TASK because the context is not activity. It is still in the same task + // of PerfTestActivity because of the same task affinity. Use NO_ANIMATION because + // this test focuses on launch time instead of animation duration. + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NO_ANIMATION); intent.putExtra(INTENT_EXTRA_LAYOUT_ID, layoutId); intent.putExtra(INTENT_EXTRA_CUSTOM_VIEWS, numViews); - return mActivityRule.launchActivity(intent); + return intent; } - protected void finishActivity() { - try { - mActivityRule.finishActivity(); - } catch (IllegalStateException e) { - // no op - } + /** + * Launch test activity with give layout and parameter + */ + protected CustomTestActivity launchActivity(int layoutId, int numViews) { + final Intent intent = getLaunchIntent(layoutId, numViews); + return (CustomTestActivity) sInstrumentation.startActivitySync(intent); } } diff --git a/apct-tests/perftests/contentcapture/src/android/view/contentcapture/CustomTestActivity.java b/apct-tests/perftests/contentcapture/src/android/view/contentcapture/CustomTestActivity.java index e509837f441a..c24e79f511bb 100644 --- a/apct-tests/perftests/contentcapture/src/android/view/contentcapture/CustomTestActivity.java +++ b/apct-tests/perftests/contentcapture/src/android/view/contentcapture/CustomTestActivity.java @@ -19,6 +19,10 @@ package android.view.contentcapture; import android.app.Activity; import android.graphics.drawable.Drawable; import android.os.Bundle; +import android.os.Looper; +import android.os.RemoteCallback; +import android.view.Choreographer; +import android.view.View; import android.widget.LinearLayout; import android.widget.TextView; @@ -31,6 +35,8 @@ import com.android.perftests.contentcapture.R; public class CustomTestActivity extends Activity { public static final String INTENT_EXTRA_LAYOUT_ID = "layout_id"; public static final String INTENT_EXTRA_CUSTOM_VIEWS = "custom_view_number"; + static final String INTENT_EXTRA_FINISH_ON_IDLE = "finish"; + static final String INTENT_EXTRA_DRAW_CALLBACK = "draw_callback"; public static final int MAX_VIEWS = 500; private static final int CUSTOM_CONTAINER_LAYOUT_ID = R.layout.test_container_activity; @@ -47,6 +53,34 @@ public class CustomTestActivity extends Activity { getIntent().getIntExtra(INTENT_EXTRA_CUSTOM_VIEWS, MAX_VIEWS)); } } + + final RemoteCallback drawCallback = getIntent().getParcelableExtra( + INTENT_EXTRA_DRAW_CALLBACK, RemoteCallback.class); + if (drawCallback != null) { + getWindow().getDecorView().addOnAttachStateChangeListener( + new View.OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(View v) { + Choreographer.getInstance().postCallback( + Choreographer.CALLBACK_COMMIT, + // Report that the first frame is drawn. + () -> drawCallback.sendResult(null), null /* token */); + } + + @Override + public void onViewDetachedFromWindow(View v) { + } + }); + } + + if (getIntent().getBooleanExtra(INTENT_EXTRA_FINISH_ON_IDLE, false)) { + Looper.myQueue().addIdleHandler(() -> { + // Finish without animation. + finish(); + overridePendingTransition(0 /* enterAnim */, 0 /* exitAnim */); + return false; + }); + } } private void createCustomViews(LinearLayout root, int number) { diff --git a/apct-tests/perftests/contentcapture/src/android/view/contentcapture/LoginTest.java b/apct-tests/perftests/contentcapture/src/android/view/contentcapture/LoginTest.java index 725750976d98..aa95dfdfdf16 100644 --- a/apct-tests/perftests/contentcapture/src/android/view/contentcapture/LoginTest.java +++ b/apct-tests/perftests/contentcapture/src/android/view/contentcapture/LoginTest.java @@ -15,9 +15,10 @@ */ package android.view.contentcapture; -import static com.android.compatibility.common.util.ActivitiesWatcher.ActivityLifecycle.CREATED; import static com.android.compatibility.common.util.ActivitiesWatcher.ActivityLifecycle.DESTROYED; +import android.content.Intent; +import android.os.RemoteCallback; import android.perftests.utils.BenchmarkState; import android.view.View; @@ -80,17 +81,32 @@ public class LoginTest extends AbstractContentCapturePerfTestCase { } private void testActivityLaunchTime(int layoutId, int numViews) throws Throwable { + final Object drawNotifier = new Object(); + final Intent intent = getLaunchIntent(layoutId, numViews); + intent.putExtra(CustomTestActivity.INTENT_EXTRA_FINISH_ON_IDLE, true); + intent.putExtra(CustomTestActivity.INTENT_EXTRA_DRAW_CALLBACK, + new RemoteCallback(result -> { + synchronized (drawNotifier) { + drawNotifier.notifyAll(); + } + })); final ActivityWatcher watcher = startWatcher(); final BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { - launchActivity(layoutId, numViews); + mEntryActivity.startActivity(intent); + synchronized (drawNotifier) { + try { + drawNotifier.wait(GENERIC_TIMEOUT_MS); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } // Ignore the time to finish the activity state.pauseTiming(); - watcher.waitFor(CREATED); - finishActivity(); watcher.waitFor(DESTROYED); + sInstrumentation.waitForIdleSync(); state.resumeTiming(); } } @@ -142,12 +158,12 @@ public class LoginTest extends AbstractContentCapturePerfTestCase { while (state.keepRunning()) { // Only count the time of onVisibilityAggregated() state.pauseTiming(); - mActivityRule.runOnUiThread(() -> { + sInstrumentation.runOnMainSync(() -> { state.resumeTiming(); view.onVisibilityAggregated(false); state.pauseTiming(); }); - mActivityRule.runOnUiThread(() -> { + sInstrumentation.runOnMainSync(() -> { state.resumeTiming(); view.onVisibilityAggregated(true); state.pauseTiming(); diff --git a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java index 9caf99e5b827..19ab5bcc9967 100644 --- a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java +++ b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java @@ -30,6 +30,7 @@ import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; +import android.app.Notification; import android.compat.Compatibility; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledAfter; @@ -403,6 +404,13 @@ public class JobInfo implements Parcelable { public static final int FLAG_DATA_TRANSFER = 1 << 5; /** + * Whether it's a user initiated job or not. + * + * @hide + */ + public static final int FLAG_USER_INITIATED = 1 << 6; + + /** * @hide */ public static final int CONSTRAINT_FLAG_CHARGING = 1 << 0; @@ -738,6 +746,14 @@ public class JobInfo implements Parcelable { } /** + * @see JobInfo.Builder#setUserInitiated(boolean) + * @hide + */ + public boolean isUserInitiated() { + return (flags & FLAG_USER_INITIATED) != 0; + } + + /** * @see JobInfo.Builder#setImportantWhileForeground(boolean) */ public boolean isImportantWhileForeground() { @@ -1849,15 +1865,8 @@ public class JobInfo implements Parcelable { * * <p> * For user-initiated transfers that must be started immediately, call - * {@link #setExpedited(boolean) setExpedited(true)}. Otherwise, the system may defer the - * job to a more opportune time. Using {@link #setExpedited(boolean) setExpedited(true)} - * with this API will only be allowed for foreground apps and when the user has clearly - * interacted with the app. {@link #setExpedited(boolean) setExpedited(true)} will return - * {@link JobScheduler#RESULT_FAILURE} for a data transfer job if the app is in the - * background. Apps that successfully schedule data transfer jobs with - * {@link #setExpedited(boolean) setExpedited(true)} will not have quotas applied to them, - * though they may still be stopped for system health or constraint reasons. The system will - * also give a user the ability to stop a data transfer job via the Task Manager. + * {@link #setUserInitiated(boolean) setUserInitiated(true)}. Otherwise, the system may + * defer the job to a more opportune time. * * <p> * If you want to perform more than one data transfer job, consider enqueuing multiple @@ -1877,6 +1886,50 @@ public class JobInfo implements Parcelable { } /** + * Indicates that this job is being scheduled to fulfill an explicit user request. + * As such, user-initiated jobs can only be scheduled when the app is in the foreground + * or in a state where launching an activity is allowed, as defined + * <a href= + * "https://developer.android.com/guide/components/activities/background-starts#exceptions"> + * here</a>. Attempting to schedule one outside of these conditions will throw a + * {@link SecurityException}. + * + * <p> + * This should <b>NOT</b> be used for automatic features. + * + * <p> + * All user-initiated jobs must have an associated notification, set via + * {@link JobService#setNotification(JobParameters, int, Notification, int)}, and will be + * shown in the Task Manager when running. + * + * <p> + * These jobs will not be subject to quotas and will be started immediately once scheduled + * if all constraints are met and the device system health allows for additional tasks. + * + * @see JobInfo#isUserInitiated() + * @hide + */ + @NonNull + public Builder setUserInitiated(boolean userInitiated) { + if (userInitiated) { + mFlags |= FLAG_USER_INITIATED; + if (mPriority == PRIORITY_DEFAULT) { + // The default priority for UIJs is MAX, but only change this if .setPriority() + // hasn't been called yet. + mPriority = PRIORITY_MAX; + } + } else { + if (mPriority == PRIORITY_MAX && (mFlags & FLAG_USER_INITIATED) != 0) { + // Reset the priority for the job, but only change this if .setPriority() + // hasn't been called yet. + mPriority = PRIORITY_DEFAULT; + } + mFlags &= (~FLAG_USER_INITIATED); + } + return this; + } + + /** * Setting this to true indicates that this job is important while the scheduling app * is in the foreground or on the temporary whitelist for background restrictions. * This means that the system will relax doze restrictions on this job during this time. @@ -2086,10 +2139,12 @@ public class JobInfo implements Parcelable { } final boolean isExpedited = (flags & FLAG_EXPEDITED) != 0; + final boolean isUserInitiated = (flags & FLAG_USER_INITIATED) != 0; switch (mPriority) { case PRIORITY_MAX: - if (!isExpedited) { - throw new IllegalArgumentException("Only expedited jobs can have max priority"); + if (!(isExpedited || isUserInitiated)) { + throw new IllegalArgumentException( + "Only expedited or user-initiated jobs can have max priority"); } break; case PRIORITY_HIGH: @@ -2118,14 +2173,20 @@ public class JobInfo implements Parcelable { if (isPeriodic) { throw new IllegalArgumentException("An expedited job cannot be periodic"); } + if ((flags & FLAG_DATA_TRANSFER) != 0) { + throw new IllegalArgumentException( + "An expedited job cannot also be a data transfer job"); + } + if (isUserInitiated) { + throw new IllegalArgumentException("An expedited job cannot be user-initiated"); + } if (mPriority != PRIORITY_MAX && mPriority != PRIORITY_HIGH) { throw new IllegalArgumentException( "An expedited job must be high or max priority. Don't use expedited jobs" + " for unimportant tasks."); } - if (((constraintFlags & ~CONSTRAINT_FLAG_STORAGE_NOT_LOW) != 0 - || (flags & ~(FLAG_EXPEDITED | FLAG_EXEMPT_FROM_APP_STANDBY - | FLAG_DATA_TRANSFER)) != 0)) { + if ((constraintFlags & ~CONSTRAINT_FLAG_STORAGE_NOT_LOW) != 0 + || (flags & ~(FLAG_EXPEDITED | FLAG_EXEMPT_FROM_APP_STANDBY)) != 0) { throw new IllegalArgumentException( "An expedited job can only have network and storage-not-low constraints"); } @@ -2152,6 +2213,33 @@ public class JobInfo implements Parcelable { "A data transfer job must specify a valid network type"); } } + + if (isUserInitiated) { + if (hasEarlyConstraint) { + throw new IllegalArgumentException("A user-initiated job cannot have a time delay"); + } + if (hasLateConstraint) { + throw new IllegalArgumentException("A user-initiated job cannot have a deadline"); + } + if (isPeriodic) { + throw new IllegalArgumentException("A user-initiated job cannot be periodic"); + } + if ((flags & FLAG_PREFETCH) != 0) { + throw new IllegalArgumentException( + "A user-initiated job cannot also be a prefetch job"); + } + if (mPriority != PRIORITY_MAX) { + throw new IllegalArgumentException("A user-initiated job must be max priority."); + } + if ((constraintFlags & CONSTRAINT_FLAG_DEVICE_IDLE) != 0) { + throw new IllegalArgumentException( + "A user-initiated job cannot have a device-idle constraint"); + } + if (triggerContentUris != null && triggerContentUris.length > 0) { + throw new IllegalArgumentException( + "Can't call addTriggerContentUri() on a user-initiated job"); + } + } } /** diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java index 30986dde6b91..651853bae68e 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobConcurrencyManager.java @@ -761,8 +761,8 @@ class JobConcurrencyManager { if (js != null) { mWorkCountTracker.incrementRunningJobCount(jsc.getRunningJobWorkType()); assignment.workType = jsc.getRunningJobWorkType(); - if (js.startedAsExpeditedJob && js.lastEvaluatedBias == JobInfo.BIAS_TOP_APP) { - info.numRunningTopEj++; + if (js.startedWithImmediacyPrivilege) { + info.numRunningImmediacyPrivileged++; } } @@ -829,11 +829,9 @@ class JobConcurrencyManager { continue; } - final boolean isTopEj = nextPending.shouldTreatAsExpeditedJob() - && nextPending.lastEvaluatedBias == JobInfo.BIAS_TOP_APP; + final boolean hasImmediacyPrivilege = hasImmediacyPrivilegeLocked(nextPending); if (DEBUG && isSimilarJobRunningLocked(nextPending)) { - Slog.w(TAG, "Already running similar " + (isTopEj ? "TOP-EJ" : "job") - + " to: " + nextPending); + Slog.w(TAG, "Already running similar job to: " + nextPending); } // Factoring minChangedWaitingTimeMs into the min waiting time effectively limits @@ -876,23 +874,25 @@ class JobConcurrencyManager { final JobStatus runningJob = assignment.context.getRunningJobLocked(); // Maybe stop the job if it has had its day in the sun. Only allow replacing // for one of the following conditions: - // 1. We're putting in the current TOP app's EJ + // 1. We're putting in a job that has the privilege of running immediately // 2. There aren't too many jobs running AND the current job started when the // app was in the background // 3. There aren't too many jobs running AND the current job started when the // app was on TOP, but the app has since left TOP // 4. There aren't too many jobs running AND the current job started when the - // app was on TOP, the app is still TOP, but there are too many TOP+EJs + // app was on TOP, the app is still TOP, but there are too many + // immediacy-privileged jobs // running (because we don't want them to starve out other apps and the // current job has already run for the minimum guaranteed time). // 5. This new job could be waiting for too long for a slot to open up - boolean canReplace = isTopEj; // Case 1 + boolean canReplace = hasImmediacyPrivilege; // Case 1 if (!canReplace && !isInOverage) { final int currentJobBias = mService.evaluateJobBiasLocked(runningJob); canReplace = runningJob.lastEvaluatedBias < JobInfo.BIAS_TOP_APP // Case 2 || currentJobBias < JobInfo.BIAS_TOP_APP // Case 3 // Case 4 - || info.numRunningTopEj > .5 * mWorkTypeConfig.getMaxTotal(); + || info.numRunningImmediacyPrivileged + > (mWorkTypeConfig.getMaxTotal() / 2); } if (!canReplace && mMaxWaitTimeBypassEnabled) { // Case 5 if (nextPending.shouldTreatAsExpeditedJob()) { @@ -919,7 +919,7 @@ class JobConcurrencyManager { } } } - if (selectedContext == null && (!isInOverage || isTopEj)) { + if (selectedContext == null && (!isInOverage || hasImmediacyPrivilege)) { int lowestBiasSeen = Integer.MAX_VALUE; long newMinPreferredUidOnlyWaitingTimeMs = Long.MAX_VALUE; for (int p = preferredUidOnly.size() - 1; p >= 0; --p) { @@ -962,12 +962,13 @@ class JobConcurrencyManager { info.minPreferredUidOnlyWaitingTimeMs = newMinPreferredUidOnlyWaitingTimeMs; } } - // Make sure to run EJs for the TOP app immediately. - if (isTopEj) { + // Make sure to run jobs with special privilege immediately. + if (hasImmediacyPrivilege) { if (selectedContext != null && selectedContext.context.getRunningJobLocked() != null) { - // We're "replacing" a currently running job, but we want TOP EJs to start - // immediately, so we'll start the EJ on a fresh available context and + // We're "replacing" a currently running job, but we want immediacy-privileged + // jobs to start immediately, so we'll start the privileged jobs on a fresh + // available context and // stop this currently running job to replace in two steps. changed.add(selectedContext); projectedRunningCount--; @@ -1029,6 +1030,7 @@ class JobConcurrencyManager { projectedRunningCount--; } if (selectedContext.newJob != null) { + selectedContext.newJob.startedWithImmediacyPrivilege = hasImmediacyPrivilege; projectedRunningCount++; minChangedWaitingTimeMs = Math.min(minChangedWaitingTimeMs, mService.getMinJobExecutionGuaranteeMs(selectedContext.newJob)); @@ -1103,6 +1105,18 @@ class JobConcurrencyManager { mActivePkgStats.forEach(mPackageStatsStagingCountClearer); } + @VisibleForTesting + @GuardedBy("mLock") + boolean hasImmediacyPrivilegeLocked(@NonNull JobStatus job) { + // EJs & user-initiated jobs for the TOP app should run immediately. + // However, even for user-initiated jobs, if the app has not recently been in TOP or BAL + // state, we don't give the immediacy privilege so that we can try and maintain + // reasonably concurrency behavior. + return job.lastEvaluatedBias == JobInfo.BIAS_TOP_APP + // TODO(): include BAL state for user-initiated jobs + && (job.shouldTreatAsExpeditedJob() || job.shouldTreatAsUserInitiated()); + } + @GuardedBy("mLock") void onUidBiasChangedLocked(int prevBias, int newBias) { if (prevBias != JobInfo.BIAS_TOP_APP && newBias != JobInfo.BIAS_TOP_APP) { @@ -1361,7 +1375,7 @@ class JobConcurrencyManager { mActiveServices.remove(worker); if (mIdleContexts.size() < MAX_RETAINED_OBJECTS) { // Don't need to save all new contexts, but keep some extra around in case we need - // extras for another TOP+EJ overage. + // extras for another immediacy privileged overage. mIdleContexts.add(worker); } else { mNumDroppedContexts++; @@ -1403,7 +1417,8 @@ class JobConcurrencyManager { } if (respectConcurrencyLimit) { worker.clearPreferredUid(); - // We're over the limit (because the TOP app scheduled a lot of EJs), but we should + // We're over the limit (because there were a lot of immediacy-privileged jobs + // scheduled), but we should // be able to stop the other jobs soon so don't start running anything new until we // get back below the limit. noteConcurrency(); @@ -1627,17 +1642,17 @@ class JobConcurrencyManager { } } else if (mWorkCountTracker.getPendingJobCount(WORK_TYPE_EJ) > 0) { return "blocking " + workTypeToString(WORK_TYPE_EJ) + " queue"; - } else if (js.startedAsExpeditedJob && js.lastEvaluatedBias == JobInfo.BIAS_TOP_APP) { - // Try not to let TOP + EJ starve out other apps. - int topEjCount = 0; + } else if (js.startedWithImmediacyPrivilege) { + // Try not to let jobs with immediacy privilege starve out other apps. + int immediacyPrivilegeCount = 0; for (int r = mRunningJobs.size() - 1; r >= 0; --r) { JobStatus j = mRunningJobs.valueAt(r); - if (j.startedAsExpeditedJob && j.lastEvaluatedBias == JobInfo.BIAS_TOP_APP) { - topEjCount++; + if (j.startedWithImmediacyPrivilege) { + immediacyPrivilegeCount++; } } - if (topEjCount > .5 * mWorkTypeConfig.getMaxTotal()) { - return "prevent top EJ dominance"; + if (immediacyPrivilegeCount > mWorkTypeConfig.getMaxTotal() / 2) { + return "prevent immediacy privilege dominance"; } } // No other pending EJs. Return null so we don't let regular jobs preempt an EJ. @@ -1688,6 +1703,40 @@ class JobConcurrencyManager { return foundSome; } + /** + * Returns the estimated network bytes if the job is running. Returns {@code null} if the job + * isn't running. + */ + @Nullable + @GuardedBy("mLock") + Pair<Long, Long> getEstimatedNetworkBytesLocked(String pkgName, int uid, int jobId) { + for (int i = 0; i < mActiveServices.size(); i++) { + final JobServiceContext jc = mActiveServices.get(i); + final JobStatus js = jc.getRunningJobLocked(); + if (js != null && js.matches(uid, jobId) && js.getSourcePackageName().equals(pkgName)) { + return jc.getEstimatedNetworkBytes(); + } + } + return null; + } + + /** + * Returns the transferred network bytes if the job is running. Returns {@code null} if the job + * isn't running. + */ + @Nullable + @GuardedBy("mLock") + Pair<Long, Long> getTransferredNetworkBytesLocked(String pkgName, int uid, int jobId) { + for (int i = 0; i < mActiveServices.size(); i++) { + final JobServiceContext jc = mActiveServices.get(i); + final JobStatus js = jc.getRunningJobLocked(); + if (js != null && js.matches(uid, jobId) && js.getSourcePackageName().equals(pkgName)) { + return jc.getTransferredNetworkBytes(); + } + } + return null; + } + @NonNull private JobServiceContext createNewJobServiceContext() { return mInjector.createJobServiceContext(mService, this, mNotificationCoordinator, @@ -2566,11 +2615,11 @@ class JobConcurrencyManager { @VisibleForTesting static final class AssignmentInfo { public long minPreferredUidOnlyWaitingTimeMs; - public int numRunningTopEj; + public int numRunningImmediacyPrivileged; void clear() { minPreferredUidOnlyWaitingTimeMs = 0; - numRunningTopEj = 0; + numRunningImmediacyPrivileged = 0; } } 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 535f8d4cad45..e9b966083851 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java @@ -86,6 +86,7 @@ import android.util.ArrayMap; import android.util.ArraySet; import android.util.IndentingPrintWriter; import android.util.Log; +import android.util.Pair; import android.util.Slog; import android.util.SparseArray; import android.util.SparseBooleanArray; @@ -2850,8 +2851,8 @@ public class JobSchedulerService extends com.android.server.SystemService } final boolean shouldForceBatchJob; - if (job.shouldTreatAsExpeditedJob()) { - // Never batch expedited jobs, even for RESTRICTED apps. + if (job.shouldTreatAsExpeditedJob() || job.shouldTreatAsUserInitiated()) { + // Never batch expedited or user-initiated jobs, even for RESTRICTED apps. shouldForceBatchJob = false; } else if (job.getEffectiveStandbyBucket() == RESTRICTED_INDEX) { // Restricted jobs must always be batched @@ -4168,6 +4169,100 @@ public class JobSchedulerService extends com.android.server.SystemService } } + int getEstimatedNetworkBytes(PrintWriter pw, String pkgName, int userId, int jobId, + int byteOption) { + try { + final int uid = AppGlobals.getPackageManager().getPackageUid(pkgName, 0, + userId != UserHandle.USER_ALL ? userId : UserHandle.USER_SYSTEM); + if (uid < 0) { + pw.print("unknown("); + pw.print(pkgName); + pw.println(")"); + return JobSchedulerShellCommand.CMD_ERR_NO_PACKAGE; + } + + synchronized (mLock) { + final JobStatus js = mJobs.getJobByUidAndJobId(uid, jobId); + if (DEBUG) { + Slog.d(TAG, "get-estimated-network-bytes " + uid + "/" + jobId + ": " + js); + } + if (js == null) { + pw.print("unknown("); UserHandle.formatUid(pw, uid); + pw.print("/jid"); pw.print(jobId); pw.println(")"); + return JobSchedulerShellCommand.CMD_ERR_NO_JOB; + } + + final long downloadBytes; + final long uploadBytes; + final Pair<Long, Long> bytes = + mConcurrencyManager.getEstimatedNetworkBytesLocked(pkgName, uid, jobId); + if (bytes == null) { + downloadBytes = js.getEstimatedNetworkDownloadBytes(); + uploadBytes = js.getEstimatedNetworkUploadBytes(); + } else { + downloadBytes = bytes.first; + uploadBytes = bytes.second; + } + if (byteOption == JobSchedulerShellCommand.BYTE_OPTION_DOWNLOAD) { + pw.println(downloadBytes); + } else { + pw.println(uploadBytes); + } + pw.println(); + } + } catch (RemoteException e) { + // can't happen + } + return 0; + } + + int getTransferredNetworkBytes(PrintWriter pw, String pkgName, int userId, int jobId, + int byteOption) { + try { + final int uid = AppGlobals.getPackageManager().getPackageUid(pkgName, 0, + userId != UserHandle.USER_ALL ? userId : UserHandle.USER_SYSTEM); + if (uid < 0) { + pw.print("unknown("); + pw.print(pkgName); + pw.println(")"); + return JobSchedulerShellCommand.CMD_ERR_NO_PACKAGE; + } + + synchronized (mLock) { + final JobStatus js = mJobs.getJobByUidAndJobId(uid, jobId); + if (DEBUG) { + Slog.d(TAG, "get-transferred-network-bytes " + uid + "/" + jobId + ": " + js); + } + if (js == null) { + pw.print("unknown("); UserHandle.formatUid(pw, uid); + pw.print("/jid"); pw.print(jobId); pw.println(")"); + return JobSchedulerShellCommand.CMD_ERR_NO_JOB; + } + + final long downloadBytes; + final long uploadBytes; + final Pair<Long, Long> bytes = + mConcurrencyManager.getTransferredNetworkBytesLocked(pkgName, uid, jobId); + if (bytes == null) { + downloadBytes = 0; + uploadBytes = 0; + } else { + downloadBytes = bytes.first; + uploadBytes = bytes.second; + } + if (byteOption == JobSchedulerShellCommand.BYTE_OPTION_DOWNLOAD) { + pw.println(downloadBytes); + } else { + pw.println(uploadBytes); + } + pw.println(); + } + } catch (RemoteException e) { + // can't happen + } + return 0; + } + private boolean checkRunLongJobsPermission(int packageUid, String packageName) { // Returns true if both the appop and permission are granted. return PermissionChecker.checkPermissionForPreflight(getTestableContext(), diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java index 27268d267001..36ba8dd10bd5 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java @@ -32,6 +32,9 @@ public final class JobSchedulerShellCommand extends BasicShellCommandHandler { public static final int CMD_ERR_NO_JOB = -1001; public static final int CMD_ERR_CONSTRAINTS = -1002; + static final int BYTE_OPTION_DOWNLOAD = 0; + static final int BYTE_OPTION_UPLOAD = 1; + JobSchedulerService mInternal; IPackageManager mPM; @@ -59,10 +62,18 @@ public final class JobSchedulerShellCommand extends BasicShellCommandHandler { return getBatteryCharging(pw); case "get-battery-not-low": return getBatteryNotLow(pw); + case "get-estimated-download-bytes": + return getEstimatedNetworkBytes(pw, BYTE_OPTION_DOWNLOAD); + case "get-estimated-upload-bytes": + return getEstimatedNetworkBytes(pw, BYTE_OPTION_UPLOAD); case "get-storage-seq": return getStorageSeq(pw); case "get-storage-not-low": return getStorageNotLow(pw); + case "get-transferred-download-bytes": + return getTransferredNetworkBytes(pw, BYTE_OPTION_DOWNLOAD); + case "get-transferred-upload-bytes": + return getTransferredNetworkBytes(pw, BYTE_OPTION_UPLOAD); case "get-job-state": return getJobState(pw); case "heartbeat": @@ -304,6 +315,43 @@ public final class JobSchedulerShellCommand extends BasicShellCommandHandler { return 0; } + private int getEstimatedNetworkBytes(PrintWriter pw, int byteOption) throws Exception { + checkPermission("get estimated bytes"); + + int userId = UserHandle.USER_SYSTEM; + + String opt; + while ((opt = getNextOption()) != null) { + switch (opt) { + case "-u": + case "--user": + userId = UserHandle.parseUserArg(getNextArgRequired()); + break; + + default: + pw.println("Error: unknown option '" + opt + "'"); + return -1; + } + } + + if (userId == UserHandle.USER_CURRENT) { + userId = ActivityManager.getCurrentUser(); + } + + final String pkgName = getNextArgRequired(); + final String jobIdStr = getNextArgRequired(); + final int jobId = Integer.parseInt(jobIdStr); + + final long ident = Binder.clearCallingIdentity(); + try { + int ret = mInternal.getEstimatedNetworkBytes(pw, pkgName, userId, jobId, byteOption); + printError(ret, pkgName, userId, jobId); + return ret; + } finally { + Binder.restoreCallingIdentity(ident); + } + } + private int getStorageSeq(PrintWriter pw) { int seq = mInternal.getStorageSeq(); pw.println(seq); @@ -316,8 +364,45 @@ public final class JobSchedulerShellCommand extends BasicShellCommandHandler { return 0; } + private int getTransferredNetworkBytes(PrintWriter pw, int byteOption) throws Exception { + checkPermission("get transferred bytes"); + + int userId = UserHandle.USER_SYSTEM; + + String opt; + while ((opt = getNextOption()) != null) { + switch (opt) { + case "-u": + case "--user": + userId = UserHandle.parseUserArg(getNextArgRequired()); + break; + + default: + pw.println("Error: unknown option '" + opt + "'"); + return -1; + } + } + + if (userId == UserHandle.USER_CURRENT) { + userId = ActivityManager.getCurrentUser(); + } + + final String pkgName = getNextArgRequired(); + final String jobIdStr = getNextArgRequired(); + final int jobId = Integer.parseInt(jobIdStr); + + final long ident = Binder.clearCallingIdentity(); + try { + int ret = mInternal.getTransferredNetworkBytes(pw, pkgName, userId, jobId, byteOption); + printError(ret, pkgName, userId, jobId); + return ret; + } finally { + Binder.restoreCallingIdentity(ident); + } + } + private int getJobState(PrintWriter pw) throws Exception { - checkPermission("force timeout jobs"); + checkPermission("get job state"); int userId = UserHandle.USER_SYSTEM; @@ -473,10 +558,30 @@ public final class JobSchedulerShellCommand extends BasicShellCommandHandler { pw.println(" Return whether the battery is currently considered to be charging."); pw.println(" get-battery-not-low"); pw.println(" Return whether the battery is currently considered to not be low."); + pw.println(" get-estimated-download-bytes [-u | --user USER_ID] PACKAGE JOB_ID"); + pw.println(" Return the most recent estimated download bytes for the job."); + pw.println(" Options:"); + pw.println(" -u or --user: specify which user's job is to be run; the default is"); + pw.println(" the primary or system user"); + pw.println(" get-estimated-upload-bytes [-u | --user USER_ID] PACKAGE JOB_ID"); + pw.println(" Return the most recent estimated upload bytes for the job."); + pw.println(" Options:"); + pw.println(" -u or --user: specify which user's job is to be run; the default is"); + pw.println(" the primary or system user"); pw.println(" get-storage-seq"); pw.println(" Return the last storage update sequence number that was received."); pw.println(" get-storage-not-low"); pw.println(" Return whether storage is currently considered to not be low."); + pw.println(" get-transferred-download-bytes [-u | --user USER_ID] PACKAGE JOB_ID"); + pw.println(" Return the most recent transferred download bytes for the job."); + pw.println(" Options:"); + pw.println(" -u or --user: specify which user's job is to be run; the default is"); + pw.println(" the primary or system user"); + pw.println(" get-transferred-upload-bytes [-u | --user USER_ID] PACKAGE JOB_ID"); + pw.println(" Return the most recent transferred upload bytes for the job."); + pw.println(" Options:"); + pw.println(" -u or --user: specify which user's job is to be run; the default is"); + pw.println(" the primary or system user"); pw.println(" get-job-state [-u | --user USER_ID] PACKAGE JOB_ID"); pw.println(" Return the current state of a job, may be any combination of:"); pw.println(" pending: currently on the pending list, waiting to be active"); @@ -493,5 +598,4 @@ public final class JobSchedulerShellCommand extends BasicShellCommandHandler { pw.println(" Trigger wireless charging dock state. Active by default."); pw.println(); } - } 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 df47f1787fdc..0dcfd24b263c 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java @@ -49,6 +49,7 @@ import android.os.Trace; import android.os.UserHandle; import android.util.EventLog; import android.util.IndentingPrintWriter; +import android.util.Pair; import android.util.Slog; import android.util.TimeUtils; @@ -171,6 +172,11 @@ public final class JobServiceContext implements ServiceConnection { /** The absolute maximum amount of time the job can run */ private long mMaxExecutionTimeMillis; + private long mEstimatedDownloadBytes; + private long mEstimatedUploadBytes; + private long mTransferredDownloadBytes; + private long mTransferredUploadBytes; + /** * The stop reason for a pending cancel. If there's not pending cancel, then the value should be * {@link JobParameters#STOP_REASON_UNDEFINED}. @@ -306,6 +312,9 @@ public final class JobServiceContext implements ServiceConnection { mMinExecutionGuaranteeMillis = mService.getMinJobExecutionGuaranteeMs(job); mMaxExecutionTimeMillis = Math.max(mService.getMaxJobExecutionTimeMs(job), mMinExecutionGuaranteeMillis); + mEstimatedDownloadBytes = job.getEstimatedNetworkDownloadBytes(); + mEstimatedUploadBytes = job.getEstimatedNetworkUploadBytes(); + mTransferredDownloadBytes = mTransferredUploadBytes = 0; final long whenDeferred = job.getWhenStandbyDeferred(); if (whenDeferred > 0) { @@ -524,6 +533,16 @@ public final class JobServiceContext implements ServiceConnection { return false; } + @GuardedBy("mLock") + Pair<Long, Long> getEstimatedNetworkBytes() { + return Pair.create(mEstimatedDownloadBytes, mEstimatedUploadBytes); + } + + @GuardedBy("mLock") + Pair<Long, Long> getTransferredNetworkBytes() { + return Pair.create(mTransferredDownloadBytes, mTransferredUploadBytes); + } + void doJobFinished(JobCallback cb, int jobId, boolean reschedule) { final long ident = Binder.clearCallingIdentity(); try { @@ -541,14 +560,26 @@ public final class JobServiceContext implements ServiceConnection { } } - private void doAcknowledgeGetTransferredDownloadBytesMessage(JobCallback jobCallback, int jobId, + private void doAcknowledgeGetTransferredDownloadBytesMessage(JobCallback cb, int jobId, int workId, @BytesLong long transferredBytes) { // TODO(255393346): Make sure apps call this appropriately and monitor for abuse + synchronized (mLock) { + if (!verifyCallerLocked(cb)) { + return; + } + mTransferredDownloadBytes = transferredBytes; + } } - private void doAcknowledgeGetTransferredUploadBytesMessage(JobCallback jobCallback, int jobId, + private void doAcknowledgeGetTransferredUploadBytesMessage(JobCallback cb, int jobId, int workId, @BytesLong long transferredBytes) { // TODO(255393346): Make sure apps call this appropriately and monitor for abuse + synchronized (mLock) { + if (!verifyCallerLocked(cb)) { + return; + } + mTransferredUploadBytes = transferredBytes; + } } void doAcknowledgeStopMessage(JobCallback cb, int jobId, boolean reschedule) { @@ -603,6 +634,30 @@ public final class JobServiceContext implements ServiceConnection { } } + private void doUpdateEstimatedNetworkBytes(JobCallback cb, int jobId, + @Nullable JobWorkItem item, long downloadBytes, long uploadBytes) { + // TODO(255393346): Make sure apps call this appropriately and monitor for abuse + synchronized (mLock) { + if (!verifyCallerLocked(cb)) { + return; + } + mEstimatedDownloadBytes = downloadBytes; + mEstimatedUploadBytes = uploadBytes; + } + } + + private void doUpdateTransferredNetworkBytes(JobCallback cb, int jobId, + @Nullable JobWorkItem item, long downloadBytes, long uploadBytes) { + // TODO(255393346): Make sure apps call this appropriately and monitor for abuse + synchronized (mLock) { + if (!verifyCallerLocked(cb)) { + return; + } + mTransferredDownloadBytes = downloadBytes; + mTransferredUploadBytes = uploadBytes; + } + } + private void doSetNotification(JobCallback cb, int jodId, int notificationId, Notification notification, int jobEndNotificationPolicy) { final int callingPid = Binder.getCallingPid(); @@ -627,16 +682,6 @@ public final class JobServiceContext implements ServiceConnection { } } - private void doUpdateTransferredNetworkBytes(JobCallback jobCallback, int jobId, - @Nullable JobWorkItem item, long downloadBytes, long uploadBytes) { - // TODO(255393346): Make sure apps call this appropriately and monitor for abuse - } - - private void doUpdateEstimatedNetworkBytes(JobCallback jobCallback, int jobId, - @Nullable JobWorkItem item, long downloadBytes, long uploadBytes) { - // TODO(255393346): Make sure apps call this appropriately and monitor for abuse - } - /** * We acquire/release a wakelock on onServiceConnected/unbindService. This mirrors the work * we intend to send to the client - we stop sending work when the service is unbound so until 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 6166921d64b2..3610b0a0064b 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 @@ -149,11 +149,13 @@ public final class ConnectivityController extends RestrictingController implemen // 2. Waiting connectivity jobs would be ready with connectivity // 3. An existing network satisfies a waiting connectivity job's requirements // 4. TOP proc state - // 5. Existence of treat-as-EJ EJs (not just requested EJs) - // 6. FGS proc state - // 7. EJ enqueue time - // 8. Any other important job priorities/proc states - // 9. Enqueue time + // 5. Existence of treat-as-UI UIJs (not just requested UIJs) + // 6. Existence of treat-as-EJ EJs (not just requested EJs) + // 7. FGS proc state + // 8. UIJ enqueue time + // 9. EJ enqueue time + // 10. Any other important job priorities/proc states + // 11. Enqueue time // TODO: maybe consider number of jobs // TODO: consider IMPORTANT_WHILE_FOREGROUND bit final int runningPriority = prioritizeExistenceOver(0, @@ -181,8 +183,13 @@ public final class ConnectivityController extends RestrictingController implemen if (topPriority != 0) { return topPriority; } - // They're either both TOP or both not TOP. Prioritize the app that has runnable EJs + // They're either both TOP or both not TOP. Prioritize the app that has runnable UIJs // pending. + final int uijPriority = prioritizeExistenceOver(0, us1.numUIJs, us2.numUIJs); + if (uijPriority != 0) { + return uijPriority; + } + // Still equivalent. Prioritize the app that has runnable EJs pending. final int ejPriority = prioritizeExistenceOver(0, us1.numEJs, us2.numEJs); if (ejPriority != 0) { return ejPriority; @@ -195,6 +202,12 @@ public final class ConnectivityController extends RestrictingController implemen if (fgsPriority != 0) { return fgsPriority; } + // Order them by UIJ enqueue time to help provide low UIJ latency. + if (us1.earliestUIJEnqueueTime < us2.earliestUIJEnqueueTime) { + return -1; + } else if (us1.earliestUIJEnqueueTime > us2.earliestUIJEnqueueTime) { + return 1; + } // Order them by EJ enqueue time to help provide low EJ latency. if (us1.earliestEJEnqueueTime < us2.earliestEJEnqueueTime) { return -1; @@ -414,7 +427,7 @@ public final class ConnectivityController extends RestrictingController implemen final UidStats uidStats = getUidStats(jobStatus.getSourceUid(), jobStatus.getSourcePackageName(), true); - if (jobStatus.shouldTreatAsExpeditedJob()) { + if (jobStatus.shouldTreatAsExpeditedJob() && jobStatus.shouldTreatAsUserInitiated()) { if (!jobStatus.isConstraintSatisfied(JobStatus.CONSTRAINT_CONNECTIVITY)) { // Don't request a direct hole through any of the firewalls. Instead, mark the // constraint as satisfied if the network is available, and the job will get @@ -936,10 +949,12 @@ public final class ConnectivityController extends RestrictingController implemen if (us.lastUpdatedElapsed + MIN_STATS_UPDATE_INTERVAL_MS < nowElapsed) { us.earliestEnqueueTime = Long.MAX_VALUE; us.earliestEJEnqueueTime = Long.MAX_VALUE; + us.earliestUIJEnqueueTime = Long.MAX_VALUE; us.numReadyWithConnectivity = 0; us.numRequestedNetworkAvailable = 0; us.numRegular = 0; us.numEJs = 0; + us.numUIJs = 0; for (int j = 0; j < jobs.size(); ++j) { JobStatus job = jobs.valueAt(j); @@ -956,10 +971,15 @@ public final class ConnectivityController extends RestrictingController implemen if (job.shouldTreatAsExpeditedJob() || job.startedAsExpeditedJob) { us.earliestEJEnqueueTime = Math.min(us.earliestEJEnqueueTime, job.enqueueTime); + } else if (job.shouldTreatAsUserInitiated()) { + us.earliestUIJEnqueueTime = + Math.min(us.earliestUIJEnqueueTime, job.enqueueTime); } } if (job.shouldTreatAsExpeditedJob() || job.startedAsExpeditedJob) { us.numEJs++; + } else if (job.shouldTreatAsUserInitiated()) { + us.numUIJs++; } else { us.numRegular++; } @@ -1466,8 +1486,10 @@ public final class ConnectivityController extends RestrictingController implemen public int numRequestedNetworkAvailable; public int numEJs; public int numRegular; + public int numUIJs; public long earliestEnqueueTime; public long earliestEJEnqueueTime; + public long earliestUIJEnqueueTime; public long lastUpdatedElapsed; private UidStats(int uid) { @@ -1485,6 +1507,7 @@ public final class ConnectivityController extends RestrictingController implemen pw.print("#reg", numRegular); pw.print("earliestEnqueue", earliestEnqueueTime); pw.print("earliestEJEnqueue", earliestEJEnqueueTime); + pw.print("earliestUIJEnqueue", earliestUIJEnqueueTime); pw.print("updated="); TimeUtils.formatDuration(lastUpdatedElapsed - nowElapsed, pw); pw.println("}"); diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java index 419127e6c6a9..251a4dac3685 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java @@ -388,6 +388,8 @@ public final class JobStatus { */ public boolean startedAsExpeditedJob = false; + public boolean startedWithImmediacyPrivilege = false; + // If non-null, this is work that has been enqueued for the job. public ArrayList<JobWorkItem> pendingWork; @@ -1369,9 +1371,7 @@ public final class JobStatus { * @return true if this is a job whose execution should be made visible to the user. */ public boolean isUserVisibleJob() { - // TODO(255767350): limit to user-initiated jobs - // Placeholder implementation until we have the code in - return shouldTreatAsExpeditedJob(); + return shouldTreatAsUserInitiated(); } /** @@ -1382,12 +1382,14 @@ public final class JobStatus { return appHasDozeExemption || (getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) != 0 || ((shouldTreatAsExpeditedJob() || startedAsExpeditedJob) + || shouldTreatAsUserInitiated() && (mDynamicConstraints & CONSTRAINT_DEVICE_NOT_DOZING) == 0); } boolean canRunInBatterySaver() { return (getInternalFlags() & INTERNAL_FLAG_HAS_FOREGROUND_EXEMPTION) != 0 || ((shouldTreatAsExpeditedJob() || startedAsExpeditedJob) + || shouldTreatAsUserInitiated() && (mDynamicConstraints & CONSTRAINT_BACKGROUND_NOT_RESTRICTED) == 0); } diff --git a/core/api/current.txt b/core/api/current.txt index c14ac950000c..a7bca5a9e09c 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -4150,6 +4150,8 @@ package android.app { method @Deprecated public android.app.FragmentManager getFragmentManager(); method public android.content.Intent getIntent(); method @Nullable public Object getLastNonConfigurationInstance(); + method @Nullable public String getLaunchedFromPackage(); + method public int getLaunchedFromUid(); method @NonNull public android.view.LayoutInflater getLayoutInflater(); method @Deprecated public android.app.LoaderManager getLoaderManager(); method @NonNull public String getLocalClassName(); @@ -4622,6 +4624,7 @@ package android.app { method public android.app.ActivityOptions setLaunchDisplayId(int); method public android.app.ActivityOptions setLockTaskEnabled(boolean); method public void setPendingIntentBackgroundActivityLaunchAllowed(boolean); + method @NonNull public android.app.ActivityOptions setShareIdentityEnabled(boolean); method @NonNull public android.app.ActivityOptions setSplashScreenStyle(int); method public android.os.Bundle toBundle(); method public void update(android.app.ActivityOptions); @@ -20526,6 +20529,7 @@ package android.media { field public static final int ENCODING_DOLBY_MAT = 19; // 0x13 field public static final int ENCODING_DOLBY_TRUEHD = 14; // 0xe field public static final int ENCODING_DRA = 28; // 0x1c + field public static final int ENCODING_DSD = 31; // 0x1f field public static final int ENCODING_DTS = 7; // 0x7 field public static final int ENCODING_DTS_HD = 8; // 0x8 field public static final int ENCODING_DTS_HD_MA = 29; // 0x1d @@ -20919,6 +20923,7 @@ package android.media { method public void writeToParcel(@NonNull android.os.Parcel, int); field public static final int AUDIO_ENCAPSULATION_TYPE_IEC61937 = 1; // 0x1 field public static final int AUDIO_ENCAPSULATION_TYPE_NONE = 0; // 0x0 + field public static final int AUDIO_ENCAPSULATION_TYPE_PCM = 2; // 0x2 field @NonNull public static final android.os.Parcelable.Creator<android.media.AudioProfile> CREATOR; } @@ -25460,11 +25465,21 @@ package android.media.projection { public abstract static class MediaProjection.Callback { ctor public MediaProjection.Callback(); + method public void onCapturedContentResize(int, int); method public void onStop(); } + public final class MediaProjectionConfig implements android.os.Parcelable { + method @NonNull public static android.media.projection.MediaProjectionConfig createConfigForDisplay(@IntRange(from=android.view.Display.DEFAULT_DISPLAY, to=android.view.Display.DEFAULT_DISPLAY) int); + method @NonNull public static android.media.projection.MediaProjectionConfig createConfigForUserChoice(); + method public int describeContents(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.media.projection.MediaProjectionConfig> CREATOR; + } + public final class MediaProjectionManager { - method public android.content.Intent createScreenCaptureIntent(); + method @NonNull public android.content.Intent createScreenCaptureIntent(); + method @NonNull public android.content.Intent createScreenCaptureIntent(@NonNull android.media.projection.MediaProjectionConfig); method public android.media.projection.MediaProjection getMediaProjection(int, @NonNull android.content.Intent); } @@ -27881,13 +27896,17 @@ package android.net.vcn { method @IntRange(from=0x500) public int getMaxMtu(); method @NonNull public long[] getRetryIntervalsMillis(); method @NonNull public java.util.List<android.net.vcn.VcnUnderlyingNetworkTemplate> getVcnUnderlyingNetworkPriorities(); + method public boolean hasGatewayOption(int); + field public static final int VCN_GATEWAY_OPTION_ENABLE_DATA_STALL_RECOVERY_WITH_MOBILITY = 0; // 0x0 } public static final class VcnGatewayConnectionConfig.Builder { ctor public VcnGatewayConnectionConfig.Builder(@NonNull String, @NonNull android.net.ipsec.ike.IkeTunnelConnectionParams); method @NonNull public android.net.vcn.VcnGatewayConnectionConfig.Builder addExposedCapability(int); + method @NonNull public android.net.vcn.VcnGatewayConnectionConfig.Builder addGatewayOption(int); method @NonNull public android.net.vcn.VcnGatewayConnectionConfig build(); method @NonNull public android.net.vcn.VcnGatewayConnectionConfig.Builder removeExposedCapability(int); + method @NonNull public android.net.vcn.VcnGatewayConnectionConfig.Builder removeGatewayOption(int); method @NonNull public android.net.vcn.VcnGatewayConnectionConfig.Builder setMaxMtu(@IntRange(from=0x500) int); method @NonNull public android.net.vcn.VcnGatewayConnectionConfig.Builder setRetryIntervalsMillis(@NonNull long[]); method @NonNull public android.net.vcn.VcnGatewayConnectionConfig.Builder setVcnUnderlyingNetworkPriorities(@NonNull java.util.List<android.net.vcn.VcnUnderlyingNetworkTemplate>); @@ -38277,7 +38296,7 @@ package android.security.identity { ctor public AlreadyPersonalizedException(@NonNull String, @NonNull Throwable); } - public class AuthenticationKeyMetadata { + public final class AuthenticationKeyMetadata { method @NonNull public java.time.Instant getExpirationDate(); method @IntRange(from=0) public int getUsageCount(); } @@ -52785,12 +52804,14 @@ package android.view.accessibility { method public void addAudioDescriptionRequestedChangeListener(@NonNull java.util.concurrent.Executor, @NonNull android.view.accessibility.AccessibilityManager.AudioDescriptionRequestedChangeListener); method public boolean addTouchExplorationStateChangeListener(@NonNull android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener); method public void addTouchExplorationStateChangeListener(@NonNull android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener, @Nullable android.os.Handler); + method public void addUiContrastChangeListener(@NonNull java.util.concurrent.Executor, @NonNull android.view.accessibility.AccessibilityManager.UiContrastChangeListener); method @ColorInt public int getAccessibilityFocusColor(); method public int getAccessibilityFocusStrokeWidth(); method @Deprecated public java.util.List<android.content.pm.ServiceInfo> getAccessibilityServiceList(); method public java.util.List<android.accessibilityservice.AccessibilityServiceInfo> getEnabledAccessibilityServiceList(int); method public java.util.List<android.accessibilityservice.AccessibilityServiceInfo> getInstalledAccessibilityServiceList(); method public int getRecommendedTimeoutMillis(int, int); + method @FloatRange(from=-1.0F, to=1.0f) public float getUiContrast(); method public void interrupt(); method public static boolean isAccessibilityButtonSupported(); method public boolean isAudioDescriptionRequested(); @@ -52802,6 +52823,7 @@ package android.view.accessibility { method public boolean removeAccessibilityStateChangeListener(@NonNull android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener); method public boolean removeAudioDescriptionRequestedChangeListener(@NonNull android.view.accessibility.AccessibilityManager.AudioDescriptionRequestedChangeListener); method public boolean removeTouchExplorationStateChangeListener(@NonNull android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener); + method public void removeUiContrastChangeListener(@NonNull android.view.accessibility.AccessibilityManager.UiContrastChangeListener); method public void sendAccessibilityEvent(android.view.accessibility.AccessibilityEvent); field public static final int FLAG_CONTENT_CONTROLS = 4; // 0x4 field public static final int FLAG_CONTENT_ICONS = 1; // 0x1 @@ -52824,6 +52846,10 @@ package android.view.accessibility { method public void onTouchExplorationStateChanged(boolean); } + public static interface AccessibilityManager.UiContrastChangeListener { + method public void onUiContrastChanged(@FloatRange(from=-1.0F, to=1.0f) float); + } + public class AccessibilityNodeInfo implements android.os.Parcelable { ctor public AccessibilityNodeInfo(); ctor public AccessibilityNodeInfo(@NonNull android.view.View); diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 5e4834a04a83..92722901e485 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -352,6 +352,7 @@ package android { field public static final String USE_COLORIZED_NOTIFICATIONS = "android.permission.USE_COLORIZED_NOTIFICATIONS"; field public static final String USE_RESERVED_DISK = "android.permission.USE_RESERVED_DISK"; field public static final String UWB_PRIVILEGED = "android.permission.UWB_PRIVILEGED"; + field public static final String WAKEUP_SURFACE_FLINGER = "android.permission.WAKEUP_SURFACE_FLINGER"; field public static final String WHITELIST_AUTO_REVOKE_PERMISSIONS = "android.permission.WHITELIST_AUTO_REVOKE_PERMISSIONS"; field public static final String WHITELIST_RESTRICTED_PERMISSIONS = "android.permission.WHITELIST_RESTRICTED_PERMISSIONS"; field public static final String WIFI_ACCESS_COEX_UNSAFE_CHANNELS = "android.permission.WIFI_ACCESS_COEX_UNSAFE_CHANNELS"; @@ -388,6 +389,7 @@ package android { field public static final int sdkVersion = 16844304; // 0x1010610 field public static final int supportsAmbientMode = 16844173; // 0x101058d field public static final int userRestriction = 16844164; // 0x1010584 + field public static final int visualQueryDetectionService; } public static final class R.bool { @@ -2999,6 +3001,7 @@ package android.companion.virtual { method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualKeyboard createVirtualKeyboard(@NonNull android.hardware.display.VirtualDisplay, @NonNull String, int, int); method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualMouse createVirtualMouse(@NonNull android.hardware.input.VirtualMouseConfig); method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualMouse createVirtualMouse(@NonNull android.hardware.display.VirtualDisplay, @NonNull String, int, int); + method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualNavigationTouchpad createVirtualNavigationTouchpad(@NonNull android.hardware.input.VirtualNavigationTouchpadConfig); method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualTouchscreen createVirtualTouchscreen(@NonNull android.hardware.input.VirtualTouchscreenConfig); method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualTouchscreen createVirtualTouchscreen(@NonNull android.hardware.display.VirtualDisplay, @NonNull String, int, int); method public int getDeviceId(); @@ -3607,6 +3610,7 @@ package android.content.pm { public abstract class PackageManager { method @RequiresPermission("android.permission.OBSERVE_GRANT_REVOKE_PERMISSIONS") public abstract void addOnPermissionsChangeListener(@NonNull android.content.pm.PackageManager.OnPermissionsChangedListener); method public abstract boolean arePermissionsIndividuallyControlled(); + method @NonNull public boolean[] canPackageQuery(@NonNull String, @NonNull String[]) throws android.content.pm.PackageManager.NameNotFoundException; method @NonNull public abstract java.util.List<android.content.IntentFilter> getAllIntentFilters(@NonNull String); method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS) public android.content.pm.ApplicationInfo getApplicationInfoAsUser(@NonNull String, int, @NonNull android.os.UserHandle) throws android.content.pm.PackageManager.NameNotFoundException; method @NonNull @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS) public android.content.pm.ApplicationInfo getApplicationInfoAsUser(@NonNull String, @NonNull android.content.pm.PackageManager.ApplicationInfoFlags, @NonNull android.os.UserHandle) throws android.content.pm.PackageManager.NameNotFoundException; @@ -4783,6 +4787,24 @@ package android.hardware.input { method @NonNull public android.hardware.input.VirtualMouseScrollEvent.Builder setYAxisMovement(@FloatRange(from=-1.0F, to=1.0f) float); } + public class VirtualNavigationTouchpad implements java.io.Closeable { + method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void close(); + method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void sendTouchEvent(@NonNull android.hardware.input.VirtualTouchEvent); + } + + public final class VirtualNavigationTouchpadConfig extends android.hardware.input.VirtualInputDeviceConfig implements android.os.Parcelable { + method public int describeContents(); + method public int getHeight(); + method public int getWidth(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.hardware.input.VirtualNavigationTouchpadConfig> CREATOR; + } + + public static final class VirtualNavigationTouchpadConfig.Builder extends android.hardware.input.VirtualInputDeviceConfig.Builder<android.hardware.input.VirtualNavigationTouchpadConfig.Builder> { + ctor public VirtualNavigationTouchpadConfig.Builder(@IntRange(from=1) int, @IntRange(from=1) int); + method @NonNull public android.hardware.input.VirtualNavigationTouchpadConfig build(); + } + public final class VirtualTouchEvent implements android.os.Parcelable { method public int describeContents(); method public int getAction(); diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 9730c169243c..5e02e72f2088 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -2944,6 +2944,7 @@ package android.view { } public final class MotionEvent extends android.view.InputEvent implements android.os.Parcelable { + method public int getDisplayId(); method public void setActionButton(int); method public void setButtonState(int); method public void setDisplayId(int); diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index a16d4ba7a386..b9eb44316956 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -6654,16 +6654,64 @@ public class Activity extends ContextThemeWrapper } /** - * Returns the uid who started this activity. - * @hide + * Returns the uid of the app that initially launched this activity. + * + * <p>In order to receive the launching app's uid, at least one of the following has to + * be met: + * <ul> + * <li>The app must call {@link ActivityOptions#setShareIdentityEnabled(boolean)} with a + * value of {@code true} and launch this activity with the resulting {@code + * ActivityOptions}. + * <li>The launched activity has the same uid as the launching app. + * <li>The launched activity is running in a package that is signed with the same key + * used to sign the platform (typically only system packages such as Settings will + * meet this requirement). + * </ul>. + * These are the same requirements for {@link #getLaunchedFromPackage()}; if any of these are + * met, then these methods can be used to obtain the uid and package name of the launching + * app. If none are met, then {@link Process#INVALID_UID} is returned. + * + * <p>Note, even if the above conditions are not met, the launching app's identity may + * still be available from {@link #getCallingPackage()} if this activity was started with + * {@code Activity#startActivityForResult} to allow validation of the result's recipient. + * + * @return the uid of the launching app or {@link Process#INVALID_UID} if the current + * activity cannot access the identity of the launching app + * + * @see ActivityOptions#setShareIdentityEnabled(boolean) + * @see #getLaunchedFromPackage() */ public int getLaunchedFromUid() { return ActivityClient.getInstance().getLaunchedFromUid(getActivityToken()); } /** - * Returns the package who started this activity. - * @hide + * Returns the package name of the app that initially launched this activity. + * + * <p>In order to receive the launching app's package name, at least one of the following has + * to be met: + * <ul> + * <li>The app must call {@link ActivityOptions#setShareIdentityEnabled(boolean)} with a + * value of {@code true} and launch this activity with the resulting + * {@code ActivityOptions}. + * <li>The launched activity has the same uid as the launching app. + * <li>The launched activity is running in a package that is signed with the same key + * used to sign the platform (typically only system packages such as Settings will + * meet this requirement). + * </ul>. + * These are the same requirements for {@link #getLaunchedFromUid()}; if any of these are + * met, then these methods can be used to obtain the uid and package name of the launching + * app. If none are met, then {@code null} is returned. + * + * <p>Note, even if the above conditions are not met, the launching app's identity may + * still be available from {@link #getCallingPackage()} if this activity was started with + * {@code Activity#startActivityForResult} to allow validation of the result's recipient. + * + * @return the package name of the launching app or null if the current activity + * cannot access the identity of the launching app + * + * @see ActivityOptions#setShareIdentityEnabled(boolean) + * @see #getLaunchedFromUid() */ @Nullable public String getLaunchedFromPackage() { diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java index 52ef7fbb3eca..1b923122d889 100644 --- a/core/java/android/app/ActivityOptions.java +++ b/core/java/android/app/ActivityOptions.java @@ -202,6 +202,12 @@ public class ActivityOptions extends ComponentOptions { private static final String KEY_LOCK_TASK_MODE = "android:activity.lockTaskMode"; /** + * Whether the launching app's identity should be available to the launched activity. + * @see #setShareIdentityEnabled(boolean) + */ + private static final String KEY_SHARE_IDENTITY = "android:activity.shareIdentity"; + + /** * The display id the activity should be launched into. * @see #setLaunchDisplayId(int) * @hide @@ -457,6 +463,7 @@ public class ActivityOptions extends ComponentOptions { private int mLaunchTaskId = -1; private int mPendingIntentLaunchFlags; private boolean mLockTaskMode = false; + private boolean mShareIdentity = false; private boolean mDisallowEnterPictureInPictureWhileLaunching; private boolean mApplyActivityFlagsForBubbles; private boolean mTaskAlwaysOnTop; @@ -1238,6 +1245,7 @@ public class ActivityOptions extends ComponentOptions { break; } mLockTaskMode = opts.getBoolean(KEY_LOCK_TASK_MODE, false); + mShareIdentity = opts.getBoolean(KEY_SHARE_IDENTITY, false); mLaunchDisplayId = opts.getInt(KEY_LAUNCH_DISPLAY_ID, INVALID_DISPLAY); mCallerDisplayId = opts.getInt(KEY_CALLER_DISPLAY_ID, INVALID_DISPLAY); mLaunchTaskDisplayArea = opts.getParcelable(KEY_LAUNCH_TASK_DISPLAY_AREA_TOKEN, android.window.WindowContainerToken.class); @@ -1488,6 +1496,20 @@ public class ActivityOptions extends ComponentOptions { } /** + * Returns whether the launching app has opted-in to sharing its identity with the launched + * activity. + * + * @see #setShareIdentityEnabled(boolean) + * @see Activity#getLaunchedFromUid() + * @see Activity#getLaunchedFromPackage() + * + * @hide + */ + public boolean getShareIdentity() { + return mShareIdentity; + } + + /** * Gets whether the activity want to be launched as other theme for the splash screen. * @hide */ @@ -1560,6 +1582,33 @@ public class ActivityOptions extends ComponentOptions { } /** + * Sets whether the identity of the launching app should be shared with the activity. + * + * <p>Use this option when starting an activity that needs to know the identity of the + * launching app; with this set to {@code true}, the activity will have access to the launching + * app's package name and uid. + * + * <p>Defaults to {@code false} if not set. + * + * <p>Note, even if the launching app does not explicitly enable sharing of its identity, if + * the activity is started with {@code Activity#startActivityForResult}, then {@link + * Activity#getCallingPackage()} will still return the launching app's package name to + * allow validation of the result's recipient. Also, an activity running within a package + * signed by the same key used to sign the platform (some system apps such as Settings will + * be signed with the platform's key) will have access to the launching app's identity. + * + * @param shareIdentity whether the launching app's identity should be shared with the activity + * @return {@code this} {@link ActivityOptions} instance. + * @see Activity#getLaunchedFromPackage() + * @see Activity#getLaunchedFromUid() + */ + @NonNull + public ActivityOptions setShareIdentityEnabled(boolean shareIdentity) { + mShareIdentity = shareIdentity; + return this; + } + + /** * Gets the id of the display where activity should be launched. * @return The id of the display where activity should be launched, * {@link android.view.Display#INVALID_DISPLAY} if not set. @@ -2039,6 +2088,7 @@ public class ActivityOptions extends ComponentOptions { break; } mLockTaskMode = otherOptions.mLockTaskMode; + mShareIdentity = otherOptions.mShareIdentity; mAnimSpecs = otherOptions.mAnimSpecs; mAnimationFinishedListener = otherOptions.mAnimationFinishedListener; mSpecsFuture = otherOptions.mSpecsFuture; @@ -2123,6 +2173,9 @@ public class ActivityOptions extends ComponentOptions { if (mLockTaskMode) { b.putBoolean(KEY_LOCK_TASK_MODE, mLockTaskMode); } + if (mShareIdentity) { + b.putBoolean(KEY_SHARE_IDENTITY, mShareIdentity); + } if (mLaunchDisplayId != INVALID_DISPLAY) { b.putInt(KEY_LAUNCH_DISPLAY_ID, mLaunchDisplayId); } diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java index 569f4dd8328c..4d3f9e457ae3 100644 --- a/core/java/android/app/ApplicationPackageManager.java +++ b/core/java/android/app/ApplicationPackageManager.java @@ -3816,8 +3816,17 @@ public class ApplicationPackageManager extends PackageManager { @NonNull String targetPackageName) throws NameNotFoundException { Objects.requireNonNull(sourcePackageName); Objects.requireNonNull(targetPackageName); + return canPackageQuery(sourcePackageName, new String[]{targetPackageName})[0]; + } + + @Override + @NonNull + public boolean[] canPackageQuery(@NonNull String sourcePackageName, + @NonNull String[] targetPackageNames) throws NameNotFoundException { + Objects.requireNonNull(sourcePackageName); + Objects.requireNonNull(targetPackageNames); try { - return mPM.canPackageQuery(sourcePackageName, targetPackageName, getUserId()); + return mPM.canPackageQuery(sourcePackageName, targetPackageNames, getUserId()); } catch (ParcelableException e) { e.maybeRethrow(PackageManager.NameNotFoundException.class); throw new RuntimeException(e); diff --git a/core/java/android/app/IActivityTaskManager.aidl b/core/java/android/app/IActivityTaskManager.aidl index f20503cef705..91add2783c0d 100644 --- a/core/java/android/app/IActivityTaskManager.aidl +++ b/core/java/android/app/IActivityTaskManager.aidl @@ -249,6 +249,11 @@ interface IActivityTaskManager { /** Returns an interface enabling the management of window organizers. */ IWindowOrganizerController getWindowOrganizerController(); + /** + * Sets whether we are currently in an interactive split screen resize operation where we + * are changing the docked stack size. + */ + void setSplitScreenResizing(boolean resizing); boolean supportsLocalVoiceInteraction(); // Get device configuration diff --git a/core/java/android/app/StatusBarManager.java b/core/java/android/app/StatusBarManager.java index f63f406f847d..6c430105e2f1 100644 --- a/core/java/android/app/StatusBarManager.java +++ b/core/java/android/app/StatusBarManager.java @@ -511,10 +511,26 @@ public class StatusBarManager { @SystemApi public static final int MEDIA_TRANSFER_RECEIVER_STATE_FAR_FROM_SENDER = 1; + /** + * State indicating that media transfer to this receiver device is succeeded. + * + * @hide + */ + public static final int MEDIA_TRANSFER_RECEIVER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED = 2; + + /** + * State indicating that media transfer to this receiver device is failed. + * + * @hide + */ + public static final int MEDIA_TRANSFER_RECEIVER_STATE_TRANSFER_TO_RECEIVER_FAILED = 3; + /** @hide */ @IntDef(prefix = {"MEDIA_TRANSFER_RECEIVER_STATE_"}, value = { MEDIA_TRANSFER_RECEIVER_STATE_CLOSE_TO_SENDER, MEDIA_TRANSFER_RECEIVER_STATE_FAR_FROM_SENDER, + MEDIA_TRANSFER_RECEIVER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED, + MEDIA_TRANSFER_RECEIVER_STATE_TRANSFER_TO_RECEIVER_FAILED, }) @Retention(RetentionPolicy.SOURCE) public @interface MediaTransferReceiverState {} diff --git a/core/java/android/companion/virtual/IVirtualDevice.aidl b/core/java/android/companion/virtual/IVirtualDevice.aidl index 5c47ea2aa3f0..f17d18652e98 100644 --- a/core/java/android/companion/virtual/IVirtualDevice.aidl +++ b/core/java/android/companion/virtual/IVirtualDevice.aidl @@ -33,6 +33,7 @@ import android.hardware.input.VirtualMouseRelativeEvent; import android.hardware.input.VirtualMouseScrollEvent; import android.hardware.input.VirtualTouchEvent; import android.hardware.input.VirtualTouchscreenConfig; +import android.hardware.input.VirtualNavigationTouchpadConfig; import android.os.ResultReceiver; /** @@ -84,6 +85,10 @@ interface IVirtualDevice { void createVirtualTouchscreen( in VirtualTouchscreenConfig config, IBinder token); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)") + void createVirtualNavigationTouchpad( + in VirtualNavigationTouchpadConfig config, + IBinder token); void unregisterInputDevice(IBinder token); int getInputDeviceId(IBinder token); boolean sendDpadKeyEvent(IBinder token, in VirtualKeyEvent event); diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java index 57bdf2d5212c..dba7c8e630c8 100644 --- a/core/java/android/companion/virtual/VirtualDeviceManager.java +++ b/core/java/android/companion/virtual/VirtualDeviceManager.java @@ -49,6 +49,8 @@ import android.hardware.input.VirtualKeyboard; import android.hardware.input.VirtualKeyboardConfig; import android.hardware.input.VirtualMouse; import android.hardware.input.VirtualMouseConfig; +import android.hardware.input.VirtualNavigationTouchpad; +import android.hardware.input.VirtualNavigationTouchpadConfig; import android.hardware.input.VirtualTouchscreen; import android.hardware.input.VirtualTouchscreenConfig; import android.os.Binder; @@ -660,6 +662,30 @@ public final class VirtualDeviceManager { } /** + * Creates a virtual touchpad in navigation mode. + * + * A touchpad in navigation mode means that its events are interpreted as navigation events + * (up, down, etc) instead of using them to update a cursor's absolute position. If the + * events are not consumed they are converted to DPAD events. + * + * @param config the configurations of the virtual navigation touchpad. + */ + @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) + @NonNull + public VirtualNavigationTouchpad createVirtualNavigationTouchpad( + @NonNull VirtualNavigationTouchpadConfig config) { + try { + final IBinder token = new Binder( + "android.hardware.input.VirtualNavigationTouchpad:" + + config.getInputDeviceName()); + mVirtualDevice.createVirtualNavigationTouchpad(config, token); + return new VirtualNavigationTouchpad(mVirtualDevice, token); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * Creates a virtual touchscreen. * * @param display the display that the events inputted through this device should @@ -776,11 +802,11 @@ public final class VirtualDeviceManager { private String getVirtualDisplayName() { try { - // Currently this just use the association ID, which means all of the virtual - // displays created using the same virtual device will have the same name. The name - // should only be used for informational purposes, and not for identifying the - // display in code. - return "VirtualDevice_" + mVirtualDevice.getAssociationId(); + // Currently this just use the device ID, which means all of the virtual displays + // created using the same virtual device will have the same name. The name should + // only be used for informational purposes, and not for identifying the display in + // code. + return "VirtualDevice_" + mVirtualDevice.getDeviceId(); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl index 35afe9f727db..81bea2e3573f 100644 --- a/core/java/android/content/pm/IPackageManager.aidl +++ b/core/java/android/content/pm/IPackageManager.aidl @@ -797,5 +797,5 @@ interface IPackageManager { void setKeepUninstalledPackages(in List<String> packageList); - boolean canPackageQuery(String sourcePackageName, String targetPackageName, int userId); + boolean[] canPackageQuery(String sourcePackageName, in String[] targetPackageNames, int userId); } diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index ec490d10d45e..8ae53100d4d5 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -10388,6 +10388,30 @@ public abstract class PackageManager { } /** + * Same as {@link #canPackageQuery(String, String)} but accepts an array of target packages to + * be queried. + * + * @param sourcePackageName The source package that would receive details about the + * target package. + * @param targetPackageNames An array of target packages whose details would be shared with the + * source package. + * @return An array of booleans where each member specifies whether the source package is able + * to query for details about the target package given by the corresponding value at the same + * index in the array of target packages. + * @throws NameNotFoundException if either a given package can not be found on the + * system, or if the caller is not able to query for details about the source or + * target packages. + * @hide + */ + @SystemApi + @NonNull + public boolean[] canPackageQuery(@NonNull String sourcePackageName, + @NonNull String[] targetPackageNames) throws NameNotFoundException { + throw new UnsupportedOperationException( + "canPackageQuery not implemented in subclass"); + } + + /** * Makes a package that provides an authority {@code visibleAuthority} become visible to the * application {@code recipientUid}. * diff --git a/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java index 829908fc11d6..7409187cfb49 100644 --- a/core/java/android/hardware/display/DisplayManagerInternal.java +++ b/core/java/android/hardware/display/DisplayManagerInternal.java @@ -439,9 +439,6 @@ public abstract class DisplayManagerInternal { // 1 (brighter). Set to Float.NaN if there's no override. public float screenAutoBrightnessAdjustmentOverride; - // If true, enables automatic brightness control. - public boolean useAutoBrightness; - // If true, scales the brightness to a fraction of desired (as defined by // screenLowPowerBrightnessFactor). public boolean lowPowerMode; @@ -471,7 +468,6 @@ public abstract class DisplayManagerInternal { policy = POLICY_BRIGHT; useProximitySensor = false; screenBrightnessOverride = PowerManager.BRIGHTNESS_INVALID_FLOAT; - useAutoBrightness = false; screenAutoBrightnessAdjustmentOverride = Float.NaN; screenLowPowerBrightnessFactor = 0.5f; blockScreenOn = false; @@ -491,7 +487,6 @@ public abstract class DisplayManagerInternal { policy = other.policy; useProximitySensor = other.useProximitySensor; screenBrightnessOverride = other.screenBrightnessOverride; - useAutoBrightness = other.useAutoBrightness; screenAutoBrightnessAdjustmentOverride = other.screenAutoBrightnessAdjustmentOverride; screenLowPowerBrightnessFactor = other.screenLowPowerBrightnessFactor; blockScreenOn = other.blockScreenOn; @@ -513,7 +508,6 @@ public abstract class DisplayManagerInternal { && useProximitySensor == other.useProximitySensor && floatEquals(screenBrightnessOverride, other.screenBrightnessOverride) - && useAutoBrightness == other.useAutoBrightness && floatEquals(screenAutoBrightnessAdjustmentOverride, other.screenAutoBrightnessAdjustmentOverride) && screenLowPowerBrightnessFactor @@ -539,7 +533,6 @@ public abstract class DisplayManagerInternal { return "policy=" + policyToString(policy) + ", useProximitySensor=" + useProximitySensor + ", screenBrightnessOverride=" + screenBrightnessOverride - + ", useAutoBrightness=" + useAutoBrightness + ", screenAutoBrightnessAdjustmentOverride=" + screenAutoBrightnessAdjustmentOverride + ", screenLowPowerBrightnessFactor=" + screenLowPowerBrightnessFactor diff --git a/core/java/android/hardware/input/VirtualNavigationTouchpad.java b/core/java/android/hardware/input/VirtualNavigationTouchpad.java new file mode 100644 index 000000000000..2854034cd127 --- /dev/null +++ b/core/java/android/hardware/input/VirtualNavigationTouchpad.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.hardware.input; + +import android.annotation.NonNull; +import android.annotation.RequiresPermission; +import android.annotation.SystemApi; +import android.companion.virtual.IVirtualDevice; +import android.os.IBinder; +import android.os.RemoteException; + +/** + * A virtual navigation touchpad representing a touch-based input mechanism on a remote device. + * + * <p>This registers an InputDevice that is interpreted like a physically-connected device and + * dispatches received events to it. + * + * <p>The virtual touchpad will be in navigation mode. Motion results in focus traversal in the same + * manner as D-Pad navigation if the events are not consumed. + * + * @see android.view.InputDevice#SOURCE_TOUCH_NAVIGATION + * + * @hide + */ +@SystemApi +public class VirtualNavigationTouchpad extends VirtualInputDevice { + + /** @hide */ + public VirtualNavigationTouchpad(IVirtualDevice virtualDevice, IBinder token) { + super(virtualDevice, token); + } + + /** + * Sends a touch event to the system. + * + * @param event the event to send + */ + @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) + public void sendTouchEvent(@NonNull VirtualTouchEvent event) { + try { + mVirtualDevice.sendTouchEvent(mToken, event); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } +} diff --git a/core/java/android/hardware/input/VirtualNavigationTouchpadConfig.aidl b/core/java/android/hardware/input/VirtualNavigationTouchpadConfig.aidl new file mode 100644 index 000000000000..d9124910adff --- /dev/null +++ b/core/java/android/hardware/input/VirtualNavigationTouchpadConfig.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.hardware.input; + +parcelable VirtualNavigationTouchpadConfig; diff --git a/core/java/android/hardware/input/VirtualNavigationTouchpadConfig.java b/core/java/android/hardware/input/VirtualNavigationTouchpadConfig.java new file mode 100644 index 000000000000..f2805bb1029e --- /dev/null +++ b/core/java/android/hardware/input/VirtualNavigationTouchpadConfig.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.hardware.input; + +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Configurations to create virtual navigation touchpad. + * + * @hide + */ +@SystemApi +public final class VirtualNavigationTouchpadConfig extends VirtualInputDeviceConfig + implements Parcelable { + + /** The touchpad height. */ + private final int mHeight; + /** The touchpad width. */ + private final int mWidth; + + private VirtualNavigationTouchpadConfig(@NonNull Builder builder) { + super(builder); + mHeight = builder.mHeight; + mWidth = builder.mWidth; + } + + private VirtualNavigationTouchpadConfig(@NonNull Parcel in) { + super(in); + mHeight = in.readInt(); + mWidth = in.readInt(); + } + + /** Returns the touchpad height. */ + public int getHeight() { + return mHeight; + } + + /** Returns the touchpad width. */ + public int getWidth() { + return mWidth; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(mHeight); + dest.writeInt(mWidth); + } + + @NonNull + public static final Creator<VirtualNavigationTouchpadConfig> CREATOR = + new Creator<VirtualNavigationTouchpadConfig>() { + @Override + public VirtualNavigationTouchpadConfig createFromParcel(Parcel in) { + return new VirtualNavigationTouchpadConfig(in); + } + + @Override + public VirtualNavigationTouchpadConfig[] newArray(int size) { + return new VirtualNavigationTouchpadConfig[size]; + } + }; + + /** + * Builder for creating a {@link VirtualNavigationTouchpadConfig}. + */ + public static final class Builder extends VirtualInputDeviceConfig.Builder<Builder> { + + private final int mHeight; + private final int mWidth; + + public Builder(@IntRange(from = 1) int touchpadHeight, + @IntRange(from = 1) int touchpadWidth) { + if (touchpadHeight <= 0 || touchpadWidth <= 0) { + throw new IllegalArgumentException( + "Cannot create a virtual navigation touchpad, touchpad dimensions must be " + + "positive. Got: (" + touchpadHeight + ", " + + touchpadWidth + ")"); + } + mHeight = touchpadHeight; + mWidth = touchpadWidth; + } + + /** + * Builds the {@link VirtualNavigationTouchpadConfig} instance. + */ + @NonNull + public VirtualNavigationTouchpadConfig build() { + return new VirtualNavigationTouchpadConfig(this); + } + } +} diff --git a/core/java/android/net/vcn/VcnGatewayConnectionConfig.java b/core/java/android/net/vcn/VcnGatewayConnectionConfig.java index 2339656979b5..b8850f427cfc 100644 --- a/core/java/android/net/vcn/VcnGatewayConnectionConfig.java +++ b/core/java/android/net/vcn/VcnGatewayConnectionConfig.java @@ -42,6 +42,7 @@ import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; @@ -130,6 +131,30 @@ public final class VcnGatewayConnectionConfig { }) public @interface VcnSupportedCapability {} + /** + * Perform mobility update to attempt recovery from suspected data stalls. + * + * <p>If set, the gatway connection will monitor the data stall detection of the VCN network. + * When there is a suspected data stall, the gateway connection will attempt recovery by + * performing a mobility update on the underlying IKE session. + */ + public static final int VCN_GATEWAY_OPTION_ENABLE_DATA_STALL_RECOVERY_WITH_MOBILITY = 0; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef( + prefix = {"VCN_GATEWAY_OPTION_"}, + value = { + VCN_GATEWAY_OPTION_ENABLE_DATA_STALL_RECOVERY_WITH_MOBILITY, + }) + public @interface VcnGatewayOption {} + + private static final Set<Integer> ALLOWED_GATEWAY_OPTIONS = new ArraySet<>(); + + static { + ALLOWED_GATEWAY_OPTIONS.add(VCN_GATEWAY_OPTION_ENABLE_DATA_STALL_RECOVERY_WITH_MOBILITY); + } + private static final int DEFAULT_MAX_MTU = 1500; /** @@ -201,6 +226,9 @@ public final class VcnGatewayConnectionConfig { private static final String RETRY_INTERVAL_MS_KEY = "mRetryIntervalsMs"; @NonNull private final long[] mRetryIntervalsMs; + private static final String GATEWAY_OPTIONS_KEY = "mGatewayOptions"; + @NonNull private final Set<Integer> mGatewayOptions; + /** Builds a VcnGatewayConnectionConfig with the specified parameters. */ private VcnGatewayConnectionConfig( @NonNull String gatewayConnectionName, @@ -208,12 +236,14 @@ public final class VcnGatewayConnectionConfig { @NonNull Set<Integer> exposedCapabilities, @NonNull List<VcnUnderlyingNetworkTemplate> underlyingNetworkTemplates, @NonNull long[] retryIntervalsMs, - @IntRange(from = MIN_MTU_V6) int maxMtu) { + @IntRange(from = MIN_MTU_V6) int maxMtu, + @NonNull Set<Integer> gatewayOptions) { mGatewayConnectionName = gatewayConnectionName; mTunnelConnectionParams = tunnelConnectionParams; mExposedCapabilities = new TreeSet(exposedCapabilities); mRetryIntervalsMs = retryIntervalsMs; mMaxMtu = maxMtu; + mGatewayOptions = Collections.unmodifiableSet(new HashSet(gatewayOptions)); mUnderlyingNetworkTemplates = new ArrayList<>(underlyingNetworkTemplates); if (mUnderlyingNetworkTemplates.isEmpty()) { @@ -256,6 +286,20 @@ public final class VcnGatewayConnectionConfig { VcnUnderlyingNetworkTemplate::fromPersistableBundle); } + final PersistableBundle gatewayOptionsBundle = in.getPersistableBundle(GATEWAY_OPTIONS_KEY); + + if (gatewayOptionsBundle == null) { + // GATEWAY_OPTIONS_KEY was added in Android U. Thus VcnGatewayConnectionConfig created + // on old platforms will not have this data and will be assigned with the default value + mGatewayOptions = Collections.emptySet(); + } else { + mGatewayOptions = + new HashSet<>( + PersistableBundleUtils.toList( + gatewayOptionsBundle, + PersistableBundleUtils.INTEGER_DESERIALIZER)); + } + mRetryIntervalsMs = in.getLongArray(RETRY_INTERVAL_MS_KEY); mMaxMtu = in.getInt(MAX_MTU_KEY); @@ -279,6 +323,10 @@ public final class VcnGatewayConnectionConfig { Preconditions.checkArgument( mMaxMtu >= MIN_MTU_V6, "maxMtu must be at least IPv6 min MTU (1280)"); + + for (int option : mGatewayOptions) { + validateGatewayOption(option); + } } private static void checkValidCapability(int capability) { @@ -315,6 +363,12 @@ public final class VcnGatewayConnectionConfig { } } + private static void validateGatewayOption(int option) { + if (!ALLOWED_GATEWAY_OPTIONS.contains(option)) { + throw new IllegalArgumentException("Invalid vcn gateway option: " + option); + } + } + /** * Returns the configured Gateway Connection name. * @@ -399,6 +453,19 @@ public final class VcnGatewayConnectionConfig { } /** + * Checks if the given VCN gateway option is enabled. + * + * @param option the option to check. + * @throws IllegalArgumentException if the provided option is invalid. + * @see Builder#addGatewayOption(int) + * @see Builder#removeGatewayOption(int) + */ + public boolean hasGatewayOption(@VcnGatewayOption int option) { + validateGatewayOption(option); + return mGatewayOptions.contains(option); + } + + /** * Converts this config to a PersistableBundle. * * @hide @@ -418,11 +485,16 @@ public final class VcnGatewayConnectionConfig { PersistableBundleUtils.fromList( mUnderlyingNetworkTemplates, VcnUnderlyingNetworkTemplate::toPersistableBundle); + final PersistableBundle gatewayOptionsBundle = + PersistableBundleUtils.fromList( + new ArrayList<>(mGatewayOptions), + PersistableBundleUtils.INTEGER_SERIALIZER); result.putString(GATEWAY_CONNECTION_NAME_KEY, mGatewayConnectionName); result.putPersistableBundle(TUNNEL_CONNECTION_PARAMS_KEY, tunnelConnectionParamsBundle); result.putPersistableBundle(EXPOSED_CAPABILITIES_KEY, exposedCapsBundle); result.putPersistableBundle(UNDERLYING_NETWORK_TEMPLATES_KEY, networkTemplatesBundle); + result.putPersistableBundle(GATEWAY_OPTIONS_KEY, gatewayOptionsBundle); result.putLongArray(RETRY_INTERVAL_MS_KEY, mRetryIntervalsMs); result.putInt(MAX_MTU_KEY, mMaxMtu); @@ -437,7 +509,8 @@ public final class VcnGatewayConnectionConfig { mExposedCapabilities, mUnderlyingNetworkTemplates, Arrays.hashCode(mRetryIntervalsMs), - mMaxMtu); + mMaxMtu, + mGatewayOptions); } @Override @@ -452,7 +525,8 @@ public final class VcnGatewayConnectionConfig { && mExposedCapabilities.equals(rhs.mExposedCapabilities) && mUnderlyingNetworkTemplates.equals(rhs.mUnderlyingNetworkTemplates) && Arrays.equals(mRetryIntervalsMs, rhs.mRetryIntervalsMs) - && mMaxMtu == rhs.mMaxMtu; + && mMaxMtu == rhs.mMaxMtu + && mGatewayOptions.equals(rhs.mGatewayOptions); } /** @@ -470,6 +544,8 @@ public final class VcnGatewayConnectionConfig { @NonNull private long[] mRetryIntervalsMs = DEFAULT_RETRY_INTERVALS_MS; private int mMaxMtu = DEFAULT_MAX_MTU; + @NonNull private final Set<Integer> mGatewayOptions = new ArraySet<>(); + // TODO: (b/175829816) Consider VCN-exposed capabilities that may be transport dependent. // Consider the case where the VCN might only expose MMS on WiFi, but defer to MMS // when on Cell. @@ -628,6 +704,34 @@ public final class VcnGatewayConnectionConfig { } /** + * Enables the specified VCN gateway option. + * + * @param option the option to be enabled + * @return this {@link Builder} instance, for chaining + * @throws IllegalArgumentException if the provided option is invalid + */ + @NonNull + public Builder addGatewayOption(@VcnGatewayOption int option) { + validateGatewayOption(option); + mGatewayOptions.add(option); + return this; + } + + /** + * Resets (disables) the specified VCN gateway option. + * + * @param option the option to be disabled + * @return this {@link Builder} instance, for chaining + * @throws IllegalArgumentException if the provided option is invalid + */ + @NonNull + public Builder removeGatewayOption(@VcnGatewayOption int option) { + validateGatewayOption(option); + mGatewayOptions.remove(option); + return this; + } + + /** * Builds and validates the VcnGatewayConnectionConfig. * * @return an immutable VcnGatewayConnectionConfig instance @@ -640,7 +744,8 @@ public final class VcnGatewayConnectionConfig { mExposedCapabilities, mUnderlyingNetworkTemplates, mRetryIntervalsMs, - mMaxMtu); + mMaxMtu, + mGatewayOptions); } } } diff --git a/core/java/android/os/Looper.java b/core/java/android/os/Looper.java index a529ac6569bd..712d328e9dc9 100644 --- a/core/java/android/os/Looper.java +++ b/core/java/android/os/Looper.java @@ -177,12 +177,15 @@ public final class Looper { final long traceTag = me.mTraceTag; long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs; long slowDeliveryThresholdMs = me.mSlowDeliveryThresholdMs; - if (thresholdOverride > 0) { + + final boolean hasOverride = thresholdOverride >= 0; + if (hasOverride) { slowDispatchThresholdMs = thresholdOverride; slowDeliveryThresholdMs = thresholdOverride; } - final boolean logSlowDelivery = (slowDeliveryThresholdMs > 0) && (msg.when > 0); - final boolean logSlowDispatch = (slowDispatchThresholdMs > 0); + final boolean logSlowDelivery = (slowDeliveryThresholdMs > 0 || hasOverride) + && (msg.when > 0); + final boolean logSlowDispatch = (slowDispatchThresholdMs > 0 || hasOverride); final boolean needStartTime = logSlowDelivery || logSlowDispatch; final boolean needEndTime = logSlowDispatch; @@ -283,7 +286,7 @@ public final class Looper { SystemProperties.getInt("log.looper." + Process.myUid() + "." + Thread.currentThread().getName() - + ".slow", 0); + + ".slow", -1); me.mSlowDeliveryDetected = false; diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 90aca14cf860..25f15d8171de 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -10723,6 +10723,49 @@ public final class Settings { "back_gesture_inset_scale_right"; /** + * Indicates whether the trackpad back gesture is enabled. + * <p>Type: int (0 for false, 1 for true) + * + * @hide + */ + public static final String TRACKPAD_GESTURE_BACK_ENABLED = "trackpad_gesture_back_enabled"; + + /** + * Indicates whether the trackpad home gesture is enabled. + * <p>Type: int (0 for false, 1 for true) + * + * @hide + */ + public static final String TRACKPAD_GESTURE_HOME_ENABLED = "trackpad_gesture_home_enabled"; + + /** + * Indicates whether the trackpad overview gesture is enabled. + * <p>Type: int (0 for false, 1 for true) + * + * @hide + */ + public static final String TRACKPAD_GESTURE_OVERVIEW_ENABLED = + "trackpad_gesture_overview_enabled"; + + /** + * Indicates whether the trackpad notification gesture is enabled. + * <p>Type: int (0 for false, 1 for true) + * + * @hide + */ + public static final String TRACKPAD_GESTURE_NOTIFICATION_ENABLED = + "trackpad_gesture_notification_enabled"; + + /** + * Indicates whether the trackpad quick switch gesture is enabled. + * <p>Type: int (0 for false, 1 for true) + * + * @hide + */ + public static final String TRACKPAD_GESTURE_QUICK_SWITCH_ENABLED = + "trackpad_gesture_quick_switch_enabled"; + + /** * Current provider of proximity-based sharing services. * Default value in @string/config_defaultNearbySharingComponent. * No VALIDATOR as this setting will not be backed up. diff --git a/core/java/android/service/voice/AbstractHotwordDetector.java b/core/java/android/service/voice/AbstractHotwordDetector.java index 28357efeafd0..c90ab6777515 100644 --- a/core/java/android/service/voice/AbstractHotwordDetector.java +++ b/core/java/android/service/voice/AbstractHotwordDetector.java @@ -25,7 +25,9 @@ import android.app.ActivityThread; import android.app.compat.CompatChanges; import android.media.AudioFormat; import android.media.permission.Identity; +import android.os.Binder; import android.os.Handler; +import android.os.IBinder; import android.os.Looper; import android.os.ParcelFileDescriptor; import android.os.PersistableBundle; @@ -49,19 +51,20 @@ abstract class AbstractHotwordDetector implements HotwordDetector { private final IVoiceInteractionManagerService mManagerService; private final Handler mHandler; private final HotwordDetector.Callback mCallback; - private final int mDetectorType; private Consumer<AbstractHotwordDetector> mOnDestroyListener; private final AtomicBoolean mIsDetectorActive; + /** + * A token which is used by voice interaction system service to identify different detectors. + */ + private final IBinder mToken = new Binder(); AbstractHotwordDetector( IVoiceInteractionManagerService managerService, - HotwordDetector.Callback callback, - int detectorType) { + HotwordDetector.Callback callback) { mManagerService = managerService; // TODO: this needs to be supplied from above mHandler = new Handler(Looper.getMainLooper()); mCallback = callback; - mDetectorType = detectorType; mIsDetectorActive = new AtomicBoolean(true); } @@ -94,6 +97,7 @@ abstract class AbstractHotwordDetector implements HotwordDetector { audioStream, audioFormat, options, + mToken, new BinderCallback(mHandler, mCallback)); } catch (RemoteException e) { e.rethrowFromSystemServer(); @@ -111,7 +115,7 @@ abstract class AbstractHotwordDetector implements HotwordDetector { } throwIfDetectorIsNoLongerActive(); try { - mManagerService.updateState(options, sharedMemory); + mManagerService.updateState(options, sharedMemory, mToken); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -128,7 +132,7 @@ abstract class AbstractHotwordDetector implements HotwordDetector { Identity identity = new Identity(); identity.packageName = ActivityThread.currentOpPackageName(); try { - mManagerService.initAndVerifyDetector(identity, options, sharedMemory, callback, + mManagerService.initAndVerifyDetector(identity, options, sharedMemory, mToken, callback, detectorType); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); @@ -151,6 +155,11 @@ abstract class AbstractHotwordDetector implements HotwordDetector { return; } mIsDetectorActive.set(false); + try { + mManagerService.destroyDetector(mToken); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } synchronized (mLock) { mOnDestroyListener.accept(this); } diff --git a/core/java/android/service/voice/AlwaysOnHotwordDetector.java b/core/java/android/service/voice/AlwaysOnHotwordDetector.java index 320280b9e68b..9008bf7d48ec 100644 --- a/core/java/android/service/voice/AlwaysOnHotwordDetector.java +++ b/core/java/android/service/voice/AlwaysOnHotwordDetector.java @@ -293,7 +293,7 @@ public class AlwaysOnHotwordDetector extends AbstractHotwordDetector { private final Handler mHandler; private final IBinder mBinder = new Binder(); private final int mTargetSdkVersion; - private final boolean mSupportHotwordDetectionService; + private final boolean mSupportSandboxedDetectionService; @GuardedBy("mLock") private boolean mIsAvailabilityOverriddenByTestApi = false; @@ -756,7 +756,7 @@ public class AlwaysOnHotwordDetector extends AbstractHotwordDetector { * @param callback A non-null Callback for receiving the recognition events. * @param modelManagementService A service that allows management of sound models. * @param targetSdkVersion The target SDK version. - * @param supportHotwordDetectionService {@code true} if HotwordDetectionService should be + * @param SupportSandboxedDetectionService {@code true} if HotwordDetectionService should be * triggered, otherwise {@code false}. * * @hide @@ -764,10 +764,8 @@ public class AlwaysOnHotwordDetector extends AbstractHotwordDetector { public AlwaysOnHotwordDetector(String text, Locale locale, Callback callback, KeyphraseEnrollmentInfo keyphraseEnrollmentInfo, IVoiceInteractionManagerService modelManagementService, int targetSdkVersion, - boolean supportHotwordDetectionService) { - super(modelManagementService, callback, - supportHotwordDetectionService ? DETECTOR_TYPE_TRUSTED_HOTWORD_DSP - : DETECTOR_TYPE_NORMAL); + boolean supportSandboxedDetectionService) { + super(modelManagementService, callback); mHandler = new MyHandler(); mText = text; @@ -777,12 +775,12 @@ public class AlwaysOnHotwordDetector extends AbstractHotwordDetector { mInternalCallback = new SoundTriggerListener(mHandler); mModelManagementService = modelManagementService; mTargetSdkVersion = targetSdkVersion; - mSupportHotwordDetectionService = supportHotwordDetectionService; + mSupportSandboxedDetectionService = supportSandboxedDetectionService; } @Override void initialize(@Nullable PersistableBundle options, @Nullable SharedMemory sharedMemory) { - if (mSupportHotwordDetectionService) { + if (mSupportSandboxedDetectionService) { initAndVerifyDetector(options, sharedMemory, mInternalCallback, DETECTOR_TYPE_TRUSTED_HOTWORD_DSP); } @@ -814,7 +812,7 @@ public class AlwaysOnHotwordDetector extends AbstractHotwordDetector { public final void updateState(@Nullable PersistableBundle options, @Nullable SharedMemory sharedMemory) throws IllegalDetectorStateException { synchronized (mLock) { - if (!mSupportHotwordDetectionService) { + if (!mSupportSandboxedDetectionService) { throw new IllegalStateException( "updateState called, but it doesn't support hotword detection service"); } @@ -1410,8 +1408,8 @@ public class AlwaysOnHotwordDetector extends AbstractHotwordDetector { * @hide */ @Override - public boolean isUsingHotwordDetectionService() { - return mSupportHotwordDetectionService; + public boolean isUsingSandboxedDetectionService() { + return mSupportSandboxedDetectionService; } /** diff --git a/core/java/android/service/voice/HotwordDetectionService.java b/core/java/android/service/voice/HotwordDetectionService.java index 552a793b6350..a47c09662a51 100644 --- a/core/java/android/service/voice/HotwordDetectionService.java +++ b/core/java/android/service/voice/HotwordDetectionService.java @@ -140,7 +140,7 @@ public abstract class HotwordDetectionService extends Service { @Nullable private IRecognitionServiceManager mIRecognitionServiceManager; - private final IHotwordDetectionService mInterface = new IHotwordDetectionService.Stub() { + private final ISandboxedDetectionService mInterface = new ISandboxedDetectionService.Stub() { @Override public void detectFromDspSource( SoundTrigger.KeyphraseRecognitionEvent event, diff --git a/core/java/android/service/voice/HotwordDetector.java b/core/java/android/service/voice/HotwordDetector.java index 7112dc666509..b7f7d54fc055 100644 --- a/core/java/android/service/voice/HotwordDetector.java +++ b/core/java/android/service/voice/HotwordDetector.java @@ -178,7 +178,7 @@ public interface HotwordDetector { /** * @hide */ - default boolean isUsingHotwordDetectionService() { + default boolean isUsingSandboxedDetectionService() { throw new UnsupportedOperationException("Not implemented. Must override in a subclass."); } diff --git a/core/java/android/service/voice/IHotwordDetectionService.aidl b/core/java/android/service/voice/ISandboxedDetectionService.aidl index 9ef930707f07..5537fd1df26e 100644 --- a/core/java/android/service/voice/IHotwordDetectionService.aidl +++ b/core/java/android/service/voice/ISandboxedDetectionService.aidl @@ -29,11 +29,11 @@ import android.view.contentcapture.IContentCaptureManager; import android.speech.IRecognitionServiceManager; /** - * Provide the interface to communicate with hotword detection service. + * Provide the interface to communicate with sandboxed detection service. * * @hide */ -oneway interface IHotwordDetectionService { +oneway interface ISandboxedDetectionService { void detectFromDspSource( in SoundTrigger.KeyphraseRecognitionEvent event, in AudioFormat audioFormat, diff --git a/core/java/android/service/voice/SoftwareHotwordDetector.java b/core/java/android/service/voice/SoftwareHotwordDetector.java index 11688df2f1a4..f1b774591394 100644 --- a/core/java/android/service/voice/SoftwareHotwordDetector.java +++ b/core/java/android/service/voice/SoftwareHotwordDetector.java @@ -59,7 +59,7 @@ class SoftwareHotwordDetector extends AbstractHotwordDetector { IVoiceInteractionManagerService managerService, AudioFormat audioFormat, HotwordDetector.Callback callback) { - super(managerService, callback, DETECTOR_TYPE_TRUSTED_HOTWORD_SOFTWARE); + super(managerService, callback); mManagerService = managerService; mAudioFormat = audioFormat; @@ -129,7 +129,7 @@ class SoftwareHotwordDetector extends AbstractHotwordDetector { * @hide */ @Override - public boolean isUsingHotwordDetectionService() { + public boolean isUsingSandboxedDetectionService() { return true; } diff --git a/core/java/android/service/voice/VoiceInteractionService.java b/core/java/android/service/voice/VoiceInteractionService.java index 7c125c7f7676..a59578ee8d9d 100644 --- a/core/java/android/service/voice/VoiceInteractionService.java +++ b/core/java/android/service/voice/VoiceInteractionService.java @@ -430,7 +430,7 @@ public class VoiceInteractionService extends Service { safelyShutdownAllHotwordDetectors(); } else { for (HotwordDetector detector : mActiveHotwordDetectors) { - if (detector.isUsingHotwordDetectionService() + if (detector.isUsingSandboxedDetectionService() != supportHotwordDetectionService) { throw new IllegalStateException( "It disallows to create trusted and non-trusted detectors " @@ -513,7 +513,7 @@ public class VoiceInteractionService extends Service { safelyShutdownAllHotwordDetectors(); } else { for (HotwordDetector detector : mActiveHotwordDetectors) { - if (!detector.isUsingHotwordDetectionService()) { + if (!detector.isUsingSandboxedDetectionService()) { throw new IllegalStateException( "It disallows to create trusted and non-trusted detectors " + "at the same time."); @@ -605,7 +605,7 @@ public class VoiceInteractionService extends Service { private void shutdownHotwordDetectionServiceIfRequiredLocked() { for (HotwordDetector detector : mActiveHotwordDetectors) { - if (detector.isUsingHotwordDetectionService()) { + if (detector.isUsingSandboxedDetectionService()) { return; } } diff --git a/core/java/android/service/voice/VoiceInteractionServiceInfo.java b/core/java/android/service/voice/VoiceInteractionServiceInfo.java index ff03cc14e73b..af29961c98bc 100644 --- a/core/java/android/service/voice/VoiceInteractionServiceInfo.java +++ b/core/java/android/service/voice/VoiceInteractionServiceInfo.java @@ -46,6 +46,7 @@ public class VoiceInteractionServiceInfo { private String mSessionService; private String mRecognitionService; private String mHotwordDetectionService; + private String mVisualQueryDetectionService; private String mSettingsActivity; private boolean mSupportsAssist; private boolean mSupportsLaunchFromKeyguard; @@ -137,6 +138,8 @@ public class VoiceInteractionServiceInfo { R.styleable.VoiceInteractionService_supportsLocalInteraction, false); mHotwordDetectionService = array.getString(com.android.internal.R.styleable .VoiceInteractionService_hotwordDetectionService); + mVisualQueryDetectionService = array.getString(com.android.internal.R.styleable + .VoiceInteractionService_visualQueryDetectionService); array.recycle(); if (mSessionService == null) { mParseError = "No sessionService specified"; @@ -190,4 +193,9 @@ public class VoiceInteractionServiceInfo { public String getHotwordDetectionService() { return mHotwordDetectionService; } + + @Nullable + public String getVisualQueryDetectionService() { + return mVisualQueryDetectionService; + } } diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java index 9d95d0b705ef..84a233ffd2ad 100644 --- a/core/java/android/service/wallpaper/WallpaperService.java +++ b/core/java/android/service/wallpaper/WallpaperService.java @@ -394,7 +394,7 @@ public abstract class WallpaperService extends Service { public void resized(ClientWindowFrames frames, boolean reportDraw, MergedConfiguration mergedConfiguration, InsetsState insetsState, boolean forceLayout, boolean alwaysConsumeSystemBars, int displayId, - int syncSeqId, boolean dragResizing) { + int syncSeqId, int resizeMode) { Message msg = mCaller.obtainMessageIO(MSG_WINDOW_RESIZED, reportDraw ? 1 : 0, mergedConfiguration); diff --git a/core/java/android/util/FeatureFlagUtils.java b/core/java/android/util/FeatureFlagUtils.java index 2c4f38de2db0..897e23ac90e3 100644 --- a/core/java/android/util/FeatureFlagUtils.java +++ b/core/java/android/util/FeatureFlagUtils.java @@ -196,6 +196,7 @@ public class FeatureFlagUtils { PERSISTENT_FLAGS.add(SETTINGS_NEW_KEYBOARD_MODIFIER_KEY); PERSISTENT_FLAGS.add(SETTINGS_NEW_KEYBOARD_TRACKPAD); PERSISTENT_FLAGS.add(SETTINGS_NEW_KEYBOARD_TRACKPAD_GESTURE); + PERSISTENT_FLAGS.add(SETTINGS_ENABLE_SPA); } /** diff --git a/core/java/android/view/AttachedSurfaceControl.java b/core/java/android/view/AttachedSurfaceControl.java index c69298192109..3b082bcfcfb2 100644 --- a/core/java/android/view/AttachedSurfaceControl.java +++ b/core/java/android/view/AttachedSurfaceControl.java @@ -140,13 +140,13 @@ public interface AttachedSurfaceControl { } /** - * Returns a SyncTarget that can be used to sync {@link AttachedSurfaceControl} in a + * Returns a SurfaceSyncGroup that can be used to sync {@link AttachedSurfaceControl} in a * {@link SurfaceSyncGroup} * * @hide */ @Nullable - default SurfaceSyncGroup.SyncTarget getSyncTarget() { + default SurfaceSyncGroup getOrCreateSurfaceSyncGroup() { return null; } } diff --git a/core/java/android/view/IWindow.aidl b/core/java/android/view/IWindow.aidl index d554514349c3..8e16f24b154f 100644 --- a/core/java/android/view/IWindow.aidl +++ b/core/java/android/view/IWindow.aidl @@ -57,7 +57,7 @@ oneway interface IWindow { void resized(in ClientWindowFrames frames, boolean reportDraw, in MergedConfiguration newMergedConfiguration, in InsetsState insetsState, boolean forceLayout, boolean alwaysConsumeSystemBars, int displayId, - int syncSeqId, boolean dragResizing); + int syncSeqId, int resizeMode); /** * Called when this window retrieved control over a specified set of insets sources. diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl index 0aba80db5378..6d9f99f76958 100644 --- a/core/java/android/view/IWindowManager.aidl +++ b/core/java/android/view/IWindowManager.aidl @@ -457,6 +457,12 @@ interface IWindowManager int getDockedStackSide(); /** + * Sets the region the user can touch the divider. This region will be excluded from the region + * which is used to cause a focus switch when dispatching touch. + */ + void setDockedTaskDividerTouchRegion(in Rect touchableRegion); + + /** * Registers a listener that will be called when the pinned task state changes. */ void registerPinnedTaskListener(int displayId, IPinnedTaskListener listener); diff --git a/core/java/android/view/MotionEvent.java b/core/java/android/view/MotionEvent.java index c8a5d8d887f9..4fbb249c507f 100644 --- a/core/java/android/view/MotionEvent.java +++ b/core/java/android/view/MotionEvent.java @@ -2199,6 +2199,7 @@ public final class MotionEvent extends InputEvent implements Parcelable { } /** @hide */ + @TestApi @Override public int getDisplayId() { return nativeGetDisplayId(mNativePtr); diff --git a/core/java/android/view/SurfaceControlViewHost.java b/core/java/android/view/SurfaceControlViewHost.java index 18897725e98f..0a134be9ca84 100644 --- a/core/java/android/view/SurfaceControlViewHost.java +++ b/core/java/android/view/SurfaceControlViewHost.java @@ -404,14 +404,6 @@ public class SurfaceControlViewHost { } /** - * @hide - */ - @TestApi - public void relayout(WindowManager.LayoutParams attrs) { - relayout(attrs, SurfaceControl.Transaction::apply); - } - - /** * Forces relayout and draw and allows to set a custom callback when it is finished * @hide */ @@ -423,6 +415,14 @@ public class SurfaceControlViewHost { } /** + * @hide + */ + @TestApi + public void relayout(WindowManager.LayoutParams attrs) { + mViewRoot.setLayoutParams(attrs, false); + } + + /** * Modify the size of the root view. * * @param width Width in pixels diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java index 33ea92de68b4..9db084e01598 100644 --- a/core/java/android/view/SurfaceView.java +++ b/core/java/android/view/SurfaceView.java @@ -982,8 +982,8 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall final boolean redrawNeeded = sizeChanged || creating || hintChanged || (mVisible && !mDrawFinished) || alphaChanged || relativeZChanged; - boolean shouldSyncBuffer = - redrawNeeded && viewRoot.wasRelayoutRequested() && viewRoot.isInLocalSync(); + boolean shouldSyncBuffer = redrawNeeded && viewRoot.wasRelayoutRequested() + && viewRoot.isInWMSRequestedSync(); SyncBufferTransactionCallback syncBufferTransactionCallback = null; if (shouldSyncBuffer) { syncBufferTransactionCallback = new SyncBufferTransactionCallback(); @@ -1073,35 +1073,34 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall private void handleSyncBufferCallback(SurfaceHolder.Callback[] callbacks, SyncBufferTransactionCallback syncBufferTransactionCallback) { - getViewRootImpl().addToSync((parentSyncGroup, syncBufferCallback) -> - redrawNeededAsync(callbacks, () -> { - Transaction t = null; - if (mBlastBufferQueue != null) { - mBlastBufferQueue.stopContinuousSyncTransaction(); - t = syncBufferTransactionCallback.waitForTransaction(); - } + final SurfaceSyncGroup surfaceSyncGroup = new SurfaceSyncGroup(); + getViewRootImpl().addToSync(surfaceSyncGroup); + redrawNeededAsync(callbacks, () -> { + Transaction t = null; + if (mBlastBufferQueue != null) { + mBlastBufferQueue.stopContinuousSyncTransaction(); + t = syncBufferTransactionCallback.waitForTransaction(); + } - syncBufferCallback.onTransactionReady(t); - onDrawFinished(); - })); + surfaceSyncGroup.onTransactionReady(t); + onDrawFinished(); + }); } private void handleSyncNoBuffer(SurfaceHolder.Callback[] callbacks) { - final SurfaceSyncGroup syncGroup = new SurfaceSyncGroup(); + final SurfaceSyncGroup surfaceSyncGroup = new SurfaceSyncGroup(); synchronized (mSyncGroups) { - mSyncGroups.add(syncGroup); + mSyncGroups.add(surfaceSyncGroup); } - syncGroup.addToSync((parentSyncGroup, syncBufferCallback) -> - redrawNeededAsync(callbacks, () -> { - syncBufferCallback.onTransactionReady(null); - onDrawFinished(); - synchronized (mSyncGroups) { - mSyncGroups.remove(syncGroup); - } - })); + redrawNeededAsync(callbacks, () -> { + synchronized (mSyncGroups) { + mSyncGroups.remove(surfaceSyncGroup); + } + surfaceSyncGroup.onTransactionReady(null); + onDrawFinished(); + }); - syncGroup.markSyncReady(); } private void redrawNeededAsync(SurfaceHolder.Callback[] callbacks, @@ -1119,7 +1118,7 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall if (viewRoot != null) { synchronized (mSyncGroups) { for (SurfaceSyncGroup syncGroup : mSyncGroups) { - viewRoot.mergeSync(syncGroup); + viewRoot.addToSync(syncGroup); } } } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index eaa6820c6864..b2973eff01e1 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -51,6 +51,8 @@ import static android.view.ViewRootImplProto.WIDTH; import static android.view.ViewRootImplProto.WINDOW_ATTRIBUTES; import static android.view.ViewRootImplProto.WIN_FRAME; import static android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION; +import static android.view.WindowCallbacks.RESIZE_MODE_FREEFORM; +import static android.view.WindowCallbacks.RESIZE_MODE_INVALID; import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS; import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS; import static android.view.WindowInsetsController.APPEARANCE_LOW_PROFILE_BARS; @@ -513,6 +515,7 @@ public final class ViewRootImpl implements ViewParent, private boolean mPendingDragResizing; private boolean mDragResizing; private boolean mInvalidateRootRequested; + private int mResizeMode = RESIZE_MODE_INVALID; private int mCanvasOffsetX; private int mCanvasOffsetY; private boolean mActivityRelaunched; @@ -592,19 +595,21 @@ public final class ViewRootImpl implements ViewParent, String mLastPerformDrawSkippedReason; /** The reason the last call to performTraversals() returned without drawing */ String mLastPerformTraversalsSkipDrawReason; - /** The state of the local sync, if one is in progress. Can be one of the states below. */ - int mLocalSyncState; + /** The state of the WMS requested sync, if one is in progress. Can be one of the states + * below. */ + int mWmsRequestSyncGroupState; - // The possible states of the local sync, see createSyncIfNeeded() - private final int LOCAL_SYNC_NONE = 0; - private final int LOCAL_SYNC_PENDING = 1; - private final int LOCAL_SYNC_RETURNED = 2; - private final int LOCAL_SYNC_MERGED = 3; + // The possible states of the WMS requested sync, see createSyncIfNeeded() + private static final int WMS_SYNC_NONE = 0; + private static final int WMS_SYNC_PENDING = 1; + private static final int WMS_SYNC_RETURNED = 2; + private static final int WMS_SYNC_MERGED = 3; /** - * Set whether the draw should send the buffer to system server. When set to true, VRI will - * create a sync transaction with BBQ and send the resulting buffer to system server. If false, - * VRI will not try to sync a buffer in BBQ, but still report when a draw occurred. + * Set whether the requested SurfaceSyncGroup should sync the buffer. When set to true, VRI will + * create a sync transaction with BBQ and send the resulting buffer back to the + * SurfaceSyncGroup. If false, VRI will not try to sync a buffer in BBQ, but still report when a + * draw occurred. */ private boolean mSyncBuffer = false; @@ -846,8 +851,19 @@ public final class ViewRootImpl implements ViewParent, return mHandwritingInitiator; } - private SurfaceSyncGroup mSyncGroup; - private SurfaceSyncGroup.TransactionReadyCallback mTransactionReadyCallback; + /** + * A SurfaceSyncGroup that is created when WMS requested to sync the buffer + */ + private SurfaceSyncGroup mWmsRequestSyncGroup; + + /** + * The SurfaceSyncGroup that represents the active VRI SurfaceSyncGroup. This is non null if + * anyone requested the SurfaceSyncGroup for this VRI to ensure that anyone trying to sync with + * this VRI are collected together. The SurfaceSyncGroup is cleared when the VRI draws since + * that is the stop point where all changes are have been applied. A new SurfaceSyncGroup is + * created after that point when something wants to sync VRI again. + */ + private SurfaceSyncGroup mActiveSurfaceSyncGroup; private static final Object sSyncProgressLock = new Object(); // The count needs to be static since it's used to enable or disable RT animations which is @@ -1790,7 +1806,7 @@ public final class ViewRootImpl implements ViewParent, CompatibilityInfo.applyOverrideScaleIfNeeded(mergedConfiguration); final boolean forceNextWindowRelayout = args.argi1 != 0; final int displayId = args.argi3; - final boolean dragResizing = args.argi5 != 0; + final int resizeMode = args.argi5; final Rect frame = frames.frame; final Rect displayFrame = frames.displayFrame; @@ -1806,14 +1822,16 @@ public final class ViewRootImpl implements ViewParent, final boolean attachedFrameChanged = LOCAL_LAYOUT && !Objects.equals(mTmpFrames.attachedFrame, attachedFrame); final boolean displayChanged = mDisplay.getDisplayId() != displayId; + final boolean resizeModeChanged = mResizeMode != resizeMode; final boolean compatScaleChanged = mTmpFrames.compatScale != compatScale; if (msg == MSG_RESIZED && !frameChanged && !configChanged && !attachedFrameChanged - && !displayChanged && !forceNextWindowRelayout + && !displayChanged && !resizeModeChanged && !forceNextWindowRelayout && !compatScaleChanged) { return; } - mPendingDragResizing = dragResizing; + mPendingDragResizing = resizeMode != RESIZE_MODE_INVALID; + mResizeMode = resizeMode; mTmpFrames.compatScale = compatScale; mInvCompatScale = 1f / compatScale; @@ -3010,7 +3028,7 @@ public final class ViewRootImpl implements ViewParent, frame.width() < desiredWindowWidth && frame.width() != mWidth) || (lp.height == ViewGroup.LayoutParams.WRAP_CONTENT && frame.height() < desiredWindowHeight && frame.height() != mHeight)); - windowShouldResize |= mDragResizing && mPendingDragResizing; + windowShouldResize |= mDragResizing && mResizeMode == RESIZE_MODE_FREEFORM; // If the activity was just relaunched, it might have unfrozen the task bounds (while // relaunching), so we need to force a call into window manager to pick up the latest @@ -3257,7 +3275,7 @@ public final class ViewRootImpl implements ViewParent, && mWinFrame.height() == mPendingBackDropFrame.height(); // TODO: Need cutout? startDragResizing(mPendingBackDropFrame, !backdropSizeMatchesFrame, - mAttachInfo.mContentInsets, mAttachInfo.mStableInsets); + mAttachInfo.mContentInsets, mAttachInfo.mStableInsets, mResizeMode); } else { // We shouldn't come here, but if we come we should end the resize. endDragResizing(); @@ -3638,6 +3656,12 @@ public final class ViewRootImpl implements ViewParent, boolean cancelAndRedraw = cancelDueToPreDrawListener || (cancelDraw && mDrewOnceForSync); if (!cancelAndRedraw) { + // A sync was already requested before the WMS requested sync. This means we need to + // sync the buffer, regardless if WMS wants to sync the buffer. + if (mActiveSurfaceSyncGroup != null) { + mSyncBuffer = true; + } + createSyncIfNeeded(); mDrewOnceForSync = true; } @@ -3651,8 +3675,8 @@ public final class ViewRootImpl implements ViewParent, mPendingTransitions.clear(); } - if (mTransactionReadyCallback != null) { - mTransactionReadyCallback.onTransactionReady(null); + if (mActiveSurfaceSyncGroup != null) { + mActiveSurfaceSyncGroup.onTransactionReady(null); } } else if (cancelAndRedraw) { mLastPerformTraversalsSkipDrawReason = cancelDueToPreDrawListener @@ -3667,8 +3691,8 @@ public final class ViewRootImpl implements ViewParent, } mPendingTransitions.clear(); } - if (!performDraw() && mTransactionReadyCallback != null) { - mTransactionReadyCallback.onTransactionReady(null); + if (!performDraw() && mActiveSurfaceSyncGroup != null) { + mActiveSurfaceSyncGroup.onTransactionReady(null); } } @@ -3682,39 +3706,40 @@ public final class ViewRootImpl implements ViewParent, if (!cancelAndRedraw) { mReportNextDraw = false; mLastReportNextDrawReason = null; - mTransactionReadyCallback = null; + mActiveSurfaceSyncGroup = null; mSyncBuffer = false; - if (isInLocalSync()) { - mSyncGroup.markSyncReady(); - mSyncGroup = null; - mLocalSyncState = LOCAL_SYNC_NONE; + if (isInWMSRequestedSync()) { + mWmsRequestSyncGroup.onTransactionReady(null); + mWmsRequestSyncGroup = null; + mWmsRequestSyncGroupState = WMS_SYNC_NONE; } } } private void createSyncIfNeeded() { - // Started a sync already or there's nothing needing to sync - if (isInLocalSync() || !mReportNextDraw) { + // WMS requested sync already started or there's nothing needing to sync + if (isInWMSRequestedSync() || !mReportNextDraw) { return; } final int seqId = mSyncSeqId; - mLocalSyncState = LOCAL_SYNC_PENDING; - mSyncGroup = new SurfaceSyncGroup(transaction -> { - mLocalSyncState = LOCAL_SYNC_RETURNED; + mWmsRequestSyncGroupState = WMS_SYNC_PENDING; + mWmsRequestSyncGroup = new SurfaceSyncGroup(t -> { + mWmsRequestSyncGroupState = WMS_SYNC_RETURNED; // Callback will be invoked on executor thread so post to main thread. mHandler.postAtFrontOfQueue(() -> { - if (transaction != null) { - mSurfaceChangedTransaction.merge(transaction); + if (t != null) { + mSurfaceChangedTransaction.merge(t); } - mLocalSyncState = LOCAL_SYNC_MERGED; + mWmsRequestSyncGroupState = WMS_SYNC_MERGED; reportDrawFinished(seqId); }); }); if (DEBUG_BLAST) { - Log.d(mTag, "Setup new sync id=" + mSyncGroup); + Log.d(mTag, "Setup new sync id=" + mWmsRequestSyncGroup); } - mSyncGroup.addToSync(mSyncTarget); + + mWmsRequestSyncGroup.addToSync(this); notifySurfaceSyncStarted(); } @@ -4365,19 +4390,11 @@ public final class ViewRootImpl implements ViewParent, return mAttachInfo.mThreadedRenderer != null && mAttachInfo.mThreadedRenderer.isEnabled(); } - void addToSync(SurfaceSyncGroup.SyncTarget syncable) { - if (!isInLocalSync()) { - return; - } - mSyncGroup.addToSync(syncable); - } - /** - * This VRI is currently in the middle of a sync request, but specifically one initiated from - * within VRI. + * This VRI is currently in the middle of a sync request that was initiated by WMS. */ - public boolean isInLocalSync() { - return mSyncGroup != null; + public boolean isInWMSRequestedSync() { + return mWmsRequestSyncGroup != null; } private void addFrameCommitCallbackIfNeeded() { @@ -4444,7 +4461,7 @@ public final class ViewRootImpl implements ViewParent, return false; } - final boolean fullRedrawNeeded = mFullRedrawNeeded || mTransactionReadyCallback != null; + final boolean fullRedrawNeeded = mFullRedrawNeeded || mActiveSurfaceSyncGroup != null; mFullRedrawNeeded = false; mIsDrawing = true; @@ -4452,9 +4469,9 @@ public final class ViewRootImpl implements ViewParent, addFrameCommitCallbackIfNeeded(); - boolean usingAsyncReport = isHardwareEnabled() && mTransactionReadyCallback != null; + boolean usingAsyncReport = isHardwareEnabled() && mActiveSurfaceSyncGroup != null; if (usingAsyncReport) { - registerCallbacksForSync(mSyncBuffer, mTransactionReadyCallback); + registerCallbacksForSync(mSyncBuffer, mActiveSurfaceSyncGroup); } else if (mHasPendingTransactions) { // These callbacks are only needed if there's no sync involved and there were calls to // applyTransactionOnDraw. These callbacks check if the draw failed for any reason and @@ -4505,11 +4522,10 @@ public final class ViewRootImpl implements ViewParent, } if (mSurfaceHolder != null && mSurface.isValid()) { - final SurfaceSyncGroup.TransactionReadyCallback transactionReadyCallback = - mTransactionReadyCallback; + final SurfaceSyncGroup surfaceSyncGroup = mActiveSurfaceSyncGroup; SurfaceCallbackHelper sch = new SurfaceCallbackHelper(() -> - mHandler.post(() -> transactionReadyCallback.onTransactionReady(null))); - mTransactionReadyCallback = null; + mHandler.post(() -> surfaceSyncGroup.onTransactionReady(null))); + mActiveSurfaceSyncGroup = null; SurfaceHolder.Callback callbacks[] = mSurfaceHolder.getCallbacks(); @@ -4520,8 +4536,8 @@ public final class ViewRootImpl implements ViewParent, } } } - if (mTransactionReadyCallback != null && !usingAsyncReport) { - mTransactionReadyCallback.onTransactionReady(null); + if (mActiveSurfaceSyncGroup != null && !usingAsyncReport) { + mActiveSurfaceSyncGroup.onTransactionReady(null); } if (mPerformContentCapture) { performContentCaptureInitialReport(); @@ -8611,8 +8627,8 @@ public final class ViewRootImpl implements ViewParent, writer.println(innerPrefix + "mLastPerformDrawFailedReason=" + mLastPerformDrawSkippedReason); } - if (mLocalSyncState != LOCAL_SYNC_NONE) { - writer.println(innerPrefix + "mLocalSyncState=" + mLocalSyncState); + if (mWmsRequestSyncGroupState != WMS_SYNC_NONE) { + writer.println(innerPrefix + "mWmsRequestSyncGroupState=" + mWmsRequestSyncGroupState); } writer.println(innerPrefix + "mLastReportedMergedConfiguration=" + mLastReportedMergedConfiguration); @@ -8825,7 +8841,7 @@ public final class ViewRootImpl implements ViewParent, @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) private void dispatchResized(ClientWindowFrames frames, boolean reportDraw, MergedConfiguration mergedConfiguration, InsetsState insetsState, boolean forceLayout, - boolean alwaysConsumeSystemBars, int displayId, int syncSeqId, boolean dragResizing) { + boolean alwaysConsumeSystemBars, int displayId, int syncSeqId, int resizeMode) { Message msg = mHandler.obtainMessage(reportDraw ? MSG_RESIZED_REPORT : MSG_RESIZED); SomeArgs args = SomeArgs.obtain(); final boolean sameProcessCall = (Binder.getCallingPid() == android.os.Process.myPid()); @@ -8847,7 +8863,7 @@ public final class ViewRootImpl implements ViewParent, args.argi2 = alwaysConsumeSystemBars ? 1 : 0; args.argi3 = displayId; args.argi4 = syncSeqId; - args.argi5 = dragResizing ? 1 : 0; + args.argi5 = resizeMode; msg.obj = args; mHandler.sendMessage(msg); @@ -10239,11 +10255,11 @@ public final class ViewRootImpl implements ViewParent, public void resized(ClientWindowFrames frames, boolean reportDraw, MergedConfiguration mergedConfiguration, InsetsState insetsState, boolean forceLayout, boolean alwaysConsumeSystemBars, int displayId, int syncSeqId, - boolean dragResizing) { + int resizeMode) { final ViewRootImpl viewAncestor = mViewAncestor.get(); if (viewAncestor != null) { viewAncestor.dispatchResized(frames, reportDraw, mergedConfiguration, insetsState, - forceLayout, alwaysConsumeSystemBars, displayId, syncSeqId, dragResizing); + forceLayout, alwaysConsumeSystemBars, displayId, syncSeqId, resizeMode); } } @@ -10448,13 +10464,13 @@ public final class ViewRootImpl implements ViewParent, * Start a drag resizing which will inform all listeners that a window resize is taking place. */ private void startDragResizing(Rect initialBounds, boolean fullscreen, Rect systemInsets, - Rect stableInsets) { + Rect stableInsets, int resizeMode) { if (!mDragResizing) { mDragResizing = true; if (mUseMTRenderer) { for (int i = mWindowCallbacks.size() - 1; i >= 0; i--) { mWindowCallbacks.get(i).onWindowDragResizeStart( - initialBounds, fullscreen, systemInsets, stableInsets); + initialBounds, fullscreen, systemInsets, stableInsets, resizeMode); } } mFullRedrawNeeded = true; @@ -11200,7 +11216,7 @@ public final class ViewRootImpl implements ViewParent, } private void registerCallbacksForSync(boolean syncBuffer, - final SurfaceSyncGroup.TransactionReadyCallback transactionReadyCallback) { + final SurfaceSyncGroup surfaceSyncGroup) { if (!isHardwareEnabled()) { return; } @@ -11227,7 +11243,7 @@ public final class ViewRootImpl implements ViewParent, // pendingDrawFinished. if ((syncResult & (SYNC_LOST_SURFACE_REWARD_IF_FOUND | SYNC_CONTEXT_IS_STOPPED)) != 0) { - transactionReadyCallback.onTransactionReady( + surfaceSyncGroup.onTransactionReady( mBlastBufferQueue.gatherPendingTransactions(frame)); return null; } @@ -11238,7 +11254,7 @@ public final class ViewRootImpl implements ViewParent, if (syncBuffer) { mBlastBufferQueue.syncNextTransaction( - transactionReadyCallback::onTransactionReady); + surfaceSyncGroup::onTransactionReady); } return didProduceBuffer -> { @@ -11258,7 +11274,7 @@ public final class ViewRootImpl implements ViewParent, // since the frame didn't draw on this vsync. It's possible the frame will // draw later, but it's better to not be sync than to block on a frame that // may never come. - transactionReadyCallback.onTransactionReady( + surfaceSyncGroup.onTransactionReady( mBlastBufferQueue.gatherPendingTransactions(frame)); return; } @@ -11267,35 +11283,23 @@ public final class ViewRootImpl implements ViewParent, // syncNextTransaction callback. Instead, just report back to the Syncer so it // knows that this sync request is complete. if (!syncBuffer) { - transactionReadyCallback.onTransactionReady(null); + surfaceSyncGroup.onTransactionReady(null); } }; } }); } - public final SurfaceSyncGroup.SyncTarget mSyncTarget = new SurfaceSyncGroup.SyncTarget() { - @Override - public void onAddedToSyncGroup(SurfaceSyncGroup parentSyncGroup, - SurfaceSyncGroup.TransactionReadyCallback transactionReadyCallback) { - updateSyncInProgressCount(parentSyncGroup); - if (!isInLocalSync()) { - // Always sync the buffer if the sync request did not come from VRI. - mSyncBuffer = true; - } - - if (mTransactionReadyCallback != null) { - Log.d(mTag, "Already set sync for the next draw."); - mTransactionReadyCallback.onTransactionReady(null); - } - if (DEBUG_BLAST) { - Log.d(mTag, "Setting syncFrameCallback"); - } - mTransactionReadyCallback = transactionReadyCallback; + @Override + public SurfaceSyncGroup getOrCreateSurfaceSyncGroup() { + if (mActiveSurfaceSyncGroup == null) { + mActiveSurfaceSyncGroup = new SurfaceSyncGroup(); + updateSyncInProgressCount(mActiveSurfaceSyncGroup); if (!mIsInTraversal && !mTraversalScheduled) { scheduleTraversals(); } } + return mActiveSurfaceSyncGroup; }; private final Executor mSimpleExecutor = Runnable::run; @@ -11320,15 +11324,10 @@ public final class ViewRootImpl implements ViewParent, }); } - @Override - public SurfaceSyncGroup.SyncTarget getSyncTarget() { - return mSyncTarget; - } - - void mergeSync(SurfaceSyncGroup otherSyncGroup) { - if (!isInLocalSync()) { + void addToSync(SurfaceSyncGroup syncable) { + if (mActiveSurfaceSyncGroup == null) { return; } - mSyncGroup.merge(otherSyncGroup); + mActiveSurfaceSyncGroup.addToSync(syncable, false /* parentSyncGroupMerge */); } } diff --git a/core/java/android/view/WindowCallbacks.java b/core/java/android/view/WindowCallbacks.java index 94b2d93455d3..a7f0ef0a0324 100644 --- a/core/java/android/view/WindowCallbacks.java +++ b/core/java/android/view/WindowCallbacks.java @@ -28,6 +28,22 @@ import android.graphics.Rect; */ public interface WindowCallbacks { + int RESIZE_MODE_INVALID = -1; + + /** + * The window is being resized by dragging one of the window corners, + * in this case the surface would be fullscreen-sized. The client should + * render to the actual frame location (instead of (0,curScrollY)). + */ + int RESIZE_MODE_FREEFORM = 0; + + /** + * The window is being resized by dragging on the docked divider. The client should render + * at (0, 0) and extend its background to the background frame passed into + * {@link IWindow#resized}. + */ + int RESIZE_MODE_DOCKED_DIVIDER = 1; + /** * Called by the system when the window got changed by the user, before the layouter got called. * It also gets called when the insets changed, or when the window switched between a fullscreen @@ -53,7 +69,7 @@ public interface WindowCallbacks { * @param stableInsets The stable insets for the window. */ void onWindowDragResizeStart(Rect initialBounds, boolean fullscreen, Rect systemInsets, - Rect stableInsets); + Rect stableInsets, int resizeMode); /** * Called when a drag resize ends. diff --git a/core/java/android/view/WindowlessWindowManager.java b/core/java/android/view/WindowlessWindowManager.java index 2d6c1d913e90..69340aa39daf 100644 --- a/core/java/android/view/WindowlessWindowManager.java +++ b/core/java/android/view/WindowlessWindowManager.java @@ -16,6 +16,8 @@ package android.view; +import static android.view.WindowCallbacks.RESIZE_MODE_INVALID; + import android.annotation.Nullable; import android.content.res.Configuration; import android.graphics.PixelFormat; @@ -548,7 +550,7 @@ public class WindowlessWindowManager implements IWindowSession { mTmpConfig.setConfiguration(mConfiguration, mConfiguration); s.mClient.resized(mTmpFrames, false /* reportDraw */, mTmpConfig, state, false /* forceLayout */, false /* alwaysConsumeSystemBars */, s.mDisplayId, - Integer.MAX_VALUE, false /* dragResizing */); + Integer.MAX_VALUE, RESIZE_MODE_INVALID); } catch (RemoteException e) { // Too bad } diff --git a/core/java/android/view/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java index 423c560d5c57..9abbba923a66 100644 --- a/core/java/android/view/accessibility/AccessibilityManager.java +++ b/core/java/android/view/accessibility/AccessibilityManager.java @@ -25,6 +25,7 @@ import android.accessibilityservice.AccessibilityServiceInfo.FeedbackType; import android.accessibilityservice.AccessibilityShortcutInfo; import android.annotation.CallbackExecutor; import android.annotation.ColorInt; +import android.annotation.FloatRange; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -75,6 +76,7 @@ import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.concurrent.Executor; /** @@ -138,6 +140,21 @@ public final class AccessibilityManager { public static final int AUTOCLICK_DELAY_DEFAULT = 600; /** + * The contrast is defined as a float in [-1, 1], with a default value of 0. + * @hide + */ + public static final float CONTRAST_MIN_VALUE = -1f; + + /** @hide */ + public static final float CONTRAST_MAX_VALUE = 1f; + + /** @hide */ + public static final float CONTRAST_DEFAULT_VALUE = 0f; + + /** @hide */ + public static final float CONTRAST_NOT_SET = Float.MIN_VALUE; + + /** * Activity action: Launch UI to manage which accessibility service or feature is assigned * to the navigation bar Accessibility button. * <p> @@ -246,6 +263,8 @@ public final class AccessibilityManager { @UnsupportedAppUsage(trackingBug = 123768939L) boolean mIsHighTextContrastEnabled; + private float mUiContrast; + boolean mIsAudioDescriptionByDefaultRequested; // accessibility tracing state @@ -270,6 +289,9 @@ public final class AccessibilityManager { private final ArrayMap<HighTextContrastChangeListener, Handler> mHighTextContrastStateChangeListeners = new ArrayMap<>(); + private final ArrayMap<UiContrastChangeListener, Executor> + mUiContrastChangeListeners = new ArrayMap<>(); + private final ArrayMap<AccessibilityServicesStateChangeListener, Executor> mServicesStateChangeListeners = new ArrayMap<>(); @@ -336,7 +358,7 @@ public final class AccessibilityManager { * * @param manager The manager that is calling back */ - void onAccessibilityServicesStateChanged(@NonNull AccessibilityManager manager); + void onAccessibilityServicesStateChanged(@NonNull AccessibilityManager manager); } /** @@ -358,6 +380,21 @@ public final class AccessibilityManager { } /** + * Listener for the UI contrast. To listen for changes to + * the UI contrast on the device, implement this interface and + * register it with the system by calling {@link #addUiContrastChangeListener}. + */ + public interface UiContrastChangeListener { + + /** + * Called when the color contrast enabled state changes. + * + * @param uiContrast The color contrast as in {@link #getUiContrast} + */ + void onUiContrastChanged(@FloatRange(from = -1.0f, to = 1.0f) float uiContrast); + } + + /** * Listener for the audio description by default state. To listen for * changes to the audio description by default state on the device, * implement this interface and register it with the system by calling @@ -471,6 +508,16 @@ public final class AccessibilityManager { updateFocusAppearanceLocked(strokeWidth, color); } } + + @Override + public void setUiContrast(float contrast) { + synchronized (mLock) { + // if value changed in the settings, update the cached value and notify listeners + if (Math.abs(mUiContrast - contrast) < 1e-10) return; + mUiContrast = contrast; + } + mHandler.obtainMessage(MyCallback.MSG_NOTIFY_CONTRAST_CHANGED).sendToTarget(); + } }; /** @@ -641,7 +688,7 @@ public final class AccessibilityManager { /** * Returns if the high text contrast in the system is enabled. * <p> - * <strong>Note:</strong> You need to query this only if you application is + * <strong>Note:</strong> You need to query this only if your application is * doing its own rendering and does not rely on the platform rendering pipeline. * </p> * @@ -661,6 +708,24 @@ public final class AccessibilityManager { } /** + * Returns the color contrast for the user. + * <p> + * <strong>Note:</strong> You need to query this only if your application is + * doing its own rendering and does not rely on the platform rendering pipeline. + * </p> + * @return The color contrast, float in [-1, 1] where + * 0 corresponds to the default contrast + * -1 corresponds to the minimum contrast that the user can set + * 1 corresponds to the maximum contrast that the user can set + */ + @FloatRange(from = -1.0f, to = 1.0f) + public float getUiContrast() { + synchronized (mLock) { + return mUiContrast; + } + } + + /** * Sends an {@link AccessibilityEvent}. * * @param event The event to send. @@ -1240,6 +1305,35 @@ public final class AccessibilityManager { } /** + * Registers a {@link UiContrastChangeListener} for the current user. + * + * @param executor The executor on which the listener should be called back. + * @param listener The listener. + */ + public void addUiContrastChangeListener( + @NonNull @CallbackExecutor Executor executor, + @NonNull UiContrastChangeListener listener) { + Objects.requireNonNull(executor); + Objects.requireNonNull(listener); + synchronized (mLock) { + mUiContrastChangeListeners.put(listener, executor); + } + } + + /** + * Unregisters a {@link UiContrastChangeListener} for the current user. + * If the listener was not registered, does nothing and returns. + * + * @param listener The listener to unregister. + */ + public void removeUiContrastChangeListener(@NonNull UiContrastChangeListener listener) { + Objects.requireNonNull(listener); + synchronized (mLock) { + mUiContrastChangeListeners.remove(listener); + } + } + + /** * Registers a {@link AudioDescriptionRequestedChangeListener} * for changes in the audio description by default state of the system. * The value could be read via {@link #isAudioDescriptionRequested}. @@ -2004,6 +2098,7 @@ public final class AccessibilityManager { mRelevantEventTypes = IntPair.second(userStateAndRelevantEvents); updateUiTimeout(service.getRecommendedTimeoutMillis()); updateFocusAppearanceLocked(service.getFocusStrokeWidth(), service.getFocusColor()); + mUiContrast = service.getUiContrast(); mService = service; } catch (RemoteException re) { Log.e(LOG_TAG, "AccessibilityManagerService is dead", re); @@ -2082,6 +2177,22 @@ public final class AccessibilityManager { } /** + * Notifies the registered {@link UiContrastChangeListener}s if the value changed. + */ + private void notifyUiContrastChanged() { + final ArrayMap<UiContrastChangeListener, Executor> listeners; + synchronized (mLock) { + listeners = new ArrayMap<>(mUiContrastChangeListeners); + } + + listeners.entrySet().forEach(entry -> { + UiContrastChangeListener listener = entry.getKey(); + Executor executor = entry.getValue(); + executor.execute(() -> listener.onUiContrastChanged(mUiContrast)); + }); + } + + /** * Notifies the registered {@link AudioDescriptionStateChangeListener}s. */ private void notifyAudioDescriptionbyDefaultStateChanged() { @@ -2171,6 +2282,7 @@ public final class AccessibilityManager { private final class MyCallback implements Handler.Callback { public static final int MSG_SET_STATE = 1; + public static final int MSG_NOTIFY_CONTRAST_CHANGED = 2; @Override public boolean handleMessage(Message message) { @@ -2182,6 +2294,9 @@ public final class AccessibilityManager { setStateLocked(state); } } break; + case MSG_NOTIFY_CONTRAST_CHANGED: { + notifyUiContrastChanged(); + } } return true; } diff --git a/core/java/android/view/accessibility/IAccessibilityManager.aidl b/core/java/android/view/accessibility/IAccessibilityManager.aidl index 364c7c8e1fb9..c2d899a50d4e 100644 --- a/core/java/android/view/accessibility/IAccessibilityManager.aidl +++ b/core/java/android/view/accessibility/IAccessibilityManager.aidl @@ -118,4 +118,6 @@ interface IAccessibilityManager { // Used by UiAutomation for tests on the InputFilter void injectInputEventToInputFilter(in InputEvent event); + + float getUiContrast(); } diff --git a/core/java/android/view/accessibility/IAccessibilityManagerClient.aidl b/core/java/android/view/accessibility/IAccessibilityManagerClient.aidl index 041399ccb8ec..931f862e581b 100644 --- a/core/java/android/view/accessibility/IAccessibilityManagerClient.aidl +++ b/core/java/android/view/accessibility/IAccessibilityManagerClient.aidl @@ -31,4 +31,6 @@ oneway interface IAccessibilityManagerClient { void setRelevantEventTypes(int eventTypes); void setFocusAppearance(int strokeWidth, int color); + + void setUiContrast(float contrast); } diff --git a/core/java/android/window/SurfaceSyncGroup.java b/core/java/android/window/SurfaceSyncGroup.java index 395073941930..250652a53677 100644 --- a/core/java/android/window/SurfaceSyncGroup.java +++ b/core/java/android/window/SurfaceSyncGroup.java @@ -40,62 +40,63 @@ import java.util.function.Supplier; * mechanism so each sync implementation doesn't need to handle it themselves. The SurfaceSyncGroup * class is used the following way. * - * 1. {@link #SurfaceSyncGroup()} constructor is called - * 2. {@link #addToSync(SyncTarget)} is called for every SyncTarget object that wants to be - * included in the sync. If the addSync is called for an {@link AttachedSurfaceControl} or - * {@link SurfaceView} it needs to be called on the UI thread. When addToSync is called, it's + * 1. {@link #addToSync(SurfaceSyncGroup, boolean)} is called for every SurfaceSyncGroup object that + * wants to be included in the sync. If the addSync is called for an {@link AttachedSurfaceControl} + * or {@link SurfaceView} it needs to be called on the UI thread. When addToSync is called, it's * guaranteed that any UI updates that were requested before addToSync but after the last frame * drew, will be included in the sync. - * 3. {@link #markSyncReady()} should be called when all the {@link SyncTarget}s have been added - * to the SurfaceSyncGroup. At this point, the SurfaceSyncGroup is closed and no more SyncTargets - * can be added to it. - * 4. The SurfaceSyncGroup will gather the data for each SyncTarget using the steps described below. - * When all the SyncTargets have finished, the syncRequestComplete will be invoked and the - * transaction will either be applied or sent to the caller. In most cases, only the - * SurfaceSyncGroup should be handling the Transaction object directly. However, there are some + * 2. {@link #markSyncReady()} should be called when all the {@link SurfaceSyncGroup}s have been + * added to the SurfaceSyncGroup. At this point, the SurfaceSyncGroup is closed and no more + * SurfaceSyncGroups can be added to it. + * 3. The SurfaceSyncGroup will gather the data for each SurfaceSyncGroup using the steps described + * below. When all the SurfaceSyncGroups have finished, the syncRequestComplete will be invoked and + * the transaction will either be applied or sent to the caller. In most cases, only the + * SurfaceSyncGroup should be handling the Transaction object directly. However, there are some * cases where the framework needs to send the Transaction elsewhere, like in ViewRootImpl, so that * option is provided. * - * The following is what happens within the {@link SurfaceSyncGroup} - * 1. Each SyncTarget will get a {@link SyncTarget#onAddedToSyncGroup} callback that contains a - * {@link TransactionReadyCallback}. - * 2. Each {@link SyncTarget} needs to invoke - * {@link TransactionReadyCallback#onTransactionReady(Transaction)}. This makes sure the - * SurfaceSyncGroup knows when the SyncTarget is complete, allowing the SurfaceSyncGroup to get the - * Transaction that contains the buffer. - * 3. When the final TransactionReadyCallback finishes for the SurfaceSyncGroup, in most cases the - * transaction is applied and then the sync complete callbacks are invoked, letting the callers know - * the sync is now complete. + * The following is what happens within the {@link android.window.SurfaceSyncGroup} + * 1. Each SurfaceSyncGroup will get a + * {@link SurfaceSyncGroup#onAddedToSyncGroup(SurfaceSyncGroup, TransactionReadyCallback)} callback + * that contains a {@link TransactionReadyCallback}. + * 2. Each {@link SurfaceSyncGroup} needs to invoke + * {@link SurfaceSyncGroup#onTransactionReady(Transaction)}. + * This makes sure the parent SurfaceSyncGroup knows when the SurfaceSyncGroup is complete, allowing + * the parent SurfaceSyncGroup to get the Transaction that contains the changes for the child + * SurfaceSyncGroup + * 3. When the final TransactionReadyCallback finishes for the child SurfaceSyncGroups, the + * transaction is either applied if it's the top most parent or the final merged transaction is sent + * up to its parent SurfaceSyncGroup. * * @hide */ -public final class SurfaceSyncGroup { +public class SurfaceSyncGroup { private static final String TAG = "SurfaceSyncGroup"; private static final boolean DEBUG = false; private static Supplier<Transaction> sTransactionFactory = Transaction::new; /** - * Class that collects the {@link SyncTarget}s and notifies when all the surfaces have + * Class that collects the {@link SurfaceSyncGroup}s and notifies when all the surfaces have * a frame ready. */ private final Object mLock = new Object(); @GuardedBy("mLock") - private final Set<Integer> mPendingSyncs = new ArraySet<>(); + private final Set<TransactionReadyCallback> mPendingSyncs = new ArraySet<>(); @GuardedBy("mLock") private final Transaction mTransaction = sTransactionFactory.get(); @GuardedBy("mLock") private boolean mSyncReady; @GuardedBy("mLock") - private Consumer<Transaction> mSyncRequestCompleteCallback; + private boolean mFinished; @GuardedBy("mLock") - private final Set<SurfaceSyncGroup> mMergedSyncGroups = new ArraySet<>(); + private TransactionReadyCallback mTransactionReadyCallback; @GuardedBy("mLock") - private boolean mFinished; + private SurfaceSyncGroup mParentSyncGroup; @GuardedBy("mLock") private final ArraySet<Pair<Executor, Runnable>> mSyncCompleteCallbacks = new ArraySet<>(); @@ -122,16 +123,16 @@ public final class SurfaceSyncGroup { /** * Creates a sync. * - * @param syncRequestComplete The complete callback that contains the syncId and transaction - * with all the sync data merged. The Transaction passed back can be - * null. + * @param transactionReadyCallback The complete callback that contains the syncId and + * transaction with all the sync data merged. The Transaction + * passed back can be null. * * NOTE: Only should be used by ViewRootImpl * @hide */ - public SurfaceSyncGroup(Consumer<Transaction> syncRequestComplete) { - mSyncRequestCompleteCallback = transaction -> { - syncRequestComplete.accept(transaction); + public SurfaceSyncGroup(Consumer<Transaction> transactionReadyCallback) { + mTransactionReadyCallback = transaction -> { + transactionReadyCallback.accept(transaction); synchronized (mLock) { for (Pair<Executor, Runnable> callback : mSyncCompleteCallbacks) { callback.first.execute(callback.second); @@ -157,6 +158,31 @@ public final class SurfaceSyncGroup { } /** + * Mark the sync set as ready to complete. No more data can be added to the specified + * syncId. + * Once the sync set is marked as ready, it will be able to complete once all Syncables in the + * set have completed their sync + */ + public void markSyncReady() { + onTransactionReady(null); + } + + /** + * Similar to {@link #markSyncReady()}, but a transaction is passed in to merge with the + * SurfaceSyncGroup. + * @param t The transaction that merges into the main Transaction for the SurfaceSyncGroup. + */ + public void onTransactionReady(@Nullable Transaction t) { + synchronized (mLock) { + mSyncReady = true; + if (t != null) { + mTransaction.merge(t); + } + checkIfSyncIsComplete(); + } + } + + /** * Add a SurfaceView to a sync set. This is different than * {@link #addToSync(AttachedSurfaceControl)} because it requires the caller to notify the start * and finish drawing in order to sync. @@ -171,7 +197,13 @@ public final class SurfaceSyncGroup { @UiThread public boolean addToSync(SurfaceView surfaceView, Consumer<SurfaceViewFrameCallback> frameCallbackConsumer) { - return addToSync(new SurfaceViewSyncTarget(surfaceView, frameCallbackConsumer)); + SurfaceSyncGroup surfaceSyncGroup = new SurfaceSyncGroup(); + if (addToSync(surfaceSyncGroup, false /* parentSyncGroupMerge */)) { + frameCallbackConsumer.accept( + () -> surfaceView.syncNextFrame(surfaceSyncGroup::onTransactionReady)); + return true; + } + return false; } /** @@ -185,29 +217,38 @@ public final class SurfaceSyncGroup { if (viewRoot == null) { return false; } - SyncTarget syncTarget = viewRoot.getSyncTarget(); - if (syncTarget == null) { + SurfaceSyncGroup surfaceSyncGroup = viewRoot.getOrCreateSurfaceSyncGroup(); + if (surfaceSyncGroup == null) { return false; } - return addToSync(syncTarget); + return addToSync(surfaceSyncGroup, false /* parentSyncGroupMerge */); } /** - * Add a {@link SyncTarget} to a sync set. The sync set will wait for all + * Add a {@link SurfaceSyncGroup} to a sync set. The sync set will wait for all * SyncableSurfaces to complete before notifying. * - * @param syncTarget A SyncTarget that implements how to handle syncing transactions. - * @return true if the SyncTarget was successfully added to the SyncGroup, false otherwise. + * @param surfaceSyncGroup A SyncableSurface that implements how to handle syncing + * buffers. + * @return true if the SyncGroup was successfully added to the current SyncGroup, false + * otherwise. */ - public boolean addToSync(SyncTarget syncTarget) { + public boolean addToSync(SurfaceSyncGroup surfaceSyncGroup, boolean parentSyncGroupMerge) { TransactionReadyCallback transactionReadyCallback = new TransactionReadyCallback() { @Override public void onTransactionReady(Transaction t) { synchronized (mLock) { if (t != null) { + // When an older parent sync group is added due to a child syncGroup getting + // added to multiple groups, we need to maintain merge order so the older + // parentSyncGroup transactions are overwritten by anything in the newer + // parentSyncGroup. + if (parentSyncGroupMerge) { + t.merge(mTransaction); + } mTransaction.merge(t); } - mPendingSyncs.remove(hashCode()); + mPendingSyncs.remove(this); checkIfSyncIsComplete(); } } @@ -216,35 +257,38 @@ public final class SurfaceSyncGroup { synchronized (mLock) { if (mSyncReady) { Log.e(TAG, "Sync " + this + " was already marked as ready. No more " - + "SyncTargets can be added."); + + "SurfaceSyncGroups can be added."); return false; } - mPendingSyncs.add(transactionReadyCallback.hashCode()); + mPendingSyncs.add(transactionReadyCallback); } - syncTarget.onAddedToSyncGroup(this, transactionReadyCallback); + surfaceSyncGroup.onAddedToSyncGroup(this, transactionReadyCallback); return true; } /** - * Mark the sync set as ready to complete. No more data can be added to the specified - * syncId. - * Once the sync set is marked as ready, it will be able to complete once all Syncables in the - * set have completed their sync + * Add a Transaction to this sync set. This allows the caller to provide other info that + * should be synced with the transactions. */ - public void markSyncReady() { + public void addTransactionToSync(Transaction t) { synchronized (mLock) { - mSyncReady = true; - checkIfSyncIsComplete(); + mTransaction.merge(t); } } @GuardedBy("mLock") private void checkIfSyncIsComplete() { - if (!mSyncReady || !mPendingSyncs.isEmpty() || !mMergedSyncGroups.isEmpty()) { + if (mFinished) { if (DEBUG) { - Log.d(TAG, "Syncable is not complete. mSyncReady=" + mSyncReady - + " mPendingSyncs=" + mPendingSyncs.size() + " mergedSyncs=" - + mMergedSyncGroups.size()); + Log.d(TAG, "SurfaceSyncGroup=" + this + " is already complete"); + } + return; + } + + if (!mSyncReady || !mPendingSyncs.isEmpty()) { + if (DEBUG) { + Log.d(TAG, "SurfaceSyncGroup=" + this + " is not complete. mSyncReady=" + + mSyncReady + " mPendingSyncs=" + mPendingSyncs.size()); } return; } @@ -252,114 +296,48 @@ public final class SurfaceSyncGroup { if (DEBUG) { Log.d(TAG, "Successfully finished sync id=" + this); } - - mSyncRequestCompleteCallback.accept(mTransaction); + mTransactionReadyCallback.onTransactionReady(mTransaction); mFinished = true; } - /** - * Add a Transaction to this sync set. This allows the caller to provide other info that - * should be synced with the transactions. - */ - public void addTransactionToSync(Transaction t) { - synchronized (mLock) { - mTransaction.merge(t); - } - } - - private void updateCallback(Consumer<Transaction> transactionConsumer) { + private void onAddedToSyncGroup(SurfaceSyncGroup parentSyncGroup, + TransactionReadyCallback transactionReadyCallback) { + boolean finished = false; synchronized (mLock) { if (mFinished) { - Log.e(TAG, "Attempting to merge SyncGroup " + this + " when sync is" - + " already complete"); - transactionConsumer.accept(null); - } - - final Consumer<Transaction> oldCallback = mSyncRequestCompleteCallback; - mSyncRequestCompleteCallback = transaction -> { - oldCallback.accept(null); - transactionConsumer.accept(transaction); - }; - } - } - - /** - * Merge a SyncGroup into this SyncGroup. Since SyncGroups could still have pending SyncTargets, - * we need to make sure those can still complete before the mergeTo SyncGroup is considered - * complete. - * - * We keep track of all the merged SyncGroups until they are marked as done, and then they - * are removed from the set. This SyncGroup is not considered done until all the merged - * SyncGroups are done. - * - * When the merged SyncGroup is complete, it will invoke the original syncRequestComplete - * callback but send an empty transaction to ensure the changes are applied early. This - * is needed in case the original sync is relying on the callback to continue processing. - * - * @param otherSyncGroup The other SyncGroup to merge into this one. - */ - public void merge(SurfaceSyncGroup otherSyncGroup) { - synchronized (mLock) { - mMergedSyncGroups.add(otherSyncGroup); - } - otherSyncGroup.updateCallback(transaction -> { - synchronized (mLock) { - mMergedSyncGroups.remove(otherSyncGroup); - if (transaction != null) { - mTransaction.merge(transaction); + finished = true; + } else { + // If this SurfaceSyncGroup was already added to a different SurfaceSyncGroup, we + // need to combine everything. We can add the old SurfaceSyncGroup parent to the new + // parent so the new parent doesn't complete until the old parent does. + // Additionally, the old parent will not get the final transaction object and + // instead will send it to the new parent, ensuring that any other SurfaceSyncGroups + // from the original parent are also combined with the new parent SurfaceSyncGroup. + if (mParentSyncGroup != null) { + Log.d(TAG, "Already part of sync group " + mParentSyncGroup + " " + this); + parentSyncGroup.addToSync(mParentSyncGroup, true /* parentSyncGroupMerge */); } - checkIfSyncIsComplete(); + mParentSyncGroup = parentSyncGroup; + final TransactionReadyCallback lastCallback = mTransactionReadyCallback; + mTransactionReadyCallback = t -> { + lastCallback.onTransactionReady(null); + transactionReadyCallback.onTransactionReady(t); + }; } - }); - } - - /** - * Wrapper class to help synchronize SurfaceViews - */ - private static class SurfaceViewSyncTarget implements SyncTarget { - private final SurfaceView mSurfaceView; - private final Consumer<SurfaceViewFrameCallback> mFrameCallbackConsumer; - - SurfaceViewSyncTarget(SurfaceView surfaceView, - Consumer<SurfaceViewFrameCallback> frameCallbackConsumer) { - mSurfaceView = surfaceView; - mFrameCallbackConsumer = frameCallbackConsumer; } - @Override - public void onAddedToSyncGroup(SurfaceSyncGroup parentSyncGroup, - TransactionReadyCallback transactionReadyCallback) { - mFrameCallbackConsumer.accept( - () -> mSurfaceView.syncNextFrame(transactionReadyCallback::onTransactionReady)); + // Invoke the callback outside of the lock when the SurfaceSyncGroup being added was already + // complete. + if (finished) { + transactionReadyCallback.onTransactionReady(null); } } - - /** - * A SyncTarget that can be added to a sync set. - */ - public interface SyncTarget { - /** - * Called when the SyncTarget has been added to a SyncGroup as is ready to begin handing a - * sync request. When invoked, the implementor is required to call - * {@link TransactionReadyCallback#onTransactionReady(Transaction)} in order for this - * SurfaceSyncGroup to fully complete. - * - * Always invoked on the thread that initiated the call to {@link #addToSync(SyncTarget)} - * - * @param parentSyncGroup The sync group this target has been added to. - * @param transactionReadyCallback A TransactionReadyCallback that the caller must invoke - * onTransactionReady - */ - void onAddedToSyncGroup(SurfaceSyncGroup parentSyncGroup, - TransactionReadyCallback transactionReadyCallback); - } - /** * Interface so the SurfaceSyncer can know when it's safe to start and when everything has been * completed. The caller should invoke the calls when the rendering has started and finished a * frame. */ - public interface TransactionReadyCallback { + private interface TransactionReadyCallback { /** * Invoked when the transaction is ready to sync. * diff --git a/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl b/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl index 9f23f2474257..b9ca557da4bd 100644 --- a/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl +++ b/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl @@ -252,11 +252,13 @@ interface IVoiceInteractionManagerService { * @param sharedMemory The unrestricted data blob to provide to the * {@link HotwordDetectionService}. Use this to provide the hotword models data or other * such data to the trusted process. + * @param token Use this to identify which detector calls this method. */ @EnforcePermission("MANAGE_HOTWORD_DETECTION") void updateState( in PersistableBundle options, - in SharedMemory sharedMemory); + in SharedMemory sharedMemory, + in IBinder token); /** * Set configuration and pass read-only data to hotword detection service when creating @@ -272,6 +274,7 @@ interface IVoiceInteractionManagerService { * @param sharedMemory The unrestricted data blob to provide to the * {@link HotwordDetectionService}. Use this to provide the hotword models data or other * such data to the trusted process. + * @param token Use this to identify which detector calls this method. * @param callback Use this to report {@link HotwordDetectionService} status. * @param detectorType Indicate which detector is used. */ @@ -280,10 +283,18 @@ interface IVoiceInteractionManagerService { in Identity originatorIdentity, in PersistableBundle options, in SharedMemory sharedMemory, + in IBinder token, in IHotwordRecognitionStatusCallback callback, int detectorType); /** + * Destroy the detector callback. + * + * @param token Indicate which callback will be destroyed. + */ + void destroyDetector(in IBinder token); + + /** * Requests to shutdown hotword detection service. */ void shutdownHotwordDetectionService(); @@ -298,6 +309,7 @@ interface IVoiceInteractionManagerService { in ParcelFileDescriptor audioStream, in AudioFormat audioFormat, in PersistableBundle options, + in IBinder token, in IMicrophoneHotwordDetectionVoiceInteractionCallback callback); /** diff --git a/core/java/com/android/internal/app/IVoiceInteractionSessionListener.aidl b/core/java/com/android/internal/app/IVoiceInteractionSessionListener.aidl index 6e409885fa13..46f78e2ee8a2 100644 --- a/core/java/com/android/internal/app/IVoiceInteractionSessionListener.aidl +++ b/core/java/com/android/internal/app/IVoiceInteractionSessionListener.aidl @@ -31,6 +31,8 @@ /** * Called when a voice session window is shown/hidden. + * Caution that there could be duplicated visibility change callbacks, it's up to the listener + * to dedup those events. */ void onVoiceSessionWindowVisibilityChanged(boolean visible); diff --git a/core/java/com/android/internal/policy/DecorView.java b/core/java/com/android/internal/policy/DecorView.java index b5b27f5289c9..145aeafb46a1 100644 --- a/core/java/com/android/internal/policy/DecorView.java +++ b/core/java/com/android/internal/policy/DecorView.java @@ -274,6 +274,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind private boolean mApplyFloatingVerticalInsets = false; private boolean mApplyFloatingHorizontalInsets = false; + private int mResizeMode = RESIZE_MODE_INVALID; private final int mResizeShadowSize; private final Paint mVerticalResizeShadowPaint = new Paint(); private final Paint mHorizontalResizeShadowPaint = new Paint(); @@ -807,7 +808,9 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind updateElevation(); mAllowUpdateElevation = true; - if (changed && mDrawLegacyNavigationBarBackground) { + if (changed + && (mResizeMode == RESIZE_MODE_DOCKED_DIVIDER + || mDrawLegacyNavigationBarBackground)) { getViewRootImpl().requestInvalidateRootRenderNode(); } } @@ -2389,7 +2392,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind @Override public void onWindowDragResizeStart(Rect initialBounds, boolean fullscreen, Rect systemInsets, - Rect stableInsets) { + Rect stableInsets, int resizeMode) { if (mWindow.isDestroyed()) { // If the owner's window is gone, we should not be able to come here anymore. releaseThreadedRenderer(); @@ -2415,6 +2418,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind updateColorViews(null /* insets */, false); } + mResizeMode = resizeMode; getViewRootImpl().requestInvalidateRootRenderNode(); } @@ -2422,6 +2426,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind public void onWindowDragResizeEnd() { releaseThreadedRenderer(); updateColorViews(null /* insets */, false); + mResizeMode = RESIZE_MODE_INVALID; getViewRootImpl().requestInvalidateRootRenderNode(); } @@ -2466,7 +2471,9 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind } private void drawResizingShadowIfNeeded(RecordingCanvas canvas) { - if (mWindow.mIsFloating || mWindow.isTranslucent() || mWindow.isShowingWallpaper()) { + if (mResizeMode != RESIZE_MODE_DOCKED_DIVIDER || mWindow.mIsFloating + || mWindow.isTranslucent() + || mWindow.isShowingWallpaper()) { return; } canvas.save(); diff --git a/core/java/com/android/internal/view/BaseIWindow.java b/core/java/com/android/internal/view/BaseIWindow.java index 4a5ed7e8970d..2ac43099c741 100644 --- a/core/java/com/android/internal/view/BaseIWindow.java +++ b/core/java/com/android/internal/view/BaseIWindow.java @@ -53,7 +53,7 @@ public class BaseIWindow extends IWindow.Stub { @Override public void resized(ClientWindowFrames frames, boolean reportDraw, MergedConfiguration mergedConfiguration, InsetsState insetsState, boolean forceLayout, - boolean alwaysConsumeSystemBars, int displayId, int seqId, boolean dragResizing) { + boolean alwaysConsumeSystemBars, int displayId, int seqId, int resizeMode) { if (reportDraw) { try { mSession.finishDrawing(this, null /* postDrawTransaction */, seqId); diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java index cc5de3ea1ec2..65f552278890 100644 --- a/core/java/com/android/internal/widget/LockPatternUtils.java +++ b/core/java/com/android/internal/widget/LockPatternUtils.java @@ -1508,7 +1508,8 @@ public class LockPatternUtils { STRONG_AUTH_REQUIRED_AFTER_LOCKOUT, STRONG_AUTH_REQUIRED_AFTER_TIMEOUT, STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN, - STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT}) + STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT, + SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED}) @Retention(RetentionPolicy.SOURCE) public @interface StrongAuthFlags {} @@ -1561,11 +1562,18 @@ public class LockPatternUtils { public static final int STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT = 0x80; /** + * Some authentication is required because the trustagent either timed out or was disabled + * manually. + */ + public static final int SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED = 0x100; + + /** * Strong auth flags that do not prevent biometric methods from being accepted as auth. * If any other flags are set, biometric authentication is disabled. */ private static final int ALLOWING_BIOMETRIC = STRONG_AUTH_NOT_REQUIRED - | SOME_AUTH_REQUIRED_AFTER_USER_REQUEST; + | SOME_AUTH_REQUIRED_AFTER_USER_REQUEST + | SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED; private final SparseIntArray mStrongAuthRequiredForUser = new SparseIntArray(); private final H mHandler; diff --git a/core/jni/android_media_AudioFormat.h b/core/jni/android_media_AudioFormat.h index 962b50147f3d..a9b19062b764 100644 --- a/core/jni/android_media_AudioFormat.h +++ b/core/jni/android_media_AudioFormat.h @@ -49,6 +49,7 @@ #define ENCODING_DRA 28 #define ENCODING_DTS_HD_MA 29 #define ENCODING_DTS_UHD_P2 30 +#define ENCODING_DSD 31 #define ENCODING_INVALID 0 #define ENCODING_DEFAULT 1 @@ -122,6 +123,8 @@ static inline audio_format_t audioFormatToNative(int audioFormat) return AUDIO_FORMAT_DTS_HD_MA; case ENCODING_DTS_UHD_P2: return AUDIO_FORMAT_DTS_UHD_P2; + case ENCODING_DSD: + return AUDIO_FORMAT_DSD; default: return AUDIO_FORMAT_INVALID; } @@ -201,6 +204,8 @@ static inline int audioFormatFromNative(audio_format_t nativeFormat) return ENCODING_DTS_UHD_P2; case AUDIO_FORMAT_DEFAULT: return ENCODING_DEFAULT; + case AUDIO_FORMAT_DSD: + return ENCODING_DSD; default: return ENCODING_INVALID; } diff --git a/core/jni/android_media_AudioProfile.h b/core/jni/android_media_AudioProfile.h index 446bd6494f95..5096250e6bd3 100644 --- a/core/jni/android_media_AudioProfile.h +++ b/core/jni/android_media_AudioProfile.h @@ -25,6 +25,7 @@ namespace android { // keep these values in sync with AudioProfile.java #define ENCAPSULATION_TYPE_NONE 0 #define ENCAPSULATION_TYPE_IEC61937 1 +#define ENCAPSULATION_TYPE_PCM 2 static inline status_t audioEncapsulationTypeFromNative( audio_encapsulation_type_t nEncapsulationType, int* encapsulationType) { @@ -36,6 +37,9 @@ static inline status_t audioEncapsulationTypeFromNative( case AUDIO_ENCAPSULATION_TYPE_IEC61937: *encapsulationType = ENCAPSULATION_TYPE_IEC61937; break; + case AUDIO_ENCAPSULATION_TYPE_PCM: + *encapsulationType = ENCAPSULATION_TYPE_PCM; + break; default: result = BAD_VALUE; } diff --git a/core/proto/android/providers/settings/secure.proto b/core/proto/android/providers/settings/secure.proto index 556636ddd210..e6f942e49565 100644 --- a/core/proto/android/providers/settings/secure.proto +++ b/core/proto/android/providers/settings/secure.proto @@ -596,6 +596,15 @@ message SecureSettingsProto { optional SettingProto theme_customization_overlay_packages = 75 [ (android.privacy).dest = DEST_AUTOMATIC ]; optional SettingProto trust_agents_initialized = 57 [ (android.privacy).dest = DEST_AUTOMATIC ]; + message TrackpadGesture { + optional SettingProto trackpad_gesture_back_enabled = 1 [ (android.privacy).dest = DEST_AUTOMATIC ]; + optional SettingProto trackpad_gesture_home_enabled = 2 [ (android.privacy).dest = DEST_AUTOMATIC ]; + optional SettingProto trackpad_gesture_overview_enabled = 3 [ (android.privacy).dest = DEST_AUTOMATIC ]; + optional SettingProto trackpad_gesture_notification_enabled = 4 [ (android.privacy).dest = DEST_AUTOMATIC ]; + optional SettingProto trackpad_gesture_quick_switch_enabled = 5 [ (android.privacy).dest = DEST_AUTOMATIC ]; + } + optional TrackpadGesture trackpad_gesture = 94; + message Tts { option (android.msg_privacy).dest = DEST_EXPLICIT; @@ -687,5 +696,5 @@ message SecureSettingsProto { // Please insert fields in alphabetical order and group them into messages // if possible (to avoid reaching the method limit). - // Next tag = 94; + // Next tag = 95; } diff --git a/services/core/jni/onload_settings.cpp b/core/proto/android/server/background_install_control.proto index b21c34a9f9b9..38e6b4d21e2d 100644 --- a/services/core/jni/onload_settings.cpp +++ b/core/proto/android/server/background_install_control.proto @@ -14,26 +14,19 @@ * limitations under the License. */ -#include "jni.h" -#include "utils/Log.h" +syntax = "proto2"; +package com.android.server.pm; -namespace android { -int register_android_server_com_android_server_pm_Settings(JNIEnv* env); -}; +option java_multiple_files = true; -using namespace android; - -extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) { - JNIEnv* env = NULL; - jint result = -1; - - if (vm->GetEnv((void**)&env, JNI_VERSION_1_4) != JNI_OK) { - ALOGE("GetEnv failed!"); - return result; - } - ALOG_ASSERT(env, "Could not retrieve the env!"); - - register_android_server_com_android_server_pm_Settings(env); +// Proto for the background installed packages. +// It's used for serializing the background installed package info to disk. +message BackgroundInstalledPackagesProto { + repeated BackgroundInstalledPackageProto bg_installed_pkg = 1; +} - return JNI_VERSION_1_4; +// Proto for the background installed package entry +message BackgroundInstalledPackageProto { + optional string package_name = 1; + optional int32 user_id = 2; } diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 2cf241f9b249..fd4d4f8a00f4 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -4975,6 +4975,15 @@ <permission android:name="android.permission.ROTATE_SURFACE_FLINGER" android:protectionLevel="signature|recents" /> + <!-- @SystemApi Allows an application to provide hints to SurfaceFlinger that can influence + its wakes up time to compose the next frame. This is a subset of the capabilities granted + by {@link #ACCESS_SURFACE_FLINGER}. + <p>Not for use by third-party applications. + @hide + --> + <permission android:name="android.permission.WAKEUP_SURFACE_FLINGER" + android:protectionLevel="signature|recents" /> + <!-- Allows an application to take screen shots and more generally get access to the frame buffer data. <p>Not for use by third-party applications. diff --git a/core/res/OWNERS b/core/res/OWNERS index a2ef4005f5cb..b878189a9617 100644 --- a/core/res/OWNERS +++ b/core/res/OWNERS @@ -21,6 +21,11 @@ shanh@google.com tsuji@google.com yamasani@google.com +# WindowManager team +# TODO(262451702): Move WindowManager configs out of config.xml in a separate file +per-file core/res/res/values/config.xml = file:/services/core/java/com/android/server/wm/OWNERS +per-file core/res/res/values/symbols.xml = file:/services/core/java/com/android/server/wm/OWNERS + # Resources finalization per-file res/xml/public-staging.xml = file:/tools/aapt2/OWNERS per-file res/xml/public-final.xml = file:/tools/aapt2/OWNERS diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index 2d832bc71684..c8b0601621ed 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -8987,6 +8987,10 @@ <!-- The service that provides {@link android.service.voice.HotwordDetectionService}. @hide @SystemApi --> <attr name="hotwordDetectionService" format="string" /> + <!-- The service that provides {@link android.service.voice.VisualQueryDetectionService}. + @hide @SystemApi --> + <attr name="visualQueryDetectionService" format="string" /> + </declare-styleable> <!-- Use <code>game-service</code> as the root tag of the XML resource that diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 2fb766e46c5e..f995a6eeb5a3 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -5281,6 +5281,10 @@ <!-- Whether using split screen aspect ratio as a default aspect ratio for unresizable apps. --> <bool name="config_letterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled">false</bool> + <!-- Whether the specific behaviour for translucent activities letterboxing is enabled. + TODO(b/255532890) Enable when ignoreOrientationRequest is set --> + <bool name="config_letterboxIsEnabledForTranslucentActivities">false</bool> + <!-- Whether a camera compat controller is enabled to allow the user to apply or revert treatment for stretched issues in camera viewfinder. --> <bool name="config_isCameraCompatControlForStretchedIssuesEnabled">false</bool> diff --git a/core/res/res/values/locale_config.xml b/core/res/res/values/locale_config.xml index 78ec14579bf1..bd93aa9402c3 100644 --- a/core/res/res/values/locale_config.xml +++ b/core/res/res/values/locale_config.xml @@ -23,7 +23,7 @@ <item>ak-GH</item> <!-- Akan (Ghana) --> <item>am-ET</item> <!-- Amharic (Ethiopia) --> <item>ar-AE</item> <!-- Arabic (United Arab Emirates) --> - <item>ar-AE-u-nu-arab</item> <!-- Arabic (United Arab Emirates, Arabic Digits) --> + <item>ar-AE-u-nu-arab</item> <!-- Arabic (United Arab Emirates, Arabic-Indic Digits) --> <item>ar-BH</item> <!-- Arabic (Bahrain) --> <item>ar-BH-u-nu-latn</item> <!-- Arabic (Bahrain, Western Digits) --> <item>ar-DJ</item> <!-- Arabic (Djibouti) --> @@ -108,6 +108,7 @@ <item>cgg-UG</item> <!-- Chiga (Uganda) --> <item>chr-US</item> <!-- Cherokee (United States) --> <item>cs-CZ</item> <!-- Czech (Czechia) --> + <item>cv-RU</item> <!-- Chuvash (Russia) --> <item>cy-GB</item> <!-- Welsh (United Kingdom) --> <item>da-DK</item> <!-- Danish (Denmark) --> <item>da-GL</item> <!-- Danish (Greenland) --> @@ -270,42 +271,42 @@ <item>fa-AF-u-nu-latn</item> <!-- Persian (Afghanistan, Western Digits) --> <item>fa-IR</item> <!-- Persian (Iran) --> <item>fa-IR-u-nu-latn</item> <!-- Persian (Iran, Western Digits) --> - <item>ff-Adlm-BF</item> <!-- Fulah (Adlam, Burkina Faso) --> - <item>ff-Adlm-BF-u-nu-latn</item> <!-- Fulah (Adlam, Burkina Faso, Western Digits) --> - <item>ff-Adlm-CM</item> <!-- Fulah (Adlam, Cameroon) --> - <item>ff-Adlm-CM-u-nu-latn</item> <!-- Fulah (Adlam, Cameroon, Western Digits) --> - <item>ff-Adlm-GH</item> <!-- Fulah (Adlam, Ghana) --> - <item>ff-Adlm-GH-u-nu-latn</item> <!-- Fulah (Adlam, Ghana, Western Digits) --> - <item>ff-Adlm-GM</item> <!-- Fulah (Adlam, Gambia) --> - <item>ff-Adlm-GM-u-nu-latn</item> <!-- Fulah (Adlam, Gambia, Western Digits) --> - <item>ff-Adlm-GN</item> <!-- Fulah (Adlam, Guinea) --> - <item>ff-Adlm-GN-u-nu-latn</item> <!-- Fulah (Adlam, Guinea, Western Digits) --> - <item>ff-Adlm-GW</item> <!-- Fulah (Adlam, Guinea-Bissau) --> - <item>ff-Adlm-GW-u-nu-latn</item> <!-- Fulah (Adlam, Guinea-Bissau, Western Digits) --> - <item>ff-Adlm-LR</item> <!-- Fulah (Adlam, Liberia) --> - <item>ff-Adlm-LR-u-nu-latn</item> <!-- Fulah (Adlam, Liberia, Western Digits) --> - <item>ff-Adlm-MR</item> <!-- Fulah (Adlam, Mauritania) --> - <item>ff-Adlm-MR-u-nu-latn</item> <!-- Fulah (Adlam, Mauritania, Western Digits) --> - <item>ff-Adlm-NE</item> <!-- Fulah (Adlam, Niger) --> - <item>ff-Adlm-NE-u-nu-latn</item> <!-- Fulah (Adlam, Niger, Western Digits) --> - <item>ff-Adlm-NG</item> <!-- Fulah (Adlam, Nigeria) --> - <item>ff-Adlm-NG-u-nu-latn</item> <!-- Fulah (Adlam, Nigeria, Western Digits) --> - <item>ff-Adlm-SL</item> <!-- Fulah (Adlam, Sierra Leone) --> - <item>ff-Adlm-SL-u-nu-latn</item> <!-- Fulah (Adlam, Sierra Leone, Western Digits) --> - <item>ff-Adlm-SN</item> <!-- Fulah (Adlam, Senegal) --> - <item>ff-Adlm-SN-u-nu-latn</item> <!-- Fulah (Adlam, Senegal, Western Digits) --> - <item>ff-Latn-BF</item> <!-- Fulah (Latin, Burkina Faso) --> - <item>ff-Latn-CM</item> <!-- Fulah (Latin, Cameroon) --> - <item>ff-Latn-GH</item> <!-- Fulah (Latin, Ghana) --> - <item>ff-Latn-GM</item> <!-- Fulah (Latin, Gambia) --> - <item>ff-Latn-GN</item> <!-- Fulah (Latin, Guinea) --> - <item>ff-Latn-GW</item> <!-- Fulah (Latin, Guinea-Bissau) --> - <item>ff-Latn-LR</item> <!-- Fulah (Latin, Liberia) --> - <item>ff-Latn-MR</item> <!-- Fulah (Latin, Mauritania) --> - <item>ff-Latn-NE</item> <!-- Fulah (Latin, Niger) --> - <item>ff-Latn-NG</item> <!-- Fulah (Latin, Nigeria) --> - <item>ff-Latn-SL</item> <!-- Fulah (Latin, Sierra Leone) --> - <item>ff-Latn-SN</item> <!-- Fulah (Latin, Senegal) --> + <item>ff-Adlm-BF</item> <!-- Fula (Adlam, Burkina Faso) --> + <item>ff-Adlm-BF-u-nu-latn</item> <!-- Fula (Adlam, Burkina Faso, Western Digits) --> + <item>ff-Adlm-CM</item> <!-- Fula (Adlam, Cameroon) --> + <item>ff-Adlm-CM-u-nu-latn</item> <!-- Fula (Adlam, Cameroon, Western Digits) --> + <item>ff-Adlm-GH</item> <!-- Fula (Adlam, Ghana) --> + <item>ff-Adlm-GH-u-nu-latn</item> <!-- Fula (Adlam, Ghana, Western Digits) --> + <item>ff-Adlm-GM</item> <!-- Fula (Adlam, Gambia) --> + <item>ff-Adlm-GM-u-nu-latn</item> <!-- Fula (Adlam, Gambia, Western Digits) --> + <item>ff-Adlm-GN</item> <!-- Fula (Adlam, Guinea) --> + <item>ff-Adlm-GN-u-nu-latn</item> <!-- Fula (Adlam, Guinea, Western Digits) --> + <item>ff-Adlm-GW</item> <!-- Fula (Adlam, Guinea-Bissau) --> + <item>ff-Adlm-GW-u-nu-latn</item> <!-- Fula (Adlam, Guinea-Bissau, Western Digits) --> + <item>ff-Adlm-LR</item> <!-- Fula (Adlam, Liberia) --> + <item>ff-Adlm-LR-u-nu-latn</item> <!-- Fula (Adlam, Liberia, Western Digits) --> + <item>ff-Adlm-MR</item> <!-- Fula (Adlam, Mauritania) --> + <item>ff-Adlm-MR-u-nu-latn</item> <!-- Fula (Adlam, Mauritania, Western Digits) --> + <item>ff-Adlm-NE</item> <!-- Fula (Adlam, Niger) --> + <item>ff-Adlm-NE-u-nu-latn</item> <!-- Fula (Adlam, Niger, Western Digits) --> + <item>ff-Adlm-NG</item> <!-- Fula (Adlam, Nigeria) --> + <item>ff-Adlm-NG-u-nu-latn</item> <!-- Fula (Adlam, Nigeria, Western Digits) --> + <item>ff-Adlm-SL</item> <!-- Fula (Adlam, Sierra Leone) --> + <item>ff-Adlm-SL-u-nu-latn</item> <!-- Fula (Adlam, Sierra Leone, Western Digits) --> + <item>ff-Adlm-SN</item> <!-- Fula (Adlam, Senegal) --> + <item>ff-Adlm-SN-u-nu-latn</item> <!-- Fula (Adlam, Senegal, Western Digits) --> + <item>ff-Latn-BF</item> <!-- Fula (Latin, Burkina Faso) --> + <item>ff-Latn-CM</item> <!-- Fula (Latin, Cameroon) --> + <item>ff-Latn-GH</item> <!-- Fula (Latin, Ghana) --> + <item>ff-Latn-GM</item> <!-- Fula (Latin, Gambia) --> + <item>ff-Latn-GN</item> <!-- Fula (Latin, Guinea) --> + <item>ff-Latn-GW</item> <!-- Fula (Latin, Guinea-Bissau) --> + <item>ff-Latn-LR</item> <!-- Fula (Latin, Liberia) --> + <item>ff-Latn-MR</item> <!-- Fula (Latin, Mauritania) --> + <item>ff-Latn-NE</item> <!-- Fula (Latin, Niger) --> + <item>ff-Latn-NG</item> <!-- Fula (Latin, Nigeria) --> + <item>ff-Latn-SL</item> <!-- Fula (Latin, Sierra Leone) --> + <item>ff-Latn-SN</item> <!-- Fula (Latin, Senegal) --> <item>fi-FI</item> <!-- Finnish (Finland) --> <item>fil-PH</item> <!-- Filipino (Philippines) --> <item>fo-DK</item> <!-- Faroese (Denmark) --> @@ -373,12 +374,13 @@ <item>ha-NG</item> <!-- Hausa (Nigeria) --> <item>haw-US</item> <!-- Hawaiian (United States) --> <item>hi-IN</item> <!-- Hindi (India) --> + <item>hi-Latn-IN</item> <!-- Hindi (Latin, India) --> <item>hr-BA</item> <!-- Croatian (Bosnia & Herzegovina) --> <item>hr-HR</item> <!-- Croatian (Croatia) --> <item>hsb-DE</item> <!-- Upper Sorbian (Germany) --> <item>hu-HU</item> <!-- Hungarian (Hungary) --> <item>hy-AM</item> <!-- Armenian (Armenia) --> - <item>ia-001</item> <!-- Interlingua (World) --> + <item>ia-001</item> <!-- Interlingua (world) --> <item>ig-NG</item> <!-- Igbo (Nigeria) --> <item>ii-CN</item> <!-- Sichuan Yi (China) --> <item>in-ID</item> <!-- Indonesian (Indonesia) --> @@ -397,6 +399,7 @@ <item>kam-KE</item> <!-- Kamba (Kenya) --> <item>kde-TZ</item> <!-- Makonde (Tanzania) --> <item>kea-CV</item> <!-- Kabuverdianu (Cape Verde) --> + <item>kgp-BR</item> <!-- Kaingang (Brazil) --> <item>khq-ML</item> <!-- Koyra Chiini (Mali) --> <item>ki-KE</item> <!-- Kikuyu (Kenya) --> <item>kk-KZ</item> <!-- Kazakh (Kazakhstan) --> @@ -408,6 +411,9 @@ <item>ko-KP</item> <!-- Korean (North Korea) --> <item>ko-KR</item> <!-- Korean (South Korea) --> <item>kok-IN</item> <!-- Konkani (India) --> + <item>ks-Arab-IN</item> <!-- Kashmiri (Arabic, India) --> + <item>ks-Arab-IN-u-nu-latn</item> <!-- Kashmiri (Arabic, India, Western Digits) --> + <item>ks-Deva-IN</item> <!-- Kashmiri (Devanagari, India) --> <item>ksb-TZ</item> <!-- Shambala (Tanzania) --> <item>ksf-CM</item> <!-- Bafia (Cameroon) --> <item>ksh-DE</item> <!-- Colognian (Germany) --> @@ -435,7 +441,7 @@ <item>mg-MG</item> <!-- Malagasy (Madagascar) --> <item>mgh-MZ</item> <!-- Makhuwa-Meetto (Mozambique) --> <item>mgo-CM</item> <!-- Metaʼ (Cameroon) --> - <item>mi-NZ</item> <!-- Maori (New Zealand) --> + <item>mi-NZ</item> <!-- Māori (New Zealand) --> <item>mk-MK</item> <!-- Macedonian (North Macedonia) --> <item>ml-IN</item> <!-- Malayalam (India) --> <item>mn-MN</item> <!-- Mongolian (Mongolia) --> @@ -500,6 +506,8 @@ <item>qu-BO</item> <!-- Quechua (Bolivia) --> <item>qu-EC</item> <!-- Quechua (Ecuador) --> <item>qu-PE</item> <!-- Quechua (Peru) --> + <item>raj-IN</item> <!-- Rajasthani (India) --> + <item>raj-IN-u-nu-latn</item> <!-- Rajasthani (India, Western Digits) --> <item>rm-CH</item> <!-- Romansh (Switzerland) --> <item>rn-BI</item> <!-- Rundi (Burundi) --> <item>ro-MD</item> <!-- Romanian (Moldova) --> @@ -514,11 +522,13 @@ <item>rw-RW</item> <!-- Kinyarwanda (Rwanda) --> <item>rwk-TZ</item> <!-- Rwa (Tanzania) --> <item>sa-IN</item> <!-- Sanskrit (India) --> - <item>sah-RU</item> <!-- Sakha (Russia) --> + <item>sa-IN-u-nu-latn</item> <!-- Sanskrit (India, Western Digits) --> + <item>sah-RU</item> <!-- Yakut (Russia) --> <item>saq-KE</item> <!-- Samburu (Kenya) --> <item>sat-IN</item> <!-- Santali (India) --> <item>sat-IN-u-nu-latn</item> <!-- Santali (India, Western Digits) --> <item>sbp-TZ</item> <!-- Sangu (Tanzania) --> + <item>sc-IT</item> <!-- Sardinian (Italy) --> <item>sd-Arab-PK</item> <!-- Sindhi (Arabic, Pakistan) --> <item>sd-Arab-PK-u-nu-latn</item> <!-- Sindhi (Arabic, Pakistan, Western Digits) --> <item>sd-Deva-IN</item> <!-- Sindhi (Devanagari, India) --> @@ -565,6 +575,8 @@ <item>teo-UG</item> <!-- Teso (Uganda) --> <item>tg-TJ</item> <!-- Tajik (Tajikistan) --> <item>th-TH</item> <!-- Thai (Thailand) --> + <item>ti-ER</item> <!-- Tigrinya (Eritrea) --> + <item>ti-ET</item> <!-- Tigrinya (Ethiopia) --> <item>tk-TM</item> <!-- Turkmen (Turkmenistan) --> <item>to-TO</item> <!-- Tongan (Tonga) --> <item>tr-CY</item> <!-- Turkish (Cyprus) --> @@ -586,10 +598,14 @@ <item>vun-TZ</item> <!-- Vunjo (Tanzania) --> <item>wae-CH</item> <!-- Walser (Switzerland) --> <item>wo-SN</item> <!-- Wolof (Senegal) --> + <item>xh-ZA</item> <!-- Xhosa (South Africa) --> <item>xog-UG</item> <!-- Soga (Uganda) --> <item>yav-CM</item> <!-- Yangben (Cameroon) --> <item>yo-BJ</item> <!-- Yoruba (Benin) --> <item>yo-NG</item> <!-- Yoruba (Nigeria) --> + <item>yrl-BR</item> <!-- Nheengatu (Brazil) --> + <item>yrl-CO</item> <!-- Nheengatu (Colombia) --> + <item>yrl-VE</item> <!-- Nheengatu (Venezuela) --> <item>yue-Hans-CN</item> <!-- Cantonese (Simplified, China) --> <item>yue-Hant-HK</item> <!-- Cantonese (Traditional, Hong Kong) --> <item>zgh-MA</item> <!-- Standard Moroccan Tamazight (Morocco) --> diff --git a/core/res/res/values/public-staging.xml b/core/res/res/values/public-staging.xml index 90141e57ce8d..0aeee10caddf 100644 --- a/core/res/res/values/public-staging.xml +++ b/core/res/res/values/public-staging.xml @@ -118,6 +118,7 @@ <public name="enableTextStylingShortcuts" /> <public name="requiredDisplayCategory"/> <public name="removed_maxConcurrentSessionsCount" /> + <public name="visualQueryDetectionService" /> </staging-public-group> <staging-public-group type="id" first-id="0x01cd0000"> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index e8304d801ab2..151530bcccfb 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -4397,6 +4397,9 @@ <!-- Set to true to make assistant show in front of the dream/screensaver. --> <java-symbol type="bool" name="config_assistantOnTopOfDream"/> + <!-- Set to true to enable letterboxing on translucent activities. --> + <java-symbol type="bool" name="config_letterboxIsEnabledForTranslucentActivities" /> + <java-symbol type="string" name="config_overrideComponentUiPackage" /> <java-symbol type="string" name="notification_channel_network_status" /> diff --git a/core/tests/utiltests/src/com/android/internal/util/LockPatternUtilsTest.java b/core/tests/utiltests/src/com/android/internal/util/LockPatternUtilsTest.java index 4679a9ea6f66..0b7019995acb 100644 --- a/core/tests/utiltests/src/com/android/internal/util/LockPatternUtilsTest.java +++ b/core/tests/utiltests/src/com/android/internal/util/LockPatternUtilsTest.java @@ -19,6 +19,9 @@ package com.android.internal.util; import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_MANAGED; import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED; +import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED; +import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT; + import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertFalse; @@ -37,6 +40,7 @@ import android.content.ComponentName; import android.content.Context; import android.content.ContextWrapper; import android.content.pm.UserInfo; +import android.os.Looper; import android.os.RemoteException; import android.os.UserManager; import android.provider.Settings; @@ -233,6 +237,45 @@ public class LockPatternUtilsTest { ComponentName.unflattenFromString("com.test/.TestAgent")); } + @Test + public void isBiometricAllowedForUser_afterTrustagentExpired_returnsTrue() + throws RemoteException { + TestStrongAuthTracker tracker = createStrongAuthTracker(); + tracker.changeStrongAuth(SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED); + + assertTrue(tracker.isBiometricAllowedForUser( + /* isStrongBiometric = */ true, + DEMO_USER_ID)); + } + + @Test + public void isBiometricAllowedForUser_afterLockout_returnsFalse() + throws RemoteException { + TestStrongAuthTracker tracker = createStrongAuthTracker(); + tracker.changeStrongAuth(STRONG_AUTH_REQUIRED_AFTER_LOCKOUT); + + assertFalse(tracker.isBiometricAllowedForUser( + /* isStrongBiometric = */ true, + DEMO_USER_ID)); + } + + + private TestStrongAuthTracker createStrongAuthTracker() { + final Context context = new ContextWrapper(InstrumentationRegistry.getTargetContext()); + return new TestStrongAuthTracker(context, Looper.getMainLooper()); + } + + private static class TestStrongAuthTracker extends LockPatternUtils.StrongAuthTracker { + + TestStrongAuthTracker(Context context, Looper looper) { + super(context, looper); + } + + public void changeStrongAuth(@StrongAuthFlags int strongAuthFlags) { + handleStrongAuthRequiredChanged(strongAuthFlags, DEMO_USER_ID); + } + } + private ILockSettings createTestLockSettings() { final Context context = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext())); mLockPatternUtils = spy(new LockPatternUtils(context)); diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index d4ac531b9c2d..1ab5e4bef2bb 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -169,6 +169,12 @@ "group": "WM_ERROR", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, + "-1944652783": { + "message": "Unable to tell MediaProjectionManagerService to stop the active projection: %s", + "level": "ERROR", + "group": "WM_DEBUG_CONTENT_RECORDING", + "at": "com\/android\/server\/wm\/ContentRecorder.java" + }, "-1941440781": { "message": "Creating Pending Move-to-back: %s", "level": "VERBOSE", @@ -715,6 +721,12 @@ "group": "WM_DEBUG_ADD_REMOVE", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, + "-1423223548": { + "message": "Unable to tell MediaProjectionManagerService about resizing the active projection: %s", + "level": "ERROR", + "group": "WM_DEBUG_CONTENT_RECORDING", + "at": "com\/android\/server\/wm\/ContentRecorder.java" + }, "-1421296808": { "message": "Moving to RESUMED: %s (in existing)", "level": "VERBOSE", diff --git a/identity/java/android/security/identity/AuthenticationKeyMetadata.java b/identity/java/android/security/identity/AuthenticationKeyMetadata.java index d4c28f8d459a..c6abc22c305f 100644 --- a/identity/java/android/security/identity/AuthenticationKeyMetadata.java +++ b/identity/java/android/security/identity/AuthenticationKeyMetadata.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 The Android Open Source Project + * Copyright 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. @@ -24,11 +24,11 @@ import java.time.Instant; /** * Data about authentication keys. */ -public class AuthenticationKeyMetadata { +public final class AuthenticationKeyMetadata { private int mUsageCount; private Instant mExpirationDate; - AuthenticationKeyMetadata(int usageCount, Instant expirationDate) { + AuthenticationKeyMetadata(int usageCount, @NonNull Instant expirationDate) { mUsageCount = usageCount; mExpirationDate = expirationDate; } diff --git a/libs/WindowManager/Shell/AndroidManifest.xml b/libs/WindowManager/Shell/AndroidManifest.xml index 8881be7326a9..36d3313a9f3b 100644 --- a/libs/WindowManager/Shell/AndroidManifest.xml +++ b/libs/WindowManager/Shell/AndroidManifest.xml @@ -21,5 +21,6 @@ <uses-permission android:name="android.permission.CAPTURE_BLACKOUT_CONTENT" /> <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" /> <uses-permission android:name="android.permission.ROTATE_SURFACE_FLINGER" /> + <uses-permission android:name="android.permission.WAKEUP_SURFACE_FLINGER" /> <uses-permission android:name="android.permission.READ_FRAME_BUFFER" /> </manifest> diff --git a/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml b/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml index 70755e6cc3cf..dcce4698c252 100644 --- a/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml +++ b/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml @@ -44,67 +44,15 @@ android:background="@color/tv_pip_menu_dim_layer" android:alpha="0"/> - <ScrollView - android:id="@+id/tv_pip_menu_scroll" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:scrollbars="none" - android:visibility="gone"/> - - <HorizontalScrollView - android:id="@+id/tv_pip_menu_horizontal_scroll" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:scrollbars="none"> - - <LinearLayout - android:id="@+id/tv_pip_menu_action_buttons" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:orientation="horizontal" - android:alpha="0"> - - <Space - android:layout_width="@dimen/pip_menu_button_wrapper_margin" - android:layout_height="@dimen/pip_menu_button_wrapper_margin"/> - - <com.android.wm.shell.common.TvWindowMenuActionButton - android:id="@+id/tv_pip_menu_fullscreen_button" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:src="@drawable/pip_ic_fullscreen_white" - android:text="@string/pip_fullscreen" /> - - <com.android.wm.shell.common.TvWindowMenuActionButton - android:id="@+id/tv_pip_menu_close_button" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:src="@drawable/pip_ic_close_white" - android:text="@string/pip_close" /> - - <!-- More TvWindowMenuActionButtons may be added here at runtime. --> - - <com.android.wm.shell.common.TvWindowMenuActionButton - android:id="@+id/tv_pip_menu_move_button" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:src="@drawable/pip_ic_move_white" - android:text="@string/pip_move" /> - - <com.android.wm.shell.common.TvWindowMenuActionButton - android:id="@+id/tv_pip_menu_expand_button" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:src="@drawable/pip_ic_collapse" - android:visibility="gone" - android:text="@string/pip_collapse" /> - - <Space - android:layout_width="@dimen/pip_menu_button_wrapper_margin" - android:layout_height="@dimen/pip_menu_button_wrapper_margin"/> - - </LinearLayout> - </HorizontalScrollView> + <com.android.internal.widget.RecyclerView + android:id="@+id/tv_pip_menu_action_buttons" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:padding="@dimen/pip_menu_button_start_end_offset" + android:clipToPadding="false" + android:alpha="0" + android:contentDescription="@string/a11y_pip_menu_entered"/> </FrameLayout> <!-- Frame around the content, just overlapping the corners to make them round --> diff --git a/libs/WindowManager/Shell/res/layout/tv_window_menu_action_button.xml b/libs/WindowManager/Shell/res/layout/tv_window_menu_action_button.xml index c4dbd39c729a..b2ac85b018be 100644 --- a/libs/WindowManager/Shell/res/layout/tv_window_menu_action_button.xml +++ b/libs/WindowManager/Shell/res/layout/tv_window_menu_action_button.xml @@ -21,6 +21,7 @@ android:layout_width="@dimen/tv_window_menu_button_size" android:layout_height="@dimen/tv_window_menu_button_size" android:padding="@dimen/tv_window_menu_button_margin" + android:duplicateParentState="true" android:stateListAnimator="@animator/tv_window_menu_action_button_animator" android:focusable="true"> diff --git a/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml b/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml index 9833a88a1c0a..0b61d7a85d9e 100644 --- a/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml +++ b/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml @@ -28,7 +28,7 @@ <dimen name="pip_menu_background_corner_radius">6dp</dimen> <dimen name="pip_menu_border_width">4dp</dimen> <dimen name="pip_menu_outer_space">24dp</dimen> - <dimen name="pip_menu_button_wrapper_margin">26dp</dimen> + <dimen name="pip_menu_button_start_end_offset">30dp</dimen> <!-- outer space minus border width --> <dimen name="pip_menu_outer_space_frame">20dp</dimen> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java index 94aeb2beb1e0..af13bf54f691 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java @@ -346,7 +346,7 @@ public class SystemWindows { public void resized(ClientWindowFrames frames, boolean reportDraw, MergedConfiguration newMergedConfiguration, InsetsState insetsState, boolean forceLayout, boolean alwaysConsumeSystemBars, int displayId, int syncSeqId, - boolean dragResizing) {} + int resizeMode) {} @Override public void insetsControlChanged(InsetsState insetsState, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TvWindowMenuActionButton.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TvWindowMenuActionButton.java index 39b0b5500cea..8ba785a1f03a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TvWindowMenuActionButton.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TvWindowMenuActionButton.java @@ -19,6 +19,8 @@ package com.android.wm.shell.common; import android.content.Context; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; +import android.graphics.drawable.Icon; +import android.os.Handler; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; @@ -30,11 +32,11 @@ import com.android.wm.shell.R; /** * A common action button for TV window menu layouts. */ -public class TvWindowMenuActionButton extends RelativeLayout implements View.OnClickListener { +public class TvWindowMenuActionButton extends RelativeLayout { private final ImageView mIconImageView; private final View mButtonBackgroundView; - private final View mButtonView; - private OnClickListener mOnClickListener; + + private Icon mCurrentIcon; public TvWindowMenuActionButton(Context context) { this(context, null, 0, 0); @@ -56,7 +58,6 @@ public class TvWindowMenuActionButton extends RelativeLayout implements View.OnC inflater.inflate(R.layout.tv_window_menu_action_button, this); mIconImageView = findViewById(R.id.icon); - mButtonView = findViewById(R.id.button); mButtonBackgroundView = findViewById(R.id.background); final int[] values = new int[]{android.R.attr.src, android.R.attr.text}; @@ -71,23 +72,6 @@ public class TvWindowMenuActionButton extends RelativeLayout implements View.OnC typedArray.recycle(); } - @Override - public void setOnClickListener(OnClickListener listener) { - // We do not want to set an OnClickListener to the TvWindowMenuActionButton itself, but only - // to the ImageView. So let's "cash" the listener we've been passed here and set a "proxy" - // listener to the ImageView. - mOnClickListener = listener; - mButtonView.setOnClickListener(listener != null ? this : null); - } - - @Override - public void onClick(View v) { - if (mOnClickListener != null) { - // Pass the correct view - this. - mOnClickListener.onClick(this); - } - } - /** * Sets the drawable for the button with the given drawable. */ @@ -104,11 +88,24 @@ public class TvWindowMenuActionButton extends RelativeLayout implements View.OnC } } + public void setImageIconAsync(Icon icon, Handler handler) { + mCurrentIcon = icon; + // Remove old image while waiting for the new one to load. + mIconImageView.setImageDrawable(null); + icon.loadDrawableAsync(mContext, d -> { + // The image hasn't been set any other way and the drawable belongs to the most + // recently set Icon. + if (mIconImageView.getDrawable() == null && mCurrentIcon == icon) { + mIconImageView.setImageDrawable(d); + } + }, handler); + } + /** * Sets the text for description the with the given string. */ public void setTextAndDescription(CharSequence text) { - mButtonView.setContentDescription(text); + setContentDescription(text); } /** @@ -118,16 +115,6 @@ public class TvWindowMenuActionButton extends RelativeLayout implements View.OnC setTextAndDescription(getContext().getString(resId)); } - @Override - public void setEnabled(boolean enabled) { - mButtonView.setEnabled(enabled); - } - - @Override - public boolean isEnabled() { - return mButtonView.isEnabled(); - } - /** * Marks this button as a custom close action button. * This changes the style of the action button to highlight that this action finishes the @@ -147,10 +134,10 @@ public class TvWindowMenuActionButton extends RelativeLayout implements View.OnC @Override public String toString() { - if (mButtonView.getContentDescription() == null) { + if (getContentDescription() == null) { return TvWindowMenuActionButton.class.getSimpleName(); } - return mButtonView.getContentDescription().toString(); + return getContentDescription().toString(); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java index 8022e9b1cd81..b144d22fc3ee 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java @@ -81,6 +81,7 @@ public abstract class TvPipModule { PipParamsChangedForwarder pipParamsChangedForwarder, DisplayController displayController, WindowManagerShellWrapper windowManagerShellWrapper, + @ShellMainThread Handler mainHandler, // needed for registerReceiverForAllUsers() @ShellMainThread ShellExecutor mainExecutor) { return Optional.of( TvPipController.create( @@ -100,6 +101,7 @@ public abstract class TvPipModule { pipParamsChangedForwarder, displayController, windowManagerShellWrapper, + mainHandler, mainExecutor)); } @@ -157,22 +159,17 @@ public abstract class TvPipModule { Context context, TvPipBoundsState tvPipBoundsState, SystemWindows systemWindows, - PipMediaController pipMediaController, @ShellMainThread Handler mainHandler) { - return new TvPipMenuController(context, tvPipBoundsState, systemWindows, pipMediaController, - mainHandler); + return new TvPipMenuController(context, tvPipBoundsState, systemWindows, mainHandler); } - // Handler needed for registerReceiverForAllUsers() @WMSingleton @Provides static TvPipNotificationController provideTvPipNotificationController(Context context, PipMediaController pipMediaController, - PipParamsChangedForwarder pipParamsChangedForwarder, - TvPipBoundsState tvPipBoundsState, - @ShellMainThread Handler mainHandler) { + PipParamsChangedForwarder pipParamsChangedForwarder) { return new TvPipNotificationController(context, pipMediaController, - pipParamsChangedForwarder, tvPipBoundsState, mainHandler); + pipParamsChangedForwarder); } @WMSingleton diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipAction.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipAction.java new file mode 100644 index 000000000000..222307fba8c2 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipAction.java @@ -0,0 +1,87 @@ +/* + * 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.wm.shell.pip.tv; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.app.Notification; +import android.app.PendingIntent; +import android.content.Context; +import android.os.Handler; + +import com.android.wm.shell.common.TvWindowMenuActionButton; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Objects; + +abstract class TvPipAction { + + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = {"ACTION_"}, value = { + ACTION_FULLSCREEN, + ACTION_CLOSE, + ACTION_MOVE, + ACTION_EXPAND_COLLAPSE, + ACTION_CUSTOM, + ACTION_CUSTOM_CLOSE + }) + public @interface ActionType { + } + + public static final int ACTION_FULLSCREEN = 0; + public static final int ACTION_CLOSE = 1; + public static final int ACTION_MOVE = 2; + public static final int ACTION_EXPAND_COLLAPSE = 3; + public static final int ACTION_CUSTOM = 4; + public static final int ACTION_CUSTOM_CLOSE = 5; + + @ActionType + private final int mActionType; + + @NonNull + private final SystemActionsHandler mSystemActionsHandler; + + TvPipAction(@ActionType int actionType, @NonNull SystemActionsHandler systemActionsHandler) { + Objects.requireNonNull(systemActionsHandler); + mActionType = actionType; + mSystemActionsHandler = systemActionsHandler; + } + + boolean isCloseAction() { + return mActionType == ACTION_CLOSE || mActionType == ACTION_CUSTOM_CLOSE; + } + + @ActionType + int getActionType() { + return mActionType; + } + + abstract void populateButton(@NonNull TvWindowMenuActionButton button, Handler mainHandler); + + abstract PendingIntent getPendingIntent(); + + void executeAction() { + mSystemActionsHandler.executeAction(mActionType); + } + + abstract Notification.Action toNotificationAction(Context context); + + interface SystemActionsHandler { + void executeAction(@TvPipAction.ActionType int actionType); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipActionsProvider.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipActionsProvider.java new file mode 100644 index 000000000000..fa62a73ca9b4 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipActionsProvider.java @@ -0,0 +1,245 @@ +/* + * 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.wm.shell.pip.tv; + +import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_CLOSE; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_CUSTOM; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_CUSTOM_CLOSE; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_EXPAND_COLLAPSE; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_FULLSCREEN; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_MOVE; +import static com.android.wm.shell.pip.tv.TvPipController.ACTION_CLOSE_PIP; +import static com.android.wm.shell.pip.tv.TvPipController.ACTION_MOVE_PIP; +import static com.android.wm.shell.pip.tv.TvPipController.ACTION_TOGGLE_EXPANDED_PIP; +import static com.android.wm.shell.pip.tv.TvPipController.ACTION_TO_FULLSCREEN; + +import android.annotation.NonNull; +import android.app.RemoteAction; +import android.content.Context; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.R; +import com.android.wm.shell.pip.PipMediaController; +import com.android.wm.shell.pip.PipUtils; +import com.android.wm.shell.protolog.ShellProtoLogGroup; + +import java.util.ArrayList; +import java.util.List; + +/** + * Creates the system TvPipActions (fullscreen, close, move, expand/collapse), and handles all the + * changes to the actions, including the custom app actions and media actions. Other components can + * listen to those changes. + */ +public class TvPipActionsProvider implements TvPipAction.SystemActionsHandler { + private static final String TAG = TvPipActionsProvider.class.getSimpleName(); + + private static final int CLOSE_ACTION_INDEX = 1; + private static final int FIRST_CUSTOM_ACTION_INDEX = 2; + + private final List<Listener> mListeners = new ArrayList<>(); + private final TvPipAction.SystemActionsHandler mSystemActionsHandler; + + private final List<TvPipAction> mActionsList; + private final TvPipSystemAction mDefaultCloseAction; + private final TvPipSystemAction mExpandCollapseAction; + + private final List<RemoteAction> mMediaActions = new ArrayList<>(); + private final List<RemoteAction> mAppActions = new ArrayList<>(); + + public TvPipActionsProvider(Context context, PipMediaController pipMediaController, + TvPipAction.SystemActionsHandler systemActionsHandler) { + mSystemActionsHandler = systemActionsHandler; + + mActionsList = new ArrayList<>(); + mActionsList.add(new TvPipSystemAction(ACTION_FULLSCREEN, R.string.pip_fullscreen, + R.drawable.pip_ic_fullscreen_white, ACTION_TO_FULLSCREEN, context, + mSystemActionsHandler)); + + mDefaultCloseAction = new TvPipSystemAction(ACTION_CLOSE, R.string.pip_close, + R.drawable.pip_ic_close_white, ACTION_CLOSE_PIP, context, mSystemActionsHandler); + mActionsList.add(mDefaultCloseAction); + + mActionsList.add(new TvPipSystemAction(ACTION_MOVE, R.string.pip_move, + R.drawable.pip_ic_move_white, ACTION_MOVE_PIP, context, mSystemActionsHandler)); + + mExpandCollapseAction = new TvPipSystemAction(ACTION_EXPAND_COLLAPSE, R.string.pip_collapse, + R.drawable.pip_ic_collapse, ACTION_TOGGLE_EXPANDED_PIP, context, + mSystemActionsHandler); + mActionsList.add(mExpandCollapseAction); + + pipMediaController.addActionListener(this::onMediaActionsChanged); + } + + @Override + public void executeAction(@TvPipAction.ActionType int actionType) { + if (mSystemActionsHandler != null) { + mSystemActionsHandler.executeAction(actionType); + } + } + + private void notifyActionsChanged(int added, int changed, int startIndex) { + for (Listener listener : mListeners) { + listener.onActionsChanged(added, changed, startIndex); + } + } + + @VisibleForTesting(visibility = PACKAGE) + public void setAppActions(@NonNull List<RemoteAction> appActions, RemoteAction closeAction) { + // Update close action. + mActionsList.set(CLOSE_ACTION_INDEX, + closeAction == null ? mDefaultCloseAction + : new TvPipCustomAction(ACTION_CUSTOM_CLOSE, closeAction, + mSystemActionsHandler)); + notifyActionsChanged(/* added= */ 0, /* updated= */ 1, CLOSE_ACTION_INDEX); + + // Replace custom actions with new ones. + mAppActions.clear(); + for (RemoteAction action : appActions) { + if (action != null && !PipUtils.remoteActionsMatch(action, closeAction)) { + // Only show actions that aren't duplicates of the custom close action. + mAppActions.add(action); + } + } + + updateCustomActions(mAppActions); + } + + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) + public void onMediaActionsChanged(List<RemoteAction> actions) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onMediaActionsChanged()", TAG); + + mMediaActions.clear(); + // Don't show disabled actions. + for (RemoteAction remoteAction : actions) { + if (remoteAction.isEnabled()) { + mMediaActions.add(remoteAction); + } + } + + updateCustomActions(mMediaActions); + } + + private void updateCustomActions(@NonNull List<RemoteAction> customActions) { + List<RemoteAction> newCustomActions = customActions; + if (newCustomActions == mMediaActions && !mAppActions.isEmpty()) { + // Don't show the media actions while there are app actions. + return; + } else if (newCustomActions == mAppActions && mAppActions.isEmpty()) { + // If all the app actions were removed, show the media actions. + newCustomActions = mMediaActions; + } + + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: replaceCustomActions, count: %d", TAG, newCustomActions.size()); + int oldCustomActionsCount = 0; + for (TvPipAction action : mActionsList) { + if (action.getActionType() == ACTION_CUSTOM) { + oldCustomActionsCount++; + } + } + mActionsList.removeIf(tvPipAction -> tvPipAction.getActionType() == ACTION_CUSTOM); + + List<TvPipAction> actions = new ArrayList<>(); + for (RemoteAction action : newCustomActions) { + actions.add(new TvPipCustomAction(ACTION_CUSTOM, action, mSystemActionsHandler)); + } + mActionsList.addAll(FIRST_CUSTOM_ACTION_INDEX, actions); + + int added = newCustomActions.size() - oldCustomActionsCount; + int changed = Math.min(newCustomActions.size(), oldCustomActionsCount); + notifyActionsChanged(added, changed, FIRST_CUSTOM_ACTION_INDEX); + } + + @VisibleForTesting(visibility = PACKAGE) + public void updateExpansionEnabled(boolean enabled) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: updateExpansionState, enabled: %b", TAG, enabled); + int actionIndex = mActionsList.indexOf(mExpandCollapseAction); + boolean actionInList = actionIndex != -1; + if (enabled && !actionInList) { + mActionsList.add(mExpandCollapseAction); + actionIndex = mActionsList.size() - 1; + } else if (!enabled && actionInList) { + mActionsList.remove(actionIndex); + } else { + return; + } + notifyActionsChanged(/* added= */ enabled ? 1 : -1, /* updated= */ 0, actionIndex); + } + + @VisibleForTesting(visibility = PACKAGE) + public void onPipExpansionToggled(boolean expanded) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onPipExpansionToggled, expanded: %b", TAG, expanded); + + mExpandCollapseAction.update( + expanded ? R.string.pip_collapse : R.string.pip_expand, + expanded ? R.drawable.pip_ic_collapse : R.drawable.pip_ic_expand); + + notifyActionsChanged(/* added= */ 0, /* updated= */ 1, + mActionsList.indexOf(mExpandCollapseAction)); + } + + List<TvPipAction> getActionsList() { + return mActionsList; + } + + @NonNull + TvPipAction getCloseAction() { + return mActionsList.get(CLOSE_ACTION_INDEX); + } + + void addListener(Listener listener) { + if (!mListeners.contains(listener)) { + mListeners.add(listener); + } + } + + /** + * Returns the index of the first action of the given action type or -1 if none can be found. + */ + int getFirstIndexOfAction(@TvPipAction.ActionType int actionType) { + for (int i = 0; i < mActionsList.size(); i++) { + if (mActionsList.get(i).getActionType() == actionType) { + return i; + } + } + return -1; + } + + /** + * Allow components to listen to updates to the actions list, including where they happen so + * that changes can be animated. + */ + interface Listener { + /** + * Notifies the listener how many actions were added/removed or updated. + * + * @param added can be positive (number of actions added), negative (number of actions + * removed) or zero (the number of actions stayed the same). + * @param updated the number of actions that might have been updated and need to be + * refreshed. + * @param startIndex The index of the first updated action. The added/removed actions start + * at (startIndex + updated). + */ + void onActionsChanged(int added, int updated, int startIndex); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java index 3e8de454bcff..76710818f8e5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java @@ -22,13 +22,16 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import android.annotation.IntDef; import android.app.ActivityManager; import android.app.ActivityTaskManager; -import android.app.PendingIntent; import android.app.RemoteAction; import android.app.TaskInfo; +import android.content.BroadcastReceiver; import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Rect; +import android.os.Handler; import android.os.RemoteException; import android.view.Gravity; @@ -67,8 +70,8 @@ import java.util.Set; */ public class TvPipController implements PipTransitionController.PipTransitionCallback, TvPipBoundsController.PipBoundsListener, TvPipMenuController.Delegate, - TvPipNotificationController.Delegate, DisplayController.OnDisplaysChangedListener, - ConfigurationChangeListener, UserChangeListener { + DisplayController.OnDisplaysChangedListener, ConfigurationChangeListener, + UserChangeListener { private static final String TAG = "TvPipController"; private static final int NONEXISTENT_TASK_ID = -1; @@ -98,6 +101,17 @@ public class TvPipController implements PipTransitionController.PipTransitionCal */ private static final int STATE_PIP_MENU = 2; + static final String ACTION_SHOW_PIP_MENU = + "com.android.wm.shell.pip.tv.notification.action.SHOW_PIP_MENU"; + static final String ACTION_CLOSE_PIP = + "com.android.wm.shell.pip.tv.notification.action.CLOSE_PIP"; + static final String ACTION_MOVE_PIP = + "com.android.wm.shell.pip.tv.notification.action.MOVE_PIP"; + static final String ACTION_TOGGLE_EXPANDED_PIP = + "com.android.wm.shell.pip.tv.notification.action.TOGGLE_EXPANDED_PIP"; + static final String ACTION_TO_FULLSCREEN = + "com.android.wm.shell.pip.tv.notification.action.FULLSCREEN"; + private final Context mContext; private final ShellController mShellController; @@ -107,6 +121,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal private final PipAppOpsListener mAppOpsListener; private final PipTaskOrganizer mPipTaskOrganizer; private final PipMediaController mPipMediaController; + private final TvPipActionsProvider mTvPipActionsProvider; private final TvPipNotificationController mPipNotificationController; private final TvPipMenuController mTvPipMenuController; private final PipTransitionController mPipTransitionController; @@ -115,14 +130,16 @@ public class TvPipController implements PipTransitionController.PipTransitionCal private final DisplayController mDisplayController; private final WindowManagerShellWrapper mWmShellWrapper; private final ShellExecutor mMainExecutor; + private final Handler mMainHandler; // For registering the broadcast receiver private final TvPipImpl mImpl = new TvPipImpl(); + private final ActionBroadcastReceiver mActionBroadcastReceiver; + @State private int mState = STATE_NO_PIP; private int mPreviousGravity = TvPipBoundsState.DEFAULT_TV_GRAVITY; private int mPinnedTaskId = NONEXISTENT_TASK_ID; - private RemoteAction mCloseAction; // How long the shell will wait for the app to close the PiP if a custom action is set. private int mPipForceCloseDelay; @@ -146,6 +163,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal PipParamsChangedForwarder pipParamsChangedForwarder, DisplayController displayController, WindowManagerShellWrapper wmShell, + Handler mainHandler, ShellExecutor mainExecutor) { return new TvPipController( context, @@ -164,6 +182,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal pipParamsChangedForwarder, displayController, wmShell, + mainHandler, mainExecutor).mImpl; } @@ -184,8 +203,10 @@ public class TvPipController implements PipTransitionController.PipTransitionCal PipParamsChangedForwarder pipParamsChangedForwarder, DisplayController displayController, WindowManagerShellWrapper wmShellWrapper, + Handler mainHandler, ShellExecutor mainExecutor) { mContext = context; + mMainHandler = mainHandler; mMainExecutor = mainExecutor; mShellController = shellController; mDisplayController = displayController; @@ -198,12 +219,17 @@ public class TvPipController implements PipTransitionController.PipTransitionCal mTvPipBoundsController.setListener(this); mPipMediaController = pipMediaController; + mTvPipActionsProvider = new TvPipActionsProvider(context, pipMediaController, + this::executeAction); mPipNotificationController = pipNotificationController; - mPipNotificationController.setDelegate(this); + mPipNotificationController.setTvPipActionsProvider(mTvPipActionsProvider); mTvPipMenuController = tvPipMenuController; mTvPipMenuController.setDelegate(this); + mTvPipMenuController.setTvPipActionsProvider(mTvPipActionsProvider); + + mActionBroadcastReceiver = new ActionBroadcastReceiver(); mAppOpsListener = pipAppOpsListener; mPipTaskOrganizer = pipTaskOrganizer; @@ -241,7 +267,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal "%s: onConfigurationChanged(), state=%s", TAG, stateToName(mState)); loadConfigurations(); - mPipNotificationController.onConfigurationChanged(mContext); + mPipNotificationController.onConfigurationChanged(); mTvPipBoundsAlgorithm.onConfigurationChanged(mContext); } @@ -256,9 +282,10 @@ public class TvPipController implements PipTransitionController.PipTransitionCal * Starts the process if bringing up the Pip menu if by issuing a command to move Pip * task/window to the "Menu" position. We'll show the actual Menu UI (eg. actions) once the Pip * task/window is properly positioned in {@link #onPipTransitionFinished(int)}. + * + * @param moveMenu If true, show the moveMenu, otherwise show the regular menu. */ - @Override - public void showPictureInPictureMenu() { + private void showPictureInPictureMenu(boolean moveMenu) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: showPictureInPictureMenu(), state=%s", TAG, stateToName(mState)); @@ -269,7 +296,11 @@ public class TvPipController implements PipTransitionController.PipTransitionCal } setState(STATE_PIP_MENU); - mTvPipMenuController.showMenu(); + if (moveMenu) { + mTvPipMenuController.showMovementMenu(); + } else { + mTvPipMenuController.showMenu(); + } updatePinnedStackBounds(); } @@ -289,8 +320,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal /** * Opens the "Pip-ed" Activity fullscreen. */ - @Override - public void movePipToFullscreen() { + private void movePipToFullscreen() { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: movePipToFullscreen(), state=%s", TAG, stateToName(mState)); @@ -298,8 +328,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal onPipDisappeared(); } - @Override - public void togglePipExpansion() { + private void togglePipExpansion() { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: togglePipExpansion()", TAG); boolean expanding = !mTvPipBoundsState.isTvPipExpanded(); @@ -310,18 +339,11 @@ public class TvPipController implements PipTransitionController.PipTransitionCal } mTvPipBoundsState.setTvPipManuallyCollapsed(!expanding); mTvPipBoundsState.setTvPipExpanded(expanding); - mPipNotificationController.updateExpansionState(); updatePinnedStackBounds(); } @Override - public void enterPipMovementMenu() { - setState(STATE_PIP_MENU); - mTvPipMenuController.showMovementMenuOnly(); - } - - @Override public void movePip(int keycode) { if (mTvPipBoundsAlgorithm.updateGravity(keycode)) { mTvPipMenuController.updateGravity(mTvPipBoundsState.getTvPipGravity()); @@ -373,30 +395,23 @@ public class TvPipController implements PipTransitionController.PipTransitionCal @Override public void onPipTargetBoundsChange(Rect targetBounds, int animationDuration) { mPipTaskOrganizer.scheduleAnimateResizePip(targetBounds, - animationDuration, rect -> mTvPipMenuController.updateExpansionState()); + animationDuration, null); mTvPipMenuController.onPipTransitionToTargetBoundsStarted(targetBounds); } /** * Closes Pip window. */ - @Override public void closePip() { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: closePip(), state=%s, loseAction=%s", TAG, stateToName(mState), - mCloseAction); - - if (mCloseAction != null) { - try { - mCloseAction.getActionIntent().send(); - } catch (PendingIntent.CanceledException e) { - ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: Failed to send close action, %s", TAG, e); - } - mMainExecutor.executeDelayed(() -> closeCurrentPiP(mPinnedTaskId), mPipForceCloseDelay); - } else { - closeCurrentPiP(mPinnedTaskId); - } + closeCurrentPiP(mPinnedTaskId); + } + + /** + * Force close the current PiP after some time in case the custom action hasn't done it by + * itself. + */ + public void customClosePip() { + mMainExecutor.executeDelayed(() -> closeCurrentPiP(mPinnedTaskId), mPipForceCloseDelay); } private void closeCurrentPiP(int pinnedTaskId) { @@ -426,6 +441,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal mPinnedTaskId = pinnedTask.taskId; mPipMediaController.onActivityPinned(); + mActionBroadcastReceiver.register(); mPipNotificationController.show(pinnedTask.topActivity.getPackageName()); } @@ -445,6 +461,8 @@ public class TvPipController implements PipTransitionController.PipTransitionCal "%s: onPipDisappeared() state=%s", TAG, stateToName(mState)); mPipNotificationController.dismiss(); + mActionBroadcastReceiver.unregister(); + mTvPipMenuController.closeMenu(); mTvPipBoundsState.resetTvPipState(); mTvPipBoundsController.onPipDismissed(); @@ -454,6 +472,11 @@ public class TvPipController implements PipTransitionController.PipTransitionCal @Override public void onPipTransitionStarted(int direction, Rect currentPipBounds) { + final boolean enterPipTransition = PipAnimationController.isInPipDirection(direction); + if (enterPipTransition && mState == STATE_NO_PIP) { + // Set the initial ability to expand the PiP when entering PiP. + updateExpansionState(); + } ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: onPipTransition_Started(), state=%s, direction=%d", TAG, stateToName(mState), direction); @@ -465,6 +488,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal "%s: onPipTransition_Canceled(), state=%s", TAG, stateToName(mState)); mTvPipMenuController.onPipTransitionFinished( PipAnimationController.isInPipDirection(direction)); + mTvPipActionsProvider.onPipExpansionToggled(mTvPipBoundsState.isTvPipExpanded()); } @Override @@ -477,6 +501,12 @@ public class TvPipController implements PipTransitionController.PipTransitionCal "%s: onPipTransition_Finished(), state=%s, direction=%d", TAG, stateToName(mState), direction); mTvPipMenuController.onPipTransitionFinished(enterPipTransition); + mTvPipActionsProvider.onPipExpansionToggled(mTvPipBoundsState.isTvPipExpanded()); + } + + private void updateExpansionState() { + mTvPipActionsProvider.updateExpansionEnabled(mTvPipBoundsState.isTvExpandedPipSupported() + && mTvPipBoundsState.getDesiredTvExpandedAspectRatio() != 0); } private void setState(@State int state) { @@ -534,8 +564,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: onActionsChanged()", TAG); - mTvPipMenuController.setAppActions(actions, closeAction); - mCloseAction = closeAction; + mTvPipActionsProvider.setAppActions(actions, closeAction); } @Override @@ -555,7 +584,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal "%s: onExpandedAspectRatioChanged: %f", TAG, ratio); mTvPipBoundsState.setDesiredTvExpandedAspectRatio(ratio, false); - mTvPipMenuController.updateExpansionState(); + updateExpansionState(); // 1) PiP is expanded and only aspect ratio changed, but wasn't disabled // --> update bounds, but don't toggle @@ -662,6 +691,90 @@ public class TvPipController implements PipTransitionController.PipTransitionCal } } + private void executeAction(@TvPipAction.ActionType int actionType) { + switch (actionType) { + case TvPipAction.ACTION_FULLSCREEN: + movePipToFullscreen(); + break; + case TvPipAction.ACTION_CLOSE: + closePip(); + break; + case TvPipAction.ACTION_MOVE: + showPictureInPictureMenu(/* moveMenu= */ true); + break; + case TvPipAction.ACTION_CUSTOM_CLOSE: + customClosePip(); + break; + case TvPipAction.ACTION_EXPAND_COLLAPSE: + togglePipExpansion(); + break; + default: + // NOOP + break; + } + } + + private class ActionBroadcastReceiver extends BroadcastReceiver { + private static final String SYSTEMUI_PERMISSION = "com.android.systemui.permission.SELF"; + + final IntentFilter mIntentFilter; + + { + mIntentFilter = new IntentFilter(); + mIntentFilter.addAction(ACTION_CLOSE_PIP); + mIntentFilter.addAction(ACTION_SHOW_PIP_MENU); + mIntentFilter.addAction(ACTION_MOVE_PIP); + mIntentFilter.addAction(ACTION_TOGGLE_EXPANDED_PIP); + mIntentFilter.addAction(ACTION_TO_FULLSCREEN); + } + + boolean mRegistered = false; + + void register() { + if (mRegistered) return; + + mContext.registerReceiverForAllUsers(this, mIntentFilter, SYSTEMUI_PERMISSION, + mMainHandler); + mRegistered = true; + } + + void unregister() { + if (!mRegistered) return; + + mContext.unregisterReceiver(this); + mRegistered = false; + } + + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: on(Broadcast)Receive(), action=%s", TAG, action); + + if (ACTION_SHOW_PIP_MENU.equals(action)) { + showPictureInPictureMenu(/* moveMenu= */ false); + } else { + executeAction(getCorrespondingActionType(action)); + } + } + + @TvPipAction.ActionType + private int getCorrespondingActionType(String broadcast) { + if (ACTION_CLOSE_PIP.equals(broadcast)) { + return TvPipAction.ACTION_CLOSE; + } else if (ACTION_MOVE_PIP.equals(broadcast)) { + return TvPipAction.ACTION_MOVE; + } else if (ACTION_TOGGLE_EXPANDED_PIP.equals(broadcast)) { + return TvPipAction.ACTION_EXPAND_COLLAPSE; + } else if (ACTION_TO_FULLSCREEN.equals(broadcast)) { + return TvPipAction.ACTION_FULLSCREEN; + } + + // Default: handle it like an action we don't know the content of. + return TvPipAction.ACTION_CUSTOM; + } + } + private class TvPipImpl implements Pip { // Not used } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipCustomAction.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipCustomAction.java new file mode 100644 index 000000000000..449a2bf09881 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipCustomAction.java @@ -0,0 +1,96 @@ +/* + * 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.wm.shell.pip.tv; + +import static android.app.Notification.Action.SEMANTIC_ACTION_DELETE; +import static android.app.Notification.Action.SEMANTIC_ACTION_NONE; + +import android.annotation.NonNull; +import android.app.Notification; +import android.app.PendingIntent; +import android.app.RemoteAction; +import android.content.Context; +import android.os.Bundle; +import android.os.Handler; + +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.common.TvWindowMenuActionButton; +import com.android.wm.shell.protolog.ShellProtoLogGroup; + +import java.util.List; +import java.util.Objects; + +/** + * A TvPipAction for actions that the app provides via {@link + * android.app.PictureInPictureParams.Builder#setCloseAction(RemoteAction)} or {@link + * android.app.PictureInPictureParams.Builder#setActions(List)}. + */ +public class TvPipCustomAction extends TvPipAction { + private static final String TAG = TvPipCustomAction.class.getSimpleName(); + + private final RemoteAction mRemoteAction; + + TvPipCustomAction(@ActionType int actionType, @NonNull RemoteAction remoteAction, + SystemActionsHandler systemActionsHandler) { + super(actionType, systemActionsHandler); + Objects.requireNonNull(remoteAction); + mRemoteAction = remoteAction; + } + + void populateButton(@NonNull TvWindowMenuActionButton button, Handler mainHandler) { + if (button == null || mainHandler == null) return; + if (mRemoteAction.getContentDescription().length() > 0) { + button.setTextAndDescription(mRemoteAction.getContentDescription()); + } else { + button.setTextAndDescription(mRemoteAction.getTitle()); + } + button.setImageIconAsync(mRemoteAction.getIcon(), mainHandler); + button.setEnabled(isCloseAction() || mRemoteAction.isEnabled()); + } + + PendingIntent getPendingIntent() { + return mRemoteAction.getActionIntent(); + } + + void executeAction() { + super.executeAction(); + try { + mRemoteAction.getActionIntent().send(); + } catch (PendingIntent.CanceledException e) { + ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Failed to send action, %s", TAG, e); + } + } + + @Override + Notification.Action toNotificationAction(Context context) { + Notification.Action.Builder builder = new Notification.Action.Builder( + mRemoteAction.getIcon(), + mRemoteAction.getTitle(), + mRemoteAction.getActionIntent()); + Bundle extras = new Bundle(); + extras.putCharSequence(Notification.EXTRA_PICTURE_CONTENT_DESCRIPTION, + mRemoteAction.getContentDescription()); + builder.addExtras(extras); + + builder.setSemanticAction(isCloseAction() + ? SEMANTIC_ACTION_DELETE : SEMANTIC_ACTION_NONE); + builder.setContextual(true); + return builder.build(); + } + +} 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 ab7edbfaa4ca..00e4f47e3c6b 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 @@ -41,13 +41,10 @@ import androidx.annotation.Nullable; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.R; import com.android.wm.shell.common.SystemWindows; -import com.android.wm.shell.pip.PipMediaController; import com.android.wm.shell.pip.PipMenuController; import com.android.wm.shell.protolog.ShellProtoLogGroup; -import java.util.ArrayList; import java.util.List; -import java.util.Objects; /** * Manages the visibility of the PiP Menu as user interacts with PiP. @@ -60,22 +57,20 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis private final SystemWindows mSystemWindows; private final TvPipBoundsState mTvPipBoundsState; private final Handler mMainHandler; + private TvPipActionsProvider mTvPipActionsProvider; private Delegate mDelegate; private SurfaceControl mLeash; private TvPipMenuView mPipMenuView; private View mPipBackgroundView; + private boolean mMenuIsOpen; // User can actively move the PiP via the DPAD. private boolean mInMoveMode; // Used when only showing the move menu since we want to close the menu completely when // exiting the move menu instead of showing the regular button menu. private boolean mCloseAfterExitMoveMenu; - private final List<RemoteAction> mMediaActions = new ArrayList<>(); - private final List<RemoteAction> mAppActions = new ArrayList<>(); - private RemoteAction mCloseAction; - private SyncRtSurfaceTransactionApplier mApplier; private SyncRtSurfaceTransactionApplier mBackgroundApplier; RectF mTmpSourceRectF = new RectF(); @@ -83,8 +78,7 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis Matrix mMoveTransform = new Matrix(); public TvPipMenuController(Context context, TvPipBoundsState tvPipBoundsState, - SystemWindows systemWindows, PipMediaController pipMediaController, - Handler mainHandler) { + SystemWindows systemWindows, Handler mainHandler) { mContext = context; mTvPipBoundsState = tvPipBoundsState; mSystemWindows = systemWindows; @@ -101,9 +95,6 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis context.registerReceiverForAllUsers(closeSystemDialogsBroadcastReceiver, new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS), null /* permission */, mainHandler, Context.RECEIVER_EXPORTED); - - pipMediaController.addActionListener(this::onMediaActionsChanged); - } void setDelegate(Delegate delegate) { @@ -120,6 +111,10 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis mDelegate = delegate; } + void setTvPipActionsProvider(TvPipActionsProvider tvPipActionsProvider) { + mTvPipActionsProvider = tvPipActionsProvider; + } + @Override public void attach(SurfaceControl leash) { if (mDelegate == null) { @@ -146,15 +141,19 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis int pipMenuBorderWidth = mContext.getResources() .getDimensionPixelSize(R.dimen.pip_menu_border_width); mTvPipBoundsState.setPipMenuPermanentDecorInsets(Insets.of(-pipMenuBorderWidth, - -pipMenuBorderWidth, -pipMenuBorderWidth, -pipMenuBorderWidth)); + -pipMenuBorderWidth, -pipMenuBorderWidth, -pipMenuBorderWidth)); mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.of(0, 0, 0, -pipEduTextHeight)); } private void attachPipMenuView() { - mPipMenuView = new TvPipMenuView(mContext, mMainHandler, this); + if (mTvPipActionsProvider == null) { + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Actions provider is not set", TAG); + return; + } + mPipMenuView = new TvPipMenuView(mContext, mMainHandler, this, mTvPipActionsProvider); setUpViewSurfaceZOrder(mPipMenuView, 1); addPipMenuViewToSystemWindows(mPipMenuView, MENU_WINDOW_TITLE); - maybeUpdateMenuViewActions(); } private void attachPipBackgroundView() { @@ -189,17 +188,22 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis // and the menu view has been fully remeasured and relaid out, we add a small delay here by // posting on the handler. mMainHandler.post(() -> { - mPipMenuView.onPipTransitionFinished( - enterTransition, mTvPipBoundsState.isTvPipExpanded()); + if (mPipMenuView != null) { + mPipMenuView.onPipTransitionFinished(enterTransition); + } }); } - void showMovementMenuOnly() { + void showMovementMenu() { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: showMovementMenuOnly()", TAG); setInMoveMode(true); - mCloseAfterExitMoveMenu = true; - showMenuInternal(); + if (mMenuIsOpen) { + mPipMenuView.showMoveMenu(mDelegate.getPipGravity()); + } else { + mCloseAfterExitMoveMenu = true; + showMenuInternal(); + } } @Override @@ -214,14 +218,13 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis if (mPipMenuView == null) { return; } - maybeUpdateMenuViewActions(); - updateExpansionState(); + mMenuIsOpen = true; grantPipMenuFocus(true); if (mInMoveMode) { mPipMenuView.showMoveMenu(mDelegate.getPipGravity()); } else { - mPipMenuView.showButtonsMenu(); + mPipMenuView.showButtonsMenu(/* exitingMoveMode= */ false); } mPipMenuView.updateBounds(mTvPipBoundsState.getBounds()); } @@ -236,11 +239,6 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis mPipMenuView.showMovementHints(gravity); } - void updateExpansionState() { - mPipMenuView.setExpandedModeEnabled(mTvPipBoundsState.isTvExpandedPipSupported() - && mTvPipBoundsState.getDesiredTvExpandedAspectRatio() != 0); - } - private Rect calculateMenuSurfaceBounds(Rect pipBounds) { return mPipMenuView.getPipMenuContainerBounds(pipBounds); } @@ -252,6 +250,8 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis if (mPipMenuView == null) { return; } + + mMenuIsOpen = false; mPipMenuView.hideAllUserControls(); grantPipMenuFocus(false); mDelegate.onMenuClosed(); @@ -272,29 +272,19 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis } @Override - public void onEnterMoveMode() { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: onEnterMoveMode - %b, close when exiting move menu: %b", TAG, mInMoveMode, - mCloseAfterExitMoveMenu); - setInMoveMode(true); - mPipMenuView.showMoveMenu(mDelegate.getPipGravity()); - } - - @Override public boolean onExitMoveMode() { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: onExitMoveMode - %b, close when exiting move menu: %b", TAG, mInMoveMode, - mCloseAfterExitMoveMenu); + "%s: onExitMoveMode - %b, close when exiting move menu: %b", + TAG, mInMoveMode, mCloseAfterExitMoveMenu); - if (mCloseAfterExitMoveMenu) { - setInMoveMode(false); - mCloseAfterExitMoveMenu = false; - closeMenu(); - return true; - } if (mInMoveMode) { setInMoveMode(false); - mPipMenuView.showButtonsMenu(); + if (mCloseAfterExitMoveMenu) { + mCloseAfterExitMoveMenu = false; + closeMenu(); + } else { + mPipMenuView.showButtonsMenu(/* exitingMoveMode= */ true); + } return true; } return false; @@ -319,51 +309,7 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis @Override public void setAppActions(List<RemoteAction> actions, RemoteAction closeAction) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: setAppActions()", TAG); - updateAdditionalActionsList(mAppActions, actions, closeAction); - } - - private void onMediaActionsChanged(List<RemoteAction> actions) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: onMediaActionsChanged()", TAG); - - // Hide disabled actions. - List<RemoteAction> enabledActions = new ArrayList<>(); - for (RemoteAction remoteAction : actions) { - if (remoteAction.isEnabled()) { - enabledActions.add(remoteAction); - } - } - updateAdditionalActionsList(mMediaActions, enabledActions, mCloseAction); - } - - private void updateAdditionalActionsList(List<RemoteAction> destination, - @Nullable List<RemoteAction> source, RemoteAction closeAction) { - final int number = source != null ? source.size() : 0; - if (number == 0 && destination.isEmpty() && Objects.equals(closeAction, mCloseAction)) { - // Nothing changed. - return; - } - - mCloseAction = closeAction; - - destination.clear(); - if (number > 0) { - destination.addAll(source); - } - maybeUpdateMenuViewActions(); - } - - private void maybeUpdateMenuViewActions() { - if (mPipMenuView == null) { - return; - } - if (!mAppActions.isEmpty()) { - mPipMenuView.setAdditionalActions(mAppActions, mCloseAction, mMainHandler); - } else { - mPipMenuView.setAdditionalActions(mMediaActions, mCloseAction, mMainHandler); - } + // NOOP - handled via the TvPipActionsProvider } @Override @@ -544,42 +490,21 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis } @Override - public void onCloseButtonClick() { - mDelegate.closePip(); - } - - @Override - public void onFullscreenButtonClick() { - mDelegate.movePipToFullscreen(); - } - - @Override - public void onToggleExpandedMode() { - mDelegate.togglePipExpansion(); - } - - @Override public void onCloseEduText() { mTvPipBoundsState.setPipMenuTemporaryDecorInsets(Insets.NONE); mDelegate.closeEduText(); } interface Delegate { - void movePipToFullscreen(); - void movePip(int keycode); void onInMoveModeChanged(); int getPipGravity(); - void togglePipExpansion(); - void onMenuClosed(); void closeEduText(); - - void closePip(); } private void grantPipMenuFocus(boolean grantFocus) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java index 57e95c416b3c..56c602a1d4f3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java @@ -25,119 +25,102 @@ import static android.view.KeyEvent.KEYCODE_DPAD_RIGHT; import static android.view.KeyEvent.KEYCODE_DPAD_UP; import static android.view.KeyEvent.KEYCODE_ENTER; -import android.app.PendingIntent; -import android.app.RemoteAction; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_MOVE; + import android.content.Context; import android.graphics.Rect; import android.os.Handler; import android.view.Gravity; import android.view.KeyEvent; -import android.view.SurfaceControl; import android.view.View; import android.view.ViewGroup; -import android.view.ViewRootImpl; import android.view.accessibility.AccessibilityManager; import android.widget.FrameLayout; -import android.widget.HorizontalScrollView; import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.ScrollView; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import com.android.internal.protolog.common.ProtoLog; +import com.android.internal.widget.LinearLayoutManager; +import com.android.internal.widget.RecyclerView; import com.android.wm.shell.R; import com.android.wm.shell.common.TvWindowMenuActionButton; import com.android.wm.shell.pip.PipUtils; import com.android.wm.shell.protolog.ShellProtoLogGroup; -import java.util.ArrayList; import java.util.List; /** - * A View that represents Pip Menu on TV. It's responsible for displaying 3 ever-present Pip Menu - * actions: Fullscreen, Move and Close, but could also display "additional" actions, that may be set - * via a {@link #setAdditionalActions(List, RemoteAction, Handler)} call. + * A View that represents Pip Menu on TV. It's responsible for displaying the Pip menu actions from + * the TvPipActionsProvider as well as the buttons for manually moving the PiP. */ -public class TvPipMenuView extends FrameLayout implements View.OnClickListener { +public class TvPipMenuView extends FrameLayout implements TvPipActionsProvider.Listener { private static final String TAG = "TvPipMenuView"; - private static final int FIRST_CUSTOM_ACTION_POSITION = 3; + private final TvPipMenuView.Listener mListener; - private final Listener mListener; + private final TvPipActionsProvider mTvPipActionsProvider; + + private final RecyclerView mActionButtonsRecyclerView; + private final LinearLayoutManager mButtonLayoutManager; + private final RecyclerViewAdapter mRecyclerViewAdapter; - private final LinearLayout mActionButtonsContainer; - private final View mMenuFrameView; - private final List<TvWindowMenuActionButton> mAdditionalButtons = new ArrayList<>(); private final View mPipFrameView; + private final View mMenuFrameView; private final View mPipView; + + private final View mPipBackground; + private final View mDimLayer; + private final TvPipMenuEduTextDrawer mEduTextDrawer; + private final int mPipMenuOuterSpace; private final int mPipMenuBorderWidth; + private final int mPipMenuFadeAnimationDuration; + private final int mResizeAnimationDuration; + private final ImageView mArrowUp; private final ImageView mArrowRight; private final ImageView mArrowDown; private final ImageView mArrowLeft; private final TvWindowMenuActionButton mA11yDoneButton; - private final View mPipBackground; - private final View mDimLayer; - - private final ScrollView mScrollView; - private final HorizontalScrollView mHorizontalScrollView; - private View mFocusedButton; - private Rect mCurrentPipBounds; private boolean mMoveMenuIsVisible; private boolean mButtonMenuIsVisible; - - private final TvWindowMenuActionButton mExpandButton; - private final TvWindowMenuActionButton mCloseButton; - private boolean mSwitchingOrientation; - private final int mPipMenuFadeAnimationDuration; - private final int mResizeAnimationDuration; - private final AccessibilityManager mA11yManager; private final Handler mMainHandler; public TvPipMenuView(@NonNull Context context, @NonNull Handler mainHandler, - @NonNull Listener listener) { + @NonNull Listener listener, TvPipActionsProvider tvPipActionsProvider) { super(context, null, 0, 0); - inflate(context, R.layout.tv_pip_menu, this); mMainHandler = mainHandler; mListener = listener; - mA11yManager = context.getSystemService(AccessibilityManager.class); - mActionButtonsContainer = findViewById(R.id.tv_pip_menu_action_buttons); - mActionButtonsContainer.findViewById(R.id.tv_pip_menu_fullscreen_button) - .setOnClickListener(this); - - mCloseButton = mActionButtonsContainer.findViewById(R.id.tv_pip_menu_close_button); - mCloseButton.setOnClickListener(this); - mCloseButton.setIsCustomCloseAction(true); + mActionButtonsRecyclerView = findViewById(R.id.tv_pip_menu_action_buttons); + mButtonLayoutManager = new LinearLayoutManager(mContext); + mActionButtonsRecyclerView.setLayoutManager(mButtonLayoutManager); + mActionButtonsRecyclerView.setPreserveFocusAfterLayout(true); - mActionButtonsContainer.findViewById(R.id.tv_pip_menu_move_button) - .setOnClickListener(this); - mExpandButton = findViewById(R.id.tv_pip_menu_expand_button); - mExpandButton.setOnClickListener(this); + mTvPipActionsProvider = tvPipActionsProvider; + mRecyclerViewAdapter = new RecyclerViewAdapter(tvPipActionsProvider.getActionsList()); + mActionButtonsRecyclerView.setAdapter(mRecyclerViewAdapter); - mPipBackground = findViewById(R.id.tv_pip_menu_background); - mDimLayer = findViewById(R.id.tv_pip_menu_dim_layer); - - mScrollView = findViewById(R.id.tv_pip_menu_scroll); - mHorizontalScrollView = findViewById(R.id.tv_pip_menu_horizontal_scroll); + tvPipActionsProvider.addListener(this); mMenuFrameView = findViewById(R.id.tv_pip_menu_frame); mPipFrameView = findViewById(R.id.tv_pip_border); mPipView = findViewById(R.id.tv_pip); + mPipBackground = findViewById(R.id.tv_pip_menu_background); + mDimLayer = findViewById(R.id.tv_pip_menu_dim_layer); + mArrowUp = findViewById(R.id.tv_pip_menu_arrow_up); mArrowRight = findViewById(R.id.tv_pip_menu_arrow_right); mArrowDown = findViewById(R.id.tv_pip_menu_arrow_down); @@ -160,8 +143,12 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { } void onPipTransitionToTargetBoundsStarted(Rect targetBounds) { + if (targetBounds == null) { + return; + } + // Fade out content by fading in view on top. - if (mCurrentPipBounds != null && targetBounds != null) { + if (mCurrentPipBounds != null) { boolean ratioChanged = PipUtils.aspectRatioChanged( mCurrentPipBounds.width() / (float) mCurrentPipBounds.height(), targetBounds.width() / (float) targetBounds.height()); @@ -177,7 +164,7 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { // Update buttons. final boolean vertical = targetBounds.height() > targetBounds.width(); final boolean orientationChanged = - vertical != (mActionButtonsContainer.getOrientation() == LinearLayout.VERTICAL); + vertical != (mButtonLayoutManager.getOrientation() == LinearLayoutManager.VERTICAL); ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: onPipTransitionToTargetBoundsStarted(), orientation changed %b", TAG, orientationChanged); @@ -187,27 +174,27 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { if (mButtonMenuIsVisible) { mSwitchingOrientation = true; - mActionButtonsContainer.animate() + mActionButtonsRecyclerView.animate() .alpha(0) .setInterpolator(TvPipInterpolators.EXIT) .setDuration(mResizeAnimationDuration / 2) .withEndAction(() -> { - changeButtonScrollOrientation(targetBounds); - updateButtonGravity(targetBounds); + mButtonLayoutManager.setOrientation(vertical + ? LinearLayoutManager.VERTICAL : LinearLayoutManager.HORIZONTAL); // Only make buttons visible again in onPipTransitionFinished to keep in // sync with PiP content alpha animation. }); } else { - changeButtonScrollOrientation(targetBounds); - updateButtonGravity(targetBounds); + mButtonLayoutManager.setOrientation(vertical + ? LinearLayoutManager.VERTICAL : LinearLayoutManager.HORIZONTAL); } } - void onPipTransitionFinished(boolean enterTransition, boolean isTvPipExpanded) { + void onPipTransitionFinished(boolean enterTransition) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: onPipTransitionFinished()", TAG); - // Fade in content by fading out view on top. + // Fade in content by fading out view on top (faded out at every aspect ratio change). mPipBackground.animate() .alpha(0f) .setDuration(mResizeAnimationDuration / 2) @@ -218,16 +205,11 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { mEduTextDrawer.init(); } - setIsExpanded(isTvPipExpanded); - - // Update buttons. if (mSwitchingOrientation) { - mActionButtonsContainer.animate() + mActionButtonsRecyclerView.animate() .alpha(1) .setInterpolator(TvPipInterpolators.ENTER) .setDuration(mResizeAnimationDuration / 2); - } else { - refocusPreviousButton(); } mSwitchingOrientation = false; } @@ -240,107 +222,9 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { "%s: updateLayout, width: %s, height: %s", TAG, updatedBounds.width(), updatedBounds.height()); mCurrentPipBounds = updatedBounds; - if (!mSwitchingOrientation) { - updateButtonGravity(mCurrentPipBounds); - } - updatePipFrameBounds(); } - private void changeButtonScrollOrientation(Rect bounds) { - final boolean vertical = bounds.height() > bounds.width(); - - final ViewGroup oldScrollView = vertical ? mHorizontalScrollView : mScrollView; - final ViewGroup newScrollView = vertical ? mScrollView : mHorizontalScrollView; - - if (oldScrollView.getChildCount() == 1) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: orientation changed", TAG); - oldScrollView.removeView(mActionButtonsContainer); - oldScrollView.setVisibility(GONE); - mActionButtonsContainer.setOrientation(vertical ? LinearLayout.VERTICAL - : LinearLayout.HORIZONTAL); - newScrollView.addView(mActionButtonsContainer); - newScrollView.setVisibility(VISIBLE); - if (mFocusedButton != null) { - mFocusedButton.requestFocus(); - } - } - } - - /** - * Change button gravity based on new dimensions - */ - private void updateButtonGravity(Rect bounds) { - final boolean vertical = bounds.height() > bounds.width(); - // Use Math.max since the possible orientation change might not have been applied yet. - final int buttonsSize = Math.max(mActionButtonsContainer.getHeight(), - mActionButtonsContainer.getWidth()); - - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: buttons container width: %s, height: %s", TAG, - mActionButtonsContainer.getWidth(), mActionButtonsContainer.getHeight()); - - final boolean buttonsFit = - vertical ? buttonsSize < bounds.height() - : buttonsSize < bounds.width(); - final int buttonGravity = buttonsFit ? Gravity.CENTER - : (vertical ? Gravity.CENTER_HORIZONTAL : Gravity.CENTER_VERTICAL); - - final LayoutParams params = (LayoutParams) mActionButtonsContainer.getLayoutParams(); - params.gravity = buttonGravity; - mActionButtonsContainer.setLayoutParams(params); - - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: vertical: %b, buttonsFit: %b, gravity: %s", TAG, vertical, buttonsFit, - Gravity.toString(buttonGravity)); - } - - private void refocusPreviousButton() { - if (mMoveMenuIsVisible || mCurrentPipBounds == null || mFocusedButton == null) { - return; - } - final boolean vertical = mCurrentPipBounds.height() > mCurrentPipBounds.width(); - - if (!mFocusedButton.hasFocus()) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: request focus from: %s", TAG, mFocusedButton); - mFocusedButton.requestFocus(); - } else { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: already focused: %s", TAG, mFocusedButton); - } - - // Do we need to scroll? - final Rect buttonBounds = new Rect(); - final Rect scrollBounds = new Rect(); - if (vertical) { - mScrollView.getDrawingRect(scrollBounds); - } else { - mHorizontalScrollView.getDrawingRect(scrollBounds); - } - mFocusedButton.getHitRect(buttonBounds); - - if (scrollBounds.contains(buttonBounds)) { - // Button is already completely visible, don't scroll - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: not scrolling", TAG); - return; - } - - // Scrolling so the button is visible to the user. - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: scrolling to focused button", TAG); - - if (vertical) { - mScrollView.smoothScrollTo((int) mFocusedButton.getX(), - (int) mFocusedButton.getY()); - } else { - mHorizontalScrollView.smoothScrollTo((int) mFocusedButton.getX(), - (int) mFocusedButton.getY()); - } - } - Rect getPipMenuContainerBounds(Rect pipBounds) { final Rect menuUiBounds = new Rect(pipBounds); menuUiBounds.inset(-mPipMenuOuterSpace, -mPipMenuOuterSpace); @@ -370,20 +254,14 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { mPipView.setLayoutParams(pipViewParams); } - - } - - void setExpandedModeEnabled(boolean enabled) { - mExpandButton.setVisibility(enabled ? VISIBLE : GONE); - } - - void setIsExpanded(boolean expanded) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: setIsExpanded, expanded: %b", TAG, expanded); - mExpandButton.setImageResource( - expanded ? R.drawable.pip_ic_collapse : R.drawable.pip_ic_expand); - mExpandButton.setTextAndDescription( - expanded ? R.string.pip_collapse : R.string.pip_expand); + // Keep focused button within the visible area while the PiP is changing size. Otherwise, + // the button would lose focus which would cause a need for scrolling and re-focusing after + // the animation finishes, which does not look good. + View focusedChild = mActionButtonsRecyclerView.getFocusedChild(); + if (focusedChild != null) { + mActionButtonsRecyclerView.scrollToPosition( + mActionButtonsRecyclerView.getChildLayoutPosition(focusedChild)); + } } /** @@ -391,48 +269,63 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { */ void showMoveMenu(int gravity) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: showMoveMenu()", TAG); - showButtonsMenu(false); showMovementHints(gravity); + setMenuButtonsVisible(false); setFrameHighlighted(true); - mHorizontalScrollView.setFocusable(false); - mScrollView.setFocusable(false); + animateAlphaTo(mA11yManager.isEnabled() ? 1f : 0f, mDimLayer); mEduTextDrawer.closeIfNeeded(); } - void showButtonsMenu() { + + void showButtonsMenu(boolean exitingMoveMode) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: showButtonsMenu()", TAG); - showButtonsMenu(true); + "%s: showButtonsMenu(), exitingMoveMode %b", TAG, exitingMoveMode); + setMenuButtonsVisible(true); hideMovementHints(); setFrameHighlighted(true); + animateAlphaTo(1f, mDimLayer); + mEduTextDrawer.closeIfNeeded(); - mHorizontalScrollView.setFocusable(true); - mScrollView.setFocusable(true); - - // Always focus on the first button when opening the menu, except directly after moving. - if (mFocusedButton == null) { - // Focus on first button (there is a Space at position 0) - mFocusedButton = mActionButtonsContainer.getChildAt(1); - // Reset scroll position. - mScrollView.scrollTo(0, 0); - mHorizontalScrollView.scrollTo( - isLayoutRtl() ? mActionButtonsContainer.getWidth() : 0, 0); + if (exitingMoveMode) { + scrollAndRefocusButton(mTvPipActionsProvider.getFirstIndexOfAction(ACTION_MOVE), + /* alwaysScroll= */ false); + } else { + scrollAndRefocusButton(0, /* alwaysScroll= */ true); + } + } + + private void scrollAndRefocusButton(int position, boolean alwaysScroll) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: scrollAndRefocusButton, target: %d", TAG, position); + + if (alwaysScroll || !refocusButton(position)) { + mButtonLayoutManager.scrollToPositionWithOffset(position, 0); + mActionButtonsRecyclerView.post(() -> refocusButton(position)); } - refocusPreviousButton(); } /** - * Hides all menu views, including the menu frame. + * @return true if focus was requested, false if focus request could not be carried out due to + * the view for the position not being available (scrolling beforehand will be necessary). */ + private boolean refocusButton(int position) { + View itemToFocus = mButtonLayoutManager.findViewByPosition(position); + if (itemToFocus != null) { + itemToFocus.requestFocus(); + itemToFocus.requestAccessibilityFocus(); + } + return itemToFocus != null; + } + void hideAllUserControls() { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: hideAllUserControls()", TAG); - mFocusedButton = null; - showButtonsMenu(false); + setMenuButtonsVisible(false); hideMovementHints(); setFrameHighlighted(false); + animateAlphaTo(0f, mDimLayer); } @Override @@ -463,134 +356,19 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { }); } - /** - * Button order: - * - Fullscreen - * - Close - * - Custom actions (app or media actions) - * - System actions - */ - void setAdditionalActions(List<RemoteAction> actions, RemoteAction closeAction, - Handler mainHandler) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: setAdditionalActions()", TAG); - - // Replace system close action with custom close action if available - if (closeAction != null) { - setActionForButton(closeAction, mCloseButton, mainHandler); - } else { - mCloseButton.setTextAndDescription(R.string.pip_close); - mCloseButton.setImageResource(R.drawable.pip_ic_close_white); - } - mCloseButton.setIsCustomCloseAction(closeAction != null); - // Make sure the close action is always enabled - mCloseButton.setEnabled(true); - - // Make sure we exactly as many additional buttons as we have actions to display. - final int actionsNumber = actions.size(); - int buttonsNumber = mAdditionalButtons.size(); - if (actionsNumber > buttonsNumber) { - // Add buttons until we have enough to display all the actions. - while (actionsNumber > buttonsNumber) { - TvWindowMenuActionButton button = new TvWindowMenuActionButton(mContext); - button.setOnClickListener(this); - - mActionButtonsContainer.addView(button, - FIRST_CUSTOM_ACTION_POSITION + buttonsNumber); - mAdditionalButtons.add(button); - - buttonsNumber++; - } - } else if (actionsNumber < buttonsNumber) { - // Hide buttons until we as many as the actions. - while (actionsNumber < buttonsNumber) { - final View button = mAdditionalButtons.get(buttonsNumber - 1); - button.setVisibility(View.GONE); - button.setTag(null); - - buttonsNumber--; - } - } - - // "Assign" actions to the buttons. - for (int index = 0; index < actionsNumber; index++) { - final RemoteAction action = actions.get(index); - final TvWindowMenuActionButton button = mAdditionalButtons.get(index); - - // Remove action if it matches the custom close action. - if (PipUtils.remoteActionsMatch(action, closeAction)) { - button.setVisibility(GONE); - continue; - } - setActionForButton(action, button, mainHandler); - } - - if (mCurrentPipBounds != null) { - updateButtonGravity(mCurrentPipBounds); - refocusPreviousButton(); - } - } - - private void setActionForButton(RemoteAction action, TvWindowMenuActionButton button, - Handler mainHandler) { - button.setVisibility(View.VISIBLE); // Ensure the button is visible. - if (action.getContentDescription().length() > 0) { - button.setTextAndDescription(action.getContentDescription()); - } else { - button.setTextAndDescription(action.getTitle()); - } - button.setEnabled(action.isEnabled()); - button.setTag(action); - action.getIcon().loadDrawableAsync(mContext, button::setImageDrawable, mainHandler); - } - - @Nullable - SurfaceControl getWindowSurfaceControl() { - final ViewRootImpl root = getViewRootImpl(); - if (root == null) { - return null; - } - final SurfaceControl out = root.getSurfaceControl(); - if (out != null && out.isValid()) { - return out; - } - return null; - } - @Override - public void onClick(View v) { - final int id = v.getId(); - if (id == R.id.tv_pip_menu_fullscreen_button) { - mListener.onFullscreenButtonClick(); - } else if (id == R.id.tv_pip_menu_move_button) { - mListener.onEnterMoveMode(); - } else if (id == R.id.tv_pip_menu_close_button) { - mListener.onCloseButtonClick(); - } else if (id == R.id.tv_pip_menu_expand_button) { - mListener.onToggleExpandedMode(); - } else { - // This should be an "additional action" - final RemoteAction action = (RemoteAction) v.getTag(); - if (action != null) { - try { - action.getActionIntent().send(); - } catch (PendingIntent.CanceledException e) { - ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: Failed to send action, %s", TAG, e); - } - } else { - ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: RemoteAction is null", TAG); - } + public void onActionsChanged(int added, int updated, int startIndex) { + mRecyclerViewAdapter.notifyItemRangeChanged(startIndex, updated); + if (added > 0) { + mRecyclerViewAdapter.notifyItemRangeInserted(startIndex + updated, added); + } else if (added < 0) { + mRecyclerViewAdapter.notifyItemRangeRemoved(startIndex + updated, -added); } } @Override public boolean dispatchKeyEvent(KeyEvent event) { if (event.getAction() == ACTION_UP) { - if (!mMoveMenuIsVisible) { - mFocusedButton = mActionButtonsContainer.getFocusedChild(); - } if (event.getKeyCode() == KEYCODE_BACK) { mListener.onBackPress(); @@ -624,10 +402,6 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { public void showMovementHints(int gravity) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: showMovementHints(), position: %s", TAG, Gravity.toString(gravity)); - - if (mMoveMenuIsVisible) { - return; - } mMoveMenuIsVisible = true; animateAlphaTo(checkGravity(gravity, Gravity.BOTTOM) ? 1f : 0f, mArrowUp); @@ -643,9 +417,12 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { animateAlphaTo(a11yEnabled ? 1f : 0f, mA11yDoneButton); if (a11yEnabled) { + mA11yDoneButton.setVisibility(VISIBLE); mA11yDoneButton.setOnClickListener(v -> { mListener.onExitMoveMode(); }); + mA11yDoneButton.requestFocus(); + mA11yDoneButton.requestAccessibilityFocus(); } } @@ -684,33 +461,67 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { /** * Show or hide the pip buttons menu. */ - public void showButtonsMenu(boolean show) { + private void setMenuButtonsVisible(boolean visible) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: showUserActions: %b", TAG, show); - if (mButtonMenuIsVisible == show) { - return; - } - mButtonMenuIsVisible = show; - - if (show) { - mActionButtonsContainer.setVisibility(VISIBLE); - refocusPreviousButton(); - } - animateAlphaTo(show ? 1 : 0, mActionButtonsContainer); - animateAlphaTo(show ? 1 : 0, mDimLayer); - mEduTextDrawer.closeIfNeeded(); + "%s: showUserActions: %b", TAG, visible); + mButtonMenuIsVisible = visible; + animateAlphaTo(visible ? 1 : 0, mActionButtonsRecyclerView); } private void setFrameHighlighted(boolean highlighted) { mMenuFrameView.setActivated(highlighted); } + private class RecyclerViewAdapter extends + RecyclerView.Adapter<RecyclerViewAdapter.ButtonViewHolder> { + + private final List<TvPipAction> mActionList; + + RecyclerViewAdapter(List<TvPipAction> actionList) { + this.mActionList = actionList; + } + + @NonNull + @Override + public ButtonViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new ButtonViewHolder(new TvWindowMenuActionButton(mContext)); + } + + @Override + public void onBindViewHolder(@NonNull ButtonViewHolder holder, int position) { + TvPipAction action = mActionList.get(position); + action.populateButton(holder.mButton, mMainHandler); + } + + @Override + public int getItemCount() { + return mActionList.size(); + } + + private class ButtonViewHolder extends RecyclerView.ViewHolder implements OnClickListener { + TvWindowMenuActionButton mButton; + + ButtonViewHolder(@NonNull View itemView) { + super(itemView); + mButton = (TvWindowMenuActionButton) itemView; + mButton.setOnClickListener(this); + } + + @Override + public void onClick(View v) { + TvPipAction action = mActionList.get( + mActionButtonsRecyclerView.getChildLayoutPosition(v)); + if (action != null) { + action.executeAction(); + } + } + } + } + interface Listener extends TvPipMenuEduTextDrawer.Listener { void onBackPress(); - void onEnterMoveMode(); - /** * Called when a button for exiting move mode was pressed. * @@ -723,11 +534,5 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { * @return whether pip movement was handled. */ boolean onPipMovement(int keycode); - - void onCloseButtonClick(); - - void onFullscreenButtonClick(); - - void onToggleExpandedMode(); } -} +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java index e3308f0763a0..f22ee595e6c9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java @@ -16,18 +16,13 @@ package com.android.wm.shell.pip.tv; -import static android.app.Notification.Action.SEMANTIC_ACTION_DELETE; -import static android.app.Notification.Action.SEMANTIC_ACTION_NONE; - +import android.annotation.NonNull; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; -import android.app.RemoteAction; -import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; -import android.content.IntentFilter; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.graphics.Bitmap; @@ -35,7 +30,6 @@ import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; import android.media.session.MediaSession; import android.os.Bundle; -import android.os.Handler; import android.text.TextUtils; import com.android.internal.messages.nano.SystemMessageProto.SystemMessage; @@ -47,7 +41,6 @@ import com.android.wm.shell.pip.PipParamsChangedForwarder; import com.android.wm.shell.pip.PipUtils; import com.android.wm.shell.protolog.ShellProtoLogGroup; -import java.util.ArrayList; import java.util.List; /** @@ -55,39 +48,18 @@ import java.util.List; * <p>Once it's created, it will manage the PiP notification UI by itself except for handling * configuration changes and user initiated expanded PiP toggling. */ -public class TvPipNotificationController { - private static final String TAG = "TvPipNotification"; +public class TvPipNotificationController implements TvPipActionsProvider.Listener { + private static final String TAG = TvPipNotificationController.class.getSimpleName(); // Referenced in com.android.systemui.util.NotificationChannels. public static final String NOTIFICATION_CHANNEL = "TVPIP"; private static final String NOTIFICATION_TAG = "TvPip"; - private static final String SYSTEMUI_PERMISSION = "com.android.systemui.permission.SELF"; - - private static final String ACTION_SHOW_PIP_MENU = - "com.android.wm.shell.pip.tv.notification.action.SHOW_PIP_MENU"; - private static final String ACTION_CLOSE_PIP = - "com.android.wm.shell.pip.tv.notification.action.CLOSE_PIP"; - private static final String ACTION_MOVE_PIP = - "com.android.wm.shell.pip.tv.notification.action.MOVE_PIP"; - private static final String ACTION_TOGGLE_EXPANDED_PIP = - "com.android.wm.shell.pip.tv.notification.action.TOGGLE_EXPANDED_PIP"; - private static final String ACTION_FULLSCREEN = - "com.android.wm.shell.pip.tv.notification.action.FULLSCREEN"; private final Context mContext; private final PackageManager mPackageManager; private final NotificationManager mNotificationManager; private final Notification.Builder mNotificationBuilder; - private final ActionBroadcastReceiver mActionBroadcastReceiver; - private final Handler mMainHandler; - private Delegate mDelegate; - private final TvPipBoundsState mTvPipBoundsState; - - private String mDefaultTitle; - - private final List<RemoteAction> mCustomActions = new ArrayList<>(); - private final List<RemoteAction> mMediaActions = new ArrayList<>(); - private RemoteAction mCustomCloseAction; + private TvPipActionsProvider mTvPipActionsProvider; private MediaSession.Token mMediaSessionToken; @@ -95,19 +67,23 @@ public class TvPipNotificationController { private String mPackageName; private boolean mIsNotificationShown; + private String mDefaultTitle; private String mPipTitle; private String mPipSubtitle; + // Saving the actions, so they don't have to be regenerated when e.g. the PiP title changes. + @NonNull + private Notification.Action[] mPipActions; + private Bitmap mActivityIcon; public TvPipNotificationController(Context context, PipMediaController pipMediaController, - PipParamsChangedForwarder pipParamsChangedForwarder, TvPipBoundsState tvPipBoundsState, - Handler mainHandler) { + PipParamsChangedForwarder pipParamsChangedForwarder) { mContext = context; mPackageManager = context.getPackageManager(); mNotificationManager = context.getSystemService(NotificationManager.class); - mMainHandler = mainHandler; - mTvPipBoundsState = tvPipBoundsState; + + mPipActions = new Notification.Action[0]; mNotificationBuilder = new Notification.Builder(context, NOTIFICATION_CHANNEL) .setLocalOnly(true) @@ -117,34 +93,15 @@ public class TvPipNotificationController { .setOnlyAlertOnce(true) .setSmallIcon(R.drawable.pip_icon) .setAllowSystemGeneratedContextualActions(false) - .setContentIntent(createPendingIntent(context, ACTION_FULLSCREEN)) - .setDeleteIntent(getCloseAction().actionIntent) - .extend(new Notification.TvExtender() - .setContentIntent(createPendingIntent(context, ACTION_SHOW_PIP_MENU)) - .setDeleteIntent(createPendingIntent(context, ACTION_CLOSE_PIP))); - - mActionBroadcastReceiver = new ActionBroadcastReceiver(); + .setContentIntent( + createPendingIntent(context, TvPipController.ACTION_TO_FULLSCREEN)); + // TvExtender and DeleteIntent set later since they might change. - pipMediaController.addActionListener(this::onMediaActionsChanged); pipMediaController.addTokenListener(this::onMediaSessionTokenChanged); pipParamsChangedForwarder.addListener( new PipParamsChangedForwarder.PipParamsChangedCallback() { @Override - public void onExpandedAspectRatioChanged(float ratio) { - updateExpansionState(); - } - - @Override - public void onActionsChanged(List<RemoteAction> actions, - RemoteAction closeAction) { - mCustomActions.clear(); - mCustomActions.addAll(actions); - mCustomCloseAction = closeAction; - updateNotificationContent(); - } - - @Override public void onTitleChanged(String title) { mPipTitle = title; updateNotificationContent(); @@ -157,34 +114,33 @@ public class TvPipNotificationController { } }); - onConfigurationChanged(context); + onConfigurationChanged(); } - void setDelegate(Delegate delegate) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: setDelegate(), delegate=%s", - TAG, delegate); - - if (mDelegate != null) { - throw new IllegalStateException( - "The delegate has already been set and should not change."); - } - if (delegate == null) { - throw new IllegalArgumentException("The delegate must not be null."); - } + /** + * Call before showing any notification. + */ + void setTvPipActionsProvider(@NonNull TvPipActionsProvider tvPipActionsProvider) { + mTvPipActionsProvider = tvPipActionsProvider; + mTvPipActionsProvider.addListener(this); + } - mDelegate = delegate; + void onConfigurationChanged() { + mDefaultTitle = mContext.getResources().getString(R.string.pip_notification_unknown_title); + updateNotificationContent(); } void show(String packageName) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: show %s", TAG, packageName); - if (mDelegate == null) { - throw new IllegalStateException("Delegate is not set."); + if (mTvPipActionsProvider == null) { + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Missing TvPipActionsProvider", TAG); + return; } mIsNotificationShown = true; mPackageName = packageName; mActivityIcon = getActivityIcon(); - mActionBroadcastReceiver.register(); updateNotificationContent(); } @@ -194,151 +150,42 @@ public class TvPipNotificationController { mIsNotificationShown = false; mPackageName = null; - mActionBroadcastReceiver.unregister(); - mNotificationManager.cancel(NOTIFICATION_TAG, SystemMessage.NOTE_TV_PIP); } - private Notification.Action getToggleAction(boolean expanded) { - if (expanded) { - return createSystemAction(R.drawable.pip_ic_collapse, - R.string.pip_collapse, ACTION_TOGGLE_EXPANDED_PIP); - } else { - return createSystemAction(R.drawable.pip_ic_expand, R.string.pip_expand, - ACTION_TOGGLE_EXPANDED_PIP); - } - } - - private Notification.Action createSystemAction(int iconRes, int titleRes, String action) { - Notification.Action.Builder builder = new Notification.Action.Builder( - Icon.createWithResource(mContext, iconRes), - mContext.getString(titleRes), - createPendingIntent(mContext, action)); - builder.setContextual(true); - return builder.build(); - } - - private void onMediaActionsChanged(List<RemoteAction> actions) { - mMediaActions.clear(); - mMediaActions.addAll(actions); - if (mCustomActions.isEmpty()) { - updateNotificationContent(); - } - } - private void onMediaSessionTokenChanged(MediaSession.Token token) { mMediaSessionToken = token; updateNotificationContent(); } - private Notification.Action remoteToNotificationAction(RemoteAction action) { - return remoteToNotificationAction(action, SEMANTIC_ACTION_NONE); - } - - private Notification.Action remoteToNotificationAction(RemoteAction action, - int semanticAction) { - Notification.Action.Builder builder = new Notification.Action.Builder(action.getIcon(), - action.getTitle(), - action.getActionIntent()); - if (action.getContentDescription() != null) { - Bundle extras = new Bundle(); - extras.putCharSequence(Notification.EXTRA_PICTURE_CONTENT_DESCRIPTION, - action.getContentDescription()); - builder.addExtras(extras); - } - builder.setSemanticAction(semanticAction); - builder.setContextual(true); - return builder.build(); - } - - private Notification.Action[] getNotificationActions() { - final List<Notification.Action> actions = new ArrayList<>(); - - // 1. Fullscreen - actions.add(getFullscreenAction()); - // 2. Close - actions.add(getCloseAction()); - // 3. App actions - final List<RemoteAction> appActions = - mCustomActions.isEmpty() ? mMediaActions : mCustomActions; - for (RemoteAction appAction : appActions) { - if (PipUtils.remoteActionsMatch(mCustomCloseAction, appAction) - || !appAction.isEnabled()) { - continue; - } - actions.add(remoteToNotificationAction(appAction)); - } - // 4. Move - actions.add(getMoveAction()); - // 5. Toggle expansion (if expanded PiP enabled) - if (mTvPipBoundsState.getDesiredTvExpandedAspectRatio() > 0 - && mTvPipBoundsState.isTvExpandedPipSupported()) { - actions.add(getToggleAction(mTvPipBoundsState.isTvPipExpanded())); - } - return actions.toArray(new Notification.Action[0]); - } - - private Notification.Action getCloseAction() { - if (mCustomCloseAction == null) { - return createSystemAction(R.drawable.pip_ic_close_white, R.string.pip_close, - ACTION_CLOSE_PIP); - } else { - return remoteToNotificationAction(mCustomCloseAction, SEMANTIC_ACTION_DELETE); - } - } - - private Notification.Action getFullscreenAction() { - return createSystemAction(R.drawable.pip_ic_fullscreen_white, - R.string.pip_fullscreen, ACTION_FULLSCREEN); - } - - private Notification.Action getMoveAction() { - return createSystemAction(R.drawable.pip_ic_move_white, R.string.pip_move, - ACTION_MOVE_PIP); - } - - /** - * Called by {@link TvPipController} when the configuration is changed. - */ - void onConfigurationChanged(Context context) { - mDefaultTitle = context.getResources().getString(R.string.pip_notification_unknown_title); - updateNotificationContent(); - } - - void updateExpansionState() { - updateNotificationContent(); - } - private void updateNotificationContent() { if (mPackageManager == null || !mIsNotificationShown) { return; } - Notification.Action[] actions = getNotificationActions(); ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: update(), title: %s, subtitle: %s, mediaSessionToken: %s, #actions: %s", TAG, - getNotificationTitle(), mPipSubtitle, mMediaSessionToken, actions.length); - for (Notification.Action action : actions) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: action: %s", TAG, - action.toString()); - } - + getNotificationTitle(), mPipSubtitle, mMediaSessionToken, mPipActions.length); mNotificationBuilder .setWhen(System.currentTimeMillis()) .setContentTitle(getNotificationTitle()) .setContentText(mPipSubtitle) .setSubText(getApplicationLabel(mPackageName)) - .setActions(actions); + .setActions(mPipActions); setPipIcon(); Bundle extras = new Bundle(); extras.putParcelable(Notification.EXTRA_MEDIA_SESSION, mMediaSessionToken); mNotificationBuilder.setExtras(extras); + PendingIntent closeIntent = mTvPipActionsProvider.getCloseAction().getPendingIntent(); + mNotificationBuilder.setDeleteIntent(closeIntent); // TvExtender not recognized if not set last. mNotificationBuilder.extend(new Notification.TvExtender() - .setContentIntent(createPendingIntent(mContext, ACTION_SHOW_PIP_MENU)) - .setDeleteIntent(createPendingIntent(mContext, ACTION_CLOSE_PIP))); + .setContentIntent( + createPendingIntent(mContext, TvPipController.ACTION_SHOW_PIP_MENU)) + .setDeleteIntent(closeIntent)); + mNotificationManager.notify(NOTIFICATION_TAG, SystemMessage.NOTE_TV_PIP, mNotificationBuilder.build()); } @@ -390,68 +237,20 @@ public class TvPipNotificationController { return ImageUtils.buildScaledBitmap(drawable, width, height, /* allowUpscaling */ true); } - private static PendingIntent createPendingIntent(Context context, String action) { + static PendingIntent createPendingIntent(Context context, String action) { return PendingIntent.getBroadcast(context, 0, new Intent(action).setPackage(context.getPackageName()), PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); } - private class ActionBroadcastReceiver extends BroadcastReceiver { - final IntentFilter mIntentFilter; - { - mIntentFilter = new IntentFilter(); - mIntentFilter.addAction(ACTION_CLOSE_PIP); - mIntentFilter.addAction(ACTION_SHOW_PIP_MENU); - mIntentFilter.addAction(ACTION_MOVE_PIP); - mIntentFilter.addAction(ACTION_TOGGLE_EXPANDED_PIP); - mIntentFilter.addAction(ACTION_FULLSCREEN); - } - boolean mRegistered = false; - - void register() { - if (mRegistered) return; - - mContext.registerReceiverForAllUsers(this, mIntentFilter, SYSTEMUI_PERMISSION, - mMainHandler); - mRegistered = true; - } - - void unregister() { - if (!mRegistered) return; - - mContext.unregisterReceiver(this); - mRegistered = false; - } - - @Override - public void onReceive(Context context, Intent intent) { - final String action = intent.getAction(); - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: on(Broadcast)Receive(), action=%s", TAG, action); - - if (ACTION_SHOW_PIP_MENU.equals(action)) { - mDelegate.showPictureInPictureMenu(); - } else if (ACTION_CLOSE_PIP.equals(action)) { - mDelegate.closePip(); - } else if (ACTION_MOVE_PIP.equals(action)) { - mDelegate.enterPipMovementMenu(); - } else if (ACTION_TOGGLE_EXPANDED_PIP.equals(action)) { - mDelegate.togglePipExpansion(); - } else if (ACTION_FULLSCREEN.equals(action)) { - mDelegate.movePipToFullscreen(); - } + @Override + public void onActionsChanged(int added, int updated, int startIndex) { + List<TvPipAction> actions = mTvPipActionsProvider.getActionsList(); + mPipActions = new Notification.Action[actions.size()]; + for (int i = 0; i < mPipActions.length; i++) { + mPipActions[i] = actions.get(i).toNotificationAction(mContext); } + updateNotificationContent(); } - interface Delegate { - void showPictureInPictureMenu(); - - void closePip(); - - void enterPipMovementMenu(); - - void togglePipExpansion(); - - void movePipToFullscreen(); - } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipSystemAction.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipSystemAction.java new file mode 100644 index 000000000000..93b6a908e3f4 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipSystemAction.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.pip.tv; + +import static android.app.Notification.Action.SEMANTIC_ACTION_DELETE; +import static android.app.Notification.Action.SEMANTIC_ACTION_NONE; + +import android.annotation.DrawableRes; +import android.annotation.NonNull; +import android.annotation.StringRes; +import android.app.Notification; +import android.app.PendingIntent; +import android.content.Context; +import android.graphics.drawable.Icon; +import android.os.Handler; + +import com.android.wm.shell.common.TvWindowMenuActionButton; + +/** + * A TvPipAction for actions that the system provides, i.e. fullscreen, default close, move, + * expand/collapse. + */ +public class TvPipSystemAction extends TvPipAction { + + @StringRes + private int mTitleResource; + @DrawableRes + private int mIconResource; + + private final PendingIntent mBroadcastIntent; + + TvPipSystemAction(@ActionType int actionType, @StringRes int title, @DrawableRes int icon, + String broadcastAction, @NonNull Context context, + SystemActionsHandler systemActionsHandler) { + super(actionType, systemActionsHandler); + update(title, icon); + mBroadcastIntent = TvPipNotificationController.createPendingIntent(context, + broadcastAction); + } + + void update(@StringRes int title, @DrawableRes int icon) { + mTitleResource = title; + mIconResource = icon; + } + + void populateButton(@NonNull TvWindowMenuActionButton button, Handler mainHandler) { + button.setTextAndDescription(mTitleResource); + button.setImageResource(mIconResource); + button.setEnabled(true); + } + + PendingIntent getPendingIntent() { + return mBroadcastIntent; + } + + @Override + Notification.Action toNotificationAction(Context context) { + Notification.Action.Builder builder = new Notification.Action.Builder( + Icon.createWithResource(context, mIconResource), + context.getString(mTitleResource), + mBroadcastIntent); + + builder.setSemanticAction(isCloseAction() + ? SEMANTIC_ACTION_DELETE : SEMANTIC_ACTION_NONE); + builder.setContextual(true); + return builder.build(); + } + +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index 602d0e6c0201..5be29db12b25 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -357,11 +357,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, return mMainStage.isActive(); } - boolean isSplitScreenRunningBackground() { - return !isSplitScreenVisible() && mMainStageListener.mHasChildren - && mSideStageListener.mHasChildren; - } - @StageType int getStageOfTask(int taskId) { if (mMainStage.containsTask(taskId)) { @@ -389,7 +384,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, targetStage = stagePosition == mSideStagePosition ? mSideStage : mMainStage; sideStagePosition = mSideStagePosition; } else { - exitSplitIfBackground(); + // Exit split if it running background. + exitSplitScreen(null /* childrenToTop */, EXIT_REASON_RECREATE_SPLIT); + targetStage = mSideStage; sideStagePosition = stagePosition; } @@ -685,7 +682,10 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, @Nullable PendingIntent mainPendingIntent, @Nullable Intent mainFillInIntent, @Nullable Bundle mainOptions, @SplitPosition int sidePosition, float splitRatio, RemoteAnimationAdapter adapter, InstanceId instanceId) { - exitSplitIfBackground(); + if (!isSplitScreenVisible()) { + exitSplitScreen(null /* childrenToTop */, EXIT_REASON_RECREATE_SPLIT); + } + // Init divider first to make divider leash for remote animation target. mSplitLayout.init(); mSplitLayout.setDivideRatio(splitRatio); @@ -1070,7 +1070,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSideStage.removeAllTasks(wct, false /* toTop */); mMainStage.deactivate(wct, false /* toTop */); wct.reorder(mRootTaskInfo.token, false /* onTop */); - wct.setForceTranslucent(mRootTaskInfo.token, true); wct.setBounds(mSideStage.mRootTaskInfo.token, mTempRect1); onTransitionAnimationComplete(); } else { @@ -1102,7 +1101,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mMainStage.deactivate(finishedWCT, childrenToTop == mMainStage /* toTop */); mSideStage.removeAllTasks(finishedWCT, childrenToTop == mSideStage /* toTop */); finishedWCT.reorder(mRootTaskInfo.token, false /* toTop */); - finishedWCT.setForceTranslucent(mRootTaskInfo.token, true); finishedWCT.setBounds(mSideStage.mRootTaskInfo.token, mTempRect1); mSyncQueue.queue(finishedWCT); mSyncQueue.runInSync(at -> { @@ -1122,13 +1120,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } } - /** Exit split screen if it still running background */ - public void exitSplitIfBackground() { - if (!isSplitScreenRunningBackground()) return; - - exitSplitScreen(null /* childrenToTop */, EXIT_REASON_RECREATE_SPLIT); - } - /** * Overridden by child classes. */ @@ -1451,7 +1442,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } void onChildTaskAppeared(StageListenerImpl stageListener, int taskId) { - if (stageListener == mSideStageListener && isSplitScreenRunningBackground()) { + if (stageListener == mSideStageListener && !isSplitScreenVisible() && isSplitActive() + && !mIsSplitEntering) { // Handle entring split case here if split already running background. if (mIsDropEntering) { mSplitLayout.resetDividerPosition(); @@ -1459,11 +1451,12 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSplitLayout.setDividerAtBorder(mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT); } final WindowContainerTransaction wct = new WindowContainerTransaction(); + mMainStage.reparentTopTask(wct); mMainStage.evictAllChildren(wct); mSideStage.evictOtherChildren(wct, taskId); - mMainStage.reparentTopTask(wct); updateWindowBounds(mSplitLayout, wct); wct.reorder(mRootTaskInfo.token, true); + wct.setForceTranslucent(mRootTaskInfo.token, false); mSyncQueue.queue(wct); mSyncQueue.runInSync(t -> { @@ -1471,6 +1464,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */); mIsDropEntering = false; } else { + mShowDecorImmediately = true; mSplitLayout.flingDividerToCenter(); } }); @@ -1499,6 +1493,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, if (!mainStageVisible) { wct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token, true /* setReparentLeafTaskIfRelaunch */); + wct.setForceTranslucent(mRootTaskInfo.token, true); // Both stages are not visible, check if it needs to dismiss split screen. if (mExitSplitScreenOnHide) { exitSplitScreen(null /* childrenToTop */, EXIT_REASON_RETURN_HOME); @@ -1506,6 +1501,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } else { wct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token, false /* setReparentLeafTaskIfRelaunch */); + wct.setForceTranslucent(mRootTaskInfo.token, false); } mSyncQueue.queue(wct); mSyncQueue.runInSync(t -> { @@ -1617,8 +1613,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSplitLayout.flingDividerToDismiss( mSideStagePosition != SPLIT_POSITION_BOTTOM_OR_RIGHT, EXIT_REASON_APP_FINISHED); - } else if (isSplitScreenRunningBackground()) { - // Do not exit to any stage due to running background. + } else if (!isSplitScreenVisible()) { exitSplitScreen(null /* childrenToTop */, EXIT_REASON_APP_FINISHED); } } else if (isSideStage && hasChildren && !mMainStage.isActive()) { @@ -2355,9 +2350,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, if (!isSplitScreenVisible()) { mIsDropEntering = true; } - if (isSplitScreenRunningBackground()) { - // Split running background, log exit first and start new enter request. - logExit(EXIT_REASON_RECREATE_SPLIT); + if (!isSplitScreenVisible()) { + // If split running background, exit split first. + exitSplitScreen(null /* childrenToTop */, EXIT_REASON_RECREATE_SPLIT); } mLogger.enterRequestedByDrag(position, dragSessionId); } @@ -2366,9 +2361,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, * Sets info to be logged when splitscreen is next entered. */ public void onRequestToSplit(InstanceId sessionId, int enterReason) { - if (isSplitScreenRunningBackground()) { - // Split running background, log exit first and start new enter request. - logExit(EXIT_REASON_RECREATE_SPLIT); + if (!isSplitScreenVisible()) { + // If split running background, exit split first. + exitSplitScreen(null /* childrenToTop */, EXIT_REASON_RECREATE_SPLIT); } mLogger.enterRequested(sessionId, enterReason); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java index 65da757b1396..9d6711f42efe 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java @@ -239,7 +239,7 @@ public class TaskSnapshotWindow { public void resized(ClientWindowFrames frames, boolean reportDraw, MergedConfiguration mergedConfiguration, InsetsState insetsState, boolean forceLayout, boolean alwaysConsumeSystemBars, int displayId, int seqId, - boolean dragResizing) { + int resizeMode) { final TaskSnapshotWindow snapshot = mOuter.get(); if (snapshot == null) { return; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java index afefd5dc6344..42e2b3fadf19 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java @@ -46,6 +46,7 @@ import android.window.WindowContainerTransaction; import androidx.annotation.Nullable; +import com.android.internal.annotations.VisibleForTesting; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; @@ -56,7 +57,6 @@ import com.android.wm.shell.freeform.FreeformTaskTransitionStarter; import com.android.wm.shell.transition.Transitions; import java.util.Optional; -import java.util.function.Supplier; /** * View model for the window decoration with a caption and shadows. Works with @@ -66,7 +66,6 @@ import java.util.function.Supplier; public class CaptionWindowDecorViewModel implements WindowDecorViewModel { private static final String TAG = "CaptionViewModel"; private final CaptionWindowDecoration.Factory mCaptionWindowDecorFactory; - private final Supplier<InputManager> mInputManagerSupplier; private final ActivityTaskManager mActivityTaskManager; private final ShellTaskOrganizer mTaskOrganizer; private final Context mContext; @@ -82,7 +81,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { private final SparseArray<CaptionWindowDecoration> mWindowDecorByTaskId = new SparseArray<>(); private final DragStartListenerImpl mDragStartListener = new DragStartListenerImpl(); - private EventReceiverFactory mEventReceiverFactory = new EventReceiverFactory(); + private InputMonitorFactory mInputMonitorFactory; public CaptionWindowDecorViewModel( Context context, @@ -101,10 +100,11 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { syncQueue, desktopModeController, new CaptionWindowDecoration.Factory(), - InputManager::getInstance); + new InputMonitorFactory()); } - public CaptionWindowDecorViewModel( + @VisibleForTesting + CaptionWindowDecorViewModel( Context context, Handler mainHandler, Choreographer mainChoreographer, @@ -113,8 +113,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { SyncTransactionQueue syncQueue, Optional<DesktopModeController> desktopModeController, CaptionWindowDecoration.Factory captionWindowDecorFactory, - Supplier<InputManager> inputManagerSupplier) { - + InputMonitorFactory inputMonitorFactory) { mContext = context; mMainHandler = mainHandler; mMainChoreographer = mainChoreographer; @@ -125,11 +124,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { mDesktopModeController = desktopModeController; mCaptionWindowDecorFactory = captionWindowDecorFactory; - mInputManagerSupplier = inputManagerSupplier; - } - - void setEventReceiverFactory(EventReceiverFactory eventReceiverFactory) { - mEventReceiverFactory = eventReceiverFactory; + mInputMonitorFactory = inputMonitorFactory; } @Override @@ -205,7 +200,6 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { decoration.close(); int displayId = taskInfo.displayId; if (mEventReceiversByDisplay.contains(displayId)) { - EventReceiver eventReceiver = mEventReceiversByDisplay.get(displayId); removeTaskFromEventReceiver(displayId); } } @@ -408,12 +402,6 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { } } - class EventReceiverFactory { - EventReceiver create(InputMonitor inputMonitor, InputChannel channel, Looper looper) { - return new EventReceiver(inputMonitor, channel, looper); - } - } - /** * Handle MotionEvents relevant to focused task's caption that don't directly touch it * @@ -500,11 +488,11 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { } private void createInputChannel(int displayId) { - InputManager inputManager = mInputManagerSupplier.get(); + InputManager inputManager = InputManager.getInstance(); InputMonitor inputMonitor = - inputManager.monitorGestureInput("caption-touch", mContext.getDisplayId()); - EventReceiver eventReceiver = mEventReceiverFactory.create( - inputMonitor, inputMonitor.getInputChannel(), Looper.myLooper()); + mInputMonitorFactory.create(inputManager, mContext); + EventReceiver eventReceiver = new EventReceiver(inputMonitor, + inputMonitor.getInputChannel(), Looper.myLooper()); mEventReceiversByDisplay.put(displayId, eventReceiver); } @@ -562,4 +550,12 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { mWindowDecorByTaskId.get(taskId).closeHandleMenu(); } } + + static class InputMonitorFactory { + InputMonitor create(InputManager inputManager, Context context) { + return inputManager.monitorGestureInput("caption-touch", context.getDisplayId()); + } + } } + + diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java index 92154968855f..7f85988d1377 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java @@ -33,6 +33,7 @@ import android.view.View; import android.view.ViewRootImpl; import android.view.WindowManager; import android.view.WindowlessWindowManager; +import android.window.TaskConstants; import android.window.WindowContainerTransaction; import com.android.wm.shell.ShellTaskOrganizer; @@ -195,7 +196,9 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> .setParent(mTaskSurface) .build(); - startT.setTrustedOverlay(mDecorationContainerSurface, true); + startT.setTrustedOverlay(mDecorationContainerSurface, true) + .setLayer(mDecorationContainerSurface, + TaskConstants.TASK_CHILD_LAYER_WINDOW_DECORATIONS); } final Rect taskBounds = taskConfig.windowConfiguration.getBounds(); @@ -213,8 +216,6 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> outResult.mDecorContainerOffsetX, outResult.mDecorContainerOffsetY) .setWindowCrop(mDecorationContainerSurface, outResult.mWidth, outResult.mHeight) - // TODO(b/244455401): Change the z-order when it's better organized - .setLayer(mDecorationContainerSurface, mTaskInfo.numActivities + 1) .show(mDecorationContainerSurface); // TaskBackgroundSurface @@ -225,6 +226,8 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> .setEffectLayer() .setParent(mTaskSurface) .build(); + + startT.setLayer(mTaskBackgroundSurface, TaskConstants.TASK_CHILD_LAYER_TASK_BACKGROUND); } float shadowRadius = loadDimension(resources, params.mShadowRadiusId); @@ -236,8 +239,6 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> taskBounds.height()) .setShadowRadius(mTaskBackgroundSurface, shadowRadius) .setColor(mTaskBackgroundSurface, mTmpColor) - // TODO(b/244455401): Change the z-order when it's better organized - .setLayer(mTaskBackgroundSurface, -1) .show(mTaskBackgroundSurface); // CaptionContainerSurface, CaptionWindowManager diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipActionProviderTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipActionProviderTest.java new file mode 100644 index 000000000000..91040e954845 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/tv/TvPipActionProviderTest.java @@ -0,0 +1,309 @@ +/* + * 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.wm.shell.pip.tv; + +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_CLOSE; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_CUSTOM; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_CUSTOM_CLOSE; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_EXPAND_COLLAPSE; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_FULLSCREEN; +import static com.android.wm.shell.pip.tv.TvPipAction.ACTION_MOVE; + +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.app.PendingIntent; +import android.app.RemoteAction; +import android.graphics.drawable.Icon; +import android.test.suitebuilder.annotation.SmallTest; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.util.Log; + +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.pip.PipMediaController; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.List; + +/** + * Unit tests for {@link TvPipActionsProvider} + */ +@SmallTest +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper +public class TvPipActionProviderTest extends ShellTestCase { + private static final String TAG = TvPipActionProviderTest.class.getSimpleName(); + private TvPipActionsProvider mActionsProvider; + + @Mock + private PipMediaController mMockPipMediaController; + @Mock + private TvPipActionsProvider.Listener mMockListener; + @Mock + private TvPipAction.SystemActionsHandler mMockSystemActionsHandler; + @Mock + private Icon mMockIcon; + @Mock + private PendingIntent mMockPendingIntent; + + private RemoteAction createRemoteAction(int identifier) { + return new RemoteAction(mMockIcon, "" + identifier, "" + identifier, mMockPendingIntent); + } + + private List<RemoteAction> createRemoteActions(int numberOfActions) { + List<RemoteAction> actions = new ArrayList<>(); + for (int i = 0; i < numberOfActions; i++) { + actions.add(createRemoteAction(i)); + } + return actions; + } + + private boolean checkActionsMatch(List<TvPipAction> actions, int[] actionTypes) { + for (int i = 0; i < actions.size(); i++) { + int type = actions.get(i).getActionType(); + if (type != actionTypes[i]) { + Log.e(TAG, "Action at index " + i + ": found " + type + + ", expected " + actionTypes[i]); + return false; + } + } + return true; + } + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mActionsProvider = new TvPipActionsProvider(mContext, mMockPipMediaController, + mMockSystemActionsHandler); + } + + @Test + public void defaultSystemActions_regularPip() { + mActionsProvider.updateExpansionEnabled(false); + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_MOVE})); + } + + @Test + public void defaultSystemActions_expandedPip() { + mActionsProvider.updateExpansionEnabled(true); + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_MOVE, ACTION_EXPAND_COLLAPSE})); + } + + @Test + public void expandedPip_enableExpansion_enable() { + // PiP has expanded PiP disabled. + mActionsProvider.updateExpansionEnabled(false); + + mActionsProvider.addListener(mMockListener); + mActionsProvider.updateExpansionEnabled(true); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_MOVE, ACTION_EXPAND_COLLAPSE})); + verify(mMockListener).onActionsChanged(/* added= */ 1, /* updated= */ 0, /* index= */ 3); + } + + @Test + public void expandedPip_enableExpansion_disable() { + mActionsProvider.updateExpansionEnabled(true); + + mActionsProvider.addListener(mMockListener); + mActionsProvider.updateExpansionEnabled(false); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_MOVE})); + verify(mMockListener).onActionsChanged(/* added= */ -1, /* updated= */ 0, /* index= */ 3); + } + + @Test + public void expandedPip_enableExpansion_AlreadyEnabled() { + mActionsProvider.updateExpansionEnabled(true); + + mActionsProvider.addListener(mMockListener); + mActionsProvider.updateExpansionEnabled(true); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_MOVE, ACTION_EXPAND_COLLAPSE})); + } + + @Test + public void expandedPip_toggleExpansion() { + // PiP has expanded PiP enabled, but is in a collapsed state + mActionsProvider.updateExpansionEnabled(true); + mActionsProvider.onPipExpansionToggled(/* expanded= */ false); + + mActionsProvider.addListener(mMockListener); + mActionsProvider.onPipExpansionToggled(/* expanded= */ true); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_MOVE, ACTION_EXPAND_COLLAPSE})); + verify(mMockListener).onActionsChanged(0, 1, 3); + } + + @Test + public void customActions_added() { + mActionsProvider.updateExpansionEnabled(false); + mActionsProvider.addListener(mMockListener); + + mActionsProvider.setAppActions(createRemoteActions(2), null); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_CUSTOM, ACTION_CUSTOM, + ACTION_MOVE})); + verify(mMockListener).onActionsChanged(/* added= */ 2, /* updated= */ 0, /* index= */ 2); + } + + @Test + public void customActions_replacedMore() { + mActionsProvider.updateExpansionEnabled(false); + mActionsProvider.setAppActions(createRemoteActions(2), null); + + mActionsProvider.addListener(mMockListener); + mActionsProvider.setAppActions(createRemoteActions(3), null); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_CUSTOM, ACTION_CUSTOM, + ACTION_CUSTOM, ACTION_MOVE})); + verify(mMockListener).onActionsChanged(/* added= */ 1, /* updated= */ 2, /* index= */ 2); + } + + @Test + public void customActions_replacedLess() { + mActionsProvider.updateExpansionEnabled(false); + mActionsProvider.setAppActions(createRemoteActions(2), null); + + mActionsProvider.addListener(mMockListener); + mActionsProvider.setAppActions(createRemoteActions(0), null); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_MOVE})); + verify(mMockListener).onActionsChanged(/* added= */ -2, /* updated= */ 0, /* index= */ 2); + } + + @Test + public void customCloseAdded() { + mActionsProvider.updateExpansionEnabled(false); + + List<RemoteAction> customActions = new ArrayList<>(); + mActionsProvider.setAppActions(customActions, null); + + mActionsProvider.addListener(mMockListener); + mActionsProvider.setAppActions(customActions, createRemoteAction(0)); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CUSTOM_CLOSE, ACTION_MOVE})); + verify(mMockListener).onActionsChanged(/* added= */ 0, /* updated= */ 1, /* index= */ 1); + } + + @Test + public void customClose_matchesOtherCustomAction() { + mActionsProvider.updateExpansionEnabled(false); + + List<RemoteAction> customActions = createRemoteActions(2); + RemoteAction customClose = createRemoteAction(/* id= */ 10); + customActions.add(customClose); + + mActionsProvider.addListener(mMockListener); + mActionsProvider.setAppActions(customActions, customClose); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CUSTOM_CLOSE, ACTION_CUSTOM, ACTION_CUSTOM, + ACTION_MOVE})); + verify(mMockListener).onActionsChanged(/* added= */ 0, /* updated= */ 1, /* index= */ 1); + verify(mMockListener).onActionsChanged(/* added= */ 2, /* updated= */ 0, /* index= */ 2); + } + + @Test + public void mediaActions_added_whileCustomActionsExist() { + mActionsProvider.updateExpansionEnabled(false); + mActionsProvider.setAppActions(createRemoteActions(2), null); + + mActionsProvider.addListener(mMockListener); + mActionsProvider.onMediaActionsChanged(createRemoteActions(3)); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_CUSTOM, ACTION_CUSTOM, + ACTION_MOVE})); + verify(mMockListener, times(0)).onActionsChanged(anyInt(), anyInt(), anyInt()); + } + + @Test + public void customActions_removed_whileMediaActionsExist() { + mActionsProvider.updateExpansionEnabled(false); + mActionsProvider.onMediaActionsChanged(createRemoteActions(2)); + mActionsProvider.setAppActions(createRemoteActions(3), null); + + mActionsProvider.addListener(mMockListener); + mActionsProvider.setAppActions(createRemoteActions(0), null); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_CUSTOM, ACTION_CUSTOM, + ACTION_MOVE})); + verify(mMockListener).onActionsChanged(/* added= */ -1, /* updated= */ 2, /* index= */ 2); + } + + @Test + public void customCloseOnly_mediaActionsShowing() { + mActionsProvider.updateExpansionEnabled(false); + mActionsProvider.onMediaActionsChanged(createRemoteActions(2)); + + mActionsProvider.addListener(mMockListener); + mActionsProvider.setAppActions(createRemoteActions(0), createRemoteAction(5)); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CUSTOM_CLOSE, ACTION_CUSTOM, ACTION_CUSTOM, + ACTION_MOVE})); + verify(mMockListener).onActionsChanged(/* added= */ 0, /* updated= */ 1, /* index= */ 1); + } + + @Test + public void customActions_showDisabledActions() { + mActionsProvider.updateExpansionEnabled(false); + + List<RemoteAction> customActions = createRemoteActions(2); + customActions.get(0).setEnabled(false); + mActionsProvider.setAppActions(customActions, null); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_CUSTOM, ACTION_CUSTOM, + ACTION_MOVE})); + } + + @Test + public void mediaActions_hideDisabledActions() { + mActionsProvider.updateExpansionEnabled(false); + + List<RemoteAction> customActions = createRemoteActions(2); + customActions.get(0).setEnabled(false); + mActionsProvider.onMediaActionsChanged(customActions); + + assertTrue(checkActionsMatch(mActionsProvider.getActionsList(), + new int[]{ACTION_FULLSCREEN, ACTION_CLOSE, ACTION_CUSTOM, ACTION_MOVE})); + } + +} + diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModelTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModelTests.java index ad6fcedd3166..0dbf30d69f75 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModelTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModelTests.java @@ -21,14 +21,15 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static org.mockito.Mockito.any; -import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.ActivityManager; +import android.hardware.display.DisplayManager; +import android.hardware.display.VirtualDisplay; import android.hardware.input.InputManager; import android.os.Handler; import android.os.Looper; @@ -37,9 +38,9 @@ import android.view.Display; import android.view.InputChannel; import android.view.InputMonitor; import android.view.SurfaceControl; +import android.view.SurfaceView; import androidx.test.filters.SmallTest; -import androidx.test.rule.GrantPermissionRule; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; @@ -55,37 +56,28 @@ import org.mockito.Mock; import java.util.ArrayList; import java.util.List; import java.util.Optional; -import java.util.function.Supplier; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; /** Tests of {@link CaptionWindowDecorViewModel} */ @SmallTest public class CaptionWindowDecorViewModelTests extends ShellTestCase { - @Mock private CaptionWindowDecoration mCaptionWindowDecoration; + private static final String TAG = "CaptionWindowDecorViewModelTests"; + + @Mock private CaptionWindowDecoration mCaptionWindowDecoration; @Mock private CaptionWindowDecoration.Factory mCaptionWindowDecorFactory; @Mock private Handler mMainHandler; - @Mock private Choreographer mMainChoreographer; - @Mock private ShellTaskOrganizer mTaskOrganizer; - @Mock private DisplayController mDisplayController; - @Mock private SyncTransactionQueue mSyncQueue; - @Mock private DesktopModeController mDesktopModeController; - @Mock private InputMonitor mInputMonitor; - - @Mock private InputChannel mInputChannel; - - @Mock private CaptionWindowDecorViewModel.EventReceiverFactory mEventReceiverFactory; - - @Mock private CaptionWindowDecorViewModel.EventReceiver mEventReceiver; - @Mock private InputManager mInputManager; + @Mock private CaptionWindowDecorViewModel.InputMonitorFactory mMockInputMonitorFactory; private final List<InputManager> mMockInputManagers = new ArrayList<>(); private CaptionWindowDecorViewModel mCaptionWindowDecorViewModel; @@ -104,44 +96,46 @@ public class CaptionWindowDecorViewModelTests extends ShellTestCase { mSyncQueue, Optional.of(mDesktopModeController), mCaptionWindowDecorFactory, - new MockObjectSupplier<>(mMockInputManagers, () -> mock(InputManager.class))); - mCaptionWindowDecorViewModel.setEventReceiverFactory(mEventReceiverFactory); + mMockInputMonitorFactory + ); doReturn(mCaptionWindowDecoration) .when(mCaptionWindowDecorFactory) .create(any(), any(), any(), any(), any(), any(), any(), any()); - when(mInputManager.monitorGestureInput(any(), anyInt())).thenReturn(mInputMonitor); - when(mEventReceiverFactory.create(any(), any(), any())).thenReturn(mEventReceiver); - when(mInputMonitor.getInputChannel()).thenReturn(mInputChannel); + when(mMockInputMonitorFactory.create(any(), any())).thenReturn(mInputMonitor); + // InputChannel cannot be mocked because it passes to InputEventReceiver. + final InputChannel[] inputChannels = InputChannel.openInputChannelPair(TAG); + inputChannels[0].dispose(); + when(mInputMonitor.getInputChannel()).thenReturn(inputChannels[1]); } @Test public void testDeleteCaptionOnChangeTransitionWhenNecessary() throws Exception { - Looper.prepare(); final int taskId = 1; final ActivityManager.RunningTaskInfo taskInfo = - createTaskInfo(taskId, WINDOWING_MODE_FREEFORM); + createTaskInfo(taskId, Display.DEFAULT_DISPLAY, WINDOWING_MODE_FREEFORM); SurfaceControl surfaceControl = mock(SurfaceControl.class); - final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class); - final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); - GrantPermissionRule.grant(android.Manifest.permission.MONITOR_INPUT); + runOnMainThread(() -> { + final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class); + final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); + + mCaptionWindowDecorViewModel.onTaskOpening(taskInfo, surfaceControl, startT, finishT); - mCaptionWindowDecorViewModel.onTaskOpening(taskInfo, surfaceControl, startT, finishT); + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_UNDEFINED); + taskInfo.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_UNDEFINED); + mCaptionWindowDecorViewModel.onTaskChanging(taskInfo, surfaceControl, startT, finishT); + }); verify(mCaptionWindowDecorFactory) .create( - mContext, - mDisplayController, - mTaskOrganizer, - taskInfo, - surfaceControl, - mMainHandler, - mMainChoreographer, - mSyncQueue); - - taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_UNDEFINED); - taskInfo.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_UNDEFINED); - mCaptionWindowDecorViewModel.onTaskChanging(taskInfo, surfaceControl, startT, finishT); + mContext, + mDisplayController, + mTaskOrganizer, + taskInfo, + surfaceControl, + mMainHandler, + mMainChoreographer, + mSyncQueue); verify(mCaptionWindowDecoration).close(); } @@ -149,70 +143,105 @@ public class CaptionWindowDecorViewModelTests extends ShellTestCase { public void testCreateCaptionOnChangeTransitionWhenNecessary() throws Exception { final int taskId = 1; final ActivityManager.RunningTaskInfo taskInfo = - createTaskInfo(taskId, WINDOWING_MODE_UNDEFINED); + createTaskInfo(taskId, Display.DEFAULT_DISPLAY, WINDOWING_MODE_UNDEFINED); SurfaceControl surfaceControl = mock(SurfaceControl.class); - final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class); - final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); - taskInfo.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_UNDEFINED); + runOnMainThread(() -> { + final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class); + final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); + taskInfo.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_UNDEFINED); + + mCaptionWindowDecorViewModel.onTaskChanging(taskInfo, surfaceControl, startT, finishT); - mCaptionWindowDecorViewModel.onTaskChanging(taskInfo, surfaceControl, startT, finishT); + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); + taskInfo.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_STANDARD); - verify(mCaptionWindowDecorFactory, never()) + mCaptionWindowDecorViewModel.onTaskChanging(taskInfo, surfaceControl, startT, finishT); + }); + verify(mCaptionWindowDecorFactory, times(1)) .create( - mContext, - mDisplayController, - mTaskOrganizer, - taskInfo, - surfaceControl, - mMainHandler, - mMainChoreographer, - mSyncQueue); - - taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); + mContext, + mDisplayController, + mTaskOrganizer, + taskInfo, + surfaceControl, + mMainHandler, + mMainChoreographer, + mSyncQueue); + } + + @Test + public void testCreateAndDisposeEventReceiver() throws Exception { + final int taskId = 1; + final ActivityManager.RunningTaskInfo taskInfo = + createTaskInfo(taskId, Display.DEFAULT_DISPLAY, WINDOWING_MODE_FREEFORM); taskInfo.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_STANDARD); + runOnMainThread(() -> { + SurfaceControl surfaceControl = mock(SurfaceControl.class); + final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class); + final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); - mCaptionWindowDecorViewModel.onTaskChanging(taskInfo, surfaceControl, startT, finishT); + mCaptionWindowDecorViewModel.onTaskOpening(taskInfo, surfaceControl, startT, finishT); - verify(mCaptionWindowDecorFactory) - .create( - mContext, - mDisplayController, - mTaskOrganizer, - taskInfo, - surfaceControl, - mMainHandler, - mMainChoreographer, - mSyncQueue); + mCaptionWindowDecorViewModel.destroyWindowDecoration(taskInfo); + }); + verify(mMockInputMonitorFactory).create(any(), any()); + verify(mInputMonitor).dispose(); } - private static ActivityManager.RunningTaskInfo createTaskInfo(int taskId, int windowingMode) { + @Test + public void testEventReceiversOnMultipleDisplays() throws Exception { + runOnMainThread(() -> { + SurfaceView surfaceView = new SurfaceView(mContext); + final DisplayManager mDm = mContext.getSystemService(DisplayManager.class); + final VirtualDisplay secondaryDisplay = mDm.createVirtualDisplay( + "testEventReceiversOnMultipleDisplays", /*width=*/ 400, /*height=*/ 400, + /*densityDpi=*/ 320, surfaceView.getHolder().getSurface(), + DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY); + int secondaryDisplayId = secondaryDisplay.getDisplay().getDisplayId(); + + final int taskId = 1; + final ActivityManager.RunningTaskInfo taskInfo = + createTaskInfo(taskId, Display.DEFAULT_DISPLAY, WINDOWING_MODE_FREEFORM); + final ActivityManager.RunningTaskInfo secondTaskInfo = + createTaskInfo(taskId + 1, secondaryDisplayId, WINDOWING_MODE_FREEFORM); + final ActivityManager.RunningTaskInfo thirdTaskInfo = + createTaskInfo(taskId + 2, secondaryDisplayId, WINDOWING_MODE_FREEFORM); + + SurfaceControl surfaceControl = mock(SurfaceControl.class); + final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class); + final SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); + + mCaptionWindowDecorViewModel.onTaskOpening(taskInfo, surfaceControl, startT, finishT); + mCaptionWindowDecorViewModel.onTaskOpening(secondTaskInfo, surfaceControl, + startT, finishT); + mCaptionWindowDecorViewModel.onTaskOpening(thirdTaskInfo, surfaceControl, + startT, finishT); + mCaptionWindowDecorViewModel.destroyWindowDecoration(thirdTaskInfo); + mCaptionWindowDecorViewModel.destroyWindowDecoration(taskInfo); + }); + verify(mMockInputMonitorFactory, times(2)).create(any(), any()); + verify(mInputMonitor, times(1)).dispose(); + } + + private void runOnMainThread(Runnable r) throws Exception { + final Handler mainHandler = new Handler(Looper.getMainLooper()); + final CountDownLatch latch = new CountDownLatch(1); + mainHandler.post(() -> { + r.run(); + latch.countDown(); + }); + latch.await(20, TimeUnit.MILLISECONDS); + } + + private static ActivityManager.RunningTaskInfo createTaskInfo(int taskId, + int displayId, int windowingMode) { ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder() - .setDisplayId(Display.DEFAULT_DISPLAY) + .setDisplayId(displayId) .setVisible(true) .build(); taskInfo.taskId = taskId; taskInfo.configuration.windowConfiguration.setWindowingMode(windowingMode); return taskInfo; } - - private static class MockObjectSupplier<T> implements Supplier<T> { - private final List<T> mObjects; - private final Supplier<T> mDefaultSupplier; - private int mNumOfCalls = 0; - - private MockObjectSupplier(List<T> objects, Supplier<T> defaultSupplier) { - mObjects = objects; - mDefaultSupplier = defaultSupplier; - } - - @Override - public T get() { - final T mock = - mNumOfCalls < mObjects.size() ? mObjects.get(mNumOfCalls) - : mDefaultSupplier.get(); - ++mNumOfCalls; - return mock; - } - } } diff --git a/media/java/android/media/AudioFormat.java b/media/java/android/media/AudioFormat.java index ccd4ed09fa94..4d3f05be367d 100644 --- a/media/java/android/media/AudioFormat.java +++ b/media/java/android/media/AudioFormat.java @@ -373,6 +373,8 @@ public final class AudioFormat implements Parcelable { * Use {@link #ENCODING_DTS_UHD_P2} to transmit DTS UHD Profile 2 (aka DTS:X Profile 2) * bitstream. */ public static final int ENCODING_DTS_UHD_P2 = 30; + /** Audio data format: Direct Stream Digital */ + public static final int ENCODING_DSD = 31; /** @hide */ public static String toLogFriendlyEncoding(int enc) { @@ -437,6 +439,8 @@ public final class AudioFormat implements Parcelable { return "ENCODING_DTS_HD_MA"; case ENCODING_DTS_UHD_P2: return "ENCODING_DTS_UHD_P2"; + case ENCODING_DSD: + return "ENCODING_DSD"; default : return "invalid encoding " + enc; } @@ -798,6 +802,7 @@ public final class AudioFormat implements Parcelable { case ENCODING_DRA: case ENCODING_DTS_HD_MA: case ENCODING_DTS_UHD_P2: + case ENCODING_DSD: return true; default: return false; @@ -837,6 +842,7 @@ public final class AudioFormat implements Parcelable { case ENCODING_DRA: case ENCODING_DTS_HD_MA: case ENCODING_DTS_UHD_P2: + case ENCODING_DSD: return true; default: return false; @@ -1211,6 +1217,7 @@ public final class AudioFormat implements Parcelable { case ENCODING_DRA: case ENCODING_DTS_HD_MA: case ENCODING_DTS_UHD_P2: + case ENCODING_DSD: mEncoding = encoding; break; case ENCODING_INVALID: @@ -1441,7 +1448,8 @@ public final class AudioFormat implements Parcelable { ENCODING_DTS_UHD_P1, ENCODING_DRA, ENCODING_DTS_HD_MA, - ENCODING_DTS_UHD_P2 } + ENCODING_DTS_UHD_P2, + ENCODING_DSD } ) @Retention(RetentionPolicy.SOURCE) public @interface Encoding {} diff --git a/media/java/android/media/AudioProfile.java b/media/java/android/media/AudioProfile.java index 5c5f837dd07a..356b765f91c0 100644 --- a/media/java/android/media/AudioProfile.java +++ b/media/java/android/media/AudioProfile.java @@ -46,11 +46,17 @@ public class AudioProfile implements Parcelable { * Encapsulation format is defined in standard IEC 61937. */ public static final int AUDIO_ENCAPSULATION_TYPE_IEC61937 = 1; + /** + * Encapsulation format is PCM, which can be used by other formats that can be wrapped in + * a PCM frame, such as DSD(Direct Stream Digital). + */ + public static final int AUDIO_ENCAPSULATION_TYPE_PCM = 2; /** @hide */ @IntDef({ AUDIO_ENCAPSULATION_TYPE_NONE, AUDIO_ENCAPSULATION_TYPE_IEC61937, + AUDIO_ENCAPSULATION_TYPE_PCM, }) @Retention(RetentionPolicy.SOURCE) public @interface EncapsulationType {} @@ -122,6 +128,7 @@ public class AudioProfile implements Parcelable { * * @see #AUDIO_ENCAPSULATION_TYPE_NONE * @see #AUDIO_ENCAPSULATION_TYPE_IEC61937 + * @see #AUDIO_ENCAPSULATION_TYPE_PCM */ public @EncapsulationType int getEncapsulationType() { return mEncapsulationType; diff --git a/media/java/android/media/audio/common/AidlConversion.java b/media/java/android/media/audio/common/AidlConversion.java index 4cf3b3e95bf0..490809c63e96 100644 --- a/media/java/android/media/audio/common/AidlConversion.java +++ b/media/java/android/media/audio/common/AidlConversion.java @@ -614,6 +614,8 @@ public class AidlConversion { switch (type) { case android.media.AudioProfile.AUDIO_ENCAPSULATION_TYPE_IEC61937: return AudioEncapsulationType.IEC61937; + case android.media.AudioProfile.AUDIO_ENCAPSULATION_TYPE_PCM: + return AudioEncapsulationType.PCM; case android.media.AudioProfile.AUDIO_ENCAPSULATION_TYPE_NONE: default: return AudioEncapsulationType.NONE; @@ -629,6 +631,8 @@ public class AidlConversion { switch (type) { case AudioEncapsulationType.IEC61937: return android.media.AudioProfile.AUDIO_ENCAPSULATION_TYPE_IEC61937; + case AudioEncapsulationType.PCM: + return android.media.AudioProfile.AUDIO_ENCAPSULATION_TYPE_PCM; case AudioEncapsulationType.NONE: default: return android.media.AudioProfile.AUDIO_ENCAPSULATION_TYPE_NONE; diff --git a/media/java/android/media/projection/IMediaProjectionCallback.aidl b/media/java/android/media/projection/IMediaProjectionCallback.aidl index f3743d1307e9..2c8de2e4eec1 100644 --- a/media/java/android/media/projection/IMediaProjectionCallback.aidl +++ b/media/java/android/media/projection/IMediaProjectionCallback.aidl @@ -19,4 +19,5 @@ package android.media.projection; /** {@hide} */ oneway interface IMediaProjectionCallback { void onStop(); + void onCapturedContentResize(int width, int height); } diff --git a/media/java/android/media/projection/IMediaProjectionManager.aidl b/media/java/android/media/projection/IMediaProjectionManager.aidl index 1d58a409718d..a63d02bb1110 100644 --- a/media/java/android/media/projection/IMediaProjectionManager.aidl +++ b/media/java/android/media/projection/IMediaProjectionManager.aidl @@ -27,12 +27,32 @@ import android.view.ContentRecordingSession; interface IMediaProjectionManager { @UnsupportedAppUsage boolean hasProjectionPermission(int uid, String packageName); + + @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest" + + ".permission.MANAGE_MEDIA_PROJECTION)") IMediaProjection createProjection(int uid, String packageName, int type, boolean permanentGrant); + boolean isValidMediaProjection(IMediaProjection projection); + + @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest" + + ".permission.MANAGE_MEDIA_PROJECTION)") MediaProjectionInfo getActiveProjectionInfo(); + + @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest" + + ".permission.MANAGE_MEDIA_PROJECTION)") void stopActiveProjection(); + + @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest" + + ".permission.MANAGE_MEDIA_PROJECTION)") + void notifyActiveProjectionCapturedContentResized(int width, int height); + + @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest" + + ".permission.MANAGE_MEDIA_PROJECTION)") void addCallback(IMediaProjectionWatcherCallback callback); + + @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest" + + ".permission.MANAGE_MEDIA_PROJECTION)") void removeCallback(IMediaProjectionWatcherCallback callback); /** diff --git a/media/java/android/media/projection/MediaProjection.java b/media/java/android/media/projection/MediaProjection.java index ae44fc575f7c..3dfff1fbfc1b 100644 --- a/media/java/android/media/projection/MediaProjection.java +++ b/media/java/android/media/projection/MediaProjection.java @@ -234,7 +234,7 @@ public final class MediaProjection { /** * Callbacks for the projection session. */ - public static abstract class Callback { + public abstract static class Callback { /** * Called when the MediaProjection session is no longer valid. * <p> @@ -243,6 +243,46 @@ public final class MediaProjection { * </p> */ public void onStop() { } + + /** + * Indicates the width and height of the captured region in pixels. Called immediately after + * capture begins to provide the app with accurate sizing for the stream. Also called + * when the region captured in this MediaProjection session is resized. + * <p> + * The given width and height, in pixels, corresponds to the same width and height that + * would be returned from {@link android.view.WindowMetrics#getBounds()} + * </p> + * <p> + * Without the application resizing the {@link VirtualDisplay} (returned from + * {@code MediaProjection#createVirtualDisplay}) and output {@link Surface} (provided + * to {@code MediaProjection#createVirtualDisplay}), the captured stream will have + * letterboxing (black bars) around the recorded content to make up for the + * difference in aspect ratio. + * </p> + * <p> + * The application can prevent the letterboxing by overriding this method, and + * updating the size of both the {@link VirtualDisplay} and output {@link Surface}: + * </p> + * + * <pre> + * @Override + * public String onCapturedContentResize(int width, int height) { + * // VirtualDisplay instance from MediaProjection#createVirtualDisplay + * virtualDisplay.resize(width, height, dpi); + * + * // Create a new Surface with the updated size (depending on the application's use + * // case, this may be through different APIs - see Surface documentation for + * // options). + * int texName; // the OpenGL texture object name + * SurfaceTexture surfaceTexture = new SurfaceTexture(texName); + * surfaceTexture.setDefaultBufferSize(width, height); + * Surface surface = new Surface(surfaceTexture); + * + * // Ensure the VirtualDisplay has the updated Surface to send the capture to. + * virtualDisplay.setSurface(surface); + * }</pre> + */ + public void onCapturedContentResize(int width, int height) { } } private final class MediaProjectionCallback extends IMediaProjectionCallback.Stub { @@ -252,6 +292,13 @@ public final class MediaProjection { cbr.onStop(); } } + + @Override + public void onCapturedContentResize(int width, int height) { + for (CallbackRecord cbr : mCallbacks.values()) { + cbr.onCapturedContentResize(width, height); + } + } } private final static class CallbackRecord { @@ -271,5 +318,9 @@ public final class MediaProjection { } }); } + + public void onCapturedContentResize(int width, int height) { + mHandler.post(() -> mCallback.onCapturedContentResize(width, height)); + } } } diff --git a/media/java/android/media/projection/MediaProjectionConfig.aidl b/media/java/android/media/projection/MediaProjectionConfig.aidl new file mode 100644 index 000000000000..f78385f43d3d --- /dev/null +++ b/media/java/android/media/projection/MediaProjectionConfig.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.projection; + +parcelable MediaProjectionConfig; diff --git a/media/java/android/media/projection/MediaProjectionConfig.java b/media/java/android/media/projection/MediaProjectionConfig.java new file mode 100644 index 000000000000..29afaa6f4c43 --- /dev/null +++ b/media/java/android/media/projection/MediaProjectionConfig.java @@ -0,0 +1,293 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.projection; + +import static android.view.Display.DEFAULT_DISPLAY; + +import static java.lang.annotation.RetentionPolicy.SOURCE; + +import android.annotation.IntDef; +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Parcelable; + +import com.android.internal.util.AnnotationValidations; +import com.android.internal.util.DataClass; + +import java.lang.annotation.Retention; + +/** + * Configure the {@link MediaProjection} session requested from + * {@link MediaProjectionManager#createScreenCaptureIntent(MediaProjectionConfig)}. + */ +@DataClass( + genEqualsHashCode = true, + genAidl = true, + genSetters = false, + genConstructor = false, + genBuilder = false, + genToString = false, + genHiddenConstDefs = true, + genHiddenGetters = true, + genConstDefs = false +) +public final class MediaProjectionConfig implements Parcelable { + + /** + * The user, rather than the host app, determines which region of the display to capture. + * @hide + */ + public static final int CAPTURE_REGION_USER_CHOICE = 0; + + /** + * The host app specifies a particular display to capture. + * @hide + */ + public static final int CAPTURE_REGION_FIXED_DISPLAY = 1; + + /** @hide */ + @IntDef(prefix = "CAPTURE_REGION_", value = { + CAPTURE_REGION_USER_CHOICE, + CAPTURE_REGION_FIXED_DISPLAY + }) + @Retention(SOURCE) + public @interface CaptureRegion { + } + + /** + * The particular display to capture. Only used when {@link #getRegionToCapture()} is + * {@link #CAPTURE_REGION_FIXED_DISPLAY}; ignored otherwise. + * + * Only supports values of {@link android.view.Display#DEFAULT_DISPLAY}. + */ + @IntRange(from = DEFAULT_DISPLAY, to = DEFAULT_DISPLAY) + private int mDisplayToCapture; + + /** + * The region to capture. Defaults to the user's choice. + */ + @CaptureRegion + private int mRegionToCapture = CAPTURE_REGION_USER_CHOICE; + + /** + * Default instance, with region set to the user's choice. + */ + private MediaProjectionConfig() { + } + + /** + * Customized instance, with region set to the provided value. + */ + private MediaProjectionConfig(@CaptureRegion int captureRegion) { + mRegionToCapture = captureRegion; + } + + /** + * Returns an instance which restricts the user to capturing a particular display. + * + * @param displayId The id of the display to capture. Only supports values of + * {@link android.view.Display#DEFAULT_DISPLAY}. + * @throws IllegalArgumentException If the given {@code displayId} is outside the range of + * supported values. + */ + @NonNull + public static MediaProjectionConfig createConfigForDisplay( + @IntRange(from = DEFAULT_DISPLAY, to = DEFAULT_DISPLAY) int displayId) { + if (displayId != DEFAULT_DISPLAY) { + throw new IllegalArgumentException( + "A config for capturing the non-default display is not supported; requested " + + "display id " + + displayId); + } + MediaProjectionConfig config = new MediaProjectionConfig(CAPTURE_REGION_FIXED_DISPLAY); + config.mDisplayToCapture = displayId; + return config; + } + + /** + * Returns an instance which allows the user to decide which region is captured. The consent + * dialog presents the user with all possible options. If the user selects display capture, + * then only the {@link android.view.Display#DEFAULT_DISPLAY} is supported. + * + * <p> + * When passed in to + * {@link MediaProjectionManager#createScreenCaptureIntent(MediaProjectionConfig)}, the consent + * dialog shown to the user will be the same as if just + * {@link MediaProjectionManager#createScreenCaptureIntent()} was invoked. + * </p> + */ + @NonNull + public static MediaProjectionConfig createConfigForUserChoice() { + return new MediaProjectionConfig(CAPTURE_REGION_USER_CHOICE); + } + + /** + * Returns string representation of the captured region. + */ + @NonNull + private static String captureRegionToString(int value) { + switch (value) { + case CAPTURE_REGION_USER_CHOICE: + return "CAPTURE_REGION_USERS_CHOICE"; + case CAPTURE_REGION_FIXED_DISPLAY: + return "CAPTURE_REGION_GIVEN_DISPLAY"; + default: + return Integer.toHexString(value); + } + } + + @Override + public String toString() { + return "MediaProjectionConfig { " + + "displayToCapture = " + mDisplayToCapture + ", " + + "regionToCapture = " + captureRegionToString(mRegionToCapture) + + " }"; + } + + + + + + // Code below generated by codegen v1.0.23. + // + // DO NOT MODIFY! + // CHECKSTYLE:OFF Generated code + // + // To regenerate run: + // $ codegen $ANDROID_BUILD_TOP/frameworks/base/media/java/android/media/projection/MediaProjectionConfig.java + // + // To exclude the generated code from IntelliJ auto-formatting enable (one-time): + // Settings > Editor > Code Style > Formatter Control + //@formatter:off + + + /** + * The particular display to capture. Only used when {@link #getRegionToCapture()} is + * {@link #CAPTURE_REGION_FIXED_DISPLAY}; ignored otherwise. + * + * Only supports values of {@link android.view.Display#DEFAULT_DISPLAY}. + * + * @hide + */ + @DataClass.Generated.Member + public @IntRange(from = DEFAULT_DISPLAY, to = DEFAULT_DISPLAY) int getDisplayToCapture() { + return mDisplayToCapture; + } + + /** + * The region to capture. Defaults to the user's choice. + * + * @hide + */ + @DataClass.Generated.Member + public @CaptureRegion int getRegionToCapture() { + return mRegionToCapture; + } + + @Override + @DataClass.Generated.Member + public boolean equals(@Nullable Object o) { + // You can override field equality logic by defining either of the methods like: + // boolean fieldNameEquals(MediaProjectionConfig other) { ... } + // boolean fieldNameEquals(FieldType otherValue) { ... } + + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + @SuppressWarnings("unchecked") + MediaProjectionConfig that = (MediaProjectionConfig) o; + //noinspection PointlessBooleanExpression + return true + && mDisplayToCapture == that.mDisplayToCapture + && mRegionToCapture == that.mRegionToCapture; + } + + @Override + @DataClass.Generated.Member + public int hashCode() { + // You can override field hashCode logic by defining methods like: + // int fieldNameHashCode() { ... } + + int _hash = 1; + _hash = 31 * _hash + mDisplayToCapture; + _hash = 31 * _hash + mRegionToCapture; + return _hash; + } + + @Override + @DataClass.Generated.Member + public void writeToParcel(@NonNull android.os.Parcel dest, int flags) { + // You can override field parcelling by defining methods like: + // void parcelFieldName(Parcel dest, int flags) { ... } + + dest.writeInt(mDisplayToCapture); + dest.writeInt(mRegionToCapture); + } + + @Override + @DataClass.Generated.Member + public int describeContents() { return 0; } + + /** @hide */ + @SuppressWarnings({"unchecked", "RedundantCast"}) + @DataClass.Generated.Member + /* package-private */ MediaProjectionConfig(@NonNull android.os.Parcel in) { + // You can override field unparcelling by defining methods like: + // static FieldType unparcelFieldName(Parcel in) { ... } + + int displayToCapture = in.readInt(); + int regionToCapture = in.readInt(); + + this.mDisplayToCapture = displayToCapture; + AnnotationValidations.validate( + IntRange.class, null, mDisplayToCapture, + "from", DEFAULT_DISPLAY, + "to", DEFAULT_DISPLAY); + this.mRegionToCapture = regionToCapture; + AnnotationValidations.validate( + CaptureRegion.class, null, mRegionToCapture); + + // onConstructed(); // You can define this method to get a callback + } + + @DataClass.Generated.Member + public static final @NonNull Parcelable.Creator<MediaProjectionConfig> CREATOR + = new Parcelable.Creator<MediaProjectionConfig>() { + @Override + public MediaProjectionConfig[] newArray(int size) { + return new MediaProjectionConfig[size]; + } + + @Override + public MediaProjectionConfig createFromParcel(@NonNull android.os.Parcel in) { + return new MediaProjectionConfig(in); + } + }; + + @DataClass.Generated( + time = 1671030124845L, + codegenVersion = "1.0.23", + sourceFile = "frameworks/base/media/java/android/media/projection/MediaProjectionConfig.java", + inputSignatures = "public static final int CAPTURE_REGION_USER_CHOICE\npublic static final int CAPTURE_REGION_FIXED_DISPLAY\nprivate @android.annotation.IntRange int mDisplayToCapture\nprivate @android.media.projection.MediaProjectionConfig.CaptureRegion int mRegionToCapture\npublic static @android.annotation.NonNull android.media.projection.MediaProjectionConfig createConfigForDisplay(int)\npublic static @android.annotation.NonNull android.media.projection.MediaProjectionConfig createConfigForUserChoice()\nprivate static @android.annotation.NonNull java.lang.String captureRegionToString(int)\npublic @java.lang.Override java.lang.String toString()\nclass MediaProjectionConfig extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genEqualsHashCode=true, genAidl=true, genSetters=false, genConstructor=false, genBuilder=false, genToString=false, genHiddenConstDefs=true, genHiddenGetters=true, genConstDefs=false)") + @Deprecated + private void __metadata() {} + + + //@formatter:on + // End of generated code + +} diff --git a/media/java/android/media/projection/MediaProjectionManager.java b/media/java/android/media/projection/MediaProjectionManager.java index b3bd98045164..a4215e68bfef 100644 --- a/media/java/android/media/projection/MediaProjectionManager.java +++ b/media/java/android/media/projection/MediaProjectionManager.java @@ -38,6 +38,13 @@ import java.util.Map; @SystemService(Context.MEDIA_PROJECTION_SERVICE) public final class MediaProjectionManager { private static final String TAG = "MediaProjectionManager"; + + /** + * Intent extra to customize the permission dialog based on the host app's preferences. + * @hide + */ + public static final String EXTRA_MEDIA_PROJECTION_CONFIG = + "android.media.projection.extra.EXTRA_MEDIA_PROJECTION_CONFIG"; /** @hide */ public static final String EXTRA_APP_TOKEN = "android.media.projection.extra.EXTRA_APP_TOKEN"; /** @hide */ @@ -64,11 +71,13 @@ public final class MediaProjectionManager { } /** - * Returns an Intent that <b>must</b> be passed to startActivityForResult() - * in order to start screen capture. The activity will prompt - * the user whether to allow screen capture. The result of this - * activity should be passed to getMediaProjection. + * Returns an {@link Intent} that <b>must</b> be passed to + * {@link Activity#startActivityForResult(Intent, int)} (or similar) in order to start screen + * capture. The activity will prompt the user whether to allow screen capture. The result of + * this activity (received by overriding {@link Activity#onActivityResult(int, int, Intent)}) + * should be passed to {@link #getMediaProjection(int, Intent)}. */ + @NonNull public Intent createScreenCaptureIntent() { Intent i = new Intent(); final ComponentName mediaProjectionPermissionDialogComponent = @@ -80,6 +89,49 @@ public final class MediaProjectionManager { } /** + * Returns an {@link Intent} that <b>must</b> be passed to + * {@link Activity#startActivityForResult(Intent, int)} (or similar) in order to start screen + * capture. Customizes the activity and resulting {@link MediaProjection} session based up + * the provided {@code config}. The activity will prompt the user whether to allow screen + * capture. The result of this activity (received by overriding + * {@link Activity#onActivityResult(int, int, Intent)}) should be passed to + * {@link #getMediaProjection(int, Intent)}. + * + * <p> + * If {@link MediaProjectionConfig} was created from: + * <li> + * <ul> + * {@link MediaProjectionConfig#createConfigForDisplay(int)}, then creates an + * {@link Intent} for capturing this particular display. The activity limits the user's + * choice to just the display specified. + * </ul> + * <ul> + * {@link MediaProjectionConfig#createConfigForUserChoice()}, then creates an + * {@link Intent} for deferring which region to capture to the user. This gives the + * user the same behaviour as calling {@link #createScreenCaptureIntent()}. The + * activity gives the user the choice between + * {@link android.view.Display#DEFAULT_DISPLAY}, or a different region. + * </ul> + * </li> + * + * @param config Customization for the {@link MediaProjection} that this {@link Intent} requests + * the user's consent for. + * @return An {@link Intent} requesting the user's consent, specialized based upon the given + * configuration. + */ + @NonNull + public Intent createScreenCaptureIntent(@NonNull MediaProjectionConfig config) { + Intent i = new Intent(); + final ComponentName mediaProjectionPermissionDialogComponent = + ComponentName.unflattenFromString(mContext.getResources() + .getString(com.android.internal.R.string + .config_mediaProjectionPermissionDialogComponent)); + i.setComponent(mediaProjectionPermissionDialogComponent); + i.putExtra(EXTRA_MEDIA_PROJECTION_CONFIG, config); + return i; + } + + /** * Retrieves the {@link MediaProjection} obtained from a successful screen * capture request. The result code and data from the request are provided * by overriding {@link Activity#onActivityResult(int, int, Intent) diff --git a/media/tests/projection/Android.bp b/media/tests/projection/Android.bp new file mode 100644 index 000000000000..08d950128ce2 --- /dev/null +++ b/media/tests/projection/Android.bp @@ -0,0 +1,46 @@ +//######################################################################## +// Build MediaProjectionTests package +//######################################################################## + +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +android_test { + name: "MediaProjectionTests", + + srcs: ["**/*.java"], + + libs: [ + "android.test.base", + "android.test.mock", + "android.test.runner", + ], + + static_libs: [ + "androidx.test.runner", + "androidx.test.rules", + "androidx.test.ext.junit", + "mockito-target-extended-minus-junit4", + "platform-test-annotations", + "testng", + "truth-prebuilt", + ], + + // Needed for mockito-target-extended-minus-junit4 + jni_libs: [ + "libdexmakerjvmtiagent", + "libstaticjvmtiagent", + ], + + test_suites: ["device-tests"], + + platform_apis: true, + + certificate: "platform", +} diff --git a/media/tests/projection/AndroidManifest.xml b/media/tests/projection/AndroidManifest.xml new file mode 100644 index 000000000000..62f148cfdde1 --- /dev/null +++ b/media/tests/projection/AndroidManifest.xml @@ -0,0 +1,32 @@ +<!-- + ~ 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. + --> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + android:installLocation="internalOnly" + package="android.media.projection.mediaprojectiontests" + android:sharedUserId="com.android.uid.test"> + <uses-permission android:name="android.permission.READ_COMPAT_CHANGE_CONFIG" /> + + <application android:debuggable="true" + android:testOnly="true"> + <uses-library android:name="android.test.mock" android:required="true"/> + <uses-library android:name="android.test.runner"/> + </application> + + <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="android.media.projection.mediaprojectiontests" + android:label="MediaProjection package tests"/> +</manifest> diff --git a/media/tests/projection/AndroidTest.xml b/media/tests/projection/AndroidTest.xml new file mode 100644 index 000000000000..f64930a0eb3f --- /dev/null +++ b/media/tests/projection/AndroidTest.xml @@ -0,0 +1,32 @@ +<!-- + ~ 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. + --> + +<configuration description="Runs MediaProjection package Tests."> + <option name="test-suite-tag" value="apct" /> + <option name="test-suite-tag" value="apct-instrumentation" /> + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="cleanup-apks" value="true" /> + <option name="install-arg" value="-t" /> + <option name="test-file-name" value="MediaProjectionTests.apk" /> + </target_preparer> + + <option name="test-tag" value="MediaProjectionTests" /> + <test class="com.android.tradefed.testtype.AndroidJUnitTest"> + <option name="package" value="android.media.projection.mediaprojectiontests" /> + <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> + <option name="hidden-api-checks" value="false" /> + </test> +</configuration> diff --git a/media/tests/projection/TEST_MAPPING b/media/tests/projection/TEST_MAPPING new file mode 100644 index 000000000000..ddb68af10734 --- /dev/null +++ b/media/tests/projection/TEST_MAPPING @@ -0,0 +1,13 @@ +{ + "presubmit": [ + { + "name": "FrameworksServicesTests", + "options": [ + {"include-filter": "android.media.projection.mediaprojectiontests"}, + {"exclude-annotation": "android.platform.test.annotations.FlakyTest"}, + {"exclude-annotation": "androidx.test.filters.FlakyTest"}, + {"exclude-annotation": "org.junit.Ignore"} + ] + } + ] +} diff --git a/media/tests/projection/src/android/media/projection/MediaProjectionConfigTest.java b/media/tests/projection/src/android/media/projection/MediaProjectionConfigTest.java new file mode 100644 index 000000000000..a30f2e3c7c88 --- /dev/null +++ b/media/tests/projection/src/android/media/projection/MediaProjectionConfigTest.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.projection; + +import static android.media.projection.MediaProjectionConfig.CAPTURE_REGION_FIXED_DISPLAY; +import static android.media.projection.MediaProjectionConfig.CAPTURE_REGION_USER_CHOICE; +import static android.view.Display.DEFAULT_DISPLAY; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertThrows; + +import android.os.Parcel; +import android.platform.test.annotations.Presubmit; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Tests for the {@link MediaProjectionConfig} class. + * + * Build/Install/Run: + * atest MediaProjectionTests:MediaProjectionConfigTest + */ +@SmallTest +@Presubmit +@RunWith(AndroidJUnit4.class) +public class MediaProjectionConfigTest { + private static final MediaProjectionConfig DISPLAY_CONFIG = + MediaProjectionConfig.createConfigForDisplay(DEFAULT_DISPLAY); + private static final MediaProjectionConfig USERS_CHOICE_CONFIG = + MediaProjectionConfig.createConfigForUserChoice(); + + @Test + public void testParcelable() { + Parcel parcel = Parcel.obtain(); + DISPLAY_CONFIG.writeToParcel(parcel, 0 /* flags */); + parcel.setDataPosition(0); + MediaProjectionConfig config = MediaProjectionConfig.CREATOR.createFromParcel(parcel); + assertThat(DISPLAY_CONFIG).isEqualTo(config); + parcel.recycle(); + } + + @Test + public void testCreateDisplayConfig() { + assertThrows(IllegalArgumentException.class, + () -> MediaProjectionConfig.createConfigForDisplay(-1)); + assertThrows(IllegalArgumentException.class, + () -> MediaProjectionConfig.createConfigForDisplay(DEFAULT_DISPLAY + 1)); + assertThat(DISPLAY_CONFIG.getRegionToCapture()).isEqualTo(CAPTURE_REGION_FIXED_DISPLAY); + assertThat(DISPLAY_CONFIG.getDisplayToCapture()).isEqualTo(DEFAULT_DISPLAY); + } + + @Test + public void testCreateUsersChoiceConfig() { + assertThat(USERS_CHOICE_CONFIG.getRegionToCapture()).isEqualTo(CAPTURE_REGION_USER_CHOICE); + } + + @Test + public void testEquals() { + assertThat(MediaProjectionConfig.createConfigForUserChoice()).isEqualTo( + USERS_CHOICE_CONFIG); + assertThat(DISPLAY_CONFIG).isNotEqualTo(USERS_CHOICE_CONFIG); + assertThat(MediaProjectionConfig.createConfigForDisplay(DEFAULT_DISPLAY)).isEqualTo( + DISPLAY_CONFIG); + } +} diff --git a/media/tests/projection/src/android/media/projection/MediaProjectionManagerTest.java b/media/tests/projection/src/android/media/projection/MediaProjectionManagerTest.java new file mode 100644 index 000000000000..a3e49088ac4a --- /dev/null +++ b/media/tests/projection/src/android/media/projection/MediaProjectionManagerTest.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.projection; + +import static android.media.projection.MediaProjectionManager.EXTRA_MEDIA_PROJECTION_CONFIG; +import static android.view.Display.DEFAULT_DISPLAY; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.mockito.quality.Strictness.LENIENT; + +import android.annotation.NonNull; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.platform.test.annotations.Presubmit; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoSession; + +/** + * Tests for the {@link MediaProjectionManager} class. + * + * Build/Install/Run: + * atest MediaProjectionTests:MediaProjectionManagerTest + */ +@SmallTest +@Presubmit +@RunWith(AndroidJUnit4.class) +public class MediaProjectionManagerTest { + private MediaProjectionManager mMediaProjectionManager; + private Context mContext; + private MockitoSession mMockingSession; + private static final MediaProjectionConfig DISPLAY_CONFIG = + MediaProjectionConfig.createConfigForDisplay(DEFAULT_DISPLAY); + private static final MediaProjectionConfig USERS_CHOICE_CONFIG = + MediaProjectionConfig.createConfigForUserChoice(); + + @Before + public void setup() throws Exception { + mMockingSession = + mockitoSession() + .initMocks(this) + .strictness(LENIENT) + .startMocking(); + mContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + spyOn(mContext); + mMediaProjectionManager = new MediaProjectionManager(mContext); + } + + @After + public void teardown() { + mMockingSession.finishMocking(); + } + + @Test + public void testCreateScreenCaptureIntent() { + final String dialogPackage = "test.package"; + preparePermissionDialogComponent(dialogPackage); + + final Intent intent = mMediaProjectionManager.createScreenCaptureIntent(); + assertThat(intent).isNotNull(); + assertThat(intent.getComponent().getPackageName()).contains(dialogPackage); + } + + @Test + public void testCreateScreenCaptureIntent_display() { + final String dialogPackage = "test.package"; + preparePermissionDialogComponent(dialogPackage); + + final Intent intent = mMediaProjectionManager.createScreenCaptureIntent(DISPLAY_CONFIG); + assertThat(intent).isNotNull(); + assertThat(intent.getComponent().getPackageName()).contains(dialogPackage); + assertThat(intent.getParcelableExtra(EXTRA_MEDIA_PROJECTION_CONFIG, + MediaProjectionConfig.class)).isEqualTo(DISPLAY_CONFIG); + } + + @Test + public void testCreateScreenCaptureIntent_usersChoice() { + final String dialogPackage = "test.package"; + preparePermissionDialogComponent(dialogPackage); + + final Intent intent = mMediaProjectionManager.createScreenCaptureIntent( + USERS_CHOICE_CONFIG); + assertThat(intent).isNotNull(); + assertThat(intent.getComponent().getPackageName()).contains(dialogPackage); + assertThat(intent.getParcelableExtra(EXTRA_MEDIA_PROJECTION_CONFIG, + MediaProjectionConfig.class)).isEqualTo(USERS_CHOICE_CONFIG); + } + + private void preparePermissionDialogComponent(@NonNull String dialogPackage) { + final Resources mockResources = mock(Resources.class); + when(mContext.getResources()).thenReturn(mockResources); + doReturn(dialogPackage + "/.TestActivity").when(mockResources).getString( + com.android.internal.R.string + .config_mediaProjectionPermissionDialogComponent); + } +} diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java index 267c196addec..e59ba5a20611 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java @@ -168,6 +168,11 @@ public class SecureSettings { Settings.Secure.BACK_GESTURE_INSET_SCALE_LEFT, Settings.Secure.BACK_GESTURE_INSET_SCALE_RIGHT, Settings.Secure.NAVIGATION_MODE, + Settings.Secure.TRACKPAD_GESTURE_BACK_ENABLED, + Settings.Secure.TRACKPAD_GESTURE_HOME_ENABLED, + Settings.Secure.TRACKPAD_GESTURE_OVERVIEW_ENABLED, + Settings.Secure.TRACKPAD_GESTURE_NOTIFICATION_ENABLED, + Settings.Secure.TRACKPAD_GESTURE_QUICK_SWITCH_ENABLED, Settings.Secure.SKIP_GESTURE_COUNT, Settings.Secure.SKIP_TOUCH_COUNT, Settings.Secure.SILENCE_ALARMS_GESTURE_COUNT, diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java index def9c197a221..58ec349666a8 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java @@ -255,6 +255,11 @@ public class SecureSettingsValidators { new InclusiveFloatRangeValidator(0.0f, Float.MAX_VALUE)); VALIDATORS.put(Secure.BACK_GESTURE_INSET_SCALE_RIGHT, new InclusiveFloatRangeValidator(0.0f, Float.MAX_VALUE)); + VALIDATORS.put(Secure.TRACKPAD_GESTURE_BACK_ENABLED, BOOLEAN_VALIDATOR); + VALIDATORS.put(Secure.TRACKPAD_GESTURE_HOME_ENABLED, BOOLEAN_VALIDATOR); + VALIDATORS.put(Secure.TRACKPAD_GESTURE_OVERVIEW_ENABLED, BOOLEAN_VALIDATOR); + VALIDATORS.put(Secure.TRACKPAD_GESTURE_NOTIFICATION_ENABLED, BOOLEAN_VALIDATOR); + VALIDATORS.put(Secure.TRACKPAD_GESTURE_QUICK_SWITCH_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.AWARE_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.SKIP_GESTURE_COUNT, NON_NEGATIVE_INTEGER_VALIDATOR); VALIDATORS.put(Secure.SKIP_TOUCH_COUNT, NON_NEGATIVE_INTEGER_VALIDATOR); diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/quickaffordance/shared/model/KeyguardQuickAffordancePreviewConstants.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/quickaffordance/shared/model/KeyguardQuickAffordancePreviewConstants.kt new file mode 100644 index 000000000000..18e8a962dc70 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/quickaffordance/shared/model/KeyguardQuickAffordancePreviewConstants.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.shared.quickaffordance.shared.model + +object KeyguardQuickAffordancePreviewConstants { + const val MESSAGE_ID_SLOT_SELECTED = 1337 + const val KEY_SLOT_ID = "slot_id" + const val KEY_INITIALLY_SELECTED_SLOT_ID = "initially_selected_slot_id" +} diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_host_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_host_view.xml index e64b586a3e6f..8497ff094c03 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_host_view.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_host_view.xml @@ -27,6 +27,7 @@ android:layout_height="match_parent" android:clipChildren="false" android:clipToPadding="false" + android:paddingTop="@dimen/keyguard_lock_padding" android:importantForAccessibility="yes"> <!-- Needed because TYPE_WINDOW_STATE_CHANGED is sent from this view when bouncer is shown --> diff --git a/packages/SystemUI/res/drawable/controls_panel_background.xml b/packages/SystemUI/res/drawable/controls_panel_background.xml new file mode 100644 index 000000000000..9092877fc6fa --- /dev/null +++ b/packages/SystemUI/res/drawable/controls_panel_background.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2022 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + ~ + --> + +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <solid android:color="#1F1F1F" /> + <corners android:radius="@dimen/notification_corner_radius" /> +</shape>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/keyguard_bottom_affordance_bg.xml b/packages/SystemUI/res/drawable/keyguard_bottom_affordance_bg.xml index 41123c84ded1..18fcebbb65a0 100644 --- a/packages/SystemUI/res/drawable/keyguard_bottom_affordance_bg.xml +++ b/packages/SystemUI/res/drawable/keyguard_bottom_affordance_bg.xml @@ -16,13 +16,53 @@ * limitations under the License. */ --> -<shape +<selector xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" - android:shape="rectangle"> - <solid android:color="?androidprv:attr/colorSurface"/> - <size - android:width="@dimen/keyguard_affordance_width" - android:height="@dimen/keyguard_affordance_height"/> - <corners android:radius="@dimen/keyguard_affordance_fixed_radius"/> -</shape> + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"> + + <item android:state_selected="true"> + <layer-list> + <item + android:left="3dp" + android:top="3dp" + android:right="3dp" + android:bottom="3dp"> + <shape android:shape="oval"> + <solid android:color="?androidprv:attr/colorSurface"/> + <size + android:width="@dimen/keyguard_affordance_width" + android:height="@dimen/keyguard_affordance_height"/> + </shape> + </item> + + <item> + <shape android:shape="oval"> + <stroke + android:color="@color/control_primary_text" + android:width="2dp"/> + <size + android:width="@dimen/keyguard_affordance_width" + android:height="@dimen/keyguard_affordance_height"/> + </shape> + </item> + </layer-list> + </item> + + <item> + <layer-list> + <item + android:left="3dp" + android:top="3dp" + android:right="3dp" + android:bottom="3dp"> + <shape android:shape="oval"> + <solid android:color="?androidprv:attr/colorSurface"/> + <size + android:width="@dimen/keyguard_affordance_width" + android:height="@dimen/keyguard_affordance_height"/> + </shape> + </item> + </layer-list> + </item> + +</selector> diff --git a/packages/SystemUI/res/layout/controls_with_favorites.xml b/packages/SystemUI/res/layout/controls_with_favorites.xml index 9efad2269463..ee3adba00fe5 100644 --- a/packages/SystemUI/res/layout/controls_with_favorites.xml +++ b/packages/SystemUI/res/layout/controls_with_favorites.xml @@ -90,7 +90,7 @@ android:layout_weight="1" android:layout_marginLeft="@dimen/global_actions_side_margin" android:layout_marginRight="@dimen/global_actions_side_margin" - android:background="#ff0000" + android:background="@drawable/controls_panel_background" android:padding="@dimen/global_actions_side_margin" android:visibility="gone" /> diff --git a/packages/SystemUI/res/values-h411dp/dimens.xml b/packages/SystemUI/res/values-h411dp/dimens.xml new file mode 100644 index 000000000000..6b21353d0e55 --- /dev/null +++ b/packages/SystemUI/res/values-h411dp/dimens.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2022 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<resources> + <dimen name="volume_row_slider_height">137dp</dimen> +</resources> diff --git a/packages/SystemUI/res/values-h700dp/dimens.xml b/packages/SystemUI/res/values-h700dp/dimens.xml index 055308f17776..39777ab56847 100644 --- a/packages/SystemUI/res/values-h700dp/dimens.xml +++ b/packages/SystemUI/res/values-h700dp/dimens.xml @@ -17,4 +17,5 @@ <resources> <!-- Margin above the ambient indication container --> <dimen name="ambient_indication_container_margin_top">15dp</dimen> -</resources>
\ No newline at end of file + <dimen name="volume_row_slider_height">177dp</dimen> +</resources> diff --git a/packages/SystemUI/res/values-h841dp/dimens.xml b/packages/SystemUI/res/values-h841dp/dimens.xml new file mode 100644 index 000000000000..412da199f6b6 --- /dev/null +++ b/packages/SystemUI/res/values-h841dp/dimens.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2022 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<resources> + <dimen name="volume_row_slider_height">237dp</dimen> +</resources> diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 247e44d0f734..6a87630a05f2 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -776,15 +776,11 @@ <integer name="complicationFadeOutDelayMs">200</integer> <!-- Duration in milliseconds of the dream in un-blur animation. --> - <integer name="config_dreamOverlayInBlurDurationMs">249</integer> - <!-- Delay in milliseconds of the dream in un-blur animation. --> - <integer name="config_dreamOverlayInBlurDelayMs">133</integer> + <integer name="config_dreamOverlayInBlurDurationMs">250</integer> <!-- Duration in milliseconds of the dream in complications fade-in animation. --> - <integer name="config_dreamOverlayInComplicationsDurationMs">282</integer> - <!-- Delay in milliseconds of the dream in top complications fade-in animation. --> - <integer name="config_dreamOverlayInTopComplicationsDelayMs">216</integer> - <!-- Delay in milliseconds of the dream in bottom complications fade-in animation. --> - <integer name="config_dreamOverlayInBottomComplicationsDelayMs">299</integer> + <integer name="config_dreamOverlayInComplicationsDurationMs">250</integer> + <!-- Duration in milliseconds of the y-translation animation when entering a dream --> + <integer name="config_dreamOverlayInTranslationYDurationMs">917</integer> <!-- Icons that don't show in a collapsed non-keyguard statusbar --> <string-array name="config_collapsed_statusbar_icon_blocklist" translatable="false"> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index ea51a892c060..227c0dd4f667 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -758,6 +758,8 @@ <dimen name="keyguard_affordance_fixed_height">48dp</dimen> <dimen name="keyguard_affordance_fixed_width">48dp</dimen> <dimen name="keyguard_affordance_fixed_radius">24dp</dimen> + <!-- Amount the button should shake when it's not long-pressed for long enough. --> + <dimen name="keyguard_affordance_shake_amplitude">8dp</dimen> <dimen name="keyguard_affordance_horizontal_offset">32dp</dimen> <dimen name="keyguard_affordance_vertical_offset">32dp</dimen> @@ -1528,6 +1530,8 @@ <dimen name="dream_overlay_status_bar_extra_margin">8dp</dimen> <!-- Dream overlay complications related dimensions --> + <!-- The blur radius applied to the dream overlay when entering and exiting dreams --> + <dimen name="dream_overlay_anim_blur_radius">50dp</dimen> <dimen name="dream_overlay_complication_clock_time_text_size">86dp</dimen> <dimen name="dream_overlay_complication_clock_time_translation_y">28dp</dimen> <dimen name="dream_overlay_complication_home_controls_padding">28dp</dimen> @@ -1581,6 +1585,7 @@ <dimen name="dream_overlay_complication_margin">0dp</dimen> <dimen name="dream_overlay_y_offset">80dp</dimen> + <dimen name="dream_overlay_entry_y_offset">40dp</dimen> <dimen name="dream_overlay_exit_y_offset">40dp</dimen> <dimen name="status_view_margin_horizontal">0dp</dimen> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index ae1ff1ad5bfd..a2e03c326641 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2859,6 +2859,12 @@ --> <string name="keyguard_affordance_enablement_dialog_home_instruction_2">• At least one device is available</string> + <!-- + Error message shown when a button should be pressed and held to activate it, usually shown when + the user attempted to tap the button or held it for too short a time. [CHAR LIMIT=32]. + --> + <string name="keyguard_affordance_press_too_short">Press and hold to activate</string> + <!-- Text for education page of cancel button to hide the page. [CHAR_LIMIT=NONE] --> <string name="rear_display_bottom_sheet_cancel">Cancel</string> <!-- Text for the user to confirm they flipped the device around. [CHAR_LIMIT=NONE] --> diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java index 860c8e3a9f77..7da27b1d6898 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java @@ -260,7 +260,8 @@ public abstract class KeyguardAbsKeyInputViewController<T extends KeyguardAbsKey if (reason != PROMPT_REASON_NONE) { int promtReasonStringRes = mView.getPromptReasonStringRes(reason); if (promtReasonStringRes != 0) { - mMessageAreaController.setMessage(promtReasonStringRes); + mMessageAreaController.setMessage( + mView.getResources().getString(promtReasonStringRes), false); } } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java index 40423cd9ac2c..62babadc45d8 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java @@ -9,6 +9,7 @@ import android.graphics.Rect; import android.util.AttributeSet; import android.util.Log; import android.view.View; +import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.RelativeLayout; @@ -43,6 +44,21 @@ public class KeyguardClockSwitch extends RelativeLayout { public static final int LARGE = 0; public static final int SMALL = 1; + /** Returns a region for the large clock to position itself, based on the given parent. */ + public static Rect getLargeClockRegion(ViewGroup parent) { + int largeClockTopMargin = parent.getResources() + .getDimensionPixelSize(R.dimen.keyguard_large_clock_top_margin); + int targetHeight = parent.getResources() + .getDimensionPixelSize(R.dimen.large_clock_text_size) * 2; + int top = parent.getHeight() / 2 - targetHeight / 2 + + largeClockTopMargin / 2; + return new Rect( + parent.getLeft(), + top, + parent.getRight(), + top + targetHeight); + } + /** * Frame for small/large clocks */ @@ -129,17 +145,8 @@ public class KeyguardClockSwitch extends RelativeLayout { } if (mLargeClockFrame.isLaidOut()) { - int largeClockTopMargin = getResources() - .getDimensionPixelSize(R.dimen.keyguard_large_clock_top_margin); - int targetHeight = getResources() - .getDimensionPixelSize(R.dimen.large_clock_text_size) * 2; - int top = mLargeClockFrame.getHeight() / 2 - targetHeight / 2 - + largeClockTopMargin / 2; - mClock.getLargeClock().getEvents().onTargetRegionChanged(new Rect( - mLargeClockFrame.getLeft(), - top, - mLargeClockFrame.getRight(), - top + targetHeight)); + mClock.getLargeClock().getEvents().onTargetRegionChanged( + getLargeClockRegion(mLargeClockFrame)); } } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java index 2e9ad5868eba..d1c9a3090860 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java @@ -142,8 +142,11 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView> } public void startAppearAnimation() { - if (TextUtils.isEmpty(mMessageAreaController.getMessage())) { - mMessageAreaController.setMessage(getInitialMessageResId()); + if (TextUtils.isEmpty(mMessageAreaController.getMessage()) + && getInitialMessageResId() != 0) { + mMessageAreaController.setMessage( + mView.getResources().getString(getInitialMessageResId()), + /* animate= */ false); } mView.startAppearAnimation(); } @@ -163,9 +166,7 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView> } /** Determines the message to show in the bouncer when it first appears. */ - protected int getInitialMessageResId() { - return 0; - } + protected abstract int getInitialMessageResId(); /** Factory for a {@link KeyguardInputViewController}. */ public static class Factory { diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java index 5d86ccd5409e..67e3400670ba 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java @@ -52,6 +52,7 @@ public class KeyguardPINView extends KeyguardPinBasedInputView { private int mYTransOffset; private View mBouncerMessageView; @DevicePostureInt private int mLastDevicePosture = DEVICE_POSTURE_UNKNOWN; + public static final long ANIMATION_DURATION = 650; public KeyguardPINView(Context context) { this(context, null); @@ -181,7 +182,7 @@ public class KeyguardPINView extends KeyguardPinBasedInputView { if (mAppearAnimator.isRunning()) { mAppearAnimator.cancel(); } - mAppearAnimator.setDuration(650); + mAppearAnimator.setDuration(ANIMATION_DURATION); mAppearAnimator.addUpdateListener(animation -> animate(animation.getAnimatedFraction())); mAppearAnimator.start(); } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java index c985fd7bef82..c1fae9e44bd3 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordView.java @@ -24,6 +24,7 @@ import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_NON_STRONG import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_PREPARE_FOR_UPDATE; import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_RESTART; import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TIMEOUT; +import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TRUSTAGENT_EXPIRED; import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_USER_REQUEST; import android.animation.Animator; @@ -107,6 +108,8 @@ public class KeyguardPasswordView extends KeyguardAbsKeyInputView { return R.string.kg_prompt_reason_timeout_password; case PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT: return R.string.kg_prompt_reason_timeout_password; + case PROMPT_REASON_TRUSTAGENT_EXPIRED: + return R.string.kg_prompt_reason_timeout_password; case PROMPT_REASON_NONE: return 0; default: diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java index 571d2740773d..0c1748982e51 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java @@ -313,6 +313,9 @@ public class KeyguardPatternViewController case PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT: mMessageAreaController.setMessage(R.string.kg_prompt_reason_timeout_pattern); break; + case PROMPT_REASON_TRUSTAGENT_EXPIRED: + mMessageAreaController.setMessage(R.string.kg_prompt_reason_timeout_pattern); + break; case PROMPT_REASON_NONE: break; default: diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java index c46e33d9fd53..0a91150e6c39 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java @@ -22,6 +22,7 @@ import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_NON_STRONG import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_PREPARE_FOR_UPDATE; import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_RESTART; import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TIMEOUT; +import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TRUSTAGENT_EXPIRED; import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_USER_REQUEST; import android.animation.Animator; @@ -123,6 +124,8 @@ public abstract class KeyguardPinBasedInputView extends KeyguardAbsKeyInputView return R.string.kg_prompt_reason_timeout_pin; case PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT: return R.string.kg_prompt_reason_timeout_pin; + case PROMPT_REASON_TRUSTAGENT_EXPIRED: + return R.string.kg_prompt_reason_timeout_pin; case PROMPT_REASON_NONE: return 0; default: diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java index f7423ed12e68..8011efdc1ae7 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java @@ -139,4 +139,9 @@ public abstract class KeyguardPinBasedInputViewController<T extends KeyguardPinB super.startErrorAnimation(); mView.startErrorAnimation(); } + + @Override + protected int getInitialMessageResId() { + return R.string.keyguard_enter_your_pin; + } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java index f51ac325c9c1..35b2db27d879 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java @@ -74,9 +74,4 @@ public class KeyguardPinViewController return mView.startDisappearAnimation( mKeyguardUpdateMonitor.needsSlowUnlockTransition(), finishRunnable); } - - @Override - protected int getInitialMessageResId() { - return R.string.keyguard_enter_your_pin; - } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java index 8f3484a0c99b..5d7a6f122e69 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java @@ -36,8 +36,11 @@ import static com.android.systemui.plugins.FalsingManager.LOW_PENALTY; import static java.lang.Integer.max; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; import android.app.Activity; import android.app.AlertDialog; import android.app.admin.DevicePolicyManager; @@ -967,11 +970,23 @@ public class KeyguardSecurityContainer extends ConstraintLayout { } mUserSwitcherViewGroup.setAlpha(0f); - ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(mUserSwitcherViewGroup, View.ALPHA, - 1f); - alphaAnim.setInterpolator(Interpolators.ALPHA_IN); - alphaAnim.setDuration(500); - alphaAnim.start(); + ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f); + int yTrans = mView.getResources().getDimensionPixelSize(R.dimen.pin_view_trans_y_entry); + animator.setInterpolator(Interpolators.STANDARD_DECELERATE); + animator.setDuration(650); + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mUserSwitcherViewGroup.setAlpha(1f); + mUserSwitcherViewGroup.setTranslationY(0f); + } + }); + animator.addUpdateListener(animation -> { + float value = (float) animation.getAnimatedValue(); + mUserSwitcherViewGroup.setAlpha(value); + mUserSwitcherViewGroup.setTranslationY(yTrans - yTrans * value); + }); + animator.start(); } @Override diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java index ac00e9453c97..67d77e53738a 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java @@ -61,6 +61,12 @@ public interface KeyguardSecurityView { int PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT = 7; /** + * Some auth is required because the trustagent expired either from timeout or manually by the + * user + */ + int PROMPT_REASON_TRUSTAGENT_EXPIRED = 8; + + /** * Reset the view and prepare to take input. This should do things like clearing the * password or pattern and clear error messages. */ diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewFlipperController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewFlipperController.java index a5c8c7881e3b..39b567fd21b9 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewFlipperController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewFlipperController.java @@ -156,5 +156,10 @@ public class KeyguardSecurityViewFlipperController @Override public void onStartingToHide() { } + + @Override + protected int getInitialMessageResId() { + return 0; + } } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index 84ef505c0af9..3a592a91873c 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -1644,7 +1644,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab @Override public void onAuthenticationFailed() { - requestActiveUnlock( + requestActiveUnlockDismissKeyguard( ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL, "fingerprintFailure"); handleFingerprintAuthFailed(); @@ -2576,6 +2576,18 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } /** + * Attempts to trigger active unlock from trust agent with a request to dismiss the keyguard. + */ + public void requestActiveUnlockDismissKeyguard( + @NonNull ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN requestOrigin, + String extraReason + ) { + requestActiveUnlock( + requestOrigin, + extraReason + "-dismissKeyguard", true); + } + + /** * Whether the UDFPS bouncer is showing. */ public void setUdfpsBouncerShowing(boolean showing) { diff --git a/packages/SystemUI/src/com/android/systemui/camera/CameraGestureHelper.kt b/packages/SystemUI/src/com/android/systemui/camera/CameraGestureHelper.kt index e2ef2477c836..58d40d349dec 100644 --- a/packages/SystemUI/src/com/android/systemui/camera/CameraGestureHelper.kt +++ b/packages/SystemUI/src/com/android/systemui/camera/CameraGestureHelper.kt @@ -28,7 +28,6 @@ import android.os.RemoteException import android.os.UserHandle import android.util.Log import android.view.WindowManager -import androidx.annotation.VisibleForTesting import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.ActivityIntentHelper import com.android.systemui.dagger.qualifiers.Main @@ -83,7 +82,7 @@ class CameraGestureHelper @Inject constructor( */ fun launchCamera(source: Int) { val intent: Intent = getStartCameraIntent() - intent.putExtra(EXTRA_CAMERA_LAUNCH_SOURCE, source) + intent.putExtra(CameraIntents.EXTRA_LAUNCH_SOURCE, source) val wouldLaunchResolverActivity = activityIntentHelper.wouldLaunchResolverActivity( intent, KeyguardUpdateMonitor.getCurrentUser() ) @@ -149,9 +148,4 @@ class CameraGestureHelper @Inject constructor( cameraIntents.getInsecureCameraIntent() } } - - companion object { - @VisibleForTesting - const val EXTRA_CAMERA_LAUNCH_SOURCE = "com.android.systemui.camera_launch_source" - } } diff --git a/packages/SystemUI/src/com/android/systemui/camera/CameraIntents.kt b/packages/SystemUI/src/com/android/systemui/camera/CameraIntents.kt index f8a20023e47a..867faf9843fe 100644 --- a/packages/SystemUI/src/com/android/systemui/camera/CameraIntents.kt +++ b/packages/SystemUI/src/com/android/systemui/camera/CameraIntents.kt @@ -29,6 +29,7 @@ class CameraIntents { MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE val DEFAULT_INSECURE_CAMERA_INTENT_ACTION = MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA + const val EXTRA_LAUNCH_SOURCE = "com.android.systemui.camera_launch_source" @JvmStatic fun getOverrideCameraPackage(context: Context): String? { diff --git a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsAnimations.kt b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsAnimations.kt index 4aa597ef3d28..8d0edf829416 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/management/ControlsAnimations.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/management/ControlsAnimations.kt @@ -50,7 +50,12 @@ object ControlsAnimations { * Setup an activity to handle enter/exit animations. [view] should be the root of the content. * Fade and translate together. */ - fun observerForAnimations(view: ViewGroup, window: Window, intent: Intent): LifecycleObserver { + fun observerForAnimations( + view: ViewGroup, + window: Window, + intent: Intent, + animateY: Boolean = true + ): LifecycleObserver { return object : LifecycleObserver { var showAnimation = intent.getBooleanExtra(ControlsUiController.EXTRA_ANIMATE, false) @@ -61,8 +66,12 @@ object ControlsAnimations { view.transitionAlpha = 0.0f if (translationY == -1f) { - translationY = view.context.resources.getDimensionPixelSize( - R.dimen.global_actions_controls_y_translation).toFloat() + if (animateY) { + translationY = view.context.resources.getDimensionPixelSize( + R.dimen.global_actions_controls_y_translation).toFloat() + } else { + translationY = 0f + } } } diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt index 5d611c4c8212..d8d8c0ead06a 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsActivity.kt @@ -70,7 +70,8 @@ class ControlsActivity @Inject constructor( ControlsAnimations.observerForAnimations( requireViewById<ViewGroup>(R.id.control_detail_root), window, - intent + intent, + !featureFlags.isEnabled(Flags.USE_APP_PANELS) ) ) 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 fb678aa420bf..1e3e5cd1c31c 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlsUiControllerImpl.kt @@ -186,7 +186,7 @@ class ControlsUiControllerImpl @Inject constructor ( val allStructures = controlsController.get().getFavorites() val selected = getPreferredSelectedItem(allStructures) val anyPanels = controlsListingController.get().getCurrentServices() - .none { it.panelActivity != null } + .any { it.panelActivity != null } return if (controlsController.get().addSeedingFavoritesCallback(onSeedingComplete)) { ControlsActivity::class.java 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 7143be298a9d..f5764c2fdc04 100644 --- a/packages/SystemUI/src/com/android/systemui/controls/ui/PanelTaskViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/controls/ui/PanelTaskViewController.kt @@ -24,6 +24,10 @@ import android.app.PendingIntent import android.content.ComponentName import android.content.Context import android.content.Intent +import android.graphics.Color +import android.graphics.drawable.ShapeDrawable +import android.graphics.drawable.shapes.RoundRectShape +import com.android.systemui.R import com.android.systemui.util.boundsOnScreen import com.android.wm.shell.TaskView import java.util.concurrent.Executor @@ -64,6 +68,16 @@ class PanelTaskViewController( options.taskAlwaysOnTop = true taskView.post { + val roundedCorner = + activityContext.resources.getDimensionPixelSize( + R.dimen.notification_corner_radius + ) + val radii = FloatArray(8) { roundedCorner.toFloat() } + taskView.background = + ShapeDrawable(RoundRectShape(radii, null, null)).apply { + setTint(Color.TRANSPARENT) + } + taskView.clipToOutline = true taskView.startActivity( pendingIntent, fillInIntent, diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt index 0087c8439370..9b8ef71882e9 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt @@ -21,11 +21,12 @@ import android.animation.AnimatorSet import android.animation.ValueAnimator import android.view.View import android.view.animation.Interpolator -import androidx.annotation.FloatRange import androidx.core.animation.doOnEnd import com.android.systemui.animation.Interpolators import com.android.systemui.dreams.complication.ComplicationHostViewController import com.android.systemui.dreams.complication.ComplicationLayoutParams +import com.android.systemui.dreams.complication.ComplicationLayoutParams.POSITION_BOTTOM +import com.android.systemui.dreams.complication.ComplicationLayoutParams.POSITION_TOP import com.android.systemui.dreams.complication.ComplicationLayoutParams.Position import com.android.systemui.dreams.dagger.DreamOverlayModule import com.android.systemui.statusbar.BlurUtils @@ -41,16 +42,15 @@ constructor( private val mComplicationHostViewController: ComplicationHostViewController, private val mStatusBarViewController: DreamOverlayStatusBarViewController, private val mOverlayStateController: DreamOverlayStateController, + @Named(DreamOverlayModule.DREAM_BLUR_RADIUS) private val mDreamBlurRadius: Int, @Named(DreamOverlayModule.DREAM_IN_BLUR_ANIMATION_DURATION) private val mDreamInBlurAnimDurationMs: Long, - @Named(DreamOverlayModule.DREAM_IN_BLUR_ANIMATION_DELAY) - private val mDreamInBlurAnimDelayMs: Long, @Named(DreamOverlayModule.DREAM_IN_COMPLICATIONS_ANIMATION_DURATION) private val mDreamInComplicationsAnimDurationMs: Long, - @Named(DreamOverlayModule.DREAM_IN_TOP_COMPLICATIONS_ANIMATION_DELAY) - private val mDreamInTopComplicationsAnimDelayMs: Long, - @Named(DreamOverlayModule.DREAM_IN_BOTTOM_COMPLICATIONS_ANIMATION_DELAY) - private val mDreamInBottomComplicationsAnimDelayMs: Long, + @Named(DreamOverlayModule.DREAM_IN_TRANSLATION_Y_DISTANCE) + private val mDreamInTranslationYDistance: Int, + @Named(DreamOverlayModule.DREAM_IN_TRANSLATION_Y_DURATION) + private val mDreamInTranslationYDurationMs: Long, @Named(DreamOverlayModule.DREAM_OUT_TRANSLATION_Y_DISTANCE) private val mDreamOutTranslationYDistance: Int, @Named(DreamOverlayModule.DREAM_OUT_TRANSLATION_Y_DURATION) @@ -74,7 +74,7 @@ constructor( */ private var mCurrentAlphaAtPosition = mutableMapOf<Int, Float>() - @FloatRange(from = 0.0, to = 1.0) private var mBlurProgress: Float = 0f + private var mCurrentBlurRadius: Float = 0f /** Starts the dream content and dream overlay entry animations. */ @JvmOverloads @@ -86,25 +86,23 @@ constructor( playTogether( blurAnimator( view = view, - from = 1f, - to = 0f, + fromBlurRadius = mDreamBlurRadius.toFloat(), + toBlurRadius = 0f, durationMs = mDreamInBlurAnimDurationMs, - delayMs = mDreamInBlurAnimDelayMs + interpolator = Interpolators.EMPHASIZED_DECELERATE ), alphaAnimator( from = 0f, to = 1f, durationMs = mDreamInComplicationsAnimDurationMs, - delayMs = mDreamInTopComplicationsAnimDelayMs, - position = ComplicationLayoutParams.POSITION_TOP + interpolator = Interpolators.LINEAR + ), + translationYAnimator( + from = mDreamInTranslationYDistance.toFloat(), + to = 0f, + durationMs = mDreamInTranslationYDurationMs, + interpolator = Interpolators.EMPHASIZED_DECELERATE ), - alphaAnimator( - from = 0f, - to = 1f, - durationMs = mDreamInComplicationsAnimDurationMs, - delayMs = mDreamInBottomComplicationsAnimDelayMs, - position = ComplicationLayoutParams.POSITION_BOTTOM - ) ) doOnEnd { mAnimator = null @@ -130,47 +128,48 @@ constructor( view = view, // Start the blurring wherever the entry animation ended, in // case it was cancelled early. - from = mBlurProgress, - to = 1f, - durationMs = mDreamOutBlurDurationMs + fromBlurRadius = mCurrentBlurRadius, + toBlurRadius = mDreamBlurRadius.toFloat(), + durationMs = mDreamOutBlurDurationMs, + interpolator = Interpolators.EMPHASIZED_ACCELERATE ), translationYAnimator( from = 0f, to = mDreamOutTranslationYDistance.toFloat(), durationMs = mDreamOutTranslationYDurationMs, delayMs = mDreamOutTranslationYDelayBottomMs, - position = ComplicationLayoutParams.POSITION_BOTTOM, - animInterpolator = Interpolators.EMPHASIZED_ACCELERATE + positions = POSITION_BOTTOM, + interpolator = Interpolators.EMPHASIZED_ACCELERATE ), translationYAnimator( from = 0f, to = mDreamOutTranslationYDistance.toFloat(), durationMs = mDreamOutTranslationYDurationMs, delayMs = mDreamOutTranslationYDelayTopMs, - position = ComplicationLayoutParams.POSITION_TOP, - animInterpolator = Interpolators.EMPHASIZED_ACCELERATE + positions = POSITION_TOP, + interpolator = Interpolators.EMPHASIZED_ACCELERATE ), alphaAnimator( from = mCurrentAlphaAtPosition.getOrDefault( - key = ComplicationLayoutParams.POSITION_BOTTOM, + key = POSITION_BOTTOM, defaultValue = 1f ), to = 0f, durationMs = mDreamOutAlphaDurationMs, delayMs = mDreamOutAlphaDelayBottomMs, - position = ComplicationLayoutParams.POSITION_BOTTOM + positions = POSITION_BOTTOM ), alphaAnimator( from = mCurrentAlphaAtPosition.getOrDefault( - key = ComplicationLayoutParams.POSITION_TOP, + key = POSITION_TOP, defaultValue = 1f ), to = 0f, durationMs = mDreamOutAlphaDurationMs, delayMs = mDreamOutAlphaDelayTopMs, - position = ComplicationLayoutParams.POSITION_TOP + positions = POSITION_TOP ) ) doOnEnd { @@ -194,20 +193,21 @@ constructor( private fun blurAnimator( view: View, - from: Float, - to: Float, + fromBlurRadius: Float, + toBlurRadius: Float, durationMs: Long, - delayMs: Long = 0 + delayMs: Long = 0, + interpolator: Interpolator = Interpolators.LINEAR ): Animator { - return ValueAnimator.ofFloat(from, to).apply { + return ValueAnimator.ofFloat(fromBlurRadius, toBlurRadius).apply { duration = durationMs startDelay = delayMs - interpolator = Interpolators.LINEAR + this.interpolator = interpolator addUpdateListener { animator: ValueAnimator -> - mBlurProgress = animator.animatedValue as Float + mCurrentBlurRadius = animator.animatedValue as Float mBlurUtils.applyBlur( viewRootImpl = view.viewRootImpl, - radius = mBlurUtils.blurRadiusOfRatio(mBlurProgress).toInt(), + radius = mCurrentBlurRadius.toInt(), opaque = false ) } @@ -218,18 +218,24 @@ constructor( from: Float, to: Float, durationMs: Long, - delayMs: Long, - @Position position: Int + delayMs: Long = 0, + @Position positions: Int = POSITION_TOP or POSITION_BOTTOM, + interpolator: Interpolator = Interpolators.LINEAR ): Animator { return ValueAnimator.ofFloat(from, to).apply { duration = durationMs startDelay = delayMs - interpolator = Interpolators.LINEAR + this.interpolator = interpolator addUpdateListener { va: ValueAnimator -> - setElementsAlphaAtPosition( - alpha = va.animatedValue as Float, - position = position, - fadingOut = to < from + ComplicationLayoutParams.iteratePositions( + { position: Int -> + setElementsAlphaAtPosition( + alpha = va.animatedValue as Float, + position = position, + fadingOut = to < from + ) + }, + positions ) } } @@ -239,16 +245,21 @@ constructor( from: Float, to: Float, durationMs: Long, - delayMs: Long, - @Position position: Int, - animInterpolator: Interpolator + delayMs: Long = 0, + @Position positions: Int = POSITION_TOP or POSITION_BOTTOM, + interpolator: Interpolator = Interpolators.LINEAR ): Animator { return ValueAnimator.ofFloat(from, to).apply { duration = durationMs startDelay = delayMs - interpolator = animInterpolator + this.interpolator = interpolator addUpdateListener { va: ValueAnimator -> - setElementsTranslationYAtPosition(va.animatedValue as Float, position) + ComplicationLayoutParams.iteratePositions( + { position: Int -> + setElementsTranslationYAtPosition(va.animatedValue as Float, position) + }, + positions + ) } } } @@ -263,7 +274,7 @@ constructor( CrossFadeHelper.fadeIn(view, alpha, /* remap= */ false) } } - if (position == ComplicationLayoutParams.POSITION_TOP) { + if (position == POSITION_TOP) { mStatusBarViewController.setFadeAmount(alpha, fadingOut) } } @@ -273,7 +284,7 @@ constructor( mComplicationHostViewController.getViewsAtPosition(position).forEach { v -> v.translationY = translationY } - if (position == ComplicationLayoutParams.POSITION_TOP) { + if (position == POSITION_TOP) { mStatusBarViewController.setTranslationY(translationY) } } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutParams.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutParams.java index 1755cb92da70..99e19fc96d8f 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutParams.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutParams.java @@ -251,9 +251,17 @@ public class ComplicationLayoutParams extends ViewGroup.LayoutParams { * position specified for this {@link ComplicationLayoutParams}. */ public void iteratePositions(Consumer<Integer> consumer) { + iteratePositions(consumer, mPosition); + } + + /** + * Iterates over the defined positions and invokes the specified {@link Consumer} for each + * position specified by the given {@code position}. + */ + public static void iteratePositions(Consumer<Integer> consumer, @Position int position) { for (int currentPosition = FIRST_POSITION; currentPosition <= LAST_POSITION; currentPosition <<= 1) { - if ((mPosition & currentPosition) == currentPosition) { + if ((position & currentPosition) == currentPosition) { consumer.accept(currentPosition); } } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java index ee0051220787..1065b94508f8 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java @@ -136,8 +136,15 @@ public class DreamHomeControlsComplication implements Complication { final boolean hasFavorites = mControlsComponent.getControlsController() .map(c -> !c.getFavorites().isEmpty()) .orElse(false); + boolean hasPanels = false; + for (int i = 0; i < controlsServices.size(); i++) { + if (controlsServices.get(i).getPanelActivity() != null) { + hasPanels = true; + break; + } + } final ControlsComponent.Visibility visibility = mControlsComponent.getVisibility(); - return hasFavorites && visibility != UNAVAILABLE; + return (hasFavorites || hasPanels) && visibility != UNAVAILABLE; } } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java index 448538193eaf..4aa46d4e4b5a 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java @@ -50,14 +50,14 @@ public abstract class DreamOverlayModule { public static final String BURN_IN_PROTECTION_UPDATE_INTERVAL = "burn_in_protection_update_interval"; public static final String MILLIS_UNTIL_FULL_JITTER = "millis_until_full_jitter"; + public static final String DREAM_BLUR_RADIUS = "DREAM_BLUR_RADIUS"; public static final String DREAM_IN_BLUR_ANIMATION_DURATION = "dream_in_blur_anim_duration"; - public static final String DREAM_IN_BLUR_ANIMATION_DELAY = "dream_in_blur_anim_delay"; public static final String DREAM_IN_COMPLICATIONS_ANIMATION_DURATION = "dream_in_complications_anim_duration"; - public static final String DREAM_IN_TOP_COMPLICATIONS_ANIMATION_DELAY = - "dream_in_top_complications_anim_delay"; - public static final String DREAM_IN_BOTTOM_COMPLICATIONS_ANIMATION_DELAY = - "dream_in_bottom_complications_anim_delay"; + public static final String DREAM_IN_TRANSLATION_Y_DISTANCE = + "dream_in_complications_translation_y"; + public static final String DREAM_IN_TRANSLATION_Y_DURATION = + "dream_in_complications_translation_y_duration"; public static final String DREAM_OUT_TRANSLATION_Y_DISTANCE = "dream_out_complications_translation_y"; public static final String DREAM_OUT_TRANSLATION_Y_DURATION = @@ -134,21 +134,21 @@ public abstract class DreamOverlayModule { } /** - * Duration in milliseconds of the dream in un-blur animation. + * The blur radius applied to the dream overlay at dream entry and exit. */ @Provides - @Named(DREAM_IN_BLUR_ANIMATION_DURATION) - static long providesDreamInBlurAnimationDuration(@Main Resources resources) { - return (long) resources.getInteger(R.integer.config_dreamOverlayInBlurDurationMs); + @Named(DREAM_BLUR_RADIUS) + static int providesDreamBlurRadius(@Main Resources resources) { + return resources.getDimensionPixelSize(R.dimen.dream_overlay_anim_blur_radius); } /** - * Delay in milliseconds of the dream in un-blur animation. + * Duration in milliseconds of the dream in un-blur animation. */ @Provides - @Named(DREAM_IN_BLUR_ANIMATION_DELAY) - static long providesDreamInBlurAnimationDelay(@Main Resources resources) { - return (long) resources.getInteger(R.integer.config_dreamOverlayInBlurDelayMs); + @Named(DREAM_IN_BLUR_ANIMATION_DURATION) + static long providesDreamInBlurAnimationDuration(@Main Resources resources) { + return (long) resources.getInteger(R.integer.config_dreamOverlayInBlurDurationMs); } /** @@ -161,22 +161,23 @@ public abstract class DreamOverlayModule { } /** - * Delay in milliseconds of the dream in top complications fade-in animation. + * Provides the number of pixels to translate complications when entering a dream. */ @Provides - @Named(DREAM_IN_TOP_COMPLICATIONS_ANIMATION_DELAY) - static long providesDreamInTopComplicationsAnimationDelay(@Main Resources resources) { - return (long) resources.getInteger(R.integer.config_dreamOverlayInTopComplicationsDelayMs); + @Named(DREAM_IN_TRANSLATION_Y_DISTANCE) + @DreamOverlayComponent.DreamOverlayScope + static int providesDreamInComplicationsTranslationY(@Main Resources resources) { + return resources.getDimensionPixelSize(R.dimen.dream_overlay_entry_y_offset); } /** - * Delay in milliseconds of the dream in bottom complications fade-in animation. + * Provides the duration in ms of the y-translation when dream enters. */ @Provides - @Named(DREAM_IN_BOTTOM_COMPLICATIONS_ANIMATION_DELAY) - static long providesDreamInBottomComplicationsAnimationDelay(@Main Resources resources) { - return (long) resources.getInteger( - R.integer.config_dreamOverlayInBottomComplicationsDelayMs); + @Named(DREAM_IN_TRANSLATION_Y_DURATION) + @DreamOverlayComponent.DreamOverlayScope + static long providesDreamInComplicationsTranslationYDuration(@Main Resources resources) { + return (long) resources.getInteger(R.integer.config_dreamOverlayInTranslationYDurationMs); } /** diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt index 4ae37c51f278..cbcede023708 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt @@ -21,14 +21,18 @@ import android.content.ContentProvider import android.content.ContentValues import android.content.Context import android.content.UriMatcher +import android.content.pm.PackageManager import android.content.pm.ProviderInfo import android.database.Cursor import android.database.MatrixCursor import android.net.Uri +import android.os.Binder +import android.os.Bundle import android.util.Log import com.android.systemui.SystemUIAppComponentFactoryBase import com.android.systemui.SystemUIAppComponentFactoryBase.ContextAvailableCallback import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor +import com.android.systemui.keyguard.ui.preview.KeyguardRemotePreviewManager import com.android.systemui.shared.quickaffordance.data.content.KeyguardQuickAffordanceProviderContract as Contract import javax.inject.Inject import kotlinx.coroutines.runBlocking @@ -37,6 +41,7 @@ class KeyguardQuickAffordanceProvider : ContentProvider(), SystemUIAppComponentFactoryBase.ContextInitializer { @Inject lateinit var interactor: KeyguardQuickAffordanceInteractor + @Inject lateinit var previewManager: KeyguardRemotePreviewManager private lateinit var contextAvailableCallback: ContextAvailableCallback @@ -149,6 +154,21 @@ class KeyguardQuickAffordanceProvider : return deleteSelection(uri, selectionArgs) } + override fun call(method: String, arg: String?, extras: Bundle?): Bundle? { + return if ( + requireContext() + .checkPermission( + android.Manifest.permission.BIND_WALLPAPER, + Binder.getCallingPid(), + Binder.getCallingUid(), + ) == PackageManager.PERMISSION_GRANTED + ) { + previewManager.preview(extras) + } else { + null + } + } + private fun insertSelection(values: ContentValues?): Uri? { if (values == null) { throw IllegalArgumentException("Cannot insert selection, no values passed in!") diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index d6418d0829a3..923413633351 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -25,6 +25,7 @@ import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.NAV_BA import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_OCCLUSION; import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_TRANSITION_FROM_AOD; import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_UNLOCK_ANIMATION; +import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED; import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST; import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW; import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT; @@ -142,12 +143,12 @@ import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.policy.UserSwitcherController; import com.android.systemui.util.DeviceConfigProxy; +import dagger.Lazy; + import java.io.PrintWriter; import java.util.ArrayList; import java.util.concurrent.Executor; -import dagger.Lazy; - /** * Mediates requests related to the keyguard. This includes queries about the * state of the keyguard, power management events that effect whether the keyguard @@ -822,6 +823,9 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } else if (trustAgentsEnabled && (strongAuth & SOME_AUTH_REQUIRED_AFTER_USER_REQUEST) != 0) { return KeyguardSecurityView.PROMPT_REASON_USER_REQUEST; + } else if (trustAgentsEnabled + && (strongAuth & SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED) != 0) { + return KeyguardSecurityView.PROMPT_REASON_TRUSTAGENT_EXPIRED; } else if (any && ((strongAuth & STRONG_AUTH_REQUIRED_AFTER_LOCKOUT) != 0 || mUpdateMonitor.isFingerprintLockedOut())) { return KeyguardSecurityView.PROMPT_REASON_AFTER_LOCKOUT; diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt index 2558fab216a0..394426df5552 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt @@ -130,6 +130,7 @@ constructor( state( isFeatureEnabled = component.isEnabled(), hasFavorites = favorites?.isNotEmpty() == true, + hasPanels = serviceInfos.any { it.panelActivity != null }, hasServiceInfos = serviceInfos.isNotEmpty(), iconResourceId = component.getTileImageId(), visibility = component.getVisibility(), @@ -148,13 +149,14 @@ constructor( private fun state( isFeatureEnabled: Boolean, hasFavorites: Boolean, + hasPanels: Boolean, hasServiceInfos: Boolean, visibility: ControlsComponent.Visibility, @DrawableRes iconResourceId: Int?, ): KeyguardQuickAffordanceConfig.LockScreenState { return if ( isFeatureEnabled && - hasFavorites && + (hasFavorites || hasPanels) && hasServiceInfos && iconResourceId != null && visibility == ControlsComponent.Visibility.AVAILABLE diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt index 748c6e8b75b9..57668c795d1c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt @@ -34,7 +34,6 @@ import com.android.systemui.keyguard.shared.model.KeyguardSlotPickerRepresentati import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition import com.android.systemui.plugins.ActivityStarter import com.android.systemui.settings.UserTracker -import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots import com.android.systemui.shared.quickaffordance.data.content.KeyguardQuickAffordanceProviderContract import com.android.systemui.statusbar.policy.KeyguardStateController import dagger.Lazy @@ -62,12 +61,20 @@ constructor( private val isUsingRepository: Boolean get() = featureFlags.isEnabled(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES) + /** + * Whether the UI should use the long press gesture to activate quick affordances. + * + * If `false`, the UI goes back to using single taps. + */ + val useLongPress: Boolean + get() = featureFlags.isEnabled(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES) + /** Returns an observable for the quick affordance at the given position. */ fun quickAffordance( position: KeyguardQuickAffordancePosition ): Flow<KeyguardQuickAffordanceModel> { return combine( - quickAffordanceInternal(position), + quickAffordanceAlwaysVisible(position), keyguardInteractor.isDozing, keyguardInteractor.isKeyguardShowing, ) { affordance, isDozing, isKeyguardShowing -> @@ -80,6 +87,19 @@ constructor( } /** + * Returns an observable for the quick affordance at the given position but always visible, + * regardless of lock screen state. + * + * This is useful for experiences like the lock screen preview mode, where the affordances must + * always be visible. + */ + fun quickAffordanceAlwaysVisible( + position: KeyguardQuickAffordancePosition, + ): Flow<KeyguardQuickAffordanceModel> { + return quickAffordanceInternal(position) + } + + /** * Notifies that a quick affordance has been "triggered" (clicked) by the user. * * @param configKey The configuration key corresponding to the [KeyguardQuickAffordanceModel] of @@ -290,15 +310,6 @@ constructor( } } - private fun KeyguardQuickAffordancePosition.toSlotId(): String { - return when (this) { - KeyguardQuickAffordancePosition.BOTTOM_START -> - KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START - KeyguardQuickAffordancePosition.BOTTOM_END -> - KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END - } - } - private fun String.encode(slotId: String): String { return "$slotId$DELIMITER$this" } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/quickaffordance/KeyguardQuickAffordancePosition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/quickaffordance/KeyguardQuickAffordancePosition.kt index a18b036c5189..2581b595d812 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/quickaffordance/KeyguardQuickAffordancePosition.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/quickaffordance/KeyguardQuickAffordancePosition.kt @@ -16,8 +16,17 @@ package com.android.systemui.keyguard.shared.quickaffordance +import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots + /** Enumerates all possible positions for quick affordances that can appear on the lock-screen. */ enum class KeyguardQuickAffordancePosition { BOTTOM_START, - BOTTOM_END, + BOTTOM_END; + + fun toSlotId(): String { + return when (this) { + BOTTOM_START -> KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START + BOTTOM_END -> KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt index cbe512ff83ba..ae8edfece4cb 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt @@ -16,14 +16,19 @@ package com.android.systemui.keyguard.ui.binder +import android.annotation.SuppressLint import android.graphics.drawable.Animatable2 import android.util.Size import android.util.TypedValue +import android.view.MotionEvent import android.view.View +import android.view.ViewConfiguration import android.view.ViewGroup import android.view.ViewPropertyAnimator import android.widget.ImageView import android.widget.TextView +import androidx.core.animation.CycleInterpolator +import androidx.core.animation.ObjectAnimator import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import androidx.lifecycle.Lifecycle @@ -38,8 +43,10 @@ import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewModel import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.plugins.FalsingManager +import kotlin.math.pow +import kotlin.math.sqrt +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map @@ -52,6 +59,7 @@ import kotlinx.coroutines.launch * view-binding, binding each view only once. It is okay and expected for the same instance of the * view-model to be reused for multiple view/view-binder bindings. */ +@OptIn(ExperimentalCoroutinesApi::class) object KeyguardBottomAreaViewBinder { private const val EXIT_DOZE_BUTTON_REVEAL_ANIMATION_DURATION_MS = 250L @@ -84,7 +92,8 @@ object KeyguardBottomAreaViewBinder { fun bind( view: ViewGroup, viewModel: KeyguardBottomAreaViewModel, - falsingManager: FalsingManager, + falsingManager: FalsingManager?, + messageDisplayer: (Int) -> Unit, ): Binding { val indicationArea: View = view.requireViewById(R.id.keyguard_indication_area) val ambientIndicationArea: View? = view.findViewById(R.id.ambient_indication_container) @@ -108,6 +117,7 @@ object KeyguardBottomAreaViewBinder { view = startButton, viewModel = buttonModel, falsingManager = falsingManager, + messageDisplayer = messageDisplayer, ) } } @@ -118,6 +128,7 @@ object KeyguardBottomAreaViewBinder { view = endButton, viewModel = buttonModel, falsingManager = falsingManager, + messageDisplayer = messageDisplayer, ) } } @@ -222,10 +233,12 @@ object KeyguardBottomAreaViewBinder { } } + @SuppressLint("ClickableViewAccessibility") private fun updateButton( view: ImageView, viewModel: KeyguardQuickAffordanceViewModel, - falsingManager: FalsingManager, + falsingManager: FalsingManager?, + messageDisplayer: (Int) -> Unit, ) { if (!viewModel.isVisible) { view.isVisible = false @@ -281,21 +294,126 @@ object KeyguardBottomAreaViewBinder { }, ) ) + view.backgroundTintList = - Utils.getColorAttr( - view.context, - if (viewModel.isActivated) { - com.android.internal.R.attr.colorAccentPrimary - } else { - com.android.internal.R.attr.colorSurface - } - ) + if (!viewModel.isSelected) { + Utils.getColorAttr( + view.context, + if (viewModel.isActivated) { + com.android.internal.R.attr.colorAccentPrimary + } else { + com.android.internal.R.attr.colorSurface + } + ) + } else { + null + } view.isClickable = viewModel.isClickable if (viewModel.isClickable) { - view.setOnClickListener(OnClickListener(viewModel, falsingManager)) + if (viewModel.useLongPress) { + view.setOnTouchListener(OnTouchListener(view, viewModel, messageDisplayer)) + } else { + view.setOnClickListener(OnClickListener(viewModel, checkNotNull(falsingManager))) + } } else { view.setOnClickListener(null) + view.setOnTouchListener(null) + } + + view.isSelected = viewModel.isSelected + } + + private class OnTouchListener( + private val view: View, + private val viewModel: KeyguardQuickAffordanceViewModel, + private val messageDisplayer: (Int) -> Unit, + ) : View.OnTouchListener { + + private val longPressDurationMs = ViewConfiguration.getLongPressTimeout().toLong() + private var longPressAnimator: ViewPropertyAnimator? = null + private var downTimestamp = 0L + + @SuppressLint("ClickableViewAccessibility") + override fun onTouch(v: View?, event: MotionEvent?): Boolean { + return when (event?.actionMasked) { + MotionEvent.ACTION_DOWN -> + if (viewModel.configKey != null) { + downTimestamp = System.currentTimeMillis() + longPressAnimator = + view + .animate() + .scaleX(PRESSED_SCALE) + .scaleY(PRESSED_SCALE) + .setDuration(longPressDurationMs) + .withEndAction { + view.setOnClickListener { + viewModel.onClicked( + KeyguardQuickAffordanceViewModel.OnClickedParameters( + configKey = viewModel.configKey, + expandable = Expandable.fromView(view), + ) + ) + } + view.performClick() + view.setOnClickListener(null) + } + true + } else { + false + } + MotionEvent.ACTION_MOVE -> { + if (event.historySize > 0) { + val distance = + sqrt( + (event.y - event.getHistoricalY(0)).pow(2) + + (event.x - event.getHistoricalX(0)).pow(2) + ) + if (distance > ViewConfiguration.getTouchSlop()) { + cancel() + } + } + true + } + MotionEvent.ACTION_UP -> { + if (System.currentTimeMillis() - downTimestamp < longPressDurationMs) { + messageDisplayer.invoke(R.string.keyguard_affordance_press_too_short) + val shakeAnimator = + ObjectAnimator.ofFloat( + view, + "translationX", + 0f, + view.context.resources + .getDimensionPixelSize( + R.dimen.keyguard_affordance_shake_amplitude + ) + .toFloat(), + 0f, + ) + shakeAnimator.duration = 300 + shakeAnimator.interpolator = CycleInterpolator(5f) + shakeAnimator.start() + } + cancel() + true + } + MotionEvent.ACTION_CANCEL -> { + cancel() + true + } + else -> false + } + } + + private fun cancel() { + downTimestamp = 0L + longPressAnimator?.cancel() + longPressAnimator = null + view.animate().scaleX(1f).scaleY(1f) + } + + companion object { + private const val PRESSED_SCALE = 1.5f } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt new file mode 100644 index 000000000000..a5ae8ba58d45 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.keyguard.ui.preview + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.hardware.display.DisplayManager +import android.os.Bundle +import android.os.IBinder +import android.view.Gravity +import android.view.LayoutInflater +import android.view.SurfaceControlViewHost +import android.view.View +import android.view.ViewGroup +import android.view.WindowManager +import android.widget.FrameLayout +import com.android.keyguard.ClockEventController +import com.android.keyguard.KeyguardClockSwitch +import com.android.systemui.R +import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel +import com.android.systemui.shared.clocks.ClockRegistry +import com.android.systemui.shared.quickaffordance.shared.model.KeyguardQuickAffordancePreviewConstants +import com.android.systemui.statusbar.phone.KeyguardBottomAreaView +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.DisposableHandle +import kotlinx.coroutines.runBlocking + +/** Renders the preview of the lock screen. */ +class KeyguardPreviewRenderer +@AssistedInject +constructor( + @Application private val context: Context, + @Main private val mainDispatcher: CoroutineDispatcher, + private val bottomAreaViewModel: KeyguardBottomAreaViewModel, + displayManager: DisplayManager, + private val windowManager: WindowManager, + private val clockController: ClockEventController, + private val clockRegistry: ClockRegistry, + private val broadcastDispatcher: BroadcastDispatcher, + @Assisted bundle: Bundle, +) { + + val hostToken: IBinder? = bundle.getBinder(KEY_HOST_TOKEN) + private val width: Int = bundle.getInt(KEY_VIEW_WIDTH) + private val height: Int = bundle.getInt(KEY_VIEW_HEIGHT) + + private var host: SurfaceControlViewHost + + val surfacePackage: SurfaceControlViewHost.SurfacePackage + get() = host.surfacePackage + + private var clockView: View? = null + + private val disposables = mutableSetOf<DisposableHandle>() + private var isDestroyed = false + + init { + bottomAreaViewModel.enablePreviewMode( + initiallySelectedSlotId = + bundle.getString( + KeyguardQuickAffordancePreviewConstants.KEY_INITIALLY_SELECTED_SLOT_ID, + ), + ) + runBlocking(mainDispatcher) { + host = + SurfaceControlViewHost( + context, + displayManager.getDisplay(bundle.getInt(KEY_DISPLAY_ID)), + hostToken, + ) + disposables.add(DisposableHandle { host.release() }) + } + } + + fun render() { + runBlocking(mainDispatcher) { + val rootView = FrameLayout(context) + + setUpBottomArea(rootView) + setUpClock(rootView) + + rootView.measure( + View.MeasureSpec.makeMeasureSpec( + windowManager.currentWindowMetrics.bounds.width(), + View.MeasureSpec.EXACTLY + ), + View.MeasureSpec.makeMeasureSpec( + windowManager.currentWindowMetrics.bounds.height(), + View.MeasureSpec.EXACTLY + ), + ) + rootView.layout(0, 0, rootView.measuredWidth, rootView.measuredHeight) + + // This aspect scales the view to fit in the surface and centers it + val scale: Float = + (width / rootView.measuredWidth.toFloat()).coerceAtMost( + height / rootView.measuredHeight.toFloat() + ) + + rootView.scaleX = scale + rootView.scaleY = scale + rootView.pivotX = 0f + rootView.pivotY = 0f + rootView.translationX = (width - scale * rootView.width) / 2 + rootView.translationY = (height - scale * rootView.height) / 2 + + host.setView(rootView, rootView.measuredWidth, rootView.measuredHeight) + } + } + + fun onSlotSelected(slotId: String) { + bottomAreaViewModel.onPreviewSlotSelected(slotId = slotId) + } + + fun destroy() { + isDestroyed = true + disposables.forEach { it.dispose() } + } + + private fun setUpBottomArea(parentView: ViewGroup) { + val bottomAreaView = + LayoutInflater.from(context) + .inflate( + R.layout.keyguard_bottom_area, + parentView, + false, + ) as KeyguardBottomAreaView + bottomAreaView.init( + viewModel = bottomAreaViewModel, + ) + parentView.addView( + bottomAreaView, + FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.WRAP_CONTENT, + Gravity.BOTTOM, + ), + ) + } + + private fun setUpClock(parentView: ViewGroup) { + val clockChangeListener = ClockRegistry.ClockChangeListener { onClockChanged(parentView) } + clockRegistry.registerClockChangeListener(clockChangeListener) + disposables.add( + DisposableHandle { clockRegistry.unregisterClockChangeListener(clockChangeListener) } + ) + + clockController.registerListeners(parentView) + disposables.add(DisposableHandle { clockController.unregisterListeners() }) + + val receiver = + object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + clockController.clock?.events?.onTimeTick() + } + } + broadcastDispatcher.registerReceiver( + receiver, + IntentFilter().apply { + addAction(Intent.ACTION_TIME_TICK) + addAction(Intent.ACTION_TIME_CHANGED) + }, + ) + disposables.add(DisposableHandle { broadcastDispatcher.unregisterReceiver(receiver) }) + + onClockChanged(parentView) + } + + private fun onClockChanged(parentView: ViewGroup) { + clockController.clock = clockRegistry.createCurrentClock() + clockController.clock + ?.largeClock + ?.events + ?.onTargetRegionChanged(KeyguardClockSwitch.getLargeClockRegion(parentView)) + clockView?.let { parentView.removeView(it) } + clockView = clockController.clock?.largeClock?.view?.apply { parentView.addView(this) } + } + + companion object { + private const val KEY_HOST_TOKEN = "host_token" + private const val KEY_VIEW_WIDTH = "width" + private const val KEY_VIEW_HEIGHT = "height" + private const val KEY_DISPLAY_ID = "display_id" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRendererFactory.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRendererFactory.kt new file mode 100644 index 000000000000..be1d3a18520a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRendererFactory.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.keyguard.ui.preview + +import android.os.Bundle +import dagger.assisted.AssistedFactory + +@AssistedFactory +interface KeyguardPreviewRendererFactory { + fun create(bundle: Bundle): KeyguardPreviewRenderer +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardRemotePreviewManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardRemotePreviewManager.kt new file mode 100644 index 000000000000..50722d5c68f8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardRemotePreviewManager.kt @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.keyguard.ui.preview + +import android.os.Bundle +import android.os.Handler +import android.os.IBinder +import android.os.Message +import android.os.Messenger +import android.util.ArrayMap +import android.util.Log +import androidx.annotation.VisibleForTesting +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.shared.quickaffordance.shared.model.KeyguardQuickAffordancePreviewConstants +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.runBlocking + +@SysUISingleton +class KeyguardRemotePreviewManager +@Inject +constructor( + private val previewRendererFactory: KeyguardPreviewRendererFactory, + @Main private val mainDispatcher: CoroutineDispatcher, + @Background private val backgroundHandler: Handler, +) { + private val activePreviews: ArrayMap<IBinder, PreviewLifecycleObserver> = + ArrayMap<IBinder, PreviewLifecycleObserver>() + + fun preview(request: Bundle?): Bundle? { + if (request == null) { + return null + } + + var observer: PreviewLifecycleObserver? = null + return try { + val renderer = previewRendererFactory.create(request) + + // Destroy any previous renderer associated with this token. + activePreviews[renderer.hostToken]?.let { destroyObserver(it) } + observer = PreviewLifecycleObserver(renderer, mainDispatcher, ::destroyObserver) + activePreviews[renderer.hostToken] = observer + renderer.render() + renderer.hostToken?.linkToDeath(observer, 0) + val result = Bundle() + result.putParcelable( + KEY_PREVIEW_SURFACE_PACKAGE, + renderer.surfacePackage, + ) + val messenger = + Messenger( + Handler( + backgroundHandler.looper, + observer, + ) + ) + val msg = Message.obtain() + msg.replyTo = messenger + result.putParcelable(KEY_PREVIEW_CALLBACK, msg) + result + } catch (e: Exception) { + Log.e(TAG, "Unable to generate preview", e) + observer?.let { destroyObserver(it) } + null + } + } + + private fun destroyObserver(observer: PreviewLifecycleObserver) { + observer.onDestroy()?.let { hostToken -> + if (activePreviews[hostToken] === observer) { + activePreviews.remove(hostToken) + } + } + } + + private class PreviewLifecycleObserver( + private val renderer: KeyguardPreviewRenderer, + private val mainDispatcher: CoroutineDispatcher, + private val requestDestruction: (PreviewLifecycleObserver) -> Unit, + ) : Handler.Callback, IBinder.DeathRecipient { + + private var isDestroyed = false + + override fun handleMessage(message: Message): Boolean { + when (message.what) { + KeyguardQuickAffordancePreviewConstants.MESSAGE_ID_SLOT_SELECTED -> { + message.data + .getString( + KeyguardQuickAffordancePreviewConstants.KEY_SLOT_ID, + ) + ?.let { slotId -> renderer.onSlotSelected(slotId = slotId) } + } + else -> requestDestruction(this) + } + + return true + } + + override fun binderDied() { + requestDestruction(this) + } + + fun onDestroy(): IBinder? { + if (isDestroyed) { + return null + } + + isDestroyed = true + val hostToken = renderer.hostToken + hostToken?.unlinkToDeath(this, 0) + runBlocking(mainDispatcher) { renderer.destroy() } + return hostToken + } + } + + companion object { + private const val TAG = "KeyguardRemotePreviewManager" + @VisibleForTesting const val KEY_PREVIEW_SURFACE_PACKAGE = "surface_package" + @VisibleForTesting const val KEY_PREVIEW_CALLBACK = "callback" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt index 227796f43e35..5d85680efcf4 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt @@ -24,13 +24,19 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceIn import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordanceModel import com.android.systemui.keyguard.shared.quickaffordance.ActivationState import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition +import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map /** View-model for the keyguard bottom area view */ +@OptIn(ExperimentalCoroutinesApi::class) class KeyguardBottomAreaViewModel @Inject constructor( @@ -40,6 +46,20 @@ constructor( private val burnInHelperWrapper: BurnInHelperWrapper, ) { /** + * Whether this view-model instance is powering the preview experience that renders exclusively + * in the wallpaper picker application. This should _always_ be `false` for the real lock screen + * experience. + */ + private val isInPreviewMode = MutableStateFlow(false) + + /** + * ID of the slot that's currently selected in the preview that renders exclusively in the + * wallpaper picker application. This is ignored for the actual, real lock screen experience. + */ + private val selectedPreviewSlotId = + MutableStateFlow(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START) + + /** * Whether quick affordances are "opaque enough" to be considered visible to and interactive by * the user. If they are not interactive, user input should not be allowed on them. * @@ -66,7 +86,14 @@ constructor( val isOverlayContainerVisible: Flow<Boolean> = keyguardInteractor.isDozing.map { !it }.distinctUntilChanged() /** An observable for the alpha level for the entire bottom area. */ - val alpha: Flow<Float> = bottomAreaInteractor.alpha.distinctUntilChanged() + val alpha: Flow<Float> = + isInPreviewMode.flatMapLatest { isInPreviewMode -> + if (isInPreviewMode) { + flowOf(1f) + } else { + bottomAreaInteractor.alpha.distinctUntilChanged() + } + } /** An observable for whether the indication area should be padded. */ val isIndicationAreaPadded: Flow<Boolean> = combine(startButton, endButton) { startButtonModel, endButtonModel -> @@ -94,27 +121,61 @@ constructor( * Returns whether the keyguard bottom area should be constrained to the top of the lock icon */ fun shouldConstrainToTopOfLockIcon(): Boolean = - bottomAreaInteractor.shouldConstrainToTopOfLockIcon() + bottomAreaInteractor.shouldConstrainToTopOfLockIcon() + + /** + * Puts this view-model in "preview mode", which means it's being used for UI that is rendering + * the lock screen preview in wallpaper picker / settings and not the real experience on the + * lock screen. + * + * @param initiallySelectedSlotId The ID of the initial slot to render as the selected one. + */ + fun enablePreviewMode(initiallySelectedSlotId: String?) { + isInPreviewMode.value = true + onPreviewSlotSelected( + initiallySelectedSlotId ?: KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START + ) + } + + /** + * Notifies that a slot with the given ID has been selected in the preview experience that is + * rendering in the wallpaper picker. This is ignored for the real lock screen experience. + * + * @see enablePreviewMode + */ + fun onPreviewSlotSelected(slotId: String) { + selectedPreviewSlotId.value = slotId + } private fun button( position: KeyguardQuickAffordancePosition ): Flow<KeyguardQuickAffordanceViewModel> { - return combine( - quickAffordanceInteractor.quickAffordance(position), - bottomAreaInteractor.animateDozingTransitions.distinctUntilChanged(), - areQuickAffordancesFullyOpaque, - ) { model, animateReveal, isFullyOpaque -> - model.toViewModel( - animateReveal = animateReveal, - isClickable = isFullyOpaque, - ) - } - .distinctUntilChanged() + return isInPreviewMode.flatMapLatest { isInPreviewMode -> + combine( + if (isInPreviewMode) { + quickAffordanceInteractor.quickAffordanceAlwaysVisible(position = position) + } else { + quickAffordanceInteractor.quickAffordance(position = position) + }, + bottomAreaInteractor.animateDozingTransitions.distinctUntilChanged(), + areQuickAffordancesFullyOpaque, + selectedPreviewSlotId, + ) { model, animateReveal, isFullyOpaque, selectedPreviewSlotId -> + model.toViewModel( + animateReveal = !isInPreviewMode && animateReveal, + isClickable = isFullyOpaque && !isInPreviewMode, + isSelected = + (isInPreviewMode && selectedPreviewSlotId == position.toSlotId()), + ) + } + .distinctUntilChanged() + } } private fun KeyguardQuickAffordanceModel.toViewModel( animateReveal: Boolean, isClickable: Boolean, + isSelected: Boolean, ): KeyguardQuickAffordanceViewModel { return when (this) { is KeyguardQuickAffordanceModel.Visible -> @@ -131,6 +192,8 @@ constructor( }, isClickable = isClickable, isActivated = activationState is ActivationState.Active, + isSelected = isSelected, + useLongPress = quickAffordanceInteractor.useLongPress, ) is KeyguardQuickAffordanceModel.Hidden -> KeyguardQuickAffordanceViewModel() } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt index 44f48f97b62e..cf3a6daa40bb 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt @@ -29,6 +29,8 @@ data class KeyguardQuickAffordanceViewModel( val onClicked: (OnClickedParameters) -> Unit = {}, val isClickable: Boolean = false, val isActivated: Boolean = false, + val isSelected: Boolean = false, + val useLongPress: Boolean = false, ) { data class OnClickedParameters( val configKey: String, diff --git a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java index 3e5d337bff9d..bb833df1ff69 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java +++ b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java @@ -30,9 +30,11 @@ import com.android.systemui.media.nearby.NearbyMediaDevicesManager; import com.android.systemui.media.taptotransfer.MediaTttCommandLineHelper; import com.android.systemui.media.taptotransfer.MediaTttFlags; import com.android.systemui.media.taptotransfer.common.MediaTttLogger; +import com.android.systemui.media.taptotransfer.receiver.ChipReceiverInfo; import com.android.systemui.media.taptotransfer.receiver.MediaTttReceiverLogger; import com.android.systemui.media.taptotransfer.sender.MediaTttSenderLogger; import com.android.systemui.plugins.log.LogBuffer; +import com.android.systemui.temporarydisplay.chipbar.ChipbarInfo; import java.util.Optional; @@ -95,19 +97,19 @@ public interface MediaModule { @Provides @SysUISingleton @MediaTttSenderLogger - static MediaTttLogger providesMediaTttSenderLogger( + static MediaTttLogger<ChipbarInfo> providesMediaTttSenderLogger( @MediaTttSenderLogBuffer LogBuffer buffer ) { - return new MediaTttLogger("Sender", buffer); + return new MediaTttLogger<>("Sender", buffer); } @Provides @SysUISingleton @MediaTttReceiverLogger - static MediaTttLogger providesMediaTttReceiverLogger( + static MediaTttLogger<ChipReceiverInfo> providesMediaTttReceiverLogger( @MediaTttReceiverLogBuffer LogBuffer buffer ) { - return new MediaTttLogger("Receiver", buffer); + return new MediaTttLogger<>("Receiver", buffer); } /** */ diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt index b55bedda2dc1..8aef9385fe3e 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttLogger.kt @@ -18,17 +18,21 @@ package com.android.systemui.media.taptotransfer.common import com.android.systemui.plugins.log.LogBuffer import com.android.systemui.plugins.log.LogLevel +import com.android.systemui.temporarydisplay.TemporaryViewInfo import com.android.systemui.temporarydisplay.TemporaryViewLogger /** * A logger for media tap-to-transfer events. * * @param deviceTypeTag the type of device triggering the logs -- "Sender" or "Receiver". + * + * TODO(b/245610654): We should de-couple the sender and receiver loggers, since they're vastly + * different experiences. */ -class MediaTttLogger( +class MediaTttLogger<T : TemporaryViewInfo>( deviceTypeTag: String, buffer: LogBuffer -) : TemporaryViewLogger(buffer, BASE_TAG + deviceTypeTag) { +) : TemporaryViewLogger<T>(buffer, BASE_TAG + deviceTypeTag) { /** Logs a change in the chip state for the given [mediaRouteId]. */ fun logStateChange(stateName: String, mediaRouteId: String, packageName: String?) { buffer.log( diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt index 009595a6da8b..066c1853818f 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttUtils.kt @@ -25,6 +25,7 @@ import com.android.systemui.R import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.shared.model.TintedIcon +import com.android.systemui.temporarydisplay.TemporaryViewInfo /** Utility methods for media tap-to-transfer. */ class MediaTttUtils { @@ -47,7 +48,7 @@ class MediaTttUtils { fun getIconInfoFromPackageName( context: Context, appPackageName: String?, - logger: MediaTttLogger + logger: MediaTttLogger<out TemporaryViewInfo> ): IconInfo { if (appPackageName != null) { val packageManager = context.packageManager diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ChipStateReceiver.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ChipStateReceiver.kt index 40ea1e6e87df..11348adb582c 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ChipStateReceiver.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ChipStateReceiver.kt @@ -35,6 +35,14 @@ enum class ChipStateReceiver( FAR_FROM_SENDER( StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_FAR_FROM_SENDER, MediaTttReceiverUiEvents.MEDIA_TTT_RECEIVER_FAR_FROM_SENDER + ), + TRANSFER_TO_RECEIVER_SUCCEEDED( + StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED, + MediaTttReceiverUiEvents.MEDIA_TTT_RECEIVER_TRANSFER_TO_RECEIVER_SUCCEEDED, + ), + TRANSFER_TO_RECEIVER_FAILED( + StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_TRANSFER_TO_RECEIVER_FAILED, + MediaTttReceiverUiEvents.MEDIA_TTT_RECEIVER_TRANSFER_TO_RECEIVER_FAILED, ); companion object { diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt index 1c3a53cbf815..7b9d0b4205af 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt @@ -45,8 +45,10 @@ import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.temporarydisplay.TemporaryViewDisplayController import com.android.systemui.temporarydisplay.TemporaryViewInfo +import com.android.systemui.temporarydisplay.ViewPriority import com.android.systemui.util.animation.AnimationUtil.Companion.frames import com.android.systemui.util.concurrency.DelayableExecutor +import com.android.systemui.util.time.SystemClock import com.android.systemui.util.view.ViewUtil import com.android.systemui.util.wakelock.WakeLock import javax.inject.Inject @@ -62,7 +64,7 @@ import javax.inject.Inject open class MediaTttChipControllerReceiver @Inject constructor( private val commandQueue: CommandQueue, context: Context, - @MediaTttReceiverLogger logger: MediaTttLogger, + @MediaTttReceiverLogger logger: MediaTttLogger<ChipReceiverInfo>, windowManager: WindowManager, mainExecutor: DelayableExecutor, accessibilityManager: AccessibilityManager, @@ -73,7 +75,8 @@ open class MediaTttChipControllerReceiver @Inject constructor( private val uiEventLogger: MediaTttReceiverUiEventLogger, private val viewUtil: ViewUtil, wakeLockBuilder: WakeLock.Builder, -) : TemporaryViewDisplayController<ChipReceiverInfo, MediaTttLogger>( + systemClock: SystemClock, +) : TemporaryViewDisplayController<ChipReceiverInfo, MediaTttLogger<ChipReceiverInfo>>( context, logger, windowManager, @@ -83,6 +86,7 @@ open class MediaTttChipControllerReceiver @Inject constructor( powerManager, R.layout.media_ttt_chip_receiver, wakeLockBuilder, + systemClock, ) { @SuppressLint("WrongConstant") // We're allowed to use LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS override val windowLayoutParams = commonWindowLayoutParams.apply { @@ -123,8 +127,8 @@ open class MediaTttChipControllerReceiver @Inject constructor( } uiEventLogger.logReceiverStateChange(chipState) - if (chipState == ChipStateReceiver.FAR_FROM_SENDER) { - removeView(routeInfo.id, removalReason = ChipStateReceiver.FAR_FROM_SENDER.name) + if (chipState != ChipStateReceiver.CLOSE_TO_SENDER) { + removeView(routeInfo.id, removalReason = chipState.name) return } if (appIcon == null) { @@ -290,4 +294,5 @@ data class ChipReceiverInfo( override val windowTitle: String = MediaTttUtils.WINDOW_TITLE_RECEIVER, override val wakeReason: String = MediaTttUtils.WAKE_REASON_RECEIVER, override val id: String, + override val priority: ViewPriority = ViewPriority.NORMAL, ) : TemporaryViewInfo() diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverUiEventLogger.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverUiEventLogger.kt index 39a276329a9b..6e515f27c25e 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverUiEventLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttReceiverUiEventLogger.kt @@ -34,7 +34,11 @@ enum class MediaTttReceiverUiEvents(val metricId: Int) : UiEventLogger.UiEventEn @UiEvent(doc = "See android.app.StatusBarManager.MEDIA_TRANSFER_RECEIVER_* docs") MEDIA_TTT_RECEIVER_CLOSE_TO_SENDER(982), @UiEvent(doc = "See android.app.StatusBarManager.MEDIA_TRANSFER_RECEIVER_* docs") - MEDIA_TTT_RECEIVER_FAR_FROM_SENDER(983); + MEDIA_TTT_RECEIVER_FAR_FROM_SENDER(983), + @UiEvent(doc = "See android.app.StatusBarManager.MEDIA_TRANSFER_RECEIVER_* docs") + MEDIA_TTT_RECEIVER_TRANSFER_TO_RECEIVER_SUCCEEDED(1263), + @UiEvent(doc = "See android.app.StatusBarManager.MEDIA_TRANSFER_RECEIVER_* docs") + MEDIA_TTT_RECEIVER_TRANSFER_TO_RECEIVER_FAILED(1264); override fun getId() = metricId } diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt index ec1984d78cf9..9f44d984124f 100644 --- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinator.kt @@ -30,6 +30,7 @@ import com.android.systemui.media.taptotransfer.MediaTttFlags import com.android.systemui.media.taptotransfer.common.MediaTttLogger import com.android.systemui.media.taptotransfer.common.MediaTttUtils import com.android.systemui.statusbar.CommandQueue +import com.android.systemui.temporarydisplay.ViewPriority import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator import com.android.systemui.temporarydisplay.chipbar.ChipbarEndItem import com.android.systemui.temporarydisplay.chipbar.ChipbarInfo @@ -46,7 +47,7 @@ constructor( private val chipbarCoordinator: ChipbarCoordinator, private val commandQueue: CommandQueue, private val context: Context, - @MediaTttSenderLogger private val logger: MediaTttLogger, + @MediaTttSenderLogger private val logger: MediaTttLogger<ChipbarInfo>, private val mediaTttFlags: MediaTttFlags, private val uiEventLogger: MediaTttSenderUiEventLogger, ) : CoreStartable { @@ -146,7 +147,7 @@ constructor( routeInfo: MediaRoute2Info, undoCallback: IUndoMediaTransferCallback?, context: Context, - logger: MediaTttLogger, + logger: MediaTttLogger<ChipbarInfo>, ): ChipbarInfo { val packageName = routeInfo.clientPackageName val otherDeviceName = routeInfo.name.toString() @@ -180,6 +181,7 @@ constructor( wakeReason = MediaTttUtils.WAKE_REASON_SENDER, timeoutMs = chipStateSender.timeout, id = routeInfo.id, + priority = ViewPriority.NORMAL, ) } diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java index f97385bf57e3..6c99b6764909 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java @@ -654,8 +654,9 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack } private void updateMLModelState() { - boolean newState = mIsEnabled && DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI, - SystemUiDeviceConfigFlags.USE_BACK_GESTURE_ML_MODEL, false); + boolean newState = + mIsGesturalModeEnabled && DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI, + SystemUiDeviceConfigFlags.USE_BACK_GESTURE_ML_MODEL, false); if (newState == mUseMLModel) { return; @@ -785,7 +786,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack // ML model boolean withinMinRange = x < mMLEnableWidth + mLeftInset || x >= (mDisplaySize.x - mMLEnableWidth - mRightInset); - if (!withinMinRange && mUseMLModel + if (!withinMinRange && mUseMLModel && !mMLModelIsLoading && (results = getBackGesturePredictionsCategory(x, y, app)) != -1) { withinRange = (results == 1); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialog.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialog.java index 9743c3e64950..206a62085019 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialog.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialog.java @@ -358,6 +358,9 @@ public class InternetDialog extends SystemUIDialog implements if (!isChecked && shouldShowMobileDialog()) { showTurnOffMobileDialog(); } else if (!shouldShowMobileDialog()) { + if (mInternetDialogController.isMobileDataEnabled() == isChecked) { + return; + } mInternetDialogController.setMobileDataEnabled(mContext, mDefaultDataSubId, isChecked, false); } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 41084eefb17b..a824d916ec63 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -40,6 +40,7 @@ import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_N import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED; import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; import static com.android.systemui.statusbar.StatusBarState.SHADE; +import static com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED; import static com.android.systemui.statusbar.VibratorHelper.TOUCH_VIBRATION_ATTRIBUTES; import static com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_FOLD_TO_AOD; import static com.android.systemui.util.DumpUtilsKt.asIndenting; @@ -1327,7 +1328,9 @@ public final class NotificationPanelViewController implements Dumpable { mKeyguardBottomArea.init( mKeyguardBottomAreaViewModel, mFalsingManager, - mLockIconViewController + mLockIconViewController, + stringResourceId -> + mKeyguardIndicationController.showTransientIndication(stringResourceId) ); } @@ -4722,6 +4725,7 @@ public final class NotificationPanelViewController implements Dumpable { if (!openingWithTouch || !mHasVibratedOnOpen) { mVibratorHelper.vibrate(VibrationEffect.EFFECT_TICK); mHasVibratedOnOpen = true; + mShadeLog.v("Vibrating on opening, mHasVibratedOnOpen=true"); } } } @@ -5479,6 +5483,15 @@ public final class NotificationPanelViewController implements Dumpable { mBarState = statusBarState; mKeyguardShowing = keyguardShowing; + boolean fromShadeToKeyguard = statusBarState == KEYGUARD + && (oldState == SHADE || oldState == SHADE_LOCKED); + if (mSplitShadeEnabled && fromShadeToKeyguard) { + // user can go to keyguard from different shade states and closing animation + // may not fully run - we always want to make sure we close QS when that happens + // as we never need QS open in fresh keyguard state + closeQs(); + } + if (oldState == KEYGUARD && (goingToFullShade || statusBarState == StatusBarState.SHADE_LOCKED)) { @@ -5498,27 +5511,12 @@ public final class NotificationPanelViewController implements Dumpable { mKeyguardStatusBarViewController.animateKeyguardStatusBarIn(); mNotificationStackScrollLayoutController.resetScrollPosition(); - // Only animate header if the header is visible. If not, it will partially - // animate out - // the top of QS - if (!mQsExpanded) { - // TODO(b/185683835) Nicer clipping when using new spacial model - if (mSplitShadeEnabled) { - mQs.animateHeaderSlidingOut(); - } - } } else { // this else branch means we are doing one of: // - from KEYGUARD to SHADE (but not fully expanded as when swiping from the top) // - from SHADE to KEYGUARD // - from SHADE_LOCKED to SHADE // - getting notified again about the current SHADE or KEYGUARD state - if (mSplitShadeEnabled && oldState == SHADE && statusBarState == KEYGUARD) { - // user can go to keyguard from different shade states and closing animation - // may not fully run - we always want to make sure we close QS when that happens - // as we never need QS open in fresh keyguard state - closeQs(); - } final boolean animatingUnlockedShadeToKeyguard = oldState == SHADE && statusBarState == KEYGUARD && mScreenOffAnimationController.isKeyguardShowDelayed(); @@ -6122,6 +6120,7 @@ public final class NotificationPanelViewController implements Dumpable { if (isFullyCollapsed()) { // If panel is fully collapsed, reset haptic effect before adding movement. mHasVibratedOnOpen = false; + mShadeLog.logHasVibrated(mHasVibratedOnOpen, mExpandedFraction); } addMovement(event); if (!isFullyCollapsed()) { diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt index 0b59af3435ca..5fedbeb556c2 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt @@ -140,6 +140,15 @@ class ShadeLogger @Inject constructor(@ShadeLog private val buffer: LogBuffer) { }) } + fun logHasVibrated(hasVibratedOnOpen: Boolean, fraction: Float) { + log(LogLevel.VERBOSE, { + bool1 = hasVibratedOnOpen + double1 = fraction.toDouble() + }, { + "hasVibratedOnOpen=$bool1, expansionFraction=$double1" + }) + } + fun logQsExpansionChanged( message: String, qsExpanded: Boolean, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemory.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemory.kt index 0380fff1e2af..1fcf17fe7553 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemory.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemory.kt @@ -17,10 +17,14 @@ package com.android.systemui.statusbar.notification.logging +import android.app.Notification + /** Describes usage of a notification. */ data class NotificationMemoryUsage( val packageName: String, + val uid: Int, val notificationKey: String, + val notification: Notification, val objectUsage: NotificationObjectUsage, val viewUsage: List<NotificationViewUsage> ) @@ -34,7 +38,8 @@ data class NotificationObjectUsage( val smallIcon: Int, val largeIcon: Int, val extras: Int, - val style: String?, + /** Style type, integer from [android.stats.sysui.NotificationEnums] */ + val style: Int, val styleIcon: Int, val bigPicture: Int, val extender: Int, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryDumper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryDumper.kt new file mode 100644 index 000000000000..ffd931c1bde2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryDumper.kt @@ -0,0 +1,173 @@ +/* + * + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.logging + +import android.stats.sysui.NotificationEnums +import com.android.systemui.Dumpable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dump.DumpManager +import com.android.systemui.statusbar.notification.collection.NotifPipeline +import java.io.PrintWriter +import javax.inject.Inject + +/** Dumps current notification memory use to bug reports for easier debugging. */ +@SysUISingleton +class NotificationMemoryDumper +@Inject +constructor(val dumpManager: DumpManager, val notificationPipeline: NotifPipeline) : Dumpable { + + fun init() { + dumpManager.registerNormalDumpable(javaClass.simpleName, this) + } + + override fun dump(pw: PrintWriter, args: Array<out String>) { + val memoryUse = + NotificationMemoryMeter.notificationMemoryUse(notificationPipeline.allNotifs) + .sortedWith(compareBy({ it.packageName }, { it.notificationKey })) + dumpNotificationObjects(pw, memoryUse) + dumpNotificationViewUsage(pw, memoryUse) + } + + /** Renders a table of notification object usage into passed [PrintWriter]. */ + private fun dumpNotificationObjects(pw: PrintWriter, memoryUse: List<NotificationMemoryUsage>) { + pw.println("Notification Object Usage") + pw.println("-----------") + pw.println( + "Package".padEnd(35) + + "\t\tSmall\tLarge\t${"Style".padEnd(15)}\t\tStyle\tBig\tExtend.\tExtras\tCustom" + ) + pw.println("".padEnd(35) + "\t\tIcon\tIcon\t${"".padEnd(15)}\t\tIcon\tPicture\t \t \tView") + pw.println() + + memoryUse.forEach { use -> + pw.println( + use.packageName.padEnd(35) + + "\t\t" + + "${use.objectUsage.smallIcon}\t${use.objectUsage.largeIcon}\t" + + (styleEnumToString(use.objectUsage.style).take(15) ?: "").padEnd(15) + + "\t\t${use.objectUsage.styleIcon}\t" + + "${use.objectUsage.bigPicture}\t${use.objectUsage.extender}\t" + + "${use.objectUsage.extras}\t${use.objectUsage.hasCustomView}\t" + + use.notificationKey + ) + } + + // Calculate totals for easily glanceable summary. + data class Totals( + var smallIcon: Int = 0, + var largeIcon: Int = 0, + var styleIcon: Int = 0, + var bigPicture: Int = 0, + var extender: Int = 0, + var extras: Int = 0, + ) + + val totals = + memoryUse.fold(Totals()) { t, usage -> + t.smallIcon += usage.objectUsage.smallIcon + t.largeIcon += usage.objectUsage.largeIcon + t.styleIcon += usage.objectUsage.styleIcon + t.bigPicture += usage.objectUsage.bigPicture + t.extender += usage.objectUsage.extender + t.extras += usage.objectUsage.extras + t + } + + pw.println() + pw.println("TOTALS") + pw.println( + "".padEnd(35) + + "\t\t" + + "${toKb(totals.smallIcon)}\t${toKb(totals.largeIcon)}\t" + + "".padEnd(15) + + "\t\t${toKb(totals.styleIcon)}\t" + + "${toKb(totals.bigPicture)}\t${toKb(totals.extender)}\t" + + toKb(totals.extras) + ) + pw.println() + } + + /** Renders a table of notification view usage into passed [PrintWriter] */ + private fun dumpNotificationViewUsage( + pw: PrintWriter, + memoryUse: List<NotificationMemoryUsage>, + ) { + + data class Totals( + var smallIcon: Int = 0, + var largeIcon: Int = 0, + var style: Int = 0, + var customViews: Int = 0, + var softwareBitmapsPenalty: Int = 0, + ) + + val totals = Totals() + pw.println("Notification View Usage") + pw.println("-----------") + pw.println("View Type".padEnd(24) + "\tSmall\tLarge\tStyle\tCustom\tSoftware") + pw.println("".padEnd(24) + "\tIcon\tIcon\tUse\tView\tBitmaps") + pw.println() + memoryUse + .filter { it.viewUsage.isNotEmpty() } + .forEach { use -> + pw.println(use.packageName + " " + use.notificationKey) + use.viewUsage.forEach { view -> + pw.println( + " ${view.viewType.toString().padEnd(24)}\t${view.smallIcon}" + + "\t${view.largeIcon}\t${view.style}" + + "\t${view.customViews}\t${view.softwareBitmapsPenalty}" + ) + + if (view.viewType == ViewType.TOTAL) { + totals.smallIcon += view.smallIcon + totals.largeIcon += view.largeIcon + totals.style += view.style + totals.customViews += view.customViews + totals.softwareBitmapsPenalty += view.softwareBitmapsPenalty + } + } + } + pw.println() + pw.println("TOTALS") + pw.println( + " ${"".padEnd(24)}\t${toKb(totals.smallIcon)}" + + "\t${toKb(totals.largeIcon)}\t${toKb(totals.style)}" + + "\t${toKb(totals.customViews)}\t${toKb(totals.softwareBitmapsPenalty)}" + ) + pw.println() + } + + private fun styleEnumToString(styleEnum: Int): String = + when (styleEnum) { + NotificationEnums.STYLE_UNSPECIFIED -> "Unspecified" + NotificationEnums.STYLE_NONE -> "None" + NotificationEnums.STYLE_BIG_PICTURE -> "BigPicture" + NotificationEnums.STYLE_BIG_TEXT -> "BigText" + NotificationEnums.STYLE_CALL -> "Call" + NotificationEnums.STYLE_DECORATED_CUSTOM_VIEW -> "DCustomView" + NotificationEnums.STYLE_INBOX -> "Inbox" + NotificationEnums.STYLE_MEDIA -> "Media" + NotificationEnums.STYLE_MESSAGING -> "Messaging" + NotificationEnums.STYLE_RANKER_GROUP -> "RankerGroup" + else -> "Unknown" + } + + private fun toKb(bytes: Int): String { + return (bytes / 1024).toString() + " KB" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLogger.kt new file mode 100644 index 000000000000..ec8501a79fa5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLogger.kt @@ -0,0 +1,194 @@ +/* + * + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.logging + +import android.app.StatsManager +import android.util.StatsEvent +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.shared.system.SysUiStatsLog +import com.android.systemui.statusbar.notification.collection.NotifPipeline +import com.android.systemui.util.traceSection +import java.util.concurrent.Executor +import javax.inject.Inject +import kotlin.math.roundToInt +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.runBlocking + +/** Periodically logs current state of notification memory consumption. */ +@SysUISingleton +class NotificationMemoryLogger +@Inject +constructor( + private val notificationPipeline: NotifPipeline, + private val statsManager: StatsManager, + @Main private val mainDispatcher: CoroutineDispatcher, + @Background private val backgroundExecutor: Executor +) : StatsManager.StatsPullAtomCallback { + + /** + * This class is used to accumulate and aggregate data - the fields mirror values in statd Atom + * with ONE IMPORTANT difference - the values are in bytes, not KB! + */ + internal data class NotificationMemoryUseAtomBuilder(val uid: Int, val style: Int) { + var count: Int = 0 + var countWithInflatedViews: Int = 0 + var smallIconObject: Int = 0 + var smallIconBitmapCount: Int = 0 + var largeIconObject: Int = 0 + var largeIconBitmapCount: Int = 0 + var bigPictureObject: Int = 0 + var bigPictureBitmapCount: Int = 0 + var extras: Int = 0 + var extenders: Int = 0 + var smallIconViews: Int = 0 + var largeIconViews: Int = 0 + var systemIconViews: Int = 0 + var styleViews: Int = 0 + var customViews: Int = 0 + var softwareBitmaps: Int = 0 + var seenCount = 0 + } + + fun init() { + statsManager.setPullAtomCallback( + SysUiStatsLog.NOTIFICATION_MEMORY_USE, + null, + backgroundExecutor, + this + ) + } + + /** Called by statsd to pull data. */ + override fun onPullAtom(atomTag: Int, data: MutableList<StatsEvent>): Int = + traceSection("NML#onPullAtom") { + if (atomTag != SysUiStatsLog.NOTIFICATION_MEMORY_USE) { + return StatsManager.PULL_SKIP + } + + // Notifications can only be retrieved on the main thread, so switch to that thread. + val notifications = getAllNotificationsOnMainThread() + val notificationMemoryUse = + NotificationMemoryMeter.notificationMemoryUse(notifications) + .sortedWith( + compareBy( + { it.packageName }, + { it.objectUsage.style }, + { it.notificationKey } + ) + ) + val usageData = aggregateMemoryUsageData(notificationMemoryUse) + usageData.forEach { (_, use) -> + data.add( + SysUiStatsLog.buildStatsEvent( + SysUiStatsLog.NOTIFICATION_MEMORY_USE, + use.uid, + use.style, + use.count, + use.countWithInflatedViews, + toKb(use.smallIconObject), + use.smallIconBitmapCount, + toKb(use.largeIconObject), + use.largeIconBitmapCount, + toKb(use.bigPictureObject), + use.bigPictureBitmapCount, + toKb(use.extras), + toKb(use.extenders), + toKb(use.smallIconViews), + toKb(use.largeIconViews), + toKb(use.systemIconViews), + toKb(use.styleViews), + toKb(use.customViews), + toKb(use.softwareBitmaps), + use.seenCount + ) + ) + } + + return StatsManager.PULL_SUCCESS + } + + private fun getAllNotificationsOnMainThread() = + runBlocking(mainDispatcher) { + traceSection("NML#getNotifications") { notificationPipeline.allNotifs } + } + + /** Aggregates memory usage data by package and style, returning sums. */ + private fun aggregateMemoryUsageData( + notificationMemoryUse: List<NotificationMemoryUsage> + ): Map<Pair<String, Int>, NotificationMemoryUseAtomBuilder> { + return notificationMemoryUse + .groupingBy { Pair(it.packageName, it.objectUsage.style) } + .aggregate { + _, + accumulator: NotificationMemoryUseAtomBuilder?, + element: NotificationMemoryUsage, + first -> + val use = + if (first) { + NotificationMemoryUseAtomBuilder(element.uid, element.objectUsage.style) + } else { + accumulator!! + } + + use.count++ + // If the views of the notification weren't inflated, the list of memory usage + // parameters will be empty. + if (element.viewUsage.isNotEmpty()) { + use.countWithInflatedViews++ + } + + use.smallIconObject += element.objectUsage.smallIcon + if (element.objectUsage.smallIcon > 0) { + use.smallIconBitmapCount++ + } + + use.largeIconObject += element.objectUsage.largeIcon + if (element.objectUsage.largeIcon > 0) { + use.largeIconBitmapCount++ + } + + use.bigPictureObject += element.objectUsage.bigPicture + if (element.objectUsage.bigPicture > 0) { + use.bigPictureBitmapCount++ + } + + use.extras += element.objectUsage.extras + use.extenders += element.objectUsage.extender + + // Use totals count which are more accurate when aggregated + // in this manner. + element.viewUsage + .firstOrNull { vu -> vu.viewType == ViewType.TOTAL } + ?.let { + use.smallIconViews += it.smallIcon + use.largeIconViews += it.largeIcon + use.systemIconViews += it.systemIcons + use.styleViews += it.style + use.customViews += it.style + use.softwareBitmaps += it.softwareBitmapsPenalty + } + + return@aggregate use + } + } + + /** Rounds the passed value to the nearest KB - e.g. 700B rounds to 1KB. */ + private fun toKb(value: Int): Int = (value.toFloat() / 1024f).roundToInt() +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMeter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMeter.kt index 7d39e18ab349..41fb91e3093e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMeter.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMeter.kt @@ -1,12 +1,20 @@ package com.android.systemui.statusbar.notification.logging import android.app.Notification +import android.app.Notification.BigPictureStyle +import android.app.Notification.BigTextStyle +import android.app.Notification.CallStyle +import android.app.Notification.DecoratedCustomViewStyle +import android.app.Notification.InboxStyle +import android.app.Notification.MediaStyle +import android.app.Notification.MessagingStyle import android.app.Person import android.graphics.Bitmap import android.graphics.drawable.Icon import android.os.Bundle import android.os.Parcel import android.os.Parcelable +import android.stats.sysui.NotificationEnums import androidx.annotation.WorkerThread import com.android.systemui.statusbar.notification.NotificationUtils import com.android.systemui.statusbar.notification.collection.NotificationEntry @@ -19,6 +27,7 @@ internal object NotificationMemoryMeter { private const val TV_EXTENSIONS = "android.tv.EXTENSIONS" private const val WEARABLE_EXTENSIONS = "android.wearable.EXTENSIONS" private const val WEARABLE_EXTENSIONS_BACKGROUND = "background" + private const val AUTOGROUP_KEY = "ranker_group" /** Returns a list of memory use entries for currently shown notifications. */ @WorkerThread @@ -29,12 +38,15 @@ internal object NotificationMemoryMeter { .asSequence() .map { entry -> val packageName = entry.sbn.packageName + val uid = entry.sbn.uid val notificationObjectUsage = notificationMemoryUse(entry.sbn.notification, hashSetOf()) val notificationViewUsage = NotificationMemoryViewWalker.getViewUsage(entry.row) NotificationMemoryUsage( packageName, + uid, NotificationUtils.logKey(entry.sbn.key), + entry.sbn.notification, notificationObjectUsage, notificationViewUsage ) @@ -49,7 +61,9 @@ internal object NotificationMemoryMeter { ): NotificationMemoryUsage { return NotificationMemoryUsage( entry.sbn.packageName, + entry.sbn.uid, NotificationUtils.logKey(entry.sbn.key), + entry.sbn.notification, notificationMemoryUse(entry.sbn.notification, seenBitmaps), NotificationMemoryViewWalker.getViewUsage(entry.row) ) @@ -116,7 +130,13 @@ internal object NotificationMemoryMeter { val wearExtenderBackground = computeParcelableUse(wearExtender, WEARABLE_EXTENSIONS_BACKGROUND, seenBitmaps) - val style = notification.notificationStyle + val style = + if (notification.group == AUTOGROUP_KEY) { + NotificationEnums.STYLE_RANKER_GROUP + } else { + styleEnum(notification.notificationStyle) + } + val hasCustomView = notification.contentView != null || notification.bigContentView != null val extrasSize = computeBundleSize(extras) @@ -124,7 +144,7 @@ internal object NotificationMemoryMeter { smallIcon = smallIconUse, largeIcon = largeIconUse, extras = extrasSize, - style = style?.simpleName, + style = style, styleIcon = bigPictureIconUse + peopleUse + @@ -144,6 +164,25 @@ internal object NotificationMemoryMeter { } /** + * Returns logging style enum based on current style class. + * + * @return style value in [NotificationEnums] + */ + private fun styleEnum(style: Class<out Notification.Style>?): Int = + when (style?.name) { + null -> NotificationEnums.STYLE_NONE + BigTextStyle::class.java.name -> NotificationEnums.STYLE_BIG_TEXT + BigPictureStyle::class.java.name -> NotificationEnums.STYLE_BIG_PICTURE + InboxStyle::class.java.name -> NotificationEnums.STYLE_INBOX + MediaStyle::class.java.name -> NotificationEnums.STYLE_MEDIA + DecoratedCustomViewStyle::class.java.name -> + NotificationEnums.STYLE_DECORATED_CUSTOM_VIEW + MessagingStyle::class.java.name -> NotificationEnums.STYLE_MESSAGING + CallStyle::class.java.name -> NotificationEnums.STYLE_CALL + else -> NotificationEnums.STYLE_UNSPECIFIED + } + + /** * Calculates size of the bundle data (excluding FDs and other shared objects like ashmem * bitmaps). Can be slow. */ @@ -176,7 +215,7 @@ internal object NotificationMemoryMeter { * * @return memory usage in bytes or 0 if the icon is Uri/Resource based */ - private fun computeIconUse(icon: Icon?, seenBitmaps: HashSet<Int>) = + private fun computeIconUse(icon: Icon?, seenBitmaps: HashSet<Int>): Int = when (icon?.type) { Icon.TYPE_BITMAP -> computeBitmapUse(icon.bitmap, seenBitmaps) Icon.TYPE_ADAPTIVE_BITMAP -> computeBitmapUse(icon.bitmap, seenBitmaps) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt index c09cc4306ced..f38c1e557b25 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt @@ -18,11 +18,10 @@ package com.android.systemui.statusbar.notification.logging import android.util.Log -import com.android.systemui.Dumpable import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dump.DumpManager -import com.android.systemui.statusbar.notification.collection.NotifPipeline -import java.io.PrintWriter +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import dagger.Lazy import javax.inject.Inject /** This class monitors and logs current Notification memory use. */ @@ -30,9 +29,10 @@ import javax.inject.Inject class NotificationMemoryMonitor @Inject constructor( - val notificationPipeline: NotifPipeline, - val dumpManager: DumpManager, -) : Dumpable { + private val featureFlags: FeatureFlags, + private val notificationMemoryDumper: NotificationMemoryDumper, + private val notificationMemoryLogger: Lazy<NotificationMemoryLogger>, +) { companion object { private const val TAG = "NotificationMemory" @@ -40,127 +40,10 @@ constructor( fun init() { Log.d(TAG, "NotificationMemoryMonitor initialized.") - dumpManager.registerDumpable(javaClass.simpleName, this) - } - - override fun dump(pw: PrintWriter, args: Array<out String>) { - val memoryUse = - NotificationMemoryMeter.notificationMemoryUse(notificationPipeline.allNotifs) - .sortedWith(compareBy({ it.packageName }, { it.notificationKey })) - dumpNotificationObjects(pw, memoryUse) - dumpNotificationViewUsage(pw, memoryUse) - } - - /** Renders a table of notification object usage into passed [PrintWriter]. */ - private fun dumpNotificationObjects(pw: PrintWriter, memoryUse: List<NotificationMemoryUsage>) { - pw.println("Notification Object Usage") - pw.println("-----------") - pw.println( - "Package".padEnd(35) + - "\t\tSmall\tLarge\t${"Style".padEnd(15)}\t\tStyle\tBig\tExtend.\tExtras\tCustom" - ) - pw.println("".padEnd(35) + "\t\tIcon\tIcon\t${"".padEnd(15)}\t\tIcon\tPicture\t \t \tView") - pw.println() - - memoryUse.forEach { use -> - pw.println( - use.packageName.padEnd(35) + - "\t\t" + - "${use.objectUsage.smallIcon}\t${use.objectUsage.largeIcon}\t" + - (use.objectUsage.style?.take(15) ?: "").padEnd(15) + - "\t\t${use.objectUsage.styleIcon}\t" + - "${use.objectUsage.bigPicture}\t${use.objectUsage.extender}\t" + - "${use.objectUsage.extras}\t${use.objectUsage.hasCustomView}\t" + - use.notificationKey - ) + notificationMemoryDumper.init() + if (featureFlags.isEnabled(Flags.NOTIFICATION_MEMORY_LOGGING_ENABLED)) { + Log.d(TAG, "Notification memory logging enabled.") + notificationMemoryLogger.get().init() } - - // Calculate totals for easily glanceable summary. - data class Totals( - var smallIcon: Int = 0, - var largeIcon: Int = 0, - var styleIcon: Int = 0, - var bigPicture: Int = 0, - var extender: Int = 0, - var extras: Int = 0, - ) - - val totals = - memoryUse.fold(Totals()) { t, usage -> - t.smallIcon += usage.objectUsage.smallIcon - t.largeIcon += usage.objectUsage.largeIcon - t.styleIcon += usage.objectUsage.styleIcon - t.bigPicture += usage.objectUsage.bigPicture - t.extender += usage.objectUsage.extender - t.extras += usage.objectUsage.extras - t - } - - pw.println() - pw.println("TOTALS") - pw.println( - "".padEnd(35) + - "\t\t" + - "${toKb(totals.smallIcon)}\t${toKb(totals.largeIcon)}\t" + - "".padEnd(15) + - "\t\t${toKb(totals.styleIcon)}\t" + - "${toKb(totals.bigPicture)}\t${toKb(totals.extender)}\t" + - toKb(totals.extras) - ) - pw.println() - } - - /** Renders a table of notification view usage into passed [PrintWriter] */ - private fun dumpNotificationViewUsage( - pw: PrintWriter, - memoryUse: List<NotificationMemoryUsage>, - ) { - - data class Totals( - var smallIcon: Int = 0, - var largeIcon: Int = 0, - var style: Int = 0, - var customViews: Int = 0, - var softwareBitmapsPenalty: Int = 0, - ) - - val totals = Totals() - pw.println("Notification View Usage") - pw.println("-----------") - pw.println("View Type".padEnd(24) + "\tSmall\tLarge\tStyle\tCustom\tSoftware") - pw.println("".padEnd(24) + "\tIcon\tIcon\tUse\tView\tBitmaps") - pw.println() - memoryUse - .filter { it.viewUsage.isNotEmpty() } - .forEach { use -> - pw.println(use.packageName + " " + use.notificationKey) - use.viewUsage.forEach { view -> - pw.println( - " ${view.viewType.toString().padEnd(24)}\t${view.smallIcon}" + - "\t${view.largeIcon}\t${view.style}" + - "\t${view.customViews}\t${view.softwareBitmapsPenalty}" - ) - - if (view.viewType == ViewType.TOTAL) { - totals.smallIcon += view.smallIcon - totals.largeIcon += view.largeIcon - totals.style += view.style - totals.customViews += view.customViews - totals.softwareBitmapsPenalty += view.softwareBitmapsPenalty - } - } - } - pw.println() - pw.println("TOTALS") - pw.println( - " ${"".padEnd(24)}\t${toKb(totals.smallIcon)}" + - "\t${toKb(totals.largeIcon)}\t${toKb(totals.style)}" + - "\t${toKb(totals.customViews)}\t${toKb(totals.softwareBitmapsPenalty)}" - ) - pw.println() - } - - private fun toKb(bytes: Int): String { - return (bytes / 1024).toString() + " KB" } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt index a0bee1502f51..2d042118b3d7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt @@ -50,7 +50,11 @@ internal object NotificationMemoryViewWalker { /** * Returns memory usage of public and private views contained in passed - * [ExpandableNotificationRow] + * [ExpandableNotificationRow]. Each entry will correspond to one of the [ViewType] values with + * [ViewType.TOTAL] totalling all memory use. If a type of view is missing, the corresponding + * entry will not appear in resulting list. + * + * This will return an empty list if the ExpandableNotificationRow has no views inflated. */ fun getViewUsage(row: ExpandableNotificationRow?): List<NotificationViewUsage> { if (row == null) { @@ -58,42 +62,72 @@ internal object NotificationMemoryViewWalker { } // The ordering here is significant since it determines deduplication of seen drawables. - return listOf( - getViewUsage(ViewType.PRIVATE_EXPANDED_VIEW, row.privateLayout?.expandedChild), - getViewUsage(ViewType.PRIVATE_CONTRACTED_VIEW, row.privateLayout?.contractedChild), - getViewUsage(ViewType.PRIVATE_HEADS_UP_VIEW, row.privateLayout?.headsUpChild), - getViewUsage(ViewType.PUBLIC_VIEW, row.publicLayout), - getTotalUsage(row) - ) + val perViewUsages = + listOf( + getViewUsage(ViewType.PRIVATE_EXPANDED_VIEW, row.privateLayout?.expandedChild), + getViewUsage( + ViewType.PRIVATE_CONTRACTED_VIEW, + row.privateLayout?.contractedChild + ), + getViewUsage(ViewType.PRIVATE_HEADS_UP_VIEW, row.privateLayout?.headsUpChild), + getViewUsage( + ViewType.PUBLIC_VIEW, + row.publicLayout?.expandedChild, + row.publicLayout?.contractedChild, + row.publicLayout?.headsUpChild + ), + ) + .filterNotNull() + + return if (perViewUsages.isNotEmpty()) { + // Attach summed totals field only if there was any view actually measured. + // This reduces bug report noise and makes checks for collapsed views easier. + val totals = getTotalUsage(row) + if (totals == null) { + perViewUsages + } else { + perViewUsages + totals + } + } else { + listOf() + } } /** * Calculate total usage of all views - we need to do a separate traversal to make sure we don't * double count fields. */ - private fun getTotalUsage(row: ExpandableNotificationRow): NotificationViewUsage { - val totalUsage = UsageBuilder() + private fun getTotalUsage(row: ExpandableNotificationRow): NotificationViewUsage? { val seenObjects = hashSetOf<Int>() - - row.publicLayout?.let { computeViewHierarchyUse(it, totalUsage, seenObjects) } - row.privateLayout?.let { child -> - for (view in listOf(child.expandedChild, child.contractedChild, child.headsUpChild)) { - (view as? ViewGroup)?.let { v -> - computeViewHierarchyUse(v, totalUsage, seenObjects) - } - } - } - return totalUsage.build(ViewType.TOTAL) + return getViewUsage( + ViewType.TOTAL, + row.privateLayout?.expandedChild, + row.privateLayout?.contractedChild, + row.privateLayout?.headsUpChild, + row.publicLayout?.expandedChild, + row.publicLayout?.contractedChild, + row.publicLayout?.headsUpChild, + seenObjects = seenObjects + ) } private fun getViewUsage( type: ViewType, - rootView: View?, + vararg rootViews: View?, seenObjects: HashSet<Int> = hashSetOf() - ): NotificationViewUsage { - val usageBuilder = UsageBuilder() - (rootView as? ViewGroup)?.let { computeViewHierarchyUse(it, usageBuilder, seenObjects) } - return usageBuilder.build(type) + ): NotificationViewUsage? { + val usageBuilder = lazy { UsageBuilder() } + rootViews.forEach { rootView -> + (rootView as? ViewGroup)?.let { rootViewGroup -> + computeViewHierarchyUse(rootViewGroup, usageBuilder.value, seenObjects) + } + } + + return if (usageBuilder.isInitialized()) { + usageBuilder.value.build(type) + } else { + null + } } private fun computeViewHierarchyUse( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java index aff7b4c6c515..b6cf9482f00d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java @@ -871,8 +871,7 @@ public class StackScrollAlgorithm { } for (int i = childCount - 1; i >= 0; i--) { - childrenOnTop = updateChildZValue(i, childrenOnTop, - algorithmState, ambientState, i == topHunIndex); + updateChildZValue(i, algorithmState, ambientState, i == topHunIndex); } } @@ -882,15 +881,11 @@ public class StackScrollAlgorithm { * * @param isTopHun Whether the child is a top HUN. A top HUN means a HUN that shows on the * vertically top of screen. Top HUNs should have drop shadows - * @param childrenOnTop It is greater than 0 when there's an existing HUN that is elevated - * @return childrenOnTop The decimal part represents the fraction of the elevated HUN's height - * that overlaps with QQS Panel. The integer part represents the count of - * previous HUNs whose Z positions are greater than 0. */ - protected float updateChildZValue(int i, float childrenOnTop, - StackScrollAlgorithmState algorithmState, - AmbientState ambientState, - boolean isTopHun) { + protected void updateChildZValue(int i, + StackScrollAlgorithmState algorithmState, + AmbientState ambientState, + boolean isTopHun) { ExpandableView child = algorithmState.visibleChildren.get(i); ExpandableViewState childViewState = child.getViewState(); float baseZ = ambientState.getBaseZHeight(); @@ -904,22 +899,16 @@ public class StackScrollAlgorithm { // Handles HUN shadow when Shade is opened, and AmbientState.mScrollY > 0 // Calculate the HUN's z-value based on its overlapping fraction with QQS Panel. // When scrolling down shade to make HUN back to in-position in Notification Panel, - // The over-lapping fraction goes to 0, and shadows hides gradually. - if (childrenOnTop != 0.0f) { - // To elevate the later HUN over previous HUN - childrenOnTop++; - } else { - float overlap = ambientState.getTopPadding() - + ambientState.getStackTranslation() - childViewState.getYTranslation(); - // To prevent over-shadow during HUN entry - childrenOnTop += Math.min( - 1.0f, - overlap / childViewState.height - ); - MathUtils.saturate(childrenOnTop); + // the overlapFraction goes to 0, and the pinned HUN's shadows hides gradually. + float overlap = ambientState.getTopPadding() + + ambientState.getStackTranslation() - childViewState.getYTranslation(); + + if (childViewState.height > 0) { // To avoid 0/0 problems + // To prevent over-shadow + float overlapFraction = MathUtils.saturate(overlap / childViewState.height); + childViewState.setZTranslation(baseZ + + overlapFraction * mPinnedZTranslationExtra); } - childViewState.setZTranslation(baseZ - + childrenOnTop * mPinnedZTranslationExtra); } else if (isTopHun) { // In case this is a new view that has never been measured before, we don't want to // elevate if we are currently expanded more than the notification @@ -947,15 +936,14 @@ public class StackScrollAlgorithm { } // Handles HUN shadow when shade is closed. - // While HUN is showing and Shade is closed: headerVisibleAmount stays 0, shadow stays. + // While shade is closed, and during HUN's entry: headerVisibleAmount stays 0, shadow stays. + // While shade is closed, and HUN is showing: headerVisibleAmount stays 0, shadow stays. // During HUN-to-Shade (eg. dragging down HUN to open Shade): headerVisibleAmount goes // gradually from 0 to 1, shadow hides gradually. // Header visibility is a deprecated concept, we are using headerVisibleAmount only because // this value nicely goes from 0 to 1 during the HUN-to-Shade process. - childViewState.setZTranslation(childViewState.getZTranslation() + (1.0f - child.getHeaderVisibleAmount()) * mPinnedZTranslationExtra); - return childrenOnTop; } public void setIsExpanded(boolean isExpanded) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java index 6b72e9696f83..936589c77d40 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java @@ -371,6 +371,7 @@ public class CentralSurfacesCommandQueueCallbacks implements CommandQueue.Callba if (!mKeyguardStateController.isShowing()) { final Intent cameraIntent = CameraIntents.getInsecureCameraIntent(mContext); + cameraIntent.putExtra(CameraIntents.EXTRA_LAUNCH_SOURCE, source); mCentralSurfaces.startActivityDismissingKeyguard(cameraIntent, false /* onlyProvisioned */, true /* dismissShade */, true /* disallowEnterPictureInPictureWhileLaunching */, null /* callback */, 0, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index b394535ca011..a409ad8815a8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.phone; +import static android.app.StatusBarManager.DISABLE_HOME; import static android.app.StatusBarManager.WINDOW_STATE_HIDDEN; import static android.app.StatusBarManager.WINDOW_STATE_SHOWING; import static android.app.StatusBarManager.WindowVisibleState; @@ -69,6 +70,7 @@ import android.graphics.Point; import android.hardware.devicestate.DeviceStateManager; import android.metrics.LogMaker; import android.net.Uri; +import android.os.Binder; import android.os.Bundle; import android.os.Handler; import android.os.Looper; @@ -1030,8 +1032,21 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { // set the initial view visibility int disabledFlags1 = result.mDisabledFlags1; int disabledFlags2 = result.mDisabledFlags2; - mInitController.addPostInitTask( - () -> setUpDisableFlags(disabledFlags1, disabledFlags2)); + mInitController.addPostInitTask(() -> { + setUpDisableFlags(disabledFlags1, disabledFlags2); + try { + // NOTE(b/262059863): Force-update the disable flags after applying the flags + // returned from registerStatusBar(). The result's disabled flags may be stale + // if StatusBarManager's disabled flags are updated between registering the bar and + // this handling this post-init task. We force an update in this case, and use a new + // token to not conflict with any other disabled flags already requested by SysUI + Binder token = new Binder(); + mBarService.disable(DISABLE_HOME, token, mContext.getPackageName()); + mBarService.disable(0, token, mContext.getPackageName()); + } catch (RemoteException ex) { + ex.rethrowFromSystemServer(); + } + }); mFalsingManager.addFalsingBeliefListener(mFalsingBeliefListener); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt index 78b28d203629..2ce116394236 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.kt @@ -23,7 +23,7 @@ import android.view.ViewGroup import android.view.ViewPropertyAnimator import android.view.WindowInsets import android.widget.FrameLayout -import com.android.keyguard.KeyguardUpdateMonitor +import androidx.annotation.StringRes import com.android.keyguard.LockIconViewController import com.android.systemui.R import com.android.systemui.keyguard.ui.binder.KeyguardBottomAreaViewBinder @@ -51,21 +51,29 @@ constructor( defStyleRes, ) { + interface MessageDisplayer { + fun display(@StringRes stringResourceId: Int) + } + private var ambientIndicationArea: View? = null private lateinit var binding: KeyguardBottomAreaViewBinder.Binding - private lateinit var lockIconViewController: LockIconViewController + private var lockIconViewController: LockIconViewController? = null /** Initializes the view. */ fun init( viewModel: KeyguardBottomAreaViewModel, - falsingManager: FalsingManager, - lockIconViewController: LockIconViewController, + falsingManager: FalsingManager? = null, + lockIconViewController: LockIconViewController? = null, + messageDisplayer: MessageDisplayer? = null, ) { - binding = bind( + binding = + bind( this, viewModel, falsingManager, - ) + ) { + messageDisplayer?.display(it) + } this.lockIconViewController = lockIconViewController } @@ -129,21 +137,21 @@ constructor( findViewById<View>(R.id.ambient_indication_container)?.let { val (ambientLeft, ambientTop) = it.locationOnScreen if (binding.shouldConstrainToTopOfLockIcon()) { - //make top of ambient indication view the bottom of the lock icon + // make top of ambient indication view the bottom of the lock icon it.layout( - ambientLeft, - lockIconViewController.bottom.toInt(), - right - ambientLeft, - ambientTop + it.measuredHeight + ambientLeft, + lockIconViewController?.bottom?.toInt() ?: 0, + right - ambientLeft, + ambientTop + it.measuredHeight ) } else { - //make bottom of ambient indication view the top of the lock icon - val lockLocationTop = lockIconViewController.top + // make bottom of ambient indication view the top of the lock icon + val lockLocationTop = lockIconViewController?.top ?: 0 it.layout( - ambientLeft, - lockLocationTop.toInt() - it.measuredHeight, - right - ambientLeft, - lockLocationTop.toInt() + ambientLeft, + lockLocationTop.toInt() - it.measuredHeight, + right - ambientLeft, + lockLocationTop.toInt() ) } } diff --git a/packages/SystemUI/src/com/android/systemui/stylus/OWNERS b/packages/SystemUI/src/com/android/systemui/stylus/OWNERS new file mode 100644 index 000000000000..7ccb316dbca5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/stylus/OWNERS @@ -0,0 +1,8 @@ +# Bug component: 1254381 +azappone@google.com +achalke@google.com +juliacr@google.com +madym@google.com +mgalhardo@google.com +petrcermak@google.com +vanjan@google.com
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt index ea4020861a09..db7315f311ac 100644 --- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt +++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayController.kt @@ -34,6 +34,7 @@ import com.android.systemui.CoreStartable import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.util.concurrency.DelayableExecutor +import com.android.systemui.util.time.SystemClock import com.android.systemui.util.wakelock.WakeLock /** @@ -44,8 +45,24 @@ import com.android.systemui.util.wakelock.WakeLock * * The generic type T is expected to contain all the information necessary for the subclasses to * display the view in a certain state, since they receive <T> in [updateView]. + * + * Some information about display ordering: + * + * [ViewPriority] defines different priorities for the incoming views. The incoming view will be + * displayed so long as its priority is equal to or greater than the currently displayed view. + * (Concretely, this means that a [ViewPriority.NORMAL] won't be displayed if a + * [ViewPriority.CRITICAL] is currently displayed. But otherwise, the incoming view will get + * displayed and kick out the old view). + * + * Once the currently displayed view times out, we *may* display a previously requested view if it + * still has enough time left before its own timeout. The same priority ordering applies. + * + * Note: [TemporaryViewInfo.id] is the identifier that we use to determine if a call to + * [displayView] will just update the current view with new information, or display a completely new + * view. This means that you *cannot* change the [TemporaryViewInfo.priority] or + * [TemporaryViewInfo.windowTitle] while using the same ID. */ -abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : TemporaryViewLogger>( +abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : TemporaryViewLogger<T>>( internal val context: Context, internal val logger: U, internal val windowManager: WindowManager, @@ -55,6 +72,7 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora private val powerManager: PowerManager, @LayoutRes private val viewLayoutRes: Int, private val wakeLockBuilder: WakeLock.Builder, + private val systemClock: SystemClock, ) : CoreStartable { /** * Window layout params that will be used as a starting point for the [windowLayoutParams] of @@ -78,27 +96,18 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora */ internal abstract val windowLayoutParams: WindowManager.LayoutParams - /** A container for all the display-related objects. Null if the view is not being displayed. */ - private var displayInfo: DisplayInfo? = null - - /** A [Runnable] that, when run, will cancel the pending timeout of the view. */ - private var cancelViewTimeout: Runnable? = null - /** - * A wakelock that is acquired when view is displayed and screen off, - * then released when view is removed. + * A list of the currently active views, ordered from highest priority in the beginning to + * lowest priority at the end. + * + * Whenever the current view disappears, the next-priority view will be displayed if it's still + * valid. */ - private var wakeLock: WakeLock? = null - - /** A string that keeps track of wakelock reason once it is acquired till it gets released */ - private var wakeReasonAcquired: String? = null + internal val activeViews: MutableList<DisplayInfo> = mutableListOf() - /** - * A stack of pairs of device id and temporary view info. This is used when there may be - * multiple devices in range, and we want to always display the chip for the most recently - * active device. - */ - internal val activeViews: ArrayDeque<Pair<String, T>> = ArrayDeque() + private fun getCurrentDisplayInfo(): DisplayInfo? { + return activeViews.getOrNull(0) + } /** * Displays the view with the provided [newInfo]. @@ -107,94 +116,139 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora * display the correct information in the view. * @param onViewTimeout a runnable that runs after the view timeout. */ + @Synchronized fun displayView(newInfo: T, onViewTimeout: Runnable? = null) { - val currentDisplayInfo = displayInfo - - // Update our list of active devices by removing it if necessary, then adding back at the - // front of the list - val id = newInfo.id - val position = findAndRemoveFromActiveViewsList(id) - activeViews.addFirst(Pair(id, newInfo)) - - if (currentDisplayInfo != null && - currentDisplayInfo.info.windowTitle == newInfo.windowTitle) { - // We're already displaying information in the correctly-titled window, so we just need - // to update the view. - currentDisplayInfo.info = newInfo - updateView(currentDisplayInfo.info, currentDisplayInfo.view) - } else { - if (currentDisplayInfo != null) { - // We're already displaying information but that information is under a different - // window title. So, we need to remove the old window with the old title and add a - // new window with the new title. - removeView( - id, - removalReason = "New info has new window title: ${newInfo.windowTitle}" - ) - } - - // At this point, we're guaranteed to no longer be displaying a view. - // So, set up all our callbacks and inflate the view. - configurationController.addCallback(displayScaleListener) - - wakeLock = if (!powerManager.isScreenOn) { - // If the screen is off, fully wake it so the user can see the view. - wakeLockBuilder - .setTag(newInfo.windowTitle) - .setLevelsAndFlags( - PowerManager.FULL_WAKE_LOCK or - PowerManager.ACQUIRE_CAUSES_WAKEUP - ) - .build() - } else { - // Per b/239426653, we want the view to show over the dream state. - // If the screen is on, using screen bright level will leave screen on the dream - // state but ensure the screen will not go off before wake lock is released. - wakeLockBuilder - .setTag(newInfo.windowTitle) - .setLevelsAndFlags(PowerManager.SCREEN_BRIGHT_WAKE_LOCK) - .build() - } - wakeLock?.acquire(newInfo.wakeReason) - wakeReasonAcquired = newInfo.wakeReason - logger.logViewAddition(id, newInfo.windowTitle) - inflateAndUpdateView(newInfo) - } - - // Cancel and re-set the view timeout each time we get a new state. val timeout = accessibilityManager.getRecommendedTimeoutMillis( newInfo.timeoutMs, // Not all views have controls so FLAG_CONTENT_CONTROLS might be superfluous, but // include it just to be safe. FLAG_CONTENT_ICONS or FLAG_CONTENT_TEXT or FLAG_CONTENT_CONTROLS - ) + ) + val timeExpirationMillis = systemClock.currentTimeMillis() + timeout + + val currentDisplayInfo = getCurrentDisplayInfo() + + // We're current displaying a chipbar with the same ID, we just need to update its info + if (currentDisplayInfo != null && currentDisplayInfo.info.id == newInfo.id) { + val view = checkNotNull(currentDisplayInfo.view) { + "First item in activeViews list must have a valid view" + } + logger.logViewUpdate(newInfo) + currentDisplayInfo.info = newInfo + currentDisplayInfo.timeExpirationMillis = timeExpirationMillis + updateTimeout(currentDisplayInfo, timeout, onViewTimeout) + updateView(newInfo, view) + return + } + + val newDisplayInfo = DisplayInfo( + info = newInfo, + onViewTimeout = onViewTimeout, + timeExpirationMillis = timeExpirationMillis, + // Null values will be updated to non-null if/when this view actually gets displayed + view = null, + wakeLock = null, + cancelViewTimeout = null, + ) + + // We're not displaying anything, so just render this new info + if (currentDisplayInfo == null) { + addCallbacks() + activeViews.add(newDisplayInfo) + showNewView(newDisplayInfo, timeout) + return + } + + // The currently displayed info takes higher priority than the new one. + // So, just store the new one in case the current one disappears. + if (currentDisplayInfo.info.priority > newInfo.priority) { + logger.logViewAdditionDelayed(newInfo) + // Remove any old information for this id (if it exists) and re-add it to the list in + // the right priority spot + removeFromActivesIfNeeded(newInfo.id) + var insertIndex = 0 + while (insertIndex < activeViews.size && + activeViews[insertIndex].info.priority > newInfo.priority) { + insertIndex++ + } + activeViews.add(insertIndex, newDisplayInfo) + return + } + + // Else: The newInfo should be displayed and the currentInfo should be hidden + hideView(currentDisplayInfo) + // Remove any old information for this id (if it exists) and put this info at the beginning + removeFromActivesIfNeeded(newDisplayInfo.info.id) + activeViews.add(0, newDisplayInfo) + showNewView(newDisplayInfo, timeout) + } + + private fun showNewView(newDisplayInfo: DisplayInfo, timeout: Int) { + logger.logViewAddition(newDisplayInfo.info) + createAndAcquireWakeLock(newDisplayInfo) + updateTimeout(newDisplayInfo, timeout, newDisplayInfo.onViewTimeout) + inflateAndUpdateView(newDisplayInfo) + } - // Only cancel timeout of the most recent view displayed, as it will be reset. - if (position == 0) { - cancelViewTimeout?.run() + private fun createAndAcquireWakeLock(displayInfo: DisplayInfo) { + // TODO(b/262009503): Migrate off of isScrenOn, since it's deprecated. + val newWakeLock = if (!powerManager.isScreenOn) { + // If the screen is off, fully wake it so the user can see the view. + wakeLockBuilder + .setTag(displayInfo.info.windowTitle) + .setLevelsAndFlags( + PowerManager.FULL_WAKE_LOCK or + PowerManager.ACQUIRE_CAUSES_WAKEUP + ) + .build() + } else { + // Per b/239426653, we want the view to show over the dream state. + // If the screen is on, using screen bright level will leave screen on the dream + // state but ensure the screen will not go off before wake lock is released. + wakeLockBuilder + .setTag(displayInfo.info.windowTitle) + .setLevelsAndFlags(PowerManager.SCREEN_BRIGHT_WAKE_LOCK) + .build() } - cancelViewTimeout = mainExecutor.executeDelayed( + displayInfo.wakeLock = newWakeLock + newWakeLock.acquire(displayInfo.info.wakeReason) + } + + /** + * Creates a runnable that will remove [displayInfo] in [timeout] ms from now. + * + * @param onViewTimeout an optional runnable that will be run if the view times out. + * @return a runnable that, when run, will *cancel* the view's timeout. + */ + private fun updateTimeout(displayInfo: DisplayInfo, timeout: Int, onViewTimeout: Runnable?) { + val cancelViewTimeout = mainExecutor.executeDelayed( { - removeView(id, REMOVAL_REASON_TIMEOUT) + removeView(displayInfo.info.id, REMOVAL_REASON_TIMEOUT) onViewTimeout?.run() }, timeout.toLong() ) + + displayInfo.onViewTimeout = onViewTimeout + // Cancel old view timeout and re-set it. + displayInfo.cancelViewTimeout?.run() + displayInfo.cancelViewTimeout = cancelViewTimeout } - /** Inflates a new view, updates it with [newInfo], and adds the view to the window. */ - private fun inflateAndUpdateView(newInfo: T) { + /** Inflates a new view, updates it with [DisplayInfo.info], and adds the view to the window. */ + private fun inflateAndUpdateView(displayInfo: DisplayInfo) { + val newInfo = displayInfo.info val newView = LayoutInflater .from(context) .inflate(viewLayoutRes, null) as ViewGroup - val newViewController = TouchableRegionViewController(newView, this::getTouchableRegion) - newViewController.init() + displayInfo.view = newView // We don't need to hold on to the view controller since we never set anything additional // on it -- it will be automatically cleaned up when the view is detached. - val newDisplayInfo = DisplayInfo(newView, newInfo) - displayInfo = newDisplayInfo - updateView(newDisplayInfo.info, newDisplayInfo.view) + val newViewController = TouchableRegionViewController(newView, this::getTouchableRegion) + newViewController.init() + + updateView(newInfo, newView) val paramsWithTitle = WindowManager.LayoutParams().also { it.copyFrom(windowLayoutParams) @@ -206,11 +260,15 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora } /** Removes then re-inflates the view. */ + @Synchronized private fun reinflateView() { - val currentViewInfo = displayInfo ?: return + val currentDisplayInfo = getCurrentDisplayInfo() ?: return - windowManager.removeView(currentViewInfo.view) - inflateAndUpdateView(currentViewInfo.info) + val view = checkNotNull(currentDisplayInfo.view) { + "First item in activeViews list must have a valid view" + } + windowManager.removeView(view) + inflateAndUpdateView(currentDisplayInfo) } private val displayScaleListener = object : ConfigurationController.ConfigurationListener { @@ -219,68 +277,109 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora } } + private fun addCallbacks() { + configurationController.addCallback(displayScaleListener) + } + + private fun removeCallbacks() { + configurationController.removeCallback(displayScaleListener) + } + /** - * Hides the view given its [id]. + * Completely removes the view for the given [id], both visually and from our internal store. * * @param id the id of the device responsible of displaying the temp view. * @param removalReason a short string describing why the view was removed (timeout, state * change, etc.) */ + @Synchronized fun removeView(id: String, removalReason: String) { - val currentDisplayInfo = displayInfo ?: return + logger.logViewRemoval(id, removalReason) - val removalPosition = findAndRemoveFromActiveViewsList(id) - if (removalPosition == null) { - logger.logViewRemovalIgnored(id, "view not found in the list") + val displayInfo = activeViews.firstOrNull { it.info.id == id } + if (displayInfo == null) { + logger.logViewRemovalIgnored(id, "View not found in list") return } - if (removalPosition != 0) { - logger.logViewRemovalIgnored(id, "most recent view is being displayed.") + + val currentlyDisplayedView = activeViews[0] + // Remove immediately (instead as part of the animation end runnable) so that if a new view + // event comes in while this view is animating out, we still display the new view + // appropriately. + activeViews.remove(displayInfo) + + // No need to time the view out since it's already gone + displayInfo.cancelViewTimeout?.run() + + if (displayInfo.view == null) { + logger.logViewRemovalIgnored(id, "No view to remove") return } - logger.logViewRemoval(id, removalReason) - val newViewToDisplay = if (activeViews.isEmpty()) { - null - } else { - activeViews[0].second + if (currentlyDisplayedView.info.id != id) { + logger.logViewRemovalIgnored(id, "View isn't the currently displayed view") + return } - val currentView = currentDisplayInfo.view - animateViewOut(currentView) { - windowManager.removeView(currentView) - wakeLock?.release(wakeReasonAcquired) - } + removeViewFromWindow(displayInfo) - configurationController.removeCallback(displayScaleListener) - // Re-set to null immediately (instead as part of the animation end runnable) so - // that if a new view event comes in while this view is animating out, we still display - // the new view appropriately. - displayInfo = null - // No need to time the view out since it's already gone - cancelViewTimeout?.run() + // Prune anything that's already timed out before determining if we should re-display a + // different chipbar. + removeTimedOutViews() + val newViewToDisplay = getCurrentDisplayInfo() if (newViewToDisplay != null) { - mainExecutor.executeDelayed({ displayView(newViewToDisplay)}, DISPLAY_VIEW_DELAY) + val timeout = newViewToDisplay.timeExpirationMillis - systemClock.currentTimeMillis() + // TODO(b/258019006): We may want to have a delay before showing the new view so + // that the UI translation looks a bit smoother. But, we expect this to happen + // rarely so it may not be worth the extra complexity. + showNewView(newViewToDisplay, timeout.toInt()) + } else { + removeCallbacks() } } /** - * Finds and removes the active view with the given [id] from the stack, or null if there is no - * active view with that ID - * - * @param id that temporary view belonged to. - * - * @return index of the view in the stack , otherwise null. + * Hides the view from the window, but keeps [displayInfo] around in [activeViews] in case it + * should be re-displayed later. */ - private fun findAndRemoveFromActiveViewsList(id: String): Int? { - for (i in 0 until activeViews.size) { - if (activeViews[i].first == id) { - activeViews.removeAt(i) - return i - } + private fun hideView(displayInfo: DisplayInfo) { + logger.logViewHidden(displayInfo.info) + removeViewFromWindow(displayInfo) + } + + private fun removeViewFromWindow(displayInfo: DisplayInfo) { + val view = displayInfo.view + if (view == null) { + logger.logViewRemovalIgnored(displayInfo.info.id, "View is null") + return + } + displayInfo.view = null // Need other places?? + animateViewOut(view) { + windowManager.removeView(view) + displayInfo.wakeLock?.release(displayInfo.info.wakeReason) + } + } + + @Synchronized + private fun removeTimedOutViews() { + val invalidViews = activeViews + .filter { it.timeExpirationMillis < + systemClock.currentTimeMillis() + MIN_REQUIRED_TIME_FOR_REDISPLAY } + + invalidViews.forEach { + activeViews.remove(it) + logger.logViewExpiration(it.info) + } + } + + @Synchronized + private fun removeFromActivesIfNeeded(id: String) { + val toRemove = activeViews.find { it.info.id == id } + toRemove?.let { + it.cancelViewTimeout?.run() + activeViews.remove(it) } - return null } /** @@ -311,17 +410,47 @@ abstract class TemporaryViewDisplayController<T : TemporaryViewInfo, U : Tempora } /** A container for all the display-related state objects. */ - private inner class DisplayInfo( - /** The view currently being displayed. */ - val view: ViewGroup, - - /** The info currently being displayed. */ + inner class DisplayInfo( + /** + * The view currently being displayed. + * + * Null if this info isn't currently being displayed. + */ + var view: ViewGroup?, + + /** The info that should be displayed if/when this is the highest priority view. */ var info: T, + + /** + * The system time at which this display info should expire and never be displayed again. + */ + var timeExpirationMillis: Long, + + /** + * The wake lock currently held by this view. Must be released when the view disappears. + * + * Null if this info isn't currently being displayed. + */ + var wakeLock: WakeLock?, + + /** + * See [displayView]. + */ + var onViewTimeout: Runnable?, + + /** + * A runnable that, when run, will cancel this view's timeout. + * + * Null if this info isn't currently being displayed. + */ + var cancelViewTimeout: Runnable?, ) + + // TODO(b/258019006): Add a dump method that dumps the currently active views. } private const val REMOVAL_REASON_TIMEOUT = "TIMEOUT" -const val DISPLAY_VIEW_DELAY = 50L +private const val MIN_REQUIRED_TIME_FOR_REDISPLAY = 1000 private data class IconInfo( val iconName: String, diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewInfo.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewInfo.kt index df8396051dda..5596cf68b4bc 100644 --- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewInfo.kt +++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewInfo.kt @@ -42,6 +42,20 @@ abstract class TemporaryViewInfo { * The id of the temporary view. */ abstract val id: String + + /** The priority for this view. */ + abstract val priority: ViewPriority } const val DEFAULT_TIMEOUT_MILLIS = 10000 + +/** + * The priority of the view being displayed. + * + * Must be ordered from lowest priority to highest priority. (CRITICAL is currently the highest + * priority.) + */ +enum class ViewPriority { + NORMAL, + CRITICAL, +} diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt index 133a384e7e17..ec6965a83b5a 100644 --- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/TemporaryViewLogger.kt @@ -20,20 +20,79 @@ import com.android.systemui.plugins.log.LogBuffer import com.android.systemui.plugins.log.LogLevel /** A logger for temporary view changes -- see [TemporaryViewDisplayController]. */ -open class TemporaryViewLogger( +open class TemporaryViewLogger<T : TemporaryViewInfo>( internal val buffer: LogBuffer, internal val tag: String, ) { - /** Logs that we added the view with the given [id] in a window titled [windowTitle]. */ - fun logViewAddition(id: String, windowTitle: String) { + fun logViewExpiration(info: T) { buffer.log( tag, LogLevel.DEBUG, { - str1 = windowTitle - str2 = id + str1 = info.id + str2 = info.windowTitle + str3 = info.priority.name + }, + { "View timeout has already expired; removing. id=$str1 window=$str2 priority=$str3" } + ) + } + + fun logViewUpdate(info: T) { + buffer.log( + tag, + LogLevel.DEBUG, + { + str1 = info.id + str2 = info.windowTitle + str3 = info.priority.name }, - { "View added. window=$str1 id=$str2" } + { "Existing view updated with new data. id=$str1 window=$str2 priority=$str3" } + ) + } + + fun logViewAdditionDelayed(info: T) { + buffer.log( + tag, + LogLevel.DEBUG, + { + str1 = info.id + str2 = info.windowTitle + str3 = info.priority.name + }, + { + "New view can't be displayed because higher priority view is currently " + + "displayed. New view id=$str1 window=$str2 priority=$str3" + } + ) + } + + /** Logs that we added the view with the given information. */ + fun logViewAddition(info: T) { + buffer.log( + tag, + LogLevel.DEBUG, + { + str1 = info.id + str2 = info.windowTitle + str3 = info.priority.name + }, + { "View added. id=$str1 window=$str2 priority=$str3" } + ) + } + + fun logViewHidden(info: T) { + buffer.log( + tag, + LogLevel.DEBUG, + { + str1 = info.id + str2 = info.windowTitle + str3 = info.priority.name + }, + { + "View hidden in favor of newer view. " + + "Hidden view id=$str1 window=$str2 priority=$str3" + } ) } diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt index 4d91e35856dc..14ba63a2738f 100644 --- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt @@ -43,6 +43,7 @@ import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.temporarydisplay.TemporaryViewDisplayController import com.android.systemui.util.concurrency.DelayableExecutor +import com.android.systemui.util.time.SystemClock import com.android.systemui.util.view.ViewUtil import com.android.systemui.util.wakelock.WakeLock import javax.inject.Inject @@ -77,6 +78,7 @@ open class ChipbarCoordinator @Inject constructor( private val viewUtil: ViewUtil, private val vibratorHelper: VibratorHelper, wakeLockBuilder: WakeLock.Builder, + systemClock: SystemClock, ) : TemporaryViewDisplayController<ChipbarInfo, ChipbarLogger>( context, logger, @@ -87,6 +89,7 @@ open class ChipbarCoordinator @Inject constructor( powerManager, R.layout.chipbar, wakeLockBuilder, + systemClock, ) { private lateinit var parent: ChipbarRootView diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt index a3eef8032b3b..dd4bd26e3bcd 100644 --- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt +++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt @@ -22,6 +22,7 @@ import androidx.annotation.AttrRes import com.android.systemui.common.shared.model.Text import com.android.systemui.common.shared.model.TintedIcon import com.android.systemui.temporarydisplay.TemporaryViewInfo +import com.android.systemui.temporarydisplay.ViewPriority /** * A container for all the state needed to display a chipbar via [ChipbarCoordinator]. @@ -42,6 +43,7 @@ data class ChipbarInfo( override val wakeReason: String, override val timeoutMs: Int, override val id: String, + override val priority: ViewPriority, ) : TemporaryViewInfo() { companion object { @AttrRes const val DEFAULT_ICON_TINT_ATTR = android.R.attr.textColorPrimary diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarLogger.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarLogger.kt index e477cd68673a..fcfbe0aeedf6 100644 --- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarLogger.kt @@ -29,7 +29,7 @@ class ChipbarLogger @Inject constructor( @ChipbarLog buffer: LogBuffer, -) : TemporaryViewLogger(buffer, "ChipbarLog") { +) : TemporaryViewLogger<ChipbarInfo>(buffer, "ChipbarLog") { /** * Logs that the chipbar was updated to display in a window named [windowTitle], with [text] and * [endItemDesc]. diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java index 8bbaf3dff1e5..10595439200a 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java @@ -19,6 +19,7 @@ package com.android.keyguard; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; @@ -87,6 +88,7 @@ public class KeyguardAbsKeyInputViewControllerTest extends SysuiTestCase { when(mAbsKeyInputView.isAttachedToWindow()).thenReturn(true); when(mAbsKeyInputView.requireViewById(R.id.bouncer_message_area)) .thenReturn(mKeyguardMessageArea); + when(mAbsKeyInputView.getResources()).thenReturn(getContext().getResources()); mKeyguardAbsKeyInputViewController = new KeyguardAbsKeyInputViewController(mAbsKeyInputView, mKeyguardUpdateMonitor, mSecurityMode, mLockPatternUtils, mKeyguardSecurityCallback, mKeyguardMessageAreaControllerFactory, mLatencyTracker, mFalsingCollector, @@ -99,6 +101,11 @@ public class KeyguardAbsKeyInputViewControllerTest extends SysuiTestCase { public void onResume(int reason) { super.onResume(reason); } + + @Override + protected int getInitialMessageResId() { + return 0; + } }; mKeyguardAbsKeyInputViewController.init(); reset(mKeyguardMessageAreaController); // Clear out implicit call to init. @@ -125,4 +132,22 @@ public class KeyguardAbsKeyInputViewControllerTest extends SysuiTestCase { verifyZeroInteractions(mKeyguardSecurityCallback); verifyZeroInteractions(mKeyguardMessageAreaController); } + + @Test + public void onPromptReasonNone_doesNotSetMessage() { + mKeyguardAbsKeyInputViewController.showPromptReason(0); + verify(mKeyguardMessageAreaController, never()).setMessage( + getContext().getResources().getString(R.string.kg_prompt_reason_restart_password), + false); + } + + @Test + public void onPromptReason_setsMessage() { + when(mAbsKeyInputView.getPromptReasonStringRes(1)).thenReturn( + R.string.kg_prompt_reason_restart_password); + mKeyguardAbsKeyInputViewController.showPromptReason(1); + verify(mKeyguardMessageAreaController).setMessage( + getContext().getResources().getString(R.string.kg_prompt_reason_restart_password), + false); + } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt index d20be56d6c6b..d91279399341 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt @@ -30,64 +30,54 @@ import com.android.systemui.util.concurrency.DelayableExecutor import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.ArgumentMatchers.anyString import org.mockito.Mock import org.mockito.Mockito -import org.mockito.Mockito.`when` import org.mockito.Mockito.never import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidTestingRunner::class) @TestableLooper.RunWithLooper class KeyguardPasswordViewControllerTest : SysuiTestCase() { - @Mock - private lateinit var keyguardPasswordView: KeyguardPasswordView - @Mock - private lateinit var passwordEntry: EditText - @Mock - lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor - @Mock - lateinit var securityMode: KeyguardSecurityModel.SecurityMode - @Mock - lateinit var lockPatternUtils: LockPatternUtils - @Mock - lateinit var keyguardSecurityCallback: KeyguardSecurityCallback - @Mock - lateinit var messageAreaControllerFactory: KeyguardMessageAreaController.Factory - @Mock - lateinit var latencyTracker: LatencyTracker - @Mock - lateinit var inputMethodManager: InputMethodManager - @Mock - lateinit var emergencyButtonController: EmergencyButtonController - @Mock - lateinit var mainExecutor: DelayableExecutor - @Mock - lateinit var falsingCollector: FalsingCollector - @Mock - lateinit var keyguardViewController: KeyguardViewController - @Mock - private lateinit var mKeyguardMessageArea: BouncerKeyguardMessageArea - @Mock - private lateinit var mKeyguardMessageAreaController: - KeyguardMessageAreaController<BouncerKeyguardMessageArea> + @Mock private lateinit var keyguardPasswordView: KeyguardPasswordView + @Mock private lateinit var passwordEntry: EditText + @Mock lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor + @Mock lateinit var securityMode: KeyguardSecurityModel.SecurityMode + @Mock lateinit var lockPatternUtils: LockPatternUtils + @Mock lateinit var keyguardSecurityCallback: KeyguardSecurityCallback + @Mock lateinit var messageAreaControllerFactory: KeyguardMessageAreaController.Factory + @Mock lateinit var latencyTracker: LatencyTracker + @Mock lateinit var inputMethodManager: InputMethodManager + @Mock lateinit var emergencyButtonController: EmergencyButtonController + @Mock lateinit var mainExecutor: DelayableExecutor + @Mock lateinit var falsingCollector: FalsingCollector + @Mock lateinit var keyguardViewController: KeyguardViewController + @Mock private lateinit var mKeyguardMessageArea: BouncerKeyguardMessageArea + @Mock + private lateinit var mKeyguardMessageAreaController: + KeyguardMessageAreaController<BouncerKeyguardMessageArea> - private lateinit var keyguardPasswordViewController: KeyguardPasswordViewController + private lateinit var keyguardPasswordViewController: KeyguardPasswordViewController - @Before - fun setup() { - MockitoAnnotations.initMocks(this) - Mockito.`when`( - keyguardPasswordView - .requireViewById<BouncerKeyguardMessageArea>(R.id.bouncer_message_area) - ).thenReturn(mKeyguardMessageArea) - Mockito.`when`(messageAreaControllerFactory.create(mKeyguardMessageArea)) - .thenReturn(mKeyguardMessageAreaController) - Mockito.`when`(keyguardPasswordView.passwordTextViewId).thenReturn(R.id.passwordEntry) - Mockito.`when`(keyguardPasswordView.findViewById<EditText>(R.id.passwordEntry) - ).thenReturn(passwordEntry) - keyguardPasswordViewController = KeyguardPasswordViewController( + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + Mockito.`when`( + keyguardPasswordView.requireViewById<BouncerKeyguardMessageArea>( + R.id.bouncer_message_area)) + .thenReturn(mKeyguardMessageArea) + Mockito.`when`(messageAreaControllerFactory.create(mKeyguardMessageArea)) + .thenReturn(mKeyguardMessageAreaController) + Mockito.`when`(keyguardPasswordView.passwordTextViewId).thenReturn(R.id.passwordEntry) + Mockito.`when`(keyguardPasswordView.findViewById<EditText>(R.id.passwordEntry)) + .thenReturn(passwordEntry) + `when`(keyguardPasswordView.resources).thenReturn(context.resources) + keyguardPasswordViewController = + KeyguardPasswordViewController( keyguardPasswordView, keyguardUpdateMonitor, securityMode, @@ -100,51 +90,48 @@ class KeyguardPasswordViewControllerTest : SysuiTestCase() { mainExecutor, mContext.resources, falsingCollector, - keyguardViewController - ) - } + keyguardViewController) + } - @Test - fun testFocusWhenBouncerIsShown() { - Mockito.`when`(keyguardViewController.isBouncerShowing).thenReturn(true) - Mockito.`when`(keyguardPasswordView.isShown).thenReturn(true) - keyguardPasswordViewController.onResume(KeyguardSecurityView.VIEW_REVEALED) - keyguardPasswordView.post { - verify(keyguardPasswordView).requestFocus() - verify(keyguardPasswordView).showKeyboard() - } + @Test + fun testFocusWhenBouncerIsShown() { + Mockito.`when`(keyguardViewController.isBouncerShowing).thenReturn(true) + Mockito.`when`(keyguardPasswordView.isShown).thenReturn(true) + keyguardPasswordViewController.onResume(KeyguardSecurityView.VIEW_REVEALED) + keyguardPasswordView.post { + verify(keyguardPasswordView).requestFocus() + verify(keyguardPasswordView).showKeyboard() } + } - @Test - fun testDoNotFocusWhenBouncerIsHidden() { - Mockito.`when`(keyguardViewController.isBouncerShowing).thenReturn(false) - Mockito.`when`(keyguardPasswordView.isShown).thenReturn(true) - keyguardPasswordViewController.onResume(KeyguardSecurityView.VIEW_REVEALED) - verify(keyguardPasswordView, never()).requestFocus() - } + @Test + fun testDoNotFocusWhenBouncerIsHidden() { + Mockito.`when`(keyguardViewController.isBouncerShowing).thenReturn(false) + Mockito.`when`(keyguardPasswordView.isShown).thenReturn(true) + keyguardPasswordViewController.onResume(KeyguardSecurityView.VIEW_REVEALED) + verify(keyguardPasswordView, never()).requestFocus() + } - @Test - fun testHideKeyboardWhenOnPause() { - keyguardPasswordViewController.onPause() - keyguardPasswordView.post { - verify(keyguardPasswordView).clearFocus() - verify(keyguardPasswordView).hideKeyboard() - } + @Test + fun testHideKeyboardWhenOnPause() { + keyguardPasswordViewController.onPause() + keyguardPasswordView.post { + verify(keyguardPasswordView).clearFocus() + verify(keyguardPasswordView).hideKeyboard() } + } - @Test - fun startAppearAnimation() { - keyguardPasswordViewController.startAppearAnimation() - verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_password) - } + @Test + fun startAppearAnimation() { + keyguardPasswordViewController.startAppearAnimation() + verify(mKeyguardMessageAreaController) + .setMessage(context.resources.getString(R.string.keyguard_enter_your_password), false) + } - @Test - fun startAppearAnimation_withExistingMessage() { - `when`(mKeyguardMessageAreaController.message).thenReturn("Unlock to continue.") - keyguardPasswordViewController.startAppearAnimation() - verify( - mKeyguardMessageAreaController, - never() - ).setMessage(R.string.keyguard_enter_your_password) - } + @Test + fun startAppearAnimation_withExistingMessage() { + `when`(mKeyguardMessageAreaController.message).thenReturn("Unlock to continue.") + keyguardPasswordViewController.startAppearAnimation() + verify(mKeyguardMessageAreaController, never()).setMessage(anyString(), anyBoolean()) + } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt index b3d1c8f909d8..85dbdb8330a3 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt @@ -30,97 +30,93 @@ import com.android.systemui.statusbar.policy.DevicePostureController import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.ArgumentMatchers.anyString import org.mockito.Mock +import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.Mockito.`when` -import org.mockito.Mockito.never import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidTestingRunner::class) @TestableLooper.RunWithLooper class KeyguardPatternViewControllerTest : SysuiTestCase() { - @Mock - private lateinit var mKeyguardPatternView: KeyguardPatternView + @Mock private lateinit var mKeyguardPatternView: KeyguardPatternView - @Mock - private lateinit var mKeyguardUpdateMonitor: KeyguardUpdateMonitor + @Mock private lateinit var mKeyguardUpdateMonitor: KeyguardUpdateMonitor - @Mock - private lateinit var mSecurityMode: KeyguardSecurityModel.SecurityMode + @Mock private lateinit var mSecurityMode: KeyguardSecurityModel.SecurityMode - @Mock - private lateinit var mLockPatternUtils: LockPatternUtils + @Mock private lateinit var mLockPatternUtils: LockPatternUtils - @Mock - private lateinit var mKeyguardSecurityCallback: KeyguardSecurityCallback + @Mock private lateinit var mKeyguardSecurityCallback: KeyguardSecurityCallback - @Mock - private lateinit var mLatencyTracker: LatencyTracker - private var mFalsingCollector: FalsingCollector = FalsingCollectorFake() + @Mock private lateinit var mLatencyTracker: LatencyTracker + private var mFalsingCollector: FalsingCollector = FalsingCollectorFake() - @Mock - private lateinit var mEmergencyButtonController: EmergencyButtonController + @Mock private lateinit var mEmergencyButtonController: EmergencyButtonController - @Mock - private lateinit - var mKeyguardMessageAreaControllerFactory: KeyguardMessageAreaController.Factory + @Mock + private lateinit var mKeyguardMessageAreaControllerFactory: KeyguardMessageAreaController.Factory - @Mock - private lateinit var mKeyguardMessageArea: BouncerKeyguardMessageArea + @Mock private lateinit var mKeyguardMessageArea: BouncerKeyguardMessageArea - @Mock - private lateinit var mKeyguardMessageAreaController: - KeyguardMessageAreaController<BouncerKeyguardMessageArea> + @Mock + private lateinit var mKeyguardMessageAreaController: + KeyguardMessageAreaController<BouncerKeyguardMessageArea> - @Mock - private lateinit var mLockPatternView: LockPatternView + @Mock private lateinit var mLockPatternView: LockPatternView - @Mock - private lateinit var mPostureController: DevicePostureController + @Mock private lateinit var mPostureController: DevicePostureController - private lateinit var mKeyguardPatternViewController: KeyguardPatternViewController + private lateinit var mKeyguardPatternViewController: KeyguardPatternViewController - @Before - fun setup() { - MockitoAnnotations.initMocks(this) - `when`(mKeyguardPatternView.isAttachedToWindow).thenReturn(true) - `when`(mKeyguardPatternView - .requireViewById<BouncerKeyguardMessageArea>(R.id.bouncer_message_area)) - .thenReturn(mKeyguardMessageArea) - `when`(mKeyguardPatternView.findViewById<LockPatternView>(R.id.lockPatternView)) - .thenReturn(mLockPatternView) - `when`(mKeyguardMessageAreaControllerFactory.create(mKeyguardMessageArea)) - .thenReturn(mKeyguardMessageAreaController) - mKeyguardPatternViewController = KeyguardPatternViewController( + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + `when`(mKeyguardPatternView.isAttachedToWindow).thenReturn(true) + `when`( + mKeyguardPatternView.requireViewById<BouncerKeyguardMessageArea>( + R.id.bouncer_message_area)) + .thenReturn(mKeyguardMessageArea) + `when`(mKeyguardPatternView.findViewById<LockPatternView>(R.id.lockPatternView)) + .thenReturn(mLockPatternView) + `when`(mKeyguardMessageAreaControllerFactory.create(mKeyguardMessageArea)) + .thenReturn(mKeyguardMessageAreaController) + `when`(mKeyguardPatternView.resources).thenReturn(context.resources) + mKeyguardPatternViewController = + KeyguardPatternViewController( mKeyguardPatternView, - mKeyguardUpdateMonitor, mSecurityMode, mLockPatternUtils, mKeyguardSecurityCallback, - mLatencyTracker, mFalsingCollector, mEmergencyButtonController, - mKeyguardMessageAreaControllerFactory, mPostureController - ) - } - - @Test - fun onPause_resetsText() { - mKeyguardPatternViewController.init() - mKeyguardPatternViewController.onPause() - verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_pattern) - } - - - @Test - fun startAppearAnimation() { - mKeyguardPatternViewController.startAppearAnimation() - verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_pattern) - } - - @Test - fun startAppearAnimation_withExistingMessage() { - `when`(mKeyguardMessageAreaController.message).thenReturn("Unlock to continue.") - mKeyguardPatternViewController.startAppearAnimation() - verify( - mKeyguardMessageAreaController, - never() - ).setMessage(R.string.keyguard_enter_your_password) - } + mKeyguardUpdateMonitor, + mSecurityMode, + mLockPatternUtils, + mKeyguardSecurityCallback, + mLatencyTracker, + mFalsingCollector, + mEmergencyButtonController, + mKeyguardMessageAreaControllerFactory, + mPostureController) + } + + @Test + fun onPause_resetsText() { + mKeyguardPatternViewController.init() + mKeyguardPatternViewController.onPause() + verify(mKeyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_pattern) + } + + @Test + fun startAppearAnimation() { + mKeyguardPatternViewController.startAppearAnimation() + verify(mKeyguardMessageAreaController) + .setMessage(context.resources.getString(R.string.keyguard_enter_your_pattern), false) + } + + @Test + fun startAppearAnimation_withExistingMessage() { + `when`(mKeyguardMessageAreaController.message).thenReturn("Unlock to continue.") + mKeyguardPatternViewController.startAppearAnimation() + verify(mKeyguardMessageAreaController, never()).setMessage(anyString(), anyBoolean()) + } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java index ce1101f389c0..b7421001b57e 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java @@ -16,6 +16,8 @@ package com.android.keyguard; +import static com.google.common.truth.Truth.assertThat; + import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -113,4 +115,9 @@ public class KeyguardPinBasedInputViewControllerTest extends SysuiTestCase { mKeyguardPinViewController.onResume(KeyguardSecurityView.SCREEN_ON); verify(mPasswordEntry).requestFocus(); } + + @Test + public void testGetInitialMessageResId() { + assertThat(mKeyguardPinViewController.getInitialMessageResId()).isNotEqualTo(0); + } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt index 8bcfe6f2b6f5..cdb7bbb9f823 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt @@ -31,10 +31,13 @@ import com.android.systemui.statusbar.policy.DevicePostureController import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.ArgumentMatchers.anyString import org.mockito.Mock import org.mockito.Mockito import org.mockito.Mockito.any import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations @SmallTest @@ -79,6 +82,7 @@ class KeyguardPinViewControllerTest : SysuiTestCase() { keyguardMessageAreaControllerFactory.create(any(KeyguardMessageArea::class.java)) ) .thenReturn(keyguardMessageAreaController) + `when`(keyguardPinView.resources).thenReturn(context.resources) pinViewController = KeyguardPinViewController( keyguardPinView, @@ -98,14 +102,14 @@ class KeyguardPinViewControllerTest : SysuiTestCase() { @Test fun startAppearAnimation() { pinViewController.startAppearAnimation() - verify(keyguardMessageAreaController).setMessage(R.string.keyguard_enter_your_pin) + verify(keyguardMessageAreaController) + .setMessage(context.resources.getString(R.string.keyguard_enter_your_pin), false) } @Test fun startAppearAnimation_withExistingMessage() { Mockito.`when`(keyguardMessageAreaController.message).thenReturn("Unlock to continue.") pinViewController.startAppearAnimation() - verify(keyguardMessageAreaController, Mockito.never()) - .setMessage(R.string.keyguard_enter_your_password) + verify(keyguardMessageAreaController, Mockito.never()).setMessage(anyString(), anyBoolean()) } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java index bdd29aa93b2c..0f4cf4119731 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java @@ -2083,6 +2083,96 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isFalse(); } + @Test + public void fingerprintFailure_requestActiveUnlock_dismissKeyguard() + throws RemoteException { + // GIVEN shouldTriggerActiveUnlock + bouncerFullyVisible(); + when(mLockPatternUtils.isSecure(KeyguardUpdateMonitor.getCurrentUser())).thenReturn(true); + + // GIVEN active unlock triggers on biometric failures + when(mActiveUnlockConfig.shouldAllowActiveUnlockFromOrigin( + ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL)) + .thenReturn(true); + + // WHEN fingerprint fails + mKeyguardUpdateMonitor.mFingerprintAuthenticationCallback.onAuthenticationFailed(); + + // ALWAYS request unlock with a keyguard dismissal + verify(mTrustManager).reportUserRequestedUnlock(eq(KeyguardUpdateMonitor.getCurrentUser()), + eq(true)); + } + + @Test + public void faceNonBypassFailure_requestActiveUnlock_doesNotDismissKeyguard() + throws RemoteException { + // GIVEN shouldTriggerActiveUnlock + when(mAuthController.isUdfpsFingerDown()).thenReturn(false); + keyguardIsVisible(); + keyguardNotGoingAway(); + statusBarShadeIsNotLocked(); + when(mLockPatternUtils.isSecure(KeyguardUpdateMonitor.getCurrentUser())).thenReturn(true); + + // GIVEN active unlock triggers on biometric failures + when(mActiveUnlockConfig.shouldAllowActiveUnlockFromOrigin( + ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL)) + .thenReturn(true); + + // WHEN face fails & bypass is not allowed + lockscreenBypassIsNotAllowed(); + mKeyguardUpdateMonitor.mFaceAuthenticationCallback.onAuthenticationFailed(); + + // THEN request unlock with NO keyguard dismissal + verify(mTrustManager).reportUserRequestedUnlock(eq(KeyguardUpdateMonitor.getCurrentUser()), + eq(false)); + } + + @Test + public void faceBypassFailure_requestActiveUnlock_dismissKeyguard() + throws RemoteException { + // GIVEN shouldTriggerActiveUnlock + when(mAuthController.isUdfpsFingerDown()).thenReturn(false); + keyguardIsVisible(); + keyguardNotGoingAway(); + statusBarShadeIsNotLocked(); + when(mLockPatternUtils.isSecure(KeyguardUpdateMonitor.getCurrentUser())).thenReturn(true); + + // GIVEN active unlock triggers on biometric failures + when(mActiveUnlockConfig.shouldAllowActiveUnlockFromOrigin( + ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL)) + .thenReturn(true); + + // WHEN face fails & bypass is not allowed + lockscreenBypassIsAllowed(); + mKeyguardUpdateMonitor.mFaceAuthenticationCallback.onAuthenticationFailed(); + + // THEN request unlock with a keyguard dismissal + verify(mTrustManager).reportUserRequestedUnlock(eq(KeyguardUpdateMonitor.getCurrentUser()), + eq(true)); + } + + @Test + public void faceNonBypassFailure_requestActiveUnlock_dismissKeyguard() + throws RemoteException { + // GIVEN shouldTriggerActiveUnlock + when(mAuthController.isUdfpsFingerDown()).thenReturn(false); + lockscreenBypassIsNotAllowed(); + when(mLockPatternUtils.isSecure(KeyguardUpdateMonitor.getCurrentUser())).thenReturn(true); + + // GIVEN active unlock triggers on biometric failures + when(mActiveUnlockConfig.shouldAllowActiveUnlockFromOrigin( + ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL)) + .thenReturn(true); + + // WHEN face fails & on the bouncer + bouncerFullyVisible(); + mKeyguardUpdateMonitor.mFaceAuthenticationCallback.onAuthenticationFailed(); + + // THEN request unlock with a keyguard dismissal + verify(mTrustManager).reportUserRequestedUnlock(eq(KeyguardUpdateMonitor.getCurrentUser()), + eq(true)); + } + private void userDeviceLockDown() { when(mStrongAuthTracker.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(false); when(mStrongAuthTracker.getStrongAuthForUser(mCurrentUserId)) @@ -2100,6 +2190,9 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { } private void mockCanBypassLockscreen(boolean canBypass) { + // force update the isFaceEnrolled cache: + mKeyguardUpdateMonitor.isFaceAuthEnabledForUser(getCurrentUser()); + mKeyguardUpdateMonitor.setKeyguardBypassController(mKeyguardBypassController); when(mKeyguardBypassController.canBypass()).thenReturn(canBypass); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/camera/CameraGestureHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/camera/CameraGestureHelperTest.kt index ca94ea826782..262b4b889f84 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/camera/CameraGestureHelperTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/camera/CameraGestureHelperTest.kt @@ -301,7 +301,7 @@ class CameraGestureHelperTest : SysuiTestCase() { val intent = intentCaptor.value assertThat(CameraIntents.isSecureCameraIntent(intent)).isEqualTo(isSecure) - assertThat(intent.getIntExtra(CameraGestureHelper.EXTRA_CAMERA_LAUNCH_SOURCE, -1)) + assertThat(intent.getIntExtra(CameraIntents.EXTRA_LAUNCH_SOURCE, -1)) .isEqualTo(source) } 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 779788aa0075..d172c9a2e630 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 @@ -38,6 +38,7 @@ import com.android.systemui.controls.CustomIconCache import com.android.systemui.controls.controller.ControlsController import com.android.systemui.controls.controller.StructureInfo import com.android.systemui.controls.management.ControlsListingController +import com.android.systemui.controls.management.ControlsProviderSelectorActivity import com.android.systemui.controls.settings.FakeControlsSettingsRepository import com.android.systemui.dump.DumpManager import com.android.systemui.plugins.ActivityStarter @@ -53,6 +54,7 @@ import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.capture 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 @@ -322,6 +324,45 @@ class ControlsUiControllerImplTest : SysuiTestCase() { .isFalse() } + @Test + fun testResolveActivityWhileSeeding_ControlsActivity() { + whenever(controlsController.addSeedingFavoritesCallback(any())).thenReturn(true) + assertThat(underTest.resolveActivity()).isEqualTo(ControlsActivity::class.java) + } + + @Test + fun testResolveActivityNotSeedingNoFavoritesNoPanels_ControlsProviderSelectorActivity() { + whenever(controlsController.addSeedingFavoritesCallback(any())).thenReturn(false) + whenever(controlsController.getFavorites()).thenReturn(emptyList()) + + val selectedItems = + listOf( + SelectedItem.StructureItem( + StructureInfo(ComponentName.unflattenFromString("pkg/.cls1"), "a", ArrayList()) + ), + ) + sharedPreferences + .edit() + .putString("controls_component", selectedItems[0].componentName.flattenToString()) + .putString("controls_structure", selectedItems[0].name.toString()) + .commit() + + assertThat(underTest.resolveActivity()) + .isEqualTo(ControlsProviderSelectorActivity::class.java) + } + + @Test + fun testResolveActivityNotSeedingNoDefaultNoFavoritesPanel_ControlsActivity() { + val panel = SelectedItem.PanelItem("App name", ComponentName("pkg", "cls")) + val activity = ComponentName("pkg", "activity") + val csi = ControlsServiceInfo(panel.componentName, panel.appName, activity) + whenever(controlsController.addSeedingFavoritesCallback(any())).thenReturn(true) + whenever(controlsController.getFavorites()).thenReturn(emptyList()) + whenever(controlsListingController.getCurrentServices()).thenReturn(listOf(csi)) + + assertThat(underTest.resolveActivity()).isEqualTo(ControlsActivity::class.java) + } + private fun setUpPanel(panel: SelectedItem.PanelItem): ControlsServiceInfo { val activity = ComponentName("pkg", "activity") sharedPreferences 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 5cd2ace4604a..de04ef810dd0 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 @@ -75,6 +75,7 @@ class PanelTaskViewControllerTest : SysuiTestCase() { uiExecutor.execute(it.arguments[0] as Runnable) true } + whenever(activityContext.resources).thenReturn(context.resources) uiExecutor = FakeExecutor(FakeSystemClock()) diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt index 99406ed44606..8e689cf8f17e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayAnimationsControllerTest.kt @@ -23,11 +23,11 @@ import org.mockito.MockitoAnnotations class DreamOverlayAnimationsControllerTest : SysuiTestCase() { companion object { + private const val DREAM_BLUR_RADIUS = 50 private const val DREAM_IN_BLUR_ANIMATION_DURATION = 1L - private const val DREAM_IN_BLUR_ANIMATION_DELAY = 2L private const val DREAM_IN_COMPLICATIONS_ANIMATION_DURATION = 3L - private const val DREAM_IN_TOP_COMPLICATIONS_ANIMATION_DELAY = 4L - private const val DREAM_IN_BOTTOM_COMPLICATIONS_ANIMATION_DELAY = 5L + private const val DREAM_IN_TRANSLATION_Y_DISTANCE = 6 + private const val DREAM_IN_TRANSLATION_Y_DURATION = 7L private const val DREAM_OUT_TRANSLATION_Y_DISTANCE = 6 private const val DREAM_OUT_TRANSLATION_Y_DURATION = 7L private const val DREAM_OUT_TRANSLATION_Y_DELAY_BOTTOM = 8L @@ -54,11 +54,11 @@ class DreamOverlayAnimationsControllerTest : SysuiTestCase() { hostViewController, statusBarViewController, stateController, + DREAM_BLUR_RADIUS, DREAM_IN_BLUR_ANIMATION_DURATION, - DREAM_IN_BLUR_ANIMATION_DELAY, DREAM_IN_COMPLICATIONS_ANIMATION_DURATION, - DREAM_IN_TOP_COMPLICATIONS_ANIMATION_DELAY, - DREAM_IN_BOTTOM_COMPLICATIONS_ANIMATION_DELAY, + DREAM_IN_TRANSLATION_Y_DISTANCE, + DREAM_IN_TRANSLATION_Y_DURATION, DREAM_OUT_TRANSLATION_Y_DISTANCE, DREAM_OUT_TRANSLATION_Y_DURATION, DREAM_OUT_TRANSLATION_Y_DELAY_BOTTOM, diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationLayoutParamsTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationLayoutParamsTest.java index fdb4cc4480da..e414942afb56 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationLayoutParamsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationLayoutParamsTest.java @@ -17,6 +17,10 @@ package com.android.systemui.dreams.complication; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + import android.testing.AndroidTestingRunner; import androidx.test.filters.SmallTest; @@ -29,6 +33,7 @@ import org.junit.runner.RunWith; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; +import java.util.function.Consumer; @SmallTest @RunWith(AndroidTestingRunner.class) @@ -197,4 +202,19 @@ public class ComplicationLayoutParamsTest extends SysuiTestCase { assertThat(paramsWithConstraint.constraintSpecified()).isTrue(); assertThat(paramsWithConstraint.getConstraint()).isEqualTo(constraint); } + + @Test + public void testIteratePositions() { + final int positions = ComplicationLayoutParams.POSITION_TOP + | ComplicationLayoutParams.POSITION_START + | ComplicationLayoutParams.POSITION_END; + final Consumer<Integer> consumer = mock(Consumer.class); + + ComplicationLayoutParams.iteratePositions(consumer, positions); + + verify(consumer).accept(ComplicationLayoutParams.POSITION_TOP); + verify(consumer).accept(ComplicationLayoutParams.POSITION_START); + verify(consumer).accept(ComplicationLayoutParams.POSITION_END); + verify(consumer, never()).accept(ComplicationLayoutParams.POSITION_BOTTOM); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java index e6d3a69593cd..89c728082cc5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java @@ -27,6 +27,7 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.content.ComponentName; import android.content.Context; import android.testing.AndroidTestingRunner; import android.view.View; @@ -54,6 +55,7 @@ import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -147,6 +149,19 @@ public class DreamHomeControlsComplicationTest extends SysuiTestCase { } @Test + public void complicationAvailability_serviceAvailable_noFavorites_panel_addComplication() { + final DreamHomeControlsComplication.Registrant registrant = + new DreamHomeControlsComplication.Registrant(mComplication, + mDreamOverlayStateController, mControlsComponent); + registrant.start(); + + setHaveFavorites(false); + setServiceWithPanel(); + + verify(mDreamOverlayStateController).addComplication(mComplication); + } + + @Test public void complicationAvailability_serviceNotAvailable_haveFavorites_doNotAddComplication() { final DreamHomeControlsComplication.Registrant registrant = new DreamHomeControlsComplication.Registrant(mComplication, @@ -232,6 +247,15 @@ public class DreamHomeControlsComplicationTest extends SysuiTestCase { triggerControlsListingCallback(serviceInfos); } + private void setServiceWithPanel() { + final List<ControlsServiceInfo> serviceInfos = new ArrayList<>(); + ControlsServiceInfo csi = mock(ControlsServiceInfo.class); + serviceInfos.add(csi); + when(csi.getPanelActivity()).thenReturn(new ComponentName("a", "b")); + when(mControlsListingController.getCurrentServices()).thenReturn(serviceInfos); + triggerControlsListingCallback(serviceInfos); + } + private void setDreamOverlayActive(boolean value) { when(mDreamOverlayStateController.isOverlayActive()).thenReturn(value); verify(mDreamOverlayStateController).addCallback(mStateCallbackCaptor.capture()); diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt index cef452b8ec22..09c8e6ac1268 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt @@ -20,7 +20,13 @@ package com.android.systemui.keyguard import android.content.ContentValues import android.content.pm.PackageManager import android.content.pm.ProviderInfo +import android.os.Bundle +import android.os.Handler +import android.os.IBinder import android.os.UserHandle +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.view.SurfaceControlViewHost import androidx.test.filters.SmallTest import com.android.internal.widget.LockPatternUtils import com.android.systemui.SystemUIAppComponentFactoryBase @@ -36,6 +42,9 @@ import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor +import com.android.systemui.keyguard.ui.preview.KeyguardPreviewRenderer +import com.android.systemui.keyguard.ui.preview.KeyguardPreviewRendererFactory +import com.android.systemui.keyguard.ui.preview.KeyguardRemotePreviewManager import com.android.systemui.plugins.ActivityStarter import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker @@ -43,40 +52,53 @@ import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordance import com.android.systemui.shared.quickaffordance.data.content.KeyguardQuickAffordanceProviderContract as Contract import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.FakeSharedPreferences +import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.android.systemui.util.settings.FakeSettings import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.ExperimentalCoroutinesApi +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.junit.runners.JUnit4 import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.anyString import org.mockito.Mock import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest -@RunWith(JUnit4::class) +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) class KeyguardQuickAffordanceProviderTest : SysuiTestCase() { @Mock private lateinit var lockPatternUtils: LockPatternUtils @Mock private lateinit var keyguardStateController: KeyguardStateController @Mock private lateinit var userTracker: UserTracker @Mock private lateinit var activityStarter: ActivityStarter + @Mock private lateinit var previewRendererFactory: KeyguardPreviewRendererFactory + @Mock private lateinit var previewRenderer: KeyguardPreviewRenderer + @Mock private lateinit var backgroundHandler: Handler + @Mock private lateinit var previewSurfacePackage: SurfaceControlViewHost.SurfacePackage private lateinit var underTest: KeyguardQuickAffordanceProvider + private lateinit var testScope: TestScope + @Before fun setUp() { MockitoAnnotations.initMocks(this) + whenever(previewRenderer.surfacePackage).thenReturn(previewSurfacePackage) + whenever(previewRendererFactory.create(any())).thenReturn(previewRenderer) + whenever(backgroundHandler.looper).thenReturn(TestableLooper.get(this).looper) underTest = KeyguardQuickAffordanceProvider() - val scope = CoroutineScope(IMMEDIATE) + val testDispatcher = StandardTestDispatcher() + testScope = TestScope(testDispatcher) val localUserSelectionManager = KeyguardQuickAffordanceLocalUserSelectionManager( context = context, @@ -96,7 +118,7 @@ class KeyguardQuickAffordanceProviderTest : SysuiTestCase() { ) val remoteUserSelectionManager = KeyguardQuickAffordanceRemoteUserSelectionManager( - scope = scope, + scope = testScope.backgroundScope, userTracker = userTracker, clientFactory = FakeKeyguardQuickAffordanceProviderClientFactory(userTracker), userHandle = UserHandle.SYSTEM, @@ -104,7 +126,7 @@ class KeyguardQuickAffordanceProviderTest : SysuiTestCase() { val quickAffordanceRepository = KeyguardQuickAffordanceRepository( appContext = context, - scope = scope, + scope = testScope.backgroundScope, localUserSelectionManager = localUserSelectionManager, remoteUserSelectionManager = remoteUserSelectionManager, userTracker = userTracker, @@ -123,8 +145,8 @@ class KeyguardQuickAffordanceProviderTest : SysuiTestCase() { ), legacySettingSyncer = KeyguardQuickAffordanceLegacySettingSyncer( - scope = scope, - backgroundDispatcher = IMMEDIATE, + scope = testScope.backgroundScope, + backgroundDispatcher = testDispatcher, secureSettings = FakeSettings(), selectionsManager = localUserSelectionManager, ), @@ -148,6 +170,12 @@ class KeyguardQuickAffordanceProviderTest : SysuiTestCase() { }, repository = { quickAffordanceRepository }, ) + underTest.previewManager = + KeyguardRemotePreviewManager( + previewRendererFactory = previewRendererFactory, + mainDispatcher = testDispatcher, + backgroundHandler = backgroundHandler, + ) underTest.attachInfoForTesting( context, @@ -190,7 +218,7 @@ class KeyguardQuickAffordanceProviderTest : SysuiTestCase() { @Test fun `insert and query selection`() = - runBlocking(IMMEDIATE) { + testScope.runTest { val slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START val affordanceId = AFFORDANCE_2 val affordanceName = AFFORDANCE_2_NAME @@ -214,7 +242,7 @@ class KeyguardQuickAffordanceProviderTest : SysuiTestCase() { @Test fun `query slots`() = - runBlocking(IMMEDIATE) { + testScope.runTest { assertThat(querySlots()) .isEqualTo( listOf( @@ -232,7 +260,7 @@ class KeyguardQuickAffordanceProviderTest : SysuiTestCase() { @Test fun `query affordances`() = - runBlocking(IMMEDIATE) { + testScope.runTest { assertThat(queryAffordances()) .isEqualTo( listOf( @@ -252,7 +280,7 @@ class KeyguardQuickAffordanceProviderTest : SysuiTestCase() { @Test fun `delete and query selection`() = - runBlocking(IMMEDIATE) { + testScope.runTest { insertSelection( slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, affordanceId = AFFORDANCE_1, @@ -286,7 +314,7 @@ class KeyguardQuickAffordanceProviderTest : SysuiTestCase() { @Test fun `delete all selections in a slot`() = - runBlocking(IMMEDIATE) { + testScope.runTest { insertSelection( slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, affordanceId = AFFORDANCE_1, @@ -316,6 +344,23 @@ class KeyguardQuickAffordanceProviderTest : SysuiTestCase() { ) } + @Test + fun preview() = + testScope.runTest { + val hostToken: IBinder = mock() + whenever(previewRenderer.hostToken).thenReturn(hostToken) + val extras = Bundle() + + val result = underTest.call("whatever", "anything", extras) + + verify(previewRenderer).render() + verify(hostToken).linkToDeath(any(), anyInt()) + assertThat(result!!).isNotNull() + assertThat(result.get(KeyguardRemotePreviewManager.KEY_PREVIEW_SURFACE_PACKAGE)) + .isEqualTo(previewSurfacePackage) + assertThat(result.containsKey(KeyguardRemotePreviewManager.KEY_PREVIEW_CALLBACK)) + } + private fun insertSelection( slotId: String, affordanceId: String, @@ -451,7 +496,6 @@ class KeyguardQuickAffordanceProviderTest : SysuiTestCase() { ) companion object { - private val IMMEDIATE = Dispatchers.Main.immediate private const val AFFORDANCE_1 = "affordance_1" private const val AFFORDANCE_2 = "affordance_2" private const val AFFORDANCE_1_NAME = "affordance_1_name" diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt index 322014a61a73..f8cb40885d21 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt @@ -20,13 +20,14 @@ package com.android.systemui.keyguard.data.quickaffordance import androidx.test.filters.SmallTest import com.android.systemui.R import com.android.systemui.SysuiTestCase +import com.android.systemui.controls.ControlsServiceInfo import com.android.systemui.controls.controller.ControlsController import com.android.systemui.controls.dagger.ControlsComponent import com.android.systemui.controls.management.ControlsListingController import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat -import java.util.* +import java.util.Optional import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -50,20 +51,22 @@ class HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest : SysuiTes companion object { @Parameters( name = - "feature enabled = {0}, has favorites = {1}, has service infos = {2}, can show" + - " while locked = {3}, visibility is AVAILABLE {4} - expected visible = {5}" + "feature enabled = {0}, has favorites = {1}, has panels = {2}, " + + "has service infos = {3}, can show while locked = {4}, " + + "visibility is AVAILABLE {5} - expected visible = {6}" ) @JvmStatic fun data() = - (0 until 32) + (0 until 64) .map { combination -> arrayOf( - /* isFeatureEnabled= */ combination and 0b10000 != 0, - /* hasFavorites= */ combination and 0b01000 != 0, - /* hasServiceInfos= */ combination and 0b00100 != 0, - /* canShowWhileLocked= */ combination and 0b00010 != 0, - /* visibilityAvailable= */ combination and 0b00001 != 0, - /* isVisible= */ combination == 0b11111, + /* isFeatureEnabled= */ combination and 0b100000 != 0, + /* hasFavorites = */ combination and 0b010000 != 0, + /* hasPanels = */ combination and 0b001000 != 0, + /* hasServiceInfos= */ combination and 0b000100 != 0, + /* canShowWhileLocked= */ combination and 0b000010 != 0, + /* visibilityAvailable= */ combination and 0b000001 != 0, + /* isVisible= */ combination in setOf(0b111111, 0b110111, 0b101111), ) } .toList() @@ -72,6 +75,7 @@ class HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest : SysuiTes @Mock private lateinit var component: ControlsComponent @Mock private lateinit var controlsController: ControlsController @Mock private lateinit var controlsListingController: ControlsListingController + @Mock private lateinit var controlsServiceInfo: ControlsServiceInfo @Captor private lateinit var callbackCaptor: ArgumentCaptor<ControlsListingController.ControlsListingCallback> @@ -80,10 +84,11 @@ class HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest : SysuiTes @JvmField @Parameter(0) var isFeatureEnabled: Boolean = false @JvmField @Parameter(1) var hasFavorites: Boolean = false - @JvmField @Parameter(2) var hasServiceInfos: Boolean = false - @JvmField @Parameter(3) var canShowWhileLocked: Boolean = false - @JvmField @Parameter(4) var isVisibilityAvailable: Boolean = false - @JvmField @Parameter(5) var isVisibleExpected: Boolean = false + @JvmField @Parameter(2) var hasPanels: Boolean = false + @JvmField @Parameter(3) var hasServiceInfos: Boolean = false + @JvmField @Parameter(4) var canShowWhileLocked: Boolean = false + @JvmField @Parameter(5) var isVisibilityAvailable: Boolean = false + @JvmField @Parameter(6) var isVisibleExpected: Boolean = false @Before fun setUp() { @@ -93,10 +98,13 @@ class HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest : SysuiTes whenever(component.getControlsController()).thenReturn(Optional.of(controlsController)) whenever(component.getControlsListingController()) .thenReturn(Optional.of(controlsListingController)) + if (hasPanels) { + whenever(controlsServiceInfo.panelActivity).thenReturn(mock()) + } whenever(controlsListingController.getCurrentServices()) .thenReturn( if (hasServiceInfos) { - listOf(mock(), mock()) + listOf(controlsServiceInfo, mock()) } else { emptyList() } @@ -134,10 +142,15 @@ class HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest : SysuiTes val job = underTest.lockScreenState.onEach(values::add).launchIn(this) if (canShowWhileLocked) { + val serviceInfoMock: ControlsServiceInfo = mock { + if (hasPanels) { + whenever(panelActivity).thenReturn(mock()) + } + } verify(controlsListingController).addCallback(callbackCaptor.capture()) callbackCaptor.value.onServicesUpdated( if (hasServiceInfos) { - listOf(mock()) + listOf(serviceInfoMock) } else { emptyList() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt index 11fe905b1d1f..d97571bcd8ef 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt @@ -23,6 +23,7 @@ import com.android.internal.widget.LockPatternUtils import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon +import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys @@ -49,14 +50,10 @@ import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.android.systemui.util.settings.FakeSettings import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.test.runBlockingTest -import kotlinx.coroutines.yield +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 @@ -78,6 +75,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { private lateinit var underTest: KeyguardQuickAffordanceInteractor + private lateinit var testScope: TestScope private lateinit var repository: FakeKeyguardRepository private lateinit var homeControls: FakeKeyguardQuickAffordanceConfig private lateinit var quickAccessWallet: FakeKeyguardQuickAffordanceConfig @@ -99,7 +97,8 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { ) qrCodeScanner = FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER) - val scope = CoroutineScope(IMMEDIATE) + val testDispatcher = StandardTestDispatcher() + testScope = TestScope(testDispatcher) val localUserSelectionManager = KeyguardQuickAffordanceLocalUserSelectionManager( @@ -120,7 +119,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { ) val remoteUserSelectionManager = KeyguardQuickAffordanceRemoteUserSelectionManager( - scope = scope, + scope = testScope.backgroundScope, userTracker = userTracker, clientFactory = FakeKeyguardQuickAffordanceProviderClientFactory(userTracker), userHandle = UserHandle.SYSTEM, @@ -128,14 +127,14 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { val quickAffordanceRepository = KeyguardQuickAffordanceRepository( appContext = context, - scope = scope, + scope = testScope.backgroundScope, localUserSelectionManager = localUserSelectionManager, remoteUserSelectionManager = remoteUserSelectionManager, userTracker = userTracker, legacySettingSyncer = KeyguardQuickAffordanceLegacySettingSyncer( - scope = scope, - backgroundDispatcher = IMMEDIATE, + scope = testScope.backgroundScope, + backgroundDispatcher = testDispatcher, secureSettings = FakeSettings(), selectionsManager = localUserSelectionManager, ), @@ -175,88 +174,76 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { } @Test - fun `quickAffordance - bottom start affordance is visible`() = runBlockingTest { - val configKey = BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS - homeControls.setState( - KeyguardQuickAffordanceConfig.LockScreenState.Visible( - icon = ICON, - activationState = ActivationState.Active, + fun `quickAffordance - bottom start affordance is visible`() = + testScope.runTest { + val configKey = BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS + homeControls.setState( + KeyguardQuickAffordanceConfig.LockScreenState.Visible( + icon = ICON, + activationState = ActivationState.Active, + ) ) - ) - - var latest: KeyguardQuickAffordanceModel? = null - val job = - underTest - .quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_START) - .onEach { latest = it } - .launchIn(this) - // The interactor has an onStart { emit(Hidden) } to cover for upstream configs that don't - // produce an initial value. We yield to give the coroutine time to emit the first real - // value from our config. - yield() - - assertThat(latest).isInstanceOf(KeyguardQuickAffordanceModel.Visible::class.java) - val visibleModel = latest as KeyguardQuickAffordanceModel.Visible - assertThat(visibleModel.configKey).isEqualTo(configKey) - assertThat(visibleModel.icon).isEqualTo(ICON) - assertThat(visibleModel.icon.contentDescription) - .isEqualTo(ContentDescription.Resource(res = CONTENT_DESCRIPTION_RESOURCE_ID)) - assertThat(visibleModel.activationState).isEqualTo(ActivationState.Active) - job.cancel() - } + + val collectedValue = + collectLastValue( + underTest.quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_START) + ) + + assertThat(collectedValue()) + .isInstanceOf(KeyguardQuickAffordanceModel.Visible::class.java) + val visibleModel = collectedValue() as KeyguardQuickAffordanceModel.Visible + assertThat(visibleModel.configKey).isEqualTo(configKey) + assertThat(visibleModel.icon).isEqualTo(ICON) + assertThat(visibleModel.icon.contentDescription) + .isEqualTo(ContentDescription.Resource(res = CONTENT_DESCRIPTION_RESOURCE_ID)) + assertThat(visibleModel.activationState).isEqualTo(ActivationState.Active) + } @Test - fun `quickAffordance - bottom end affordance is visible`() = runBlockingTest { - val configKey = BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET - quickAccessWallet.setState( - KeyguardQuickAffordanceConfig.LockScreenState.Visible( - icon = ICON, + fun `quickAffordance - bottom end affordance is visible`() = + testScope.runTest { + val configKey = BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET + quickAccessWallet.setState( + KeyguardQuickAffordanceConfig.LockScreenState.Visible( + icon = ICON, + ) ) - ) - - var latest: KeyguardQuickAffordanceModel? = null - val job = - underTest - .quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_END) - .onEach { latest = it } - .launchIn(this) - // The interactor has an onStart { emit(Hidden) } to cover for upstream configs that don't - // produce an initial value. We yield to give the coroutine time to emit the first real - // value from our config. - yield() - - assertThat(latest).isInstanceOf(KeyguardQuickAffordanceModel.Visible::class.java) - val visibleModel = latest as KeyguardQuickAffordanceModel.Visible - assertThat(visibleModel.configKey).isEqualTo(configKey) - assertThat(visibleModel.icon).isEqualTo(ICON) - assertThat(visibleModel.icon.contentDescription) - .isEqualTo(ContentDescription.Resource(res = CONTENT_DESCRIPTION_RESOURCE_ID)) - assertThat(visibleModel.activationState).isEqualTo(ActivationState.NotSupported) - job.cancel() - } + + val collectedValue = + collectLastValue( + underTest.quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_END) + ) + + assertThat(collectedValue()) + .isInstanceOf(KeyguardQuickAffordanceModel.Visible::class.java) + val visibleModel = collectedValue() as KeyguardQuickAffordanceModel.Visible + assertThat(visibleModel.configKey).isEqualTo(configKey) + assertThat(visibleModel.icon).isEqualTo(ICON) + assertThat(visibleModel.icon.contentDescription) + .isEqualTo(ContentDescription.Resource(res = CONTENT_DESCRIPTION_RESOURCE_ID)) + assertThat(visibleModel.activationState).isEqualTo(ActivationState.NotSupported) + } @Test - fun `quickAffordance - bottom start affordance hidden while dozing`() = runBlockingTest { - repository.setDozing(true) - homeControls.setState( - KeyguardQuickAffordanceConfig.LockScreenState.Visible( - icon = ICON, + fun `quickAffordance - bottom start affordance hidden while dozing`() = + testScope.runTest { + repository.setDozing(true) + homeControls.setState( + KeyguardQuickAffordanceConfig.LockScreenState.Visible( + icon = ICON, + ) ) - ) - - var latest: KeyguardQuickAffordanceModel? = null - val job = - underTest - .quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_START) - .onEach { latest = it } - .launchIn(this) - assertThat(latest).isEqualTo(KeyguardQuickAffordanceModel.Hidden) - job.cancel() - } + + val collectedValue = + collectLastValue( + underTest.quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_START) + ) + assertThat(collectedValue()).isEqualTo(KeyguardQuickAffordanceModel.Hidden) + } @Test fun `quickAffordance - bottom start affordance hidden when lockscreen is not showing`() = - runBlockingTest { + testScope.runTest { repository.setKeyguardShowing(false) homeControls.setState( KeyguardQuickAffordanceConfig.LockScreenState.Visible( @@ -264,19 +251,45 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { ) ) - var latest: KeyguardQuickAffordanceModel? = null - val job = - underTest - .quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_START) - .onEach { latest = it } - .launchIn(this) - assertThat(latest).isEqualTo(KeyguardQuickAffordanceModel.Hidden) - job.cancel() + val collectedValue = + collectLastValue( + underTest.quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_START) + ) + assertThat(collectedValue()).isEqualTo(KeyguardQuickAffordanceModel.Hidden) + } + + @Test + fun `quickAffordanceAlwaysVisible - even when lock screen not showing and dozing`() = + testScope.runTest { + repository.setKeyguardShowing(false) + repository.setDozing(true) + val configKey = BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS + homeControls.setState( + KeyguardQuickAffordanceConfig.LockScreenState.Visible( + icon = ICON, + activationState = ActivationState.Active, + ) + ) + + val collectedValue = + collectLastValue( + underTest.quickAffordanceAlwaysVisible( + KeyguardQuickAffordancePosition.BOTTOM_START + ) + ) + assertThat(collectedValue()) + .isInstanceOf(KeyguardQuickAffordanceModel.Visible::class.java) + val visibleModel = collectedValue() as KeyguardQuickAffordanceModel.Visible + assertThat(visibleModel.configKey).isEqualTo(configKey) + assertThat(visibleModel.icon).isEqualTo(ICON) + assertThat(visibleModel.icon.contentDescription) + .isEqualTo(ContentDescription.Resource(res = CONTENT_DESCRIPTION_RESOURCE_ID)) + assertThat(visibleModel.activationState).isEqualTo(ActivationState.Active) } @Test fun select() = - runBlocking(IMMEDIATE) { + testScope.runTest { featureFlags.set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, true) homeControls.setState( KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON) @@ -296,23 +309,18 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { ) ) - var startConfig: KeyguardQuickAffordanceModel? = null - val job1 = - underTest - .quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_START) - .onEach { startConfig = it } - .launchIn(this) - var endConfig: KeyguardQuickAffordanceModel? = null - val job2 = - underTest - .quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_END) - .onEach { endConfig = it } - .launchIn(this) + val startConfig = + collectLastValue( + underTest.quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_START) + ) + val endConfig = + collectLastValue( + underTest.quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_END) + ) underTest.select(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, homeControls.key) - yield() - yield() - assertThat(startConfig) + + assertThat(startConfig()) .isEqualTo( KeyguardQuickAffordanceModel.Visible( configKey = @@ -322,7 +330,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { activationState = ActivationState.NotSupported, ) ) - assertThat(endConfig) + assertThat(endConfig()) .isEqualTo( KeyguardQuickAffordanceModel.Hidden, ) @@ -345,9 +353,8 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, quickAccessWallet.key ) - yield() - yield() - assertThat(startConfig) + + assertThat(startConfig()) .isEqualTo( KeyguardQuickAffordanceModel.Visible( configKey = @@ -357,7 +364,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { activationState = ActivationState.NotSupported, ) ) - assertThat(endConfig) + assertThat(endConfig()) .isEqualTo( KeyguardQuickAffordanceModel.Hidden, ) @@ -377,9 +384,8 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { ) underTest.select(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, qrCodeScanner.key) - yield() - yield() - assertThat(startConfig) + + assertThat(startConfig()) .isEqualTo( KeyguardQuickAffordanceModel.Visible( configKey = @@ -389,7 +395,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { activationState = ActivationState.NotSupported, ) ) - assertThat(endConfig) + assertThat(endConfig()) .isEqualTo( KeyguardQuickAffordanceModel.Visible( configKey = @@ -420,14 +426,11 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { ), ) ) - - job1.cancel() - job2.cancel() } @Test fun `unselect - one`() = - runBlocking(IMMEDIATE) { + testScope.runTest { featureFlags.set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, true) homeControls.setState( KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON) @@ -439,34 +442,23 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON) ) - var startConfig: KeyguardQuickAffordanceModel? = null - val job1 = - underTest - .quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_START) - .onEach { startConfig = it } - .launchIn(this) - var endConfig: KeyguardQuickAffordanceModel? = null - val job2 = - underTest - .quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_END) - .onEach { endConfig = it } - .launchIn(this) + val startConfig = + collectLastValue( + underTest.quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_START) + ) + val endConfig = + collectLastValue( + underTest.quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_END) + ) underTest.select(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, homeControls.key) - yield() - yield() underTest.select(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, quickAccessWallet.key) - yield() - yield() - underTest.unselect(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, homeControls.key) - yield() - yield() - assertThat(startConfig) + assertThat(startConfig()) .isEqualTo( KeyguardQuickAffordanceModel.Hidden, ) - assertThat(endConfig) + assertThat(endConfig()) .isEqualTo( KeyguardQuickAffordanceModel.Visible( configKey = @@ -495,14 +487,12 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, quickAccessWallet.key ) - yield() - yield() - assertThat(startConfig) + assertThat(startConfig()) .isEqualTo( KeyguardQuickAffordanceModel.Hidden, ) - assertThat(endConfig) + assertThat(endConfig()) .isEqualTo( KeyguardQuickAffordanceModel.Hidden, ) @@ -513,14 +503,11 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END to emptyList(), ) ) - - job1.cancel() - job2.cancel() } @Test fun `unselect - all`() = - runBlocking(IMMEDIATE) { + testScope.runTest { featureFlags.set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, true) homeControls.setState( KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON) @@ -533,15 +520,8 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { ) underTest.select(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, homeControls.key) - yield() - yield() underTest.select(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, quickAccessWallet.key) - yield() - yield() - underTest.unselect(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, null) - yield() - yield() assertThat(underTest.getSelections()) .isEqualTo( @@ -562,8 +542,6 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, null, ) - yield() - yield() assertThat(underTest.getSelections()) .isEqualTo( @@ -584,6 +562,5 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { ) } private const val CONTENT_DESCRIPTION_RESOURCE_ID = 1337 - private val IMMEDIATE = Dispatchers.Main.immediate } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt index 83a5d0e90c84..0abff88b5faf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt @@ -23,6 +23,7 @@ import com.android.internal.widget.LockPatternUtils import com.android.systemui.SysuiTestCase import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.Icon +import com.android.systemui.coroutines.collectLastValue import com.android.systemui.doze.util.BurnInHelperWrapper import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags @@ -44,20 +45,21 @@ import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAfforda import com.android.systemui.plugins.ActivityStarter import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker +import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.FakeSharedPreferences import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever import com.android.systemui.util.settings.FakeSettings import com.google.common.truth.Truth.assertThat import kotlin.math.max import kotlin.math.min -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.test.runBlockingTest -import kotlinx.coroutines.yield +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.map +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 @@ -67,9 +69,9 @@ import org.mockito.ArgumentMatchers.anyString import org.mockito.Mock import org.mockito.Mockito import org.mockito.Mockito.verifyZeroInteractions -import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(JUnit4::class) class KeyguardBottomAreaViewModelTest : SysuiTestCase() { @@ -83,6 +85,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { private lateinit var underTest: KeyguardBottomAreaViewModel + private lateinit var testScope: TestScope private lateinit var repository: FakeKeyguardRepository private lateinit var registry: FakeKeyguardQuickAffordanceRegistry private lateinit var homeControlsQuickAffordanceConfig: FakeKeyguardQuickAffordanceConfig @@ -123,7 +126,8 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { whenever(userTracker.userHandle).thenReturn(mock()) whenever(lockPatternUtils.getStrongAuthForUser(anyInt())) .thenReturn(LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED) - val scope = CoroutineScope(IMMEDIATE) + val testDispatcher = StandardTestDispatcher() + testScope = TestScope(testDispatcher) val localUserSelectionManager = KeyguardQuickAffordanceLocalUserSelectionManager( context = context, @@ -143,7 +147,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { ) val remoteUserSelectionManager = KeyguardQuickAffordanceRemoteUserSelectionManager( - scope = scope, + scope = testScope.backgroundScope, userTracker = userTracker, clientFactory = FakeKeyguardQuickAffordanceProviderClientFactory(userTracker), userHandle = UserHandle.SYSTEM, @@ -151,14 +155,14 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { val quickAffordanceRepository = KeyguardQuickAffordanceRepository( appContext = context, - scope = scope, + scope = testScope.backgroundScope, localUserSelectionManager = localUserSelectionManager, remoteUserSelectionManager = remoteUserSelectionManager, userTracker = userTracker, legacySettingSyncer = KeyguardQuickAffordanceLegacySettingSyncer( - scope = scope, - backgroundDispatcher = IMMEDIATE, + scope = testScope.backgroundScope, + backgroundDispatcher = testDispatcher, secureSettings = FakeSettings(), selectionsManager = localUserSelectionManager, ), @@ -194,366 +198,394 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { } @Test - fun `startButton - present - visible model - starts activity on click`() = runBlockingTest { - repository.setKeyguardShowing(true) - var latest: KeyguardQuickAffordanceViewModel? = null - val job = underTest.startButton.onEach { latest = it }.launchIn(this) - - val testConfig = - TestConfig( - isVisible = true, - isClickable = true, - isActivated = true, - icon = mock(), - canShowWhileLocked = false, - intent = Intent("action"), - ) - val configKey = - setUpQuickAffordanceModel( - position = KeyguardQuickAffordancePosition.BOTTOM_START, + fun `startButton - present - visible model - starts activity on click`() = + testScope.runTest { + repository.setKeyguardShowing(true) + val latest = collectLastValue(underTest.startButton) + + val testConfig = + TestConfig( + isVisible = true, + isClickable = true, + isActivated = true, + icon = mock(), + canShowWhileLocked = false, + intent = Intent("action"), + ) + val configKey = + setUpQuickAffordanceModel( + position = KeyguardQuickAffordancePosition.BOTTOM_START, + testConfig = testConfig, + ) + + assertQuickAffordanceViewModel( + viewModel = latest(), testConfig = testConfig, + configKey = configKey, ) - - assertQuickAffordanceViewModel( - viewModel = latest, - testConfig = testConfig, - configKey = configKey, - ) - job.cancel() - } + } @Test - fun `endButton - present - visible model - do nothing on click`() = runBlockingTest { - repository.setKeyguardShowing(true) - var latest: KeyguardQuickAffordanceViewModel? = null - val job = underTest.endButton.onEach { latest = it }.launchIn(this) + fun `startButton - in preview mode - visible even when keyguard not showing`() = + testScope.runTest { + underTest.enablePreviewMode(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START) + repository.setKeyguardShowing(false) + val latest = collectLastValue(underTest.startButton) + + val icon: Icon = mock() + val configKey = + setUpQuickAffordanceModel( + position = KeyguardQuickAffordancePosition.BOTTOM_START, + testConfig = + TestConfig( + isVisible = true, + isClickable = true, + isActivated = true, + icon = icon, + canShowWhileLocked = false, + intent = Intent("action"), + ), + ) - val config = - TestConfig( - isVisible = true, - isClickable = true, - icon = mock(), - canShowWhileLocked = false, - intent = null, // This will cause it to tell the system that the click was handled. + assertQuickAffordanceViewModel( + viewModel = latest(), + testConfig = + TestConfig( + isVisible = true, + isClickable = false, + isActivated = true, + icon = icon, + canShowWhileLocked = false, + intent = Intent("action"), + ), + configKey = configKey, ) - val configKey = - setUpQuickAffordanceModel( - position = KeyguardQuickAffordancePosition.BOTTOM_END, + assertThat(latest()?.isSelected).isTrue() + } + + @Test + fun `endButton - present - visible model - do nothing on click`() = + testScope.runTest { + repository.setKeyguardShowing(true) + val latest = collectLastValue(underTest.endButton) + + val config = + TestConfig( + isVisible = true, + isClickable = true, + icon = mock(), + canShowWhileLocked = false, + intent = + null, // This will cause it to tell the system that the click was handled. + ) + val configKey = + setUpQuickAffordanceModel( + position = KeyguardQuickAffordancePosition.BOTTOM_END, + testConfig = config, + ) + + assertQuickAffordanceViewModel( + viewModel = latest(), testConfig = config, + configKey = configKey, ) - - assertQuickAffordanceViewModel( - viewModel = latest, - testConfig = config, - configKey = configKey, - ) - job.cancel() - } + } @Test - fun `startButton - not present - model is hidden`() = runBlockingTest { - var latest: KeyguardQuickAffordanceViewModel? = null - val job = underTest.startButton.onEach { latest = it }.launchIn(this) + fun `startButton - not present - model is hidden`() = + testScope.runTest { + val latest = collectLastValue(underTest.startButton) - val config = - TestConfig( - isVisible = false, - ) - val configKey = - setUpQuickAffordanceModel( - position = KeyguardQuickAffordancePosition.BOTTOM_START, + val config = + TestConfig( + isVisible = false, + ) + val configKey = + setUpQuickAffordanceModel( + position = KeyguardQuickAffordancePosition.BOTTOM_START, + testConfig = config, + ) + + assertQuickAffordanceViewModel( + viewModel = latest(), testConfig = config, + configKey = configKey, ) - - assertQuickAffordanceViewModel( - viewModel = latest, - testConfig = config, - configKey = configKey, - ) - job.cancel() - } + } @Test - fun animateButtonReveal() = runBlockingTest { - repository.setKeyguardShowing(true) - val testConfig = - TestConfig( - isVisible = true, - isClickable = true, - icon = mock(), - canShowWhileLocked = false, - intent = Intent("action"), + fun animateButtonReveal() = + testScope.runTest { + repository.setKeyguardShowing(true) + val testConfig = + TestConfig( + isVisible = true, + isClickable = true, + icon = mock(), + canShowWhileLocked = false, + intent = Intent("action"), + ) + + setUpQuickAffordanceModel( + position = KeyguardQuickAffordancePosition.BOTTOM_START, + testConfig = testConfig, ) - setUpQuickAffordanceModel( - position = KeyguardQuickAffordancePosition.BOTTOM_START, - testConfig = testConfig, - ) + val value = collectLastValue(underTest.startButton.map { it.animateReveal }) - val values = mutableListOf<Boolean>() - val job = underTest.startButton.onEach { values.add(it.animateReveal) }.launchIn(this) + assertThat(value()).isFalse() + repository.setAnimateDozingTransitions(true) + assertThat(value()).isTrue() + repository.setAnimateDozingTransitions(false) + assertThat(value()).isFalse() + } - repository.setAnimateDozingTransitions(true) - yield() - repository.setAnimateDozingTransitions(false) - yield() + @Test + fun isOverlayContainerVisible() = + testScope.runTest { + val value = collectLastValue(underTest.isOverlayContainerVisible) + + assertThat(value()).isTrue() + repository.setDozing(true) + assertThat(value()).isFalse() + repository.setDozing(false) + assertThat(value()).isTrue() + } - // Note the extra false value in the beginning. This is to cover for the initial value - // inserted by the quick affordance interactor which it does to cover for config - // implementations that don't emit an initial value. - assertThat(values).isEqualTo(listOf(false, false, true, false)) - job.cancel() - } + @Test + fun alpha() = + testScope.runTest { + val value = collectLastValue(underTest.alpha) + + assertThat(value()).isEqualTo(1f) + repository.setBottomAreaAlpha(0.1f) + assertThat(value()).isEqualTo(0.1f) + repository.setBottomAreaAlpha(0.5f) + assertThat(value()).isEqualTo(0.5f) + repository.setBottomAreaAlpha(0.2f) + assertThat(value()).isEqualTo(0.2f) + repository.setBottomAreaAlpha(0f) + assertThat(value()).isEqualTo(0f) + } @Test - fun isOverlayContainerVisible() = runBlockingTest { - val values = mutableListOf<Boolean>() - val job = underTest.isOverlayContainerVisible.onEach(values::add).launchIn(this) + fun `alpha - in preview mode - does not change`() = + testScope.runTest { + underTest.enablePreviewMode(null) + val value = collectLastValue(underTest.alpha) + + assertThat(value()).isEqualTo(1f) + repository.setBottomAreaAlpha(0.1f) + assertThat(value()).isEqualTo(1f) + repository.setBottomAreaAlpha(0.5f) + assertThat(value()).isEqualTo(1f) + repository.setBottomAreaAlpha(0.2f) + assertThat(value()).isEqualTo(1f) + repository.setBottomAreaAlpha(0f) + assertThat(value()).isEqualTo(1f) + } - repository.setDozing(true) - repository.setDozing(false) + @Test + fun isIndicationAreaPadded() = + testScope.runTest { + repository.setKeyguardShowing(true) + val value = collectLastValue(underTest.isIndicationAreaPadded) - assertThat(values).isEqualTo(listOf(true, false, true)) - job.cancel() - } + assertThat(value()).isFalse() + setUpQuickAffordanceModel( + position = KeyguardQuickAffordancePosition.BOTTOM_START, + testConfig = + TestConfig( + isVisible = true, + isClickable = true, + icon = mock(), + canShowWhileLocked = true, + ) + ) + assertThat(value()).isTrue() + setUpQuickAffordanceModel( + position = KeyguardQuickAffordancePosition.BOTTOM_END, + testConfig = + TestConfig( + isVisible = true, + isClickable = true, + icon = mock(), + canShowWhileLocked = false, + ) + ) + assertThat(value()).isTrue() + setUpQuickAffordanceModel( + position = KeyguardQuickAffordancePosition.BOTTOM_START, + testConfig = + TestConfig( + isVisible = false, + ) + ) + assertThat(value()).isTrue() + setUpQuickAffordanceModel( + position = KeyguardQuickAffordancePosition.BOTTOM_END, + testConfig = + TestConfig( + isVisible = false, + ) + ) + assertThat(value()).isFalse() + } @Test - fun alpha() = runBlockingTest { - val values = mutableListOf<Float>() - val job = underTest.alpha.onEach(values::add).launchIn(this) - - repository.setBottomAreaAlpha(0.1f) - repository.setBottomAreaAlpha(0.5f) - repository.setBottomAreaAlpha(0.2f) - repository.setBottomAreaAlpha(0f) + fun indicationAreaTranslationX() = + testScope.runTest { + val value = collectLastValue(underTest.indicationAreaTranslationX) + + assertThat(value()).isEqualTo(0f) + repository.setClockPosition(100, 100) + assertThat(value()).isEqualTo(100f) + repository.setClockPosition(200, 100) + assertThat(value()).isEqualTo(200f) + repository.setClockPosition(200, 200) + assertThat(value()).isEqualTo(200f) + repository.setClockPosition(300, 100) + assertThat(value()).isEqualTo(300f) + } - assertThat(values).isEqualTo(listOf(1f, 0.1f, 0.5f, 0.2f, 0f)) - job.cancel() - } + @Test + fun indicationAreaTranslationY() = + testScope.runTest { + val value = + collectLastValue(underTest.indicationAreaTranslationY(DEFAULT_BURN_IN_OFFSET)) + + // Negative 0 - apparently there's a difference in floating point arithmetic - FML + assertThat(value()).isEqualTo(-0f) + val expected1 = setDozeAmountAndCalculateExpectedTranslationY(0.1f) + assertThat(value()).isEqualTo(expected1) + val expected2 = setDozeAmountAndCalculateExpectedTranslationY(0.2f) + assertThat(value()).isEqualTo(expected2) + val expected3 = setDozeAmountAndCalculateExpectedTranslationY(0.5f) + assertThat(value()).isEqualTo(expected3) + val expected4 = setDozeAmountAndCalculateExpectedTranslationY(1f) + assertThat(value()).isEqualTo(expected4) + } @Test - fun isIndicationAreaPadded() = runBlockingTest { - repository.setKeyguardShowing(true) - val values = mutableListOf<Boolean>() - val job = underTest.isIndicationAreaPadded.onEach(values::add).launchIn(this) - - setUpQuickAffordanceModel( - position = KeyguardQuickAffordancePosition.BOTTOM_START, - testConfig = - TestConfig( - isVisible = true, - isClickable = true, - icon = mock(), - canShowWhileLocked = true, - ) - ) - setUpQuickAffordanceModel( - position = KeyguardQuickAffordancePosition.BOTTOM_END, - testConfig = + fun `isClickable - true when alpha at threshold`() = + testScope.runTest { + repository.setKeyguardShowing(true) + repository.setBottomAreaAlpha( + KeyguardBottomAreaViewModel.AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD + ) + + val testConfig = TestConfig( isVisible = true, isClickable = true, icon = mock(), canShowWhileLocked = false, + intent = Intent("action"), ) - ) - setUpQuickAffordanceModel( - position = KeyguardQuickAffordancePosition.BOTTOM_START, - testConfig = - TestConfig( - isVisible = false, - ) - ) - setUpQuickAffordanceModel( - position = KeyguardQuickAffordancePosition.BOTTOM_END, - testConfig = - TestConfig( - isVisible = false, - ) - ) - - assertThat(values) - .isEqualTo( - listOf( - // Initially, no button is visible so the indication area is not padded. - false, - // Once we add the first visible button, the indication area becomes padded. - // This - // continues to be true after we add the second visible button and even after we - // make the first button not visible anymore. - true, - // Once both buttons are not visible, the indication area is, again, not padded. - false, + val configKey = + setUpQuickAffordanceModel( + position = KeyguardQuickAffordancePosition.BOTTOM_START, + testConfig = testConfig, ) - ) - job.cancel() - } - - @Test - fun indicationAreaTranslationX() = runBlockingTest { - val values = mutableListOf<Float>() - val job = underTest.indicationAreaTranslationX.onEach(values::add).launchIn(this) - repository.setClockPosition(100, 100) - repository.setClockPosition(200, 100) - repository.setClockPosition(200, 200) - repository.setClockPosition(300, 100) + val latest = collectLastValue(underTest.startButton) - assertThat(values).isEqualTo(listOf(0f, 100f, 200f, 300f)) - job.cancel() - } + assertQuickAffordanceViewModel( + viewModel = latest(), + testConfig = testConfig, + configKey = configKey, + ) + } @Test - fun indicationAreaTranslationY() = runBlockingTest { - val values = mutableListOf<Float>() - val job = - underTest - .indicationAreaTranslationY(DEFAULT_BURN_IN_OFFSET) - .onEach(values::add) - .launchIn(this) - - val expectedTranslationValues = - listOf( - -0f, // Negative 0 - apparently there's a difference in floating point arithmetic - - // FML - setDozeAmountAndCalculateExpectedTranslationY(0.1f), - setDozeAmountAndCalculateExpectedTranslationY(0.2f), - setDozeAmountAndCalculateExpectedTranslationY(0.5f), - setDozeAmountAndCalculateExpectedTranslationY(1f), + fun `isClickable - true when alpha above threshold`() = + testScope.runTest { + repository.setKeyguardShowing(true) + val latest = collectLastValue(underTest.startButton) + repository.setBottomAreaAlpha( + min(1f, KeyguardBottomAreaViewModel.AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD + 0.1f), ) - assertThat(values).isEqualTo(expectedTranslationValues) - job.cancel() - } + val testConfig = + TestConfig( + isVisible = true, + isClickable = true, + icon = mock(), + canShowWhileLocked = false, + intent = Intent("action"), + ) + val configKey = + setUpQuickAffordanceModel( + position = KeyguardQuickAffordancePosition.BOTTOM_START, + testConfig = testConfig, + ) - @Test - fun `isClickable - true when alpha at threshold`() = runBlockingTest { - repository.setKeyguardShowing(true) - repository.setBottomAreaAlpha( - KeyguardBottomAreaViewModel.AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD - ) - - val testConfig = - TestConfig( - isVisible = true, - isClickable = true, - icon = mock(), - canShowWhileLocked = false, - intent = Intent("action"), - ) - val configKey = - setUpQuickAffordanceModel( - position = KeyguardQuickAffordancePosition.BOTTOM_START, + assertQuickAffordanceViewModel( + viewModel = latest(), testConfig = testConfig, + configKey = configKey, ) - - var latest: KeyguardQuickAffordanceViewModel? = null - val job = underTest.startButton.onEach { latest = it }.launchIn(this) - // The interactor has an onStart { emit(Hidden) } to cover for upstream configs that don't - // produce an initial value. We yield to give the coroutine time to emit the first real - // value from our config. - yield() - - assertQuickAffordanceViewModel( - viewModel = latest, - testConfig = testConfig, - configKey = configKey, - ) - job.cancel() - } + } @Test - fun `isClickable - true when alpha above threshold`() = runBlockingTest { - repository.setKeyguardShowing(true) - var latest: KeyguardQuickAffordanceViewModel? = null - val job = underTest.startButton.onEach { latest = it }.launchIn(this) - repository.setBottomAreaAlpha( - min(1f, KeyguardBottomAreaViewModel.AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD + 0.1f), - ) - - val testConfig = - TestConfig( - isVisible = true, - isClickable = true, - icon = mock(), - canShowWhileLocked = false, - intent = Intent("action"), - ) - val configKey = - setUpQuickAffordanceModel( - position = KeyguardQuickAffordancePosition.BOTTOM_START, - testConfig = testConfig, + fun `isClickable - false when alpha below threshold`() = + testScope.runTest { + repository.setKeyguardShowing(true) + val latest = collectLastValue(underTest.startButton) + repository.setBottomAreaAlpha( + max(0f, KeyguardBottomAreaViewModel.AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD - 0.1f), ) - assertQuickAffordanceViewModel( - viewModel = latest, - testConfig = testConfig, - configKey = configKey, - ) - job.cancel() - } + val testConfig = + TestConfig( + isVisible = true, + isClickable = false, + icon = mock(), + canShowWhileLocked = false, + intent = Intent("action"), + ) + val configKey = + setUpQuickAffordanceModel( + position = KeyguardQuickAffordancePosition.BOTTOM_START, + testConfig = testConfig, + ) - @Test - fun `isClickable - false when alpha below threshold`() = runBlockingTest { - repository.setKeyguardShowing(true) - var latest: KeyguardQuickAffordanceViewModel? = null - val job = underTest.startButton.onEach { latest = it }.launchIn(this) - repository.setBottomAreaAlpha( - max(0f, KeyguardBottomAreaViewModel.AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD - 0.1f), - ) - - val testConfig = - TestConfig( - isVisible = true, - isClickable = false, - icon = mock(), - canShowWhileLocked = false, - intent = Intent("action"), - ) - val configKey = - setUpQuickAffordanceModel( - position = KeyguardQuickAffordancePosition.BOTTOM_START, + assertQuickAffordanceViewModel( + viewModel = latest(), testConfig = testConfig, + configKey = configKey, ) - - assertQuickAffordanceViewModel( - viewModel = latest, - testConfig = testConfig, - configKey = configKey, - ) - job.cancel() - } + } @Test - fun `isClickable - false when alpha at zero`() = runBlockingTest { - repository.setKeyguardShowing(true) - var latest: KeyguardQuickAffordanceViewModel? = null - val job = underTest.startButton.onEach { latest = it }.launchIn(this) - repository.setBottomAreaAlpha(0f) - - val testConfig = - TestConfig( - isVisible = true, - isClickable = false, - icon = mock(), - canShowWhileLocked = false, - intent = Intent("action"), - ) - val configKey = - setUpQuickAffordanceModel( - position = KeyguardQuickAffordancePosition.BOTTOM_START, + fun `isClickable - false when alpha at zero`() = + testScope.runTest { + repository.setKeyguardShowing(true) + val latest = collectLastValue(underTest.startButton) + repository.setBottomAreaAlpha(0f) + + val testConfig = + TestConfig( + isVisible = true, + isClickable = false, + icon = mock(), + canShowWhileLocked = false, + intent = Intent("action"), + ) + val configKey = + setUpQuickAffordanceModel( + position = KeyguardQuickAffordancePosition.BOTTOM_START, + testConfig = testConfig, + ) + + assertQuickAffordanceViewModel( + viewModel = latest(), testConfig = testConfig, + configKey = configKey, ) + } - assertQuickAffordanceViewModel( - viewModel = latest, - testConfig = testConfig, - configKey = configKey, - ) - job.cancel() - } - - private suspend fun setDozeAmountAndCalculateExpectedTranslationY(dozeAmount: Float): Float { + private fun setDozeAmountAndCalculateExpectedTranslationY(dozeAmount: Float): Float { repository.setDozeAmount(dozeAmount) return dozeAmount * (RETURNED_BURN_IN_OFFSET - DEFAULT_BURN_IN_OFFSET) } @@ -583,7 +615,6 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { when (testConfig.isActivated) { true -> ActivationState.Active false -> ActivationState.Inactive - null -> ActivationState.NotSupported } ) } else { @@ -636,6 +667,5 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { companion object { private const val DEFAULT_BURN_IN_OFFSET = 5 private const val RETURNED_BURN_IN_OFFSET = 3 - private val IMMEDIATE = Dispatchers.Main.immediate } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt index e009e8651f2a..0e7bf8d9d465 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttLoggerTest.kt @@ -22,6 +22,7 @@ import com.android.systemui.dump.DumpManager import com.android.systemui.log.LogBufferFactory import com.android.systemui.plugins.log.LogBuffer import com.android.systemui.plugins.log.LogcatEchoTracker +import com.android.systemui.temporarydisplay.TemporaryViewInfo import com.google.common.truth.Truth.assertThat import java.io.PrintWriter import java.io.StringWriter @@ -33,7 +34,7 @@ import org.mockito.Mockito.mock class MediaTttLoggerTest : SysuiTestCase() { private lateinit var buffer: LogBuffer - private lateinit var logger: MediaTttLogger + private lateinit var logger: MediaTttLogger<TemporaryViewInfo> @Before fun setUp () { diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt index cce3e369c0b8..561867f78e60 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttUtilsTest.kt @@ -25,6 +25,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription import com.android.systemui.common.shared.model.Icon +import com.android.systemui.temporarydisplay.TemporaryViewInfo import com.android.systemui.util.mockito.any import com.google.common.truth.Truth.assertThat import org.junit.Before @@ -40,7 +41,7 @@ class MediaTttUtilsTest : SysuiTestCase() { private lateinit var appIconFromPackageName: Drawable @Mock private lateinit var packageManager: PackageManager @Mock private lateinit var applicationInfo: ApplicationInfo - @Mock private lateinit var logger: MediaTttLogger + @Mock private lateinit var logger: MediaTttLogger<TemporaryViewInfo> @Before fun setUp() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/FakeMediaTttChipControllerReceiver.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/FakeMediaTttChipControllerReceiver.kt index 4aa982ed1609..bad3f0374a31 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/FakeMediaTttChipControllerReceiver.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/FakeMediaTttChipControllerReceiver.kt @@ -27,13 +27,14 @@ import com.android.systemui.media.taptotransfer.common.MediaTttLogger import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.util.concurrency.DelayableExecutor +import com.android.systemui.util.time.SystemClock import com.android.systemui.util.view.ViewUtil import com.android.systemui.util.wakelock.WakeLock class FakeMediaTttChipControllerReceiver( commandQueue: CommandQueue, context: Context, - logger: MediaTttLogger, + logger: MediaTttLogger<ChipReceiverInfo>, windowManager: WindowManager, mainExecutor: DelayableExecutor, accessibilityManager: AccessibilityManager, @@ -44,6 +45,7 @@ class FakeMediaTttChipControllerReceiver( uiEventLogger: MediaTttReceiverUiEventLogger, viewUtil: ViewUtil, wakeLockBuilder: WakeLock.Builder, + systemClock: SystemClock, ) : MediaTttChipControllerReceiver( commandQueue, @@ -59,6 +61,7 @@ class FakeMediaTttChipControllerReceiver( uiEventLogger, viewUtil, wakeLockBuilder, + systemClock, ) { override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) { // Just bypass the animation in tests diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt index 23f7cdb45026..ef0bfb7b6700 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt @@ -67,7 +67,7 @@ class MediaTttChipControllerReceiverTest : SysuiTestCase() { @Mock private lateinit var applicationInfo: ApplicationInfo @Mock - private lateinit var logger: MediaTttLogger + private lateinit var logger: MediaTttLogger<ChipReceiverInfo> @Mock private lateinit var accessibilityManager: AccessibilityManager @Mock @@ -128,6 +128,7 @@ class MediaTttChipControllerReceiverTest : SysuiTestCase() { receiverUiEventLogger, viewUtil, fakeWakeLockBuilder, + fakeClock, ) controllerReceiver.start() @@ -155,6 +156,7 @@ class MediaTttChipControllerReceiverTest : SysuiTestCase() { receiverUiEventLogger, viewUtil, fakeWakeLockBuilder, + fakeClock, ) controllerReceiver.start() @@ -193,6 +195,36 @@ class MediaTttChipControllerReceiverTest : SysuiTestCase() { } @Test + fun commandQueueCallback_transferToReceiverSucceeded_noChipShown() { + commandQueueCallback.updateMediaTapToTransferReceiverDisplay( + StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED, + routeInfo, + null, + null + ) + + verify(windowManager, never()).addView(any(), any()) + assertThat(uiEventLoggerFake.eventId(0)).isEqualTo( + MediaTttReceiverUiEvents.MEDIA_TTT_RECEIVER_TRANSFER_TO_RECEIVER_SUCCEEDED.id + ) + } + + @Test + fun commandQueueCallback_transferToReceiverFailed_noChipShown() { + commandQueueCallback.updateMediaTapToTransferReceiverDisplay( + StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_TRANSFER_TO_RECEIVER_FAILED, + routeInfo, + null, + null + ) + + verify(windowManager, never()).addView(any(), any()) + assertThat(uiEventLoggerFake.eventId(0)).isEqualTo( + MediaTttReceiverUiEvents.MEDIA_TTT_RECEIVER_TRANSFER_TO_RECEIVER_FAILED.id + ) + } + + @Test fun commandQueueCallback_closeThenFar_chipShownThenHidden() { commandQueueCallback.updateMediaTapToTransferReceiverDisplay( StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_CLOSE_TO_SENDER, @@ -214,6 +246,48 @@ class MediaTttChipControllerReceiverTest : SysuiTestCase() { } @Test + fun commandQueueCallback_closeThenSucceeded_chipShownThenHidden() { + commandQueueCallback.updateMediaTapToTransferReceiverDisplay( + StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_CLOSE_TO_SENDER, + routeInfo, + null, + null + ) + + commandQueueCallback.updateMediaTapToTransferReceiverDisplay( + StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_TRANSFER_TO_RECEIVER_SUCCEEDED, + routeInfo, + null, + null + ) + + val viewCaptor = ArgumentCaptor.forClass(View::class.java) + verify(windowManager).addView(viewCaptor.capture(), any()) + verify(windowManager).removeView(viewCaptor.value) + } + + @Test + fun commandQueueCallback_closeThenFailed_chipShownThenHidden() { + commandQueueCallback.updateMediaTapToTransferReceiverDisplay( + StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_CLOSE_TO_SENDER, + routeInfo, + null, + null + ) + + commandQueueCallback.updateMediaTapToTransferReceiverDisplay( + StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_TRANSFER_TO_RECEIVER_FAILED, + routeInfo, + null, + null + ) + + val viewCaptor = ArgumentCaptor.forClass(View::class.java) + verify(windowManager).addView(viewCaptor.capture(), any()) + verify(windowManager).removeView(viewCaptor.value) + } + + @Test fun commandQueueCallback_closeThenFar_wakeLockAcquiredThenReleased() { commandQueueCallback.updateMediaTapToTransferReceiverDisplay( StatusBarManager.MEDIA_TRANSFER_RECEIVER_STATE_CLOSE_TO_SENDER, diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt index 311740e17310..b03a545f787f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt @@ -45,6 +45,7 @@ import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator +import com.android.systemui.temporarydisplay.chipbar.ChipbarInfo import com.android.systemui.temporarydisplay.chipbar.ChipbarLogger import com.android.systemui.temporarydisplay.chipbar.FakeChipbarCoordinator import com.android.systemui.util.concurrency.FakeExecutor @@ -83,7 +84,7 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { @Mock private lateinit var falsingManager: FalsingManager @Mock private lateinit var falsingCollector: FalsingCollector @Mock private lateinit var chipbarLogger: ChipbarLogger - @Mock private lateinit var logger: MediaTttLogger + @Mock private lateinit var logger: MediaTttLogger<ChipbarInfo> @Mock private lateinit var mediaTttFlags: MediaTttFlags @Mock private lateinit var packageManager: PackageManager @Mock private lateinit var powerManager: PowerManager @@ -142,6 +143,7 @@ class MediaTttSenderCoordinatorTest : SysuiTestCase() { viewUtil, vibratorHelper, fakeWakeLockBuilder, + fakeClock, ) chipbarCoordinator.start() diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogTest.java index 3ae842862366..b59005aa8dda 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogTest.java @@ -86,7 +86,8 @@ public class InternetDialogTest extends SysuiTestCase { private View mDialogView; private View mSubTitle; private LinearLayout mEthernet; - private LinearLayout mMobileDataToggle; + private LinearLayout mMobileDataLayout; + private Switch mMobileToggleSwitch; private LinearLayout mWifiToggle; private Switch mWifiToggleSwitch; private TextView mWifiToggleSummary; @@ -135,7 +136,8 @@ public class InternetDialogTest extends SysuiTestCase { mDialogView = mInternetDialog.mDialogView; mSubTitle = mDialogView.requireViewById(R.id.internet_dialog_subtitle); mEthernet = mDialogView.requireViewById(R.id.ethernet_layout); - mMobileDataToggle = mDialogView.requireViewById(R.id.mobile_network_layout); + mMobileDataLayout = mDialogView.requireViewById(R.id.mobile_network_layout); + mMobileToggleSwitch = mDialogView.requireViewById(R.id.mobile_toggle); mWifiToggle = mDialogView.requireViewById(R.id.turn_on_wifi_layout); mWifiToggleSwitch = mDialogView.requireViewById(R.id.wifi_toggle); mWifiToggleSummary = mDialogView.requireViewById(R.id.wifi_toggle_summary); @@ -236,7 +238,7 @@ public class InternetDialogTest extends SysuiTestCase { mInternetDialog.updateDialog(true); - assertThat(mMobileDataToggle.getVisibility()).isEqualTo(View.GONE); + assertThat(mMobileDataLayout.getVisibility()).isEqualTo(View.GONE); } @Test @@ -248,7 +250,7 @@ public class InternetDialogTest extends SysuiTestCase { mInternetDialog.updateDialog(true); - assertThat(mMobileDataToggle.getVisibility()).isEqualTo(View.GONE); + assertThat(mMobileDataLayout.getVisibility()).isEqualTo(View.GONE); // Carrier network should be visible if airplane mode ON and Wi-Fi is ON. when(mInternetDialogController.isCarrierNetworkActive()).thenReturn(true); @@ -257,7 +259,7 @@ public class InternetDialogTest extends SysuiTestCase { mInternetDialog.updateDialog(true); - assertThat(mMobileDataToggle.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mMobileDataLayout.getVisibility()).isEqualTo(View.VISIBLE); } @Test @@ -267,7 +269,7 @@ public class InternetDialogTest extends SysuiTestCase { mInternetDialog.updateDialog(true); - assertThat(mMobileDataToggle.getVisibility()).isEqualTo(View.GONE); + assertThat(mMobileDataLayout.getVisibility()).isEqualTo(View.GONE); } @Test @@ -279,7 +281,7 @@ public class InternetDialogTest extends SysuiTestCase { mInternetDialog.updateDialog(true); - assertThat(mMobileDataToggle.getVisibility()).isEqualTo(View.VISIBLE); + assertThat(mMobileDataLayout.getVisibility()).isEqualTo(View.VISIBLE); assertThat(mAirplaneModeSummaryText.getVisibility()).isEqualTo(View.VISIBLE); } @@ -316,6 +318,30 @@ public class InternetDialogTest extends SysuiTestCase { } @Test + public void updateDialog_mobileDataIsEnabled_checkMobileDataSwitch() { + doReturn(true).when(mInternetDialogController).hasActiveSubId(); + when(mInternetDialogController.isCarrierNetworkActive()).thenReturn(true); + when(mInternetDialogController.isMobileDataEnabled()).thenReturn(true); + mMobileToggleSwitch.setChecked(false); + + mInternetDialog.updateDialog(true); + + assertThat(mMobileToggleSwitch.isChecked()).isTrue(); + } + + @Test + public void updateDialog_mobileDataIsNotChanged_checkMobileDataSwitch() { + doReturn(true).when(mInternetDialogController).hasActiveSubId(); + when(mInternetDialogController.isCarrierNetworkActive()).thenReturn(true); + when(mInternetDialogController.isMobileDataEnabled()).thenReturn(false); + mMobileToggleSwitch.setChecked(false); + + mInternetDialog.updateDialog(true); + + assertThat(mMobileToggleSwitch.isChecked()).isFalse(); + } + + @Test public void updateDialog_wifiOnAndHasInternetWifi_showConnectedWifi() { mInternetDialog.dismissDialog(); doReturn(true).when(mInternetDialogController).hasActiveSubId(); @@ -695,7 +721,7 @@ public class InternetDialogTest extends SysuiTestCase { private void setNetworkVisible(boolean ethernetVisible, boolean mobileDataVisible, boolean connectedWifiVisible) { mEthernet.setVisibility(ethernetVisible ? View.VISIBLE : View.GONE); - mMobileDataToggle.setVisibility(mobileDataVisible ? View.VISIBLE : View.GONE); + mMobileDataLayout.setVisibility(mobileDataVisible ? View.VISIBLE : View.GONE); mConnectedWifi.setVisibility(connectedWifiVisible ? View.VISIBLE : View.GONE); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java index 0302dade0a8c..351274913323 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java @@ -1102,6 +1102,17 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { mStatusBarStateController.setState(KEYGUARD); + assertThat(mNotificationPanelViewController.isQsExpanded()).isEqualTo(false); + assertThat(mNotificationPanelViewController.isQsExpandImmediate()).isEqualTo(false); + } + + @Test + public void testLockedSplitShadeTransitioningToKeyguard_closesQS() { + enableSplitShade(true); + mStatusBarStateController.setState(SHADE_LOCKED); + mNotificationPanelViewController.setQsExpanded(true); + + mStatusBarStateController.setState(KEYGUARD); assertThat(mNotificationPanelViewController.isQsExpanded()).isEqualTo(false); assertThat(mNotificationPanelViewController.isQsExpandImmediate()).isEqualTo(false); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLoggerTest.kt new file mode 100644 index 000000000000..33b94e39c019 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLoggerTest.kt @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.logging + +import android.app.Notification +import android.app.StatsManager +import android.graphics.Bitmap +import android.graphics.drawable.Icon +import android.testing.AndroidTestingRunner +import android.util.StatsEvent +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.shared.system.SysUiStatsLog +import com.android.systemui.statusbar.notification.collection.NotifPipeline +import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.time.FakeSystemClock +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class NotificationMemoryLoggerTest : SysuiTestCase() { + + private val bgExecutor = FakeExecutor(FakeSystemClock()) + private val immediate = Dispatchers.Main.immediate + + @Mock private lateinit var statsManager: StatsManager + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + } + + @Test + fun onInit_registersCallback() { + val logger = createLoggerWithNotifications(listOf()) + logger.init() + verify(statsManager) + .setPullAtomCallback(SysUiStatsLog.NOTIFICATION_MEMORY_USE, null, bgExecutor, logger) + } + + @Test + fun onPullAtom_wrongAtomId_returnsSkip() { + val logger = createLoggerWithNotifications(listOf()) + val data: MutableList<StatsEvent> = mutableListOf() + assertThat(logger.onPullAtom(111, data)).isEqualTo(StatsManager.PULL_SKIP) + assertThat(data).isEmpty() + } + + @Test + fun onPullAtom_emptyNotifications_returnsZeros() { + val logger = createLoggerWithNotifications(listOf()) + val data: MutableList<StatsEvent> = mutableListOf() + assertThat(logger.onPullAtom(SysUiStatsLog.NOTIFICATION_MEMORY_USE, data)) + .isEqualTo(StatsManager.PULL_SUCCESS) + assertThat(data).isEmpty() + } + + @Test + fun onPullAtom_notificationPassed_populatesData() { + val icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)) + val notification = + Notification.Builder(context).setSmallIcon(icon).setContentTitle("title").build() + val logger = createLoggerWithNotifications(listOf(notification)) + val data: MutableList<StatsEvent> = mutableListOf() + + assertThat(logger.onPullAtom(SysUiStatsLog.NOTIFICATION_MEMORY_USE, data)) + .isEqualTo(StatsManager.PULL_SUCCESS) + assertThat(data).hasSize(1) + } + + @Test + fun onPullAtom_multipleNotificationsPassed_populatesData() { + val icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)) + val notification = + Notification.Builder(context).setSmallIcon(icon).setContentTitle("title").build() + val iconTwo = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)) + + val notificationTwo = + Notification.Builder(context) + .setStyle(Notification.BigTextStyle().bigText("text")) + .setSmallIcon(iconTwo) + .setContentTitle("titleTwo") + .build() + val logger = createLoggerWithNotifications(listOf(notification, notificationTwo)) + val data: MutableList<StatsEvent> = mutableListOf() + + assertThat(logger.onPullAtom(SysUiStatsLog.NOTIFICATION_MEMORY_USE, data)) + .isEqualTo(StatsManager.PULL_SUCCESS) + assertThat(data).hasSize(2) + } + + private fun createLoggerWithNotifications( + notifications: List<Notification> + ): NotificationMemoryLogger { + val pipeline: NotifPipeline = mock() + val notifications = + notifications.map { notification -> + NotificationEntryBuilder().setTag("test").setNotification(notification).build() + } + whenever(pipeline.allNotifs).thenReturn(notifications) + return NotificationMemoryLogger(pipeline, statsManager, immediate, bgExecutor) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMeterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMeterTest.kt index f69839b7087c..072a497f1a65 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMeterTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMeterTest.kt @@ -23,6 +23,7 @@ import android.app.Person import android.content.Intent import android.graphics.Bitmap import android.graphics.drawable.Icon +import android.stats.sysui.NotificationEnums import android.testing.AndroidTestingRunner import android.widget.RemoteViews import androidx.test.filters.SmallTest @@ -50,7 +51,27 @@ class NotificationMemoryMeterTest : SysuiTestCase() { extras = 3316, bigPicture = 0, extender = 0, - style = null, + style = NotificationEnums.STYLE_NONE, + styleIcon = 0, + hasCustomView = false, + ) + } + + @Test + fun currentNotificationMemoryUse_rankerGroupNotification() { + val notification = createBasicNotification().build() + val memoryUse = + NotificationMemoryMeter.notificationMemoryUse( + createNotificationEntry(createBasicNotification().setGroup("ranker_group").build()) + ) + assertNotificationObjectSizes( + memoryUse, + smallIcon = notification.smallIcon.bitmap.allocationByteCount, + largeIcon = notification.getLargeIcon().bitmap.allocationByteCount, + extras = 3316, + bigPicture = 0, + extender = 0, + style = NotificationEnums.STYLE_RANKER_GROUP, styleIcon = 0, hasCustomView = false, ) @@ -69,7 +90,7 @@ class NotificationMemoryMeterTest : SysuiTestCase() { extras = 3316, bigPicture = 0, extender = 0, - style = null, + style = NotificationEnums.STYLE_NONE, styleIcon = 0, hasCustomView = false, ) @@ -92,7 +113,7 @@ class NotificationMemoryMeterTest : SysuiTestCase() { extras = 3384, bigPicture = 0, extender = 0, - style = null, + style = NotificationEnums.STYLE_NONE, styleIcon = 0, hasCustomView = true, ) @@ -112,7 +133,7 @@ class NotificationMemoryMeterTest : SysuiTestCase() { extras = 3212, bigPicture = 0, extender = 0, - style = null, + style = NotificationEnums.STYLE_NONE, styleIcon = 0, hasCustomView = false, ) @@ -141,7 +162,7 @@ class NotificationMemoryMeterTest : SysuiTestCase() { extras = 4092, bigPicture = bigPicture.bitmap.allocationByteCount, extender = 0, - style = "BigPictureStyle", + style = NotificationEnums.STYLE_BIG_PICTURE, styleIcon = bigPictureIcon.bitmap.allocationByteCount, hasCustomView = false, ) @@ -167,7 +188,7 @@ class NotificationMemoryMeterTest : SysuiTestCase() { extras = 4084, bigPicture = 0, extender = 0, - style = "CallStyle", + style = NotificationEnums.STYLE_CALL, styleIcon = personIcon.bitmap.allocationByteCount, hasCustomView = false, ) @@ -203,7 +224,7 @@ class NotificationMemoryMeterTest : SysuiTestCase() { extras = 5024, bigPicture = 0, extender = 0, - style = "MessagingStyle", + style = NotificationEnums.STYLE_MESSAGING, styleIcon = personIcon.bitmap.allocationByteCount + historicPersonIcon.bitmap.allocationByteCount, @@ -225,7 +246,7 @@ class NotificationMemoryMeterTest : SysuiTestCase() { extras = 3612, bigPicture = 0, extender = 556656, - style = null, + style = NotificationEnums.STYLE_NONE, styleIcon = 0, hasCustomView = false, ) @@ -246,7 +267,7 @@ class NotificationMemoryMeterTest : SysuiTestCase() { extras = 3820, bigPicture = 0, extender = 388 + wearBackground.allocationByteCount, - style = null, + style = NotificationEnums.STYLE_NONE, styleIcon = 0, hasCustomView = false, ) @@ -272,7 +293,7 @@ class NotificationMemoryMeterTest : SysuiTestCase() { extras: Int, bigPicture: Int, extender: Int, - style: String?, + style: Int, styleIcon: Int, hasCustomView: Boolean, ) { @@ -282,11 +303,7 @@ class NotificationMemoryMeterTest : SysuiTestCase() { assertThat(memoryUse.objectUsage.smallIcon).isEqualTo(smallIcon) assertThat(memoryUse.objectUsage.largeIcon).isEqualTo(largeIcon) assertThat(memoryUse.objectUsage.bigPicture).isEqualTo(bigPicture) - if (style == null) { - assertThat(memoryUse.objectUsage.style).isNull() - } else { - assertThat(memoryUse.objectUsage.style).isEqualTo(style) - } + assertThat(memoryUse.objectUsage.style).isEqualTo(style) assertThat(memoryUse.objectUsage.styleIcon).isEqualTo(styleIcon) assertThat(memoryUse.objectUsage.hasCustomView).isEqualTo(hasCustomView) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalkerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalkerTest.kt index 3a16fb33388b..a0f50486ffff 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalkerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalkerTest.kt @@ -8,6 +8,7 @@ import android.testing.TestableLooper import android.widget.RemoteViews import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder import com.android.systemui.statusbar.notification.row.NotificationTestHelper import com.android.systemui.tests.R import com.google.common.truth.Truth.assertThat @@ -39,16 +40,84 @@ class NotificationMemoryViewWalkerTest : SysuiTestCase() { fun testViewWalker_plainNotification() { val row = testHelper.createRow() val result = NotificationMemoryViewWalker.getViewUsage(row) - assertThat(result).hasSize(5) - assertThat(result).contains(NotificationViewUsage(ViewType.PUBLIC_VIEW, 0, 0, 0, 0, 0, 0)) - assertThat(result) - .contains(NotificationViewUsage(ViewType.PRIVATE_HEADS_UP_VIEW, 0, 0, 0, 0, 0, 0)) + assertThat(result).hasSize(3) assertThat(result) .contains(NotificationViewUsage(ViewType.PRIVATE_EXPANDED_VIEW, 0, 0, 0, 0, 0, 0)) assertThat(result) .contains(NotificationViewUsage(ViewType.PRIVATE_CONTRACTED_VIEW, 0, 0, 0, 0, 0, 0)) + assertThat(result).contains(NotificationViewUsage(ViewType.TOTAL, 0, 0, 0, 0, 0, 0)) + } + + @Test + fun testViewWalker_plainNotification_withPublicView() { + val icon = Icon.createWithBitmap(Bitmap.createBitmap(20, 20, Bitmap.Config.ARGB_8888)) + val publicIcon = Icon.createWithBitmap(Bitmap.createBitmap(40, 40, Bitmap.Config.ARGB_8888)) + testHelper.setDefaultInflationFlags(NotificationRowContentBinder.FLAG_CONTENT_VIEW_ALL) + val row = + testHelper.createRow( + Notification.Builder(mContext) + .setContentText("Test") + .setContentTitle("title") + .setSmallIcon(icon) + .setPublicVersion( + Notification.Builder(mContext) + .setContentText("Public Test") + .setContentTitle("title") + .setSmallIcon(publicIcon) + .build() + ) + .build() + ) + val result = NotificationMemoryViewWalker.getViewUsage(row) + assertThat(result).hasSize(4) assertThat(result) - .contains(NotificationViewUsage(ViewType.PRIVATE_HEADS_UP_VIEW, 0, 0, 0, 0, 0, 0)) + .contains( + NotificationViewUsage( + ViewType.PRIVATE_EXPANDED_VIEW, + icon.bitmap.allocationByteCount, + 0, + 0, + 0, + 0, + icon.bitmap.allocationByteCount + ) + ) + assertThat(result) + .contains( + NotificationViewUsage( + ViewType.PRIVATE_CONTRACTED_VIEW, + icon.bitmap.allocationByteCount, + 0, + 0, + 0, + 0, + icon.bitmap.allocationByteCount + ) + ) + assertThat(result) + .contains( + NotificationViewUsage( + ViewType.PUBLIC_VIEW, + publicIcon.bitmap.allocationByteCount, + 0, + 0, + 0, + 0, + publicIcon.bitmap.allocationByteCount + ) + ) + assertThat(result) + .contains( + NotificationViewUsage( + ViewType.TOTAL, + icon.bitmap.allocationByteCount + publicIcon.bitmap.allocationByteCount, + 0, + 0, + 0, + 0, + icon.bitmap.allocationByteCount + publicIcon.bitmap.allocationByteCount + ) + ) } @Test @@ -67,7 +136,7 @@ class NotificationMemoryViewWalkerTest : SysuiTestCase() { .build() ) val result = NotificationMemoryViewWalker.getViewUsage(row) - assertThat(result).hasSize(5) + assertThat(result).hasSize(3) assertThat(result) .contains( NotificationViewUsage( @@ -95,8 +164,20 @@ class NotificationMemoryViewWalkerTest : SysuiTestCase() { icon.bitmap.allocationByteCount + largeIcon.bitmap.allocationByteCount ) ) - // Due to deduplication, this should all be 0. - assertThat(result).contains(NotificationViewUsage(ViewType.PUBLIC_VIEW, 0, 0, 0, 0, 0, 0)) + assertThat(result) + .contains( + NotificationViewUsage( + ViewType.TOTAL, + icon.bitmap.allocationByteCount, + largeIcon.bitmap.allocationByteCount, + 0, + bigPicture.allocationByteCount, + 0, + bigPicture.allocationByteCount + + icon.bitmap.allocationByteCount + + largeIcon.bitmap.allocationByteCount + ) + ) } @Test @@ -117,7 +198,7 @@ class NotificationMemoryViewWalkerTest : SysuiTestCase() { .build() ) val result = NotificationMemoryViewWalker.getViewUsage(row) - assertThat(result).hasSize(5) + assertThat(result).hasSize(3) assertThat(result) .contains( NotificationViewUsage( @@ -142,7 +223,17 @@ class NotificationMemoryViewWalkerTest : SysuiTestCase() { bitmap.allocationByteCount + icon.bitmap.allocationByteCount ) ) - // Due to deduplication, this should all be 0. - assertThat(result).contains(NotificationViewUsage(ViewType.PUBLIC_VIEW, 0, 0, 0, 0, 0, 0)) + assertThat(result) + .contains( + NotificationViewUsage( + ViewType.TOTAL, + icon.bitmap.allocationByteCount, + 0, + 0, + 0, + bitmap.allocationByteCount, + bitmap.allocationByteCount + icon.bitmap.allocationByteCount + ) + ) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt index 4d9db8c28e07..58325697a408 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt @@ -518,7 +518,7 @@ class StackScrollAlgorithmTest : SysuiTestCase() { val childHunView = createHunViewMock( isShadeOpen = true, fullyVisible = false, - headerVisibleAmount = 1f + headerVisibleAmount = 1f, ) val algorithmState = StackScrollAlgorithm.StackScrollAlgorithmState() algorithmState.visibleChildren.add(childHunView) @@ -526,7 +526,6 @@ class StackScrollAlgorithmTest : SysuiTestCase() { // When: updateChildZValue() is called for the top HUN stackScrollAlgorithm.updateChildZValue( /* i= */ 0, - /* childrenOnTop= */ 0.0f, /* StackScrollAlgorithmState= */ algorithmState, /* ambientState= */ ambientState, /* shouldElevateHun= */ true @@ -546,7 +545,7 @@ class StackScrollAlgorithmTest : SysuiTestCase() { val childHunView = createHunViewMock( isShadeOpen = true, fullyVisible = false, - headerVisibleAmount = 1f + headerVisibleAmount = 1f, ) // Use half of the HUN's height as overlap childHunView.viewState.yTranslation = (childHunView.viewState.height + 1 shr 1).toFloat() @@ -556,7 +555,6 @@ class StackScrollAlgorithmTest : SysuiTestCase() { // When: updateChildZValue() is called for the top HUN stackScrollAlgorithm.updateChildZValue( /* i= */ 0, - /* childrenOnTop= */ 0.0f, /* StackScrollAlgorithmState= */ algorithmState, /* ambientState= */ ambientState, /* shouldElevateHun= */ true @@ -580,7 +578,7 @@ class StackScrollAlgorithmTest : SysuiTestCase() { val childHunView = createHunViewMock( isShadeOpen = true, fullyVisible = true, - headerVisibleAmount = 1f + headerVisibleAmount = 1f, ) // HUN doesn't overlap with QQS Panel childHunView.viewState.yTranslation = ambientState.topPadding + @@ -591,7 +589,6 @@ class StackScrollAlgorithmTest : SysuiTestCase() { // When: updateChildZValue() is called for the top HUN stackScrollAlgorithm.updateChildZValue( /* i= */ 0, - /* childrenOnTop= */ 0.0f, /* StackScrollAlgorithmState= */ algorithmState, /* ambientState= */ ambientState, /* shouldElevateHun= */ true @@ -611,7 +608,7 @@ class StackScrollAlgorithmTest : SysuiTestCase() { val childHunView = createHunViewMock( isShadeOpen = false, fullyVisible = false, - headerVisibleAmount = 0f + headerVisibleAmount = 0f, ) childHunView.viewState.yTranslation = 0f // Shade is closed, thus childHunView's headerVisibleAmount is 0 @@ -622,7 +619,6 @@ class StackScrollAlgorithmTest : SysuiTestCase() { // When: updateChildZValue() is called for the top HUN stackScrollAlgorithm.updateChildZValue( /* i= */ 0, - /* childrenOnTop= */ 0.0f, /* StackScrollAlgorithmState= */ algorithmState, /* ambientState= */ ambientState, /* shouldElevateHun= */ true @@ -642,7 +638,7 @@ class StackScrollAlgorithmTest : SysuiTestCase() { val childHunView = createHunViewMock( isShadeOpen = false, fullyVisible = false, - headerVisibleAmount = 0.5f + headerVisibleAmount = 0.5f, ) childHunView.viewState.yTranslation = 0f // Shade is being opened, thus childHunView's headerVisibleAmount is between 0 and 1 @@ -654,7 +650,6 @@ class StackScrollAlgorithmTest : SysuiTestCase() { // When: updateChildZValue() is called for the top HUN stackScrollAlgorithm.updateChildZValue( /* i= */ 0, - /* childrenOnTop= */ 0.0f, /* StackScrollAlgorithmState= */ algorithmState, /* ambientState= */ ambientState, /* shouldElevateHun= */ true @@ -669,7 +664,7 @@ class StackScrollAlgorithmTest : SysuiTestCase() { private fun createHunViewMock( isShadeOpen: Boolean, fullyVisible: Boolean, - headerVisibleAmount: Float + headerVisibleAmount: Float, ) = mock<ExpandableNotificationRow>().apply { val childViewStateMock = createHunChildViewState(isShadeOpen, fullyVisible) @@ -680,7 +675,10 @@ class StackScrollAlgorithmTest : SysuiTestCase() { } - private fun createHunChildViewState(isShadeOpen: Boolean, fullyVisible: Boolean) = + private fun createHunChildViewState( + isShadeOpen: Boolean, + fullyVisible: Boolean, + ) = ExpandableViewState().apply { // Mock the HUN's height with ambientState.topPadding + // ambientState.stackTranslation diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt index 09f0d4a10410..82153d5610a5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt @@ -35,6 +35,7 @@ import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.capture import com.android.systemui.util.time.FakeSystemClock +import com.android.systemui.util.time.SystemClock import com.android.systemui.util.wakelock.WakeLock import com.android.systemui.util.wakelock.WakeLockFake import com.google.common.truth.Truth.assertThat @@ -59,7 +60,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { private lateinit var fakeWakeLock: WakeLockFake @Mock - private lateinit var logger: TemporaryViewLogger + private lateinit var logger: TemporaryViewLogger<ViewInfo> @Mock private lateinit var accessibilityManager: AccessibilityManager @Mock @@ -74,7 +75,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) whenever(accessibilityManager.getRecommendedTimeoutMillis(any(), any())) - .thenReturn(TIMEOUT_MS.toInt()) + .thenAnswer { it.arguments[0] } fakeClock = FakeSystemClock() fakeExecutor = FakeExecutor(fakeClock) @@ -84,14 +85,15 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { fakeWakeLockBuilder.setWakeLock(fakeWakeLock) underTest = TestController( - context, - logger, - windowManager, - fakeExecutor, - accessibilityManager, - configurationController, - powerManager, - fakeWakeLockBuilder, + context, + logger, + windowManager, + fakeExecutor, + accessibilityManager, + configurationController, + powerManager, + fakeWakeLockBuilder, + fakeClock, ) underTest.start() } @@ -112,14 +114,14 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { @Test fun displayView_logged() { - underTest.displayView( - ViewInfo( - name = "name", - windowTitle = "Fake Window Title", - ) + val info = ViewInfo( + name = "name", + windowTitle = "Fake Window Title", ) - verify(logger).logViewAddition("id", "Fake Window Title") + underTest.displayView(info) + + verify(logger).logViewAddition(info) } @Test @@ -168,10 +170,11 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { } @Test - fun displayView_twiceWithDifferentWindowTitles_oldViewRemovedNewViewAdded() { + fun displayView_twiceWithDifferentIds_oldViewRemovedNewViewAdded() { underTest.displayView( ViewInfo( name = "name", + id = "First", windowTitle = "First Fake Window Title", ) ) @@ -179,6 +182,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { underTest.displayView( ViewInfo( name = "name", + id = "Second", windowTitle = "Second Fake Window Title", ) ) @@ -263,19 +267,69 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { } @Test + fun viewUpdatedWithNewOnViewTimeoutRunnable_newRunnableUsed() { + var runnable1Run = false + underTest.displayView(ViewInfo(name = "name", id = "id1", windowTitle = "1")) { + runnable1Run = true + } + + var runnable2Run = false + underTest.displayView(ViewInfo(name = "name", id = "id1", windowTitle = "1")) { + runnable2Run = true + } + + fakeClock.advanceTime(TIMEOUT_MS + 1) + + assertThat(runnable1Run).isFalse() + assertThat(runnable2Run).isTrue() + } + + @Test + fun multipleViewsWithDifferentIds_moreRecentReplacesOlder() { + underTest.displayView( + ViewInfo( + name = "name", + windowTitle = "First Fake Window Title", + id = "id1" + ) + ) + + underTest.displayView( + ViewInfo( + name = "name", + windowTitle = "Second Fake Window Title", + id = "id2" + ) + ) + + val viewCaptor = argumentCaptor<View>() + val windowParamsCaptor = argumentCaptor<WindowManager.LayoutParams>() + + verify(windowManager, times(2)).addView(capture(viewCaptor), capture(windowParamsCaptor)) + + assertThat(windowParamsCaptor.allValues[0].title).isEqualTo("First Fake Window Title") + assertThat(windowParamsCaptor.allValues[1].title).isEqualTo("Second Fake Window Title") + verify(windowManager).removeView(viewCaptor.allValues[0]) + verify(configurationController, never()).removeCallback(any()) + } + + @Test fun multipleViewsWithDifferentIds_recentActiveViewIsDisplayed() { underTest.displayView(ViewInfo("First name", id = "id1")) verify(windowManager).addView(any(), any()) - reset(windowManager) + underTest.displayView(ViewInfo("Second name", id = "id2")) - underTest.removeView("id2", "test reason") verify(windowManager).removeView(any()) + verify(windowManager).addView(any(), any()) + reset(windowManager) - fakeClock.advanceTime(DISPLAY_VIEW_DELAY + 1) + underTest.removeView("id2", "test reason") + verify(windowManager).removeView(any()) + verify(windowManager).addView(any(), any()) assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id1") assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("First name") @@ -284,6 +338,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { verify(windowManager).removeView(any()) assertThat(underTest.activeViews.size).isEqualTo(0) + verify(configurationController).removeCallback(any()) } @Test @@ -291,19 +346,28 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { underTest.displayView(ViewInfo("First name", id = "id1")) verify(windowManager).addView(any(), any()) - reset(windowManager) + underTest.displayView(ViewInfo("Second name", id = "id2")) + + verify(windowManager).removeView(any()) + verify(windowManager).addView(any(), any()) + reset(windowManager) + + // WHEN an old view is removed underTest.removeView("id1", "test reason") + // THEN we don't update anything verify(windowManager, never()).removeView(any()) assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id2") assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("Second name") + verify(configurationController, never()).removeCallback(any()) fakeClock.advanceTime(TIMEOUT_MS + 1) verify(windowManager).removeView(any()) assertThat(underTest.activeViews.size).isEqualTo(0) + verify(configurationController).removeCallback(any()) } @Test @@ -312,33 +376,31 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { underTest.displayView(ViewInfo("Second name", id = "id2")) underTest.displayView(ViewInfo("Third name", id = "id3")) - verify(windowManager).addView(any(), any()) + verify(windowManager, times(3)).addView(any(), any()) + verify(windowManager, times(2)).removeView(any()) reset(windowManager) underTest.removeView("id3", "test reason") verify(windowManager).removeView(any()) - - fakeClock.advanceTime(DISPLAY_VIEW_DELAY + 1) - assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id2") assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("Second name") + verify(configurationController, never()).removeCallback(any()) reset(windowManager) underTest.removeView("id2", "test reason") verify(windowManager).removeView(any()) - - fakeClock.advanceTime(DISPLAY_VIEW_DELAY + 1) - assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id1") assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("First name") + verify(configurationController, never()).removeCallback(any()) reset(windowManager) fakeClock.advanceTime(TIMEOUT_MS + 1) verify(windowManager).removeView(any()) assertThat(underTest.activeViews.size).isEqualTo(0) + verify(configurationController).removeCallback(any()) } @Test @@ -347,18 +409,21 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { underTest.displayView(ViewInfo("New name", id = "id1")) verify(windowManager).addView(any(), any()) - reset(windowManager) + underTest.displayView(ViewInfo("Second name", id = "id2")) - underTest.removeView("id2", "test reason") verify(windowManager).removeView(any()) + verify(windowManager).addView(any(), any()) + reset(windowManager) - fakeClock.advanceTime(DISPLAY_VIEW_DELAY + 1) + underTest.removeView("id2", "test reason") + verify(windowManager).removeView(any()) + verify(windowManager).addView(any(), any()) assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id1") assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("New name") - assertThat(underTest.activeViews[0].second.name).isEqualTo("New name") + assertThat(underTest.activeViews[0].info.name).isEqualTo("New name") reset(windowManager) fakeClock.advanceTime(TIMEOUT_MS + 1) @@ -368,19 +433,523 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { } @Test - fun multipleViewsWithDifferentIds_viewsTimeouts_noViewLeftToDisplay() { - underTest.displayView(ViewInfo("First name", id = "id1")) - fakeClock.advanceTime(TIMEOUT_MS / 3) - underTest.displayView(ViewInfo("Second name", id = "id2")) - fakeClock.advanceTime(TIMEOUT_MS / 3) - underTest.displayView(ViewInfo("Third name", id = "id3")) + fun multipleViews_mostRecentViewRemoved_otherViewsTimedOutAndNotDisplayed() { + underTest.displayView(ViewInfo("First name", id = "id1", timeoutMs = 4000)) + fakeClock.advanceTime(1000) + underTest.displayView(ViewInfo("Second name", id = "id2", timeoutMs = 4000)) + fakeClock.advanceTime(1000) + underTest.displayView(ViewInfo("Third name", id = "id3", timeoutMs = 20000)) reset(windowManager) - fakeClock.advanceTime(TIMEOUT_MS + 1) + fakeClock.advanceTime(20000 + 1) verify(windowManager).removeView(any()) verify(windowManager, never()).addView(any(), any()) assertThat(underTest.activeViews.size).isEqualTo(0) + verify(configurationController).removeCallback(any()) + } + + @Test + fun multipleViews_mostRecentViewRemoved_viewWithShortTimeLeftNotDisplayed() { + underTest.displayView(ViewInfo("First name", id = "id1", timeoutMs = 4000)) + fakeClock.advanceTime(1000) + underTest.displayView(ViewInfo("Second name", id = "id2", timeoutMs = 2500)) + + reset(windowManager) + fakeClock.advanceTime(2500 + 1) + // At this point, 3501ms have passed, so id1 only has 499ms left which is not enough. + // So, it shouldn't be displayed. + + verify(windowManager, never()).addView(any(), any()) + assertThat(underTest.activeViews.size).isEqualTo(0) + verify(configurationController).removeCallback(any()) + } + + @Test + fun lowerThenHigherPriority_higherReplacesLower() { + underTest.displayView( + ViewInfo( + name = "normal", + windowTitle = "Normal Window Title", + id = "normal", + priority = ViewPriority.NORMAL, + ) + ) + + val viewCaptor = argumentCaptor<View>() + val windowParamsCaptor = argumentCaptor<WindowManager.LayoutParams>() + verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor)) + assertThat(windowParamsCaptor.value.title).isEqualTo("Normal Window Title") + reset(windowManager) + + underTest.displayView( + ViewInfo( + name = "critical", + windowTitle = "Critical Window Title", + id = "critical", + priority = ViewPriority.CRITICAL, + ) + ) + + verify(windowManager).removeView(viewCaptor.value) + verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor)) + assertThat(windowParamsCaptor.value.title).isEqualTo("Critical Window Title") + verify(configurationController, never()).removeCallback(any()) + } + + @Test + fun lowerThenHigherPriority_lowerPriorityRedisplayed() { + underTest.displayView( + ViewInfo( + name = "normal", + windowTitle = "Normal Window Title", + id = "normal", + priority = ViewPriority.NORMAL, + timeoutMs = 10000 + ) + ) + + underTest.displayView( + ViewInfo( + name = "critical", + windowTitle = "Critical Window Title", + id = "critical", + priority = ViewPriority.CRITICAL, + timeoutMs = 2000 + ) + ) + + val viewCaptor = argumentCaptor<View>() + val windowParamsCaptor = argumentCaptor<WindowManager.LayoutParams>() + verify(windowManager, times(2)).addView(capture(viewCaptor), capture(windowParamsCaptor)) + assertThat(windowParamsCaptor.allValues[0].title).isEqualTo("Normal Window Title") + assertThat(windowParamsCaptor.allValues[1].title).isEqualTo("Critical Window Title") + verify(windowManager).removeView(viewCaptor.allValues[0]) + + reset(windowManager) + + // WHEN the critical's timeout has expired + fakeClock.advanceTime(2000 + 1) + + // THEN the normal view is re-displayed + verify(windowManager).removeView(viewCaptor.allValues[1]) + verify(windowManager).addView(any(), capture(windowParamsCaptor)) + assertThat(windowParamsCaptor.value.title).isEqualTo("Normal Window Title") + verify(configurationController, never()).removeCallback(any()) + } + + @Test + fun lowerThenHigherPriority_lowerPriorityNotRedisplayedBecauseTimedOut() { + underTest.displayView( + ViewInfo( + name = "normal", + windowTitle = "Normal Window Title", + id = "normal", + priority = ViewPriority.NORMAL, + timeoutMs = 1000 + ) + ) + + underTest.displayView( + ViewInfo( + name = "critical", + windowTitle = "Critical Window Title", + id = "critical", + priority = ViewPriority.CRITICAL, + timeoutMs = 2000 + ) + ) + reset(windowManager) + + // WHEN the critical's timeout has expired + fakeClock.advanceTime(2000 + 1) + + // THEN the normal view is not re-displayed since it already timed out + verify(windowManager).removeView(any()) + verify(windowManager, never()).addView(any(), any()) + assertThat(underTest.activeViews).isEmpty() + verify(configurationController).removeCallback(any()) + } + + @Test + fun higherThenLowerPriority_higherStaysDisplayed() { + underTest.displayView( + ViewInfo( + name = "critical", + windowTitle = "Critical Window Title", + id = "critical", + priority = ViewPriority.CRITICAL, + ) + ) + + val viewCaptor = argumentCaptor<View>() + val windowParamsCaptor = argumentCaptor<WindowManager.LayoutParams>() + verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor)) + assertThat(windowParamsCaptor.value.title).isEqualTo("Critical Window Title") + reset(windowManager) + + underTest.displayView( + ViewInfo( + name = "normal", + windowTitle = "Normal Window Title", + id = "normal", + priority = ViewPriority.NORMAL, + ) + ) + + verify(windowManager, never()).removeView(viewCaptor.value) + verify(windowManager, never()).addView(any(), any()) + assertThat(underTest.activeViews.size).isEqualTo(2) + verify(configurationController, never()).removeCallback(any()) + } + + @Test + fun higherThenLowerPriority_lowerEventuallyDisplayed() { + underTest.displayView( + ViewInfo( + name = "critical", + windowTitle = "Critical Window Title", + id = "critical", + priority = ViewPriority.CRITICAL, + timeoutMs = 3000, + ) + ) + + val viewCaptor = argumentCaptor<View>() + val windowParamsCaptor = argumentCaptor<WindowManager.LayoutParams>() + verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor)) + assertThat(windowParamsCaptor.value.title).isEqualTo("Critical Window Title") + reset(windowManager) + + underTest.displayView( + ViewInfo( + name = "normal", + windowTitle = "Normal Window Title", + id = "normal", + priority = ViewPriority.NORMAL, + timeoutMs = 5000, + ) + ) + + verify(windowManager, never()).removeView(viewCaptor.value) + verify(windowManager, never()).addView(any(), any()) + assertThat(underTest.activeViews.size).isEqualTo(2) + + // WHEN the first critical view has timed out + fakeClock.advanceTime(3000 + 1) + + // THEN the second normal view is displayed + verify(windowManager).removeView(viewCaptor.value) + verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor)) + assertThat(windowParamsCaptor.value.title).isEqualTo("Normal Window Title") + assertThat(underTest.activeViews.size).isEqualTo(1) + verify(configurationController, never()).removeCallback(any()) + } + + @Test + fun higherThenLowerPriority_lowerNotDisplayedBecauseTimedOut() { + underTest.displayView( + ViewInfo( + name = "critical", + windowTitle = "Critical Window Title", + id = "critical", + priority = ViewPriority.CRITICAL, + timeoutMs = 3000, + ) + ) + + val viewCaptor = argumentCaptor<View>() + val windowParamsCaptor = argumentCaptor<WindowManager.LayoutParams>() + verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor)) + assertThat(windowParamsCaptor.value.title).isEqualTo("Critical Window Title") + reset(windowManager) + + underTest.displayView( + ViewInfo( + name = "normal", + windowTitle = "Normal Window Title", + id = "normal", + priority = ViewPriority.NORMAL, + timeoutMs = 200, + ) + ) + + verify(windowManager, never()).removeView(viewCaptor.value) + verify(windowManager, never()).addView(any(), any()) + assertThat(underTest.activeViews.size).isEqualTo(2) + reset(windowManager) + + // WHEN the first critical view has timed out + fakeClock.advanceTime(3000 + 1) + + // THEN the second normal view is not displayed because it's already timed out + verify(windowManager).removeView(viewCaptor.value) + verify(windowManager, never()).addView(any(), any()) + assertThat(underTest.activeViews).isEmpty() + verify(configurationController).removeCallback(any()) + } + + @Test + fun criticalThenNewCritical_newCriticalDisplayed() { + underTest.displayView( + ViewInfo( + name = "critical 1", + windowTitle = "Critical Window Title 1", + id = "critical1", + priority = ViewPriority.CRITICAL, + ) + ) + + val viewCaptor = argumentCaptor<View>() + val windowParamsCaptor = argumentCaptor<WindowManager.LayoutParams>() + verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor)) + assertThat(windowParamsCaptor.value.title).isEqualTo("Critical Window Title 1") + reset(windowManager) + + underTest.displayView( + ViewInfo( + name = "critical 2", + windowTitle = "Critical Window Title 2", + id = "critical2", + priority = ViewPriority.CRITICAL, + ) + ) + + verify(windowManager).removeView(viewCaptor.value) + verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor)) + assertThat(windowParamsCaptor.value.title).isEqualTo("Critical Window Title 2") + assertThat(underTest.activeViews.size).isEqualTo(2) + verify(configurationController, never()).removeCallback(any()) + } + + @Test + fun normalThenNewNormal_newNormalDisplayed() { + underTest.displayView( + ViewInfo( + name = "normal 1", + windowTitle = "Normal Window Title 1", + id = "normal1", + priority = ViewPriority.NORMAL, + ) + ) + + val viewCaptor = argumentCaptor<View>() + val windowParamsCaptor = argumentCaptor<WindowManager.LayoutParams>() + verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor)) + assertThat(windowParamsCaptor.value.title).isEqualTo("Normal Window Title 1") + reset(windowManager) + + underTest.displayView( + ViewInfo( + name = "normal 2", + windowTitle = "Normal Window Title 2", + id = "normal2", + priority = ViewPriority.NORMAL, + ) + ) + + verify(windowManager).removeView(viewCaptor.value) + verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor)) + assertThat(windowParamsCaptor.value.title).isEqualTo("Normal Window Title 2") + assertThat(underTest.activeViews.size).isEqualTo(2) + verify(configurationController, never()).removeCallback(any()) + } + + @Test + fun lowerPriorityViewUpdatedWhileHigherPriorityDisplayed_eventuallyDisplaysUpdated() { + // First, display a lower priority view + underTest.displayView( + ViewInfo( + name = "normal", + windowTitle = "Normal Window Title", + id = "normal", + priority = ViewPriority.NORMAL, + // At the end of the test, we'll verify that this information isn't re-displayed. + // Use a super long timeout so that, when we verify it wasn't re-displayed, we know + // that it wasn't because the view just timed out. + timeoutMs = 100000, + ) + ) + + val viewCaptor = argumentCaptor<View>() + val windowParamsCaptor = argumentCaptor<WindowManager.LayoutParams>() + verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor)) + assertThat(windowParamsCaptor.value.title).isEqualTo("Normal Window Title") + reset(windowManager) + + // Then, display a higher priority view + fakeClock.advanceTime(1000) + underTest.displayView( + ViewInfo( + name = "critical", + windowTitle = "Critical Window Title", + id = "critical", + priority = ViewPriority.CRITICAL, + timeoutMs = 3000, + ) + ) + + verify(windowManager).removeView(viewCaptor.value) + verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor)) + assertThat(windowParamsCaptor.value.title).isEqualTo("Critical Window Title") + assertThat(underTest.activeViews.size).isEqualTo(2) + reset(windowManager) + + // While the higher priority view is displayed, update the lower priority view with new + // information + fakeClock.advanceTime(1000) + val updatedViewInfo = ViewInfo( + name = "normal with update", + windowTitle = "Normal Window Title", + id = "normal", + priority = ViewPriority.NORMAL, + timeoutMs = 4000, + ) + underTest.displayView(updatedViewInfo) + + verify(windowManager, never()).removeView(viewCaptor.value) + verify(windowManager, never()).addView(any(), any()) + assertThat(underTest.activeViews.size).isEqualTo(2) + reset(windowManager) + + // WHEN the higher priority view times out + fakeClock.advanceTime(2001) + + // THEN the higher priority view disappears and the lower priority view *with the updated + // information* gets displayed. + verify(windowManager).removeView(viewCaptor.value) + verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor)) + assertThat(windowParamsCaptor.value.title).isEqualTo("Normal Window Title") + assertThat(underTest.activeViews.size).isEqualTo(1) + assertThat(underTest.mostRecentViewInfo).isEqualTo(updatedViewInfo) + reset(windowManager) + + // WHEN the updated view times out + fakeClock.advanceTime(2001) + + // THEN the old information is never displayed + verify(windowManager).removeView(viewCaptor.value) + verify(windowManager, never()).addView(any(), any()) + assertThat(underTest.activeViews.size).isEqualTo(0) + } + + @Test + fun oldViewUpdatedWhileNewViewDisplayed_eventuallyDisplaysUpdated() { + // First, display id1 view + underTest.displayView( + ViewInfo( + name = "name 1", + windowTitle = "Name 1 Title", + id = "id1", + priority = ViewPriority.NORMAL, + // At the end of the test, we'll verify that this information isn't re-displayed. + // Use a super long timeout so that, when we verify it wasn't re-displayed, we know + // that it wasn't because the view just timed out. + timeoutMs = 100000, + ) + ) + + val viewCaptor = argumentCaptor<View>() + val windowParamsCaptor = argumentCaptor<WindowManager.LayoutParams>() + verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor)) + assertThat(windowParamsCaptor.value.title).isEqualTo("Name 1 Title") + reset(windowManager) + + // Then, display a new id2 view + fakeClock.advanceTime(1000) + underTest.displayView( + ViewInfo( + name = "name 2", + windowTitle = "Name 2 Title", + id = "id2", + priority = ViewPriority.NORMAL, + timeoutMs = 3000, + ) + ) + + verify(windowManager).removeView(viewCaptor.value) + verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor)) + assertThat(windowParamsCaptor.value.title).isEqualTo("Name 2 Title") + assertThat(underTest.activeViews.size).isEqualTo(2) + reset(windowManager) + + // While the id2 view is displayed, re-display the id1 view with new information + fakeClock.advanceTime(1000) + val updatedViewInfo = ViewInfo( + name = "name 1 with update", + windowTitle = "Name 1 Title", + id = "id1", + priority = ViewPriority.NORMAL, + timeoutMs = 3000, + ) + underTest.displayView(updatedViewInfo) + + verify(windowManager).removeView(viewCaptor.value) + verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor)) + assertThat(windowParamsCaptor.value.title).isEqualTo("Name 1 Title") + assertThat(underTest.activeViews.size).isEqualTo(2) + reset(windowManager) + + // WHEN the id1 view with new information times out + fakeClock.advanceTime(3001) + + // THEN the id1 view disappears and the old id1 information is never displayed + verify(windowManager).removeView(viewCaptor.value) + verify(windowManager, never()).addView(any(), any()) + assertThat(underTest.activeViews.size).isEqualTo(0) + } + + @Test + fun oldViewUpdatedWhileNewViewDisplayed_usesNewTimeout() { + // First, display id1 view + underTest.displayView( + ViewInfo( + name = "name 1", + windowTitle = "Name 1 Title", + id = "id1", + priority = ViewPriority.NORMAL, + timeoutMs = 5000, + ) + ) + + // Then, display a new id2 view + fakeClock.advanceTime(1000) + underTest.displayView( + ViewInfo( + name = "name 2", + windowTitle = "Name 2 Title", + id = "id2", + priority = ViewPriority.NORMAL, + timeoutMs = 3000, + ) + ) + reset(windowManager) + + // While the id2 view is displayed, re-display the id1 view with new information *and a + // longer timeout* + fakeClock.advanceTime(1000) + val updatedViewInfo = ViewInfo( + name = "name 1 with update", + windowTitle = "Name 1 Title", + id = "id1", + priority = ViewPriority.NORMAL, + timeoutMs = 30000, + ) + underTest.displayView(updatedViewInfo) + + val viewCaptor = argumentCaptor<View>() + val windowParamsCaptor = argumentCaptor<WindowManager.LayoutParams>() + verify(windowManager).addView(capture(viewCaptor), capture(windowParamsCaptor)) + assertThat(windowParamsCaptor.value.title).isEqualTo("Name 1 Title") + assertThat(underTest.activeViews.size).isEqualTo(2) + reset(windowManager) + + // WHEN id1's *old* timeout occurs + fakeClock.advanceTime(3001) + + // THEN id1 is still displayed because it was updated with a new timeout + verify(windowManager, never()).removeView(viewCaptor.value) + assertThat(underTest.activeViews.size).isEqualTo(1) } @Test @@ -395,6 +964,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { verify(windowManager).removeView(any()) verify(logger).logViewRemoval(deviceId, reason) + verify(configurationController).removeCallback(any()) } @Test @@ -414,14 +984,15 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { inner class TestController( context: Context, - logger: TemporaryViewLogger, + logger: TemporaryViewLogger<ViewInfo>, windowManager: WindowManager, @Main mainExecutor: DelayableExecutor, accessibilityManager: AccessibilityManager, configurationController: ConfigurationController, powerManager: PowerManager, wakeLockBuilder: WakeLock.Builder, - ) : TemporaryViewDisplayController<ViewInfo, TemporaryViewLogger>( + systemClock: SystemClock, + ) : TemporaryViewDisplayController<ViewInfo, TemporaryViewLogger<ViewInfo>>( context, logger, windowManager, @@ -431,6 +1002,7 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { powerManager, R.layout.chipbar, wakeLockBuilder, + systemClock, ) { var mostRecentViewInfo: ViewInfo? = null @@ -447,12 +1019,13 @@ class TemporaryViewDisplayControllerTest : SysuiTestCase() { override fun start() {} } - inner class ViewInfo( + data class ViewInfo( val name: String, override val windowTitle: String = "Window Title", override val wakeReason: String = "WAKE_REASON", - override val timeoutMs: Int = 1, + override val timeoutMs: Int = TIMEOUT_MS.toInt(), override val id: String = "id", + override val priority: ViewPriority = ViewPriority.NORMAL, ) : TemporaryViewInfo() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewLoggerTest.kt index 116b8fe62b37..2e66b205bfd5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewLoggerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/TemporaryViewLoggerTest.kt @@ -32,7 +32,7 @@ import org.mockito.Mockito @SmallTest class TemporaryViewLoggerTest : SysuiTestCase() { private lateinit var buffer: LogBuffer - private lateinit var logger: TemporaryViewLogger + private lateinit var logger: TemporaryViewLogger<TemporaryViewInfo> @Before fun setUp() { @@ -44,13 +44,22 @@ class TemporaryViewLoggerTest : SysuiTestCase() { @Test fun logViewAddition_bufferHasLog() { - logger.logViewAddition("test id", "Test Window Title") + val info = + object : TemporaryViewInfo() { + override val id: String = "test id" + override val priority: ViewPriority = ViewPriority.CRITICAL + override val windowTitle: String = "Test Window Title" + override val wakeReason: String = "wake reason" + } + + logger.logViewAddition(info) val stringWriter = StringWriter() buffer.dump(PrintWriter(stringWriter), tailLength = 0) val actualString = stringWriter.toString() assertThat(actualString).contains(TAG) + assertThat(actualString).contains("test id") assertThat(actualString).contains("Test Window Title") } diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt index 7014f93fba4a..2e4d8e74ad6e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt @@ -39,6 +39,7 @@ import com.android.systemui.common.shared.model.TintedIcon import com.android.systemui.plugins.FalsingManager import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.policy.ConfigurationController +import com.android.systemui.temporarydisplay.ViewPriority import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq @@ -105,6 +106,7 @@ class ChipbarCoordinatorTest : SysuiTestCase() { viewUtil, vibratorHelper, fakeWakeLockBuilder, + fakeClock, ) underTest.start() } @@ -408,6 +410,7 @@ class ChipbarCoordinatorTest : SysuiTestCase() { wakeReason = WAKE_REASON, timeoutMs = TIMEOUT, id = DEVICE_ID, + priority = ViewPriority.NORMAL, ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt index beedf9f337bc..d5167b3890b9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/FakeChipbarCoordinator.kt @@ -26,6 +26,7 @@ import com.android.systemui.plugins.FalsingManager import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.util.concurrency.DelayableExecutor +import com.android.systemui.util.time.SystemClock import com.android.systemui.util.view.ViewUtil import com.android.systemui.util.wakelock.WakeLock @@ -43,6 +44,7 @@ class FakeChipbarCoordinator( viewUtil: ViewUtil, vibratorHelper: VibratorHelper, wakeLockBuilder: WakeLock.Builder, + systemClock: SystemClock, ) : ChipbarCoordinator( context, @@ -57,6 +59,7 @@ class FakeChipbarCoordinator( viewUtil, vibratorHelper, wakeLockBuilder, + systemClock, ) { override fun animateViewOut(view: ViewGroup, onAnimationEnd: Runnable) { // Just bypass the animation in tests diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 4fbbd799a3db..59c1c544cf88 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -26,8 +26,11 @@ import static android.accessibilityservice.AccessibilityTrace.FLAGS_USER_BROADCA import static android.accessibilityservice.AccessibilityTrace.FLAGS_WINDOW_MAGNIFICATION_CONNECTION; import static android.accessibilityservice.AccessibilityTrace.FLAGS_WINDOW_MANAGER_INTERNAL; import static android.provider.Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED; +import static android.provider.Settings.Secure.CONTRAST_LEVEL; import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_BUTTON; import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_SHORTCUT_KEY; +import static android.view.accessibility.AccessibilityManager.CONTRAST_DEFAULT_VALUE; +import static android.view.accessibility.AccessibilityManager.CONTRAST_NOT_SET; import static android.view.accessibility.AccessibilityManager.ShortcutType; import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_COMPONENT_NAME; @@ -1902,6 +1905,16 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub return false; } + private boolean readUiContrastLocked(AccessibilityUserState userState) { + float contrast = Settings.Secure.getFloatForUser(mContext.getContentResolver(), + CONTRAST_LEVEL, CONTRAST_DEFAULT_VALUE, userState.mUserId); + if (Math.abs(userState.getUiContrastLocked() - contrast) >= 1e-10) { + userState.setUiContrastLocked(contrast); + return true; + } + return false; + } + /** * Performs {@link AccessibilityService}s delayed notification. The delay is configurable * and denotes the period after the last event before notifying the service. @@ -2568,6 +2581,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub somethingChanged |= readMagnificationModeForDefaultDisplayLocked(userState); somethingChanged |= readMagnificationCapabilitiesLocked(userState); somethingChanged |= readMagnificationFollowTypingLocked(userState); + somethingChanged |= readUiContrastLocked(userState); return somethingChanged; } @@ -3709,6 +3723,19 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub return mProxyManager.unregisterProxy(displayId); } + @Override public float getUiContrast() { + if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_MANAGER)) { + mTraceManager.logTrace(LOG_TAG + ".getUiContrast", FLAGS_ACCESSIBILITY_MANAGER); + } + synchronized (mLock) { + AccessibilityUserState userState = getCurrentUserStateLocked(); + float contrast = userState.getUiContrastLocked(); + if (contrast != CONTRAST_NOT_SET) return contrast; + readUiContrastLocked(userState); + return userState.getUiContrastLocked(); + } + } + @Override public void dump(FileDescriptor fd, final PrintWriter pw, String[] args) { if (!DumpUtils.checkDumpPermission(mContext, LOG_TAG, pw)) return; @@ -4156,6 +4183,9 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub private final Uri mMagnificationFollowTypingUri = Settings.Secure.getUriFor( Settings.Secure.ACCESSIBILITY_MAGNIFICATION_FOLLOW_TYPING_ENABLED); + private final Uri mUiContrastUri = Settings.Secure.getUriFor( + CONTRAST_LEVEL); + public AccessibilityContentObserver(Handler handler) { super(handler); } @@ -4196,6 +4226,8 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub mMagnificationCapabilityUri, false, this, UserHandle.USER_ALL); contentResolver.registerContentObserver( mMagnificationFollowTypingUri, false, this, UserHandle.USER_ALL); + contentResolver.registerContentObserver( + mUiContrastUri, false, this, UserHandle.USER_ALL); } @Override @@ -4265,6 +4297,10 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } } else if (mMagnificationFollowTypingUri.equals(uri)) { readMagnificationFollowTypingLocked(userState); + } else if (mUiContrastUri.equals(uri)) { + if (readUiContrastLocked(userState)) { + updateUiContrastLocked(userState); + } } } } @@ -4554,7 +4590,22 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub userState.getFocusColorLocked()); })); }); + } + private void updateUiContrastLocked(AccessibilityUserState userState) { + if (userState.mUserId != mCurrentUserId) { + return; + } + if (mTraceManager.isA11yTracingEnabledForTypes(FLAGS_ACCESSIBILITY_SERVICE_CLIENT)) { + mTraceManager.logTrace(LOG_TAG + ".updateUiContrastLocked", + FLAGS_ACCESSIBILITY_SERVICE_CLIENT, "userState=" + userState); + } + float contrast = userState.getUiContrastLocked(); + mMainHandler.post(() -> { + broadcastToClients(userState, ignoreRemoteException(client -> { + client.mCallback.setUiContrast(contrast); + })); + }); } public AccessibilityTraceManager getTraceManager() { diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java index 0db169fd76c3..43730fce0cb7 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java @@ -26,6 +26,8 @@ import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_ import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_NONE; import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_BUTTON; import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_SHORTCUT_KEY; +import static android.view.accessibility.AccessibilityManager.CONTRAST_DEFAULT_VALUE; +import static android.view.accessibility.AccessibilityManager.CONTRAST_NOT_SET; import static android.view.accessibility.AccessibilityManager.ShortcutType; import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME; @@ -143,6 +145,8 @@ class AccessibilityUserState { private final int mFocusStrokeWidthDefaultValue; // The default value of the focus color. private final int mFocusColorDefaultValue; + /** The color contrast in [-1, 1] */ + private float mUiContrast = CONTRAST_DEFAULT_VALUE; private Context mContext; @@ -217,6 +221,7 @@ class AccessibilityUserState { mFocusStrokeWidth = mFocusStrokeWidthDefaultValue; mFocusColor = mFocusColorDefaultValue; mMagnificationFollowTypingEnabled = true; + mUiContrast = CONTRAST_NOT_SET; } void addServiceLocked(AccessibilityServiceConnection serviceConnection) { @@ -983,6 +988,7 @@ class AccessibilityUserState { return mFocusColor; } + /** * Sets the stroke width and color of the focus rectangle. * @@ -1008,4 +1014,20 @@ class AccessibilityUserState { } return false; } + + /** + * Get the color contrast + * @return color contrast in [-1, 1] + */ + public float getUiContrastLocked() { + return mUiContrast; + } + + /** + * Set the color contrast + * @param contrast the new color contrast in [-1, 1] + */ + public void setUiContrastLocked(float contrast) { + mUiContrast = contrast; + } } diff --git a/services/companion/java/com/android/server/companion/virtual/InputController.java b/services/companion/java/com/android/server/companion/virtual/InputController.java index 0cea3d0575c6..97b5d6ddf6b6 100644 --- a/services/companion/java/com/android/server/companion/virtual/InputController.java +++ b/services/companion/java/com/android/server/companion/virtual/InputController.java @@ -61,15 +61,19 @@ class InputController { private static final AtomicLong sNextPhysId = new AtomicLong(1); + static final String NAVIGATION_TOUCHPAD_DEVICE_TYPE = "touchNavigation"; + static final String PHYS_TYPE_DPAD = "Dpad"; static final String PHYS_TYPE_KEYBOARD = "Keyboard"; static final String PHYS_TYPE_MOUSE = "Mouse"; static final String PHYS_TYPE_TOUCHSCREEN = "Touchscreen"; + static final String PHYS_TYPE_NAVIGATION_TOUCHPAD = "NavigationTouchpad"; @StringDef(prefix = { "PHYS_TYPE_" }, value = { PHYS_TYPE_DPAD, PHYS_TYPE_KEYBOARD, PHYS_TYPE_MOUSE, PHYS_TYPE_TOUCHSCREEN, + PHYS_TYPE_NAVIGATION_TOUCHPAD, }) @Retention(RetentionPolicy.SOURCE) @interface PhysType { @@ -190,6 +194,28 @@ class InputController { } } + void createNavigationTouchpad( + @NonNull String deviceName, + int vendorId, + int productId, + @NonNull IBinder deviceToken, + int displayId, + int touchpadHeight, + int touchpadWidth) { + final String phys = createPhys(PHYS_TYPE_NAVIGATION_TOUCHPAD); + mInputManagerInternal.setTypeAssociation(phys, NAVIGATION_TOUCHPAD_DEVICE_TYPE); + try { + createDeviceInternal(InputDeviceDescriptor.TYPE_NAVIGATION_TOUCHPAD, deviceName, + vendorId, productId, deviceToken, displayId, phys, + () -> mNativeWrapper.openUinputTouchscreen(deviceName, vendorId, productId, + phys, touchpadHeight, touchpadWidth)); + } catch (DeviceCreationException e) { + mInputManagerInternal.unsetTypeAssociation(phys); + throw new RuntimeException( + "Failed to create virtual navigation touchpad device '" + deviceName + "'.", e); + } + } + void unregisterInputDevice(@NonNull IBinder token) { synchronized (mLock) { final InputDeviceDescriptor inputDeviceDescriptor = mInputDeviceDescriptors.remove( @@ -207,7 +233,13 @@ class InputController { InputDeviceDescriptor inputDeviceDescriptor) { token.unlinkToDeath(inputDeviceDescriptor.getDeathRecipient(), /* flags= */ 0); mNativeWrapper.closeUinput(inputDeviceDescriptor.getFileDescriptor()); + InputManager.getInstance().removeUniqueIdAssociation(inputDeviceDescriptor.getPhys()); + // Type associations are added in the case of navigation touchpads. Those should be removed + // once the input device gets closed. + if (inputDeviceDescriptor.getType() == InputDeviceDescriptor.TYPE_NAVIGATION_TOUCHPAD) { + mInputManagerInternal.unsetTypeAssociation(inputDeviceDescriptor.getPhys()); + } // Reset values to the default if all virtual mice are unregistered, or set display // id if there's another mouse (choose the most recent). The inputDeviceDescriptor must be @@ -509,11 +541,13 @@ class InputController { static final int TYPE_MOUSE = 2; static final int TYPE_TOUCHSCREEN = 3; static final int TYPE_DPAD = 4; + static final int TYPE_NAVIGATION_TOUCHPAD = 5; @IntDef(prefix = { "TYPE_" }, value = { TYPE_KEYBOARD, TYPE_MOUSE, TYPE_TOUCHSCREEN, TYPE_DPAD, + TYPE_NAVIGATION_TOUCHPAD, }) @Retention(RetentionPolicy.SOURCE) @interface Type { diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java index 58198612d80e..12ad9f1cf580 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java @@ -53,6 +53,7 @@ import android.hardware.input.VirtualMouseButtonEvent; import android.hardware.input.VirtualMouseConfig; import android.hardware.input.VirtualMouseRelativeEvent; import android.hardware.input.VirtualMouseScrollEvent; +import android.hardware.input.VirtualNavigationTouchpadConfig; import android.hardware.input.VirtualTouchEvent; import android.hardware.input.VirtualTouchscreenConfig; import android.os.Binder; @@ -108,7 +109,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub private VirtualAudioController mVirtualAudioController; @VisibleForTesting final Set<Integer> mVirtualDisplayIds = new ArraySet<>(); - private final OnDeviceCloseListener mListener; + private final OnDeviceCloseListener mOnDeviceCloseListener; private final IBinder mAppToken; private final VirtualDeviceParams mParams; private final Map<Integer, PowerManager.WakeLock> mPerDisplayWakelocks = new ArrayMap<>(); @@ -155,7 +156,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub IBinder token, int ownerUid, int deviceId, - OnDeviceCloseListener listener, + OnDeviceCloseListener onDeviceCloseListener, PendingTrampolineCallback pendingTrampolineCallback, IVirtualDeviceActivityListener activityListener, Consumer<ArraySet<Integer>> runningAppsChangedCallback, @@ -168,7 +169,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub deviceId, /* inputController= */ null, /* sensorController= */ null, - listener, + onDeviceCloseListener, pendingTrampolineCallback, activityListener, runningAppsChangedCallback, @@ -184,7 +185,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub int deviceId, InputController inputController, SensorController sensorController, - OnDeviceCloseListener listener, + OnDeviceCloseListener onDeviceCloseListener, PendingTrampolineCallback pendingTrampolineCallback, IVirtualDeviceActivityListener activityListener, Consumer<ArraySet<Integer>> runningAppsChangedCallback, @@ -212,7 +213,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub } else { mSensorController = sensorController; } - mListener = listener; + mOnDeviceCloseListener = onDeviceCloseListener; try { token.linkToDeath(this, 0); } catch (RemoteException e) { @@ -330,7 +331,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub mVirtualAudioController = null; } } - mListener.onClose(mAssociationInfo.getId()); + mOnDeviceCloseListener.onClose(mDeviceId); mAppToken.unlinkToDeath(this, 0); final long ident = Binder.clearCallingIdentity(); @@ -491,6 +492,38 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub } @Override // Binder call + public void createVirtualNavigationTouchpad(VirtualNavigationTouchpadConfig config, + @NonNull IBinder deviceToken) { + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.CREATE_VIRTUAL_DEVICE, + "Permission required to create a virtual navigation touchpad"); + synchronized (mVirtualDeviceLock) { + if (!mVirtualDisplayIds.contains(config.getAssociatedDisplayId())) { + throw new SecurityException( + "Cannot create a virtual navigation touchpad for a display not associated " + + "with this virtual device"); + } + } + int touchpadHeight = config.getHeight(); + int touchpadWidth = config.getWidth(); + if (touchpadHeight <= 0 || touchpadWidth <= 0) { + throw new IllegalArgumentException( + "Cannot create a virtual navigation touchpad, touchpad dimensions must be positive." + + " Got: (" + touchpadHeight + ", " + touchpadWidth + ")"); + } + + final long ident = Binder.clearCallingIdentity(); + try { + mInputController.createNavigationTouchpad( + config.getInputDeviceName(), config.getVendorId(), + config.getProductId(), deviceToken, config.getAssociatedDisplayId(), + touchpadHeight, touchpadWidth); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + @Override // Binder call public void unregisterInputDevice(IBinder token) { mContext.enforceCallingOrSelfPermission( android.Manifest.permission.CREATE_VIRTUAL_DEVICE, @@ -650,6 +683,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub @Override protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) { fout.println(" VirtualDevice: "); + fout.println(" mDeviceId: " + mDeviceId); fout.println(" mAssociationId: " + mAssociationInfo.getId()); fout.println(" mParams: " + mParams); fout.println(" mVirtualDisplayIds: "); @@ -839,7 +873,7 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub } interface OnDeviceCloseListener { - void onClose(int associationId); + void onClose(int deviceId); } interface PendingTrampolineCallback { diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java index 0d8bba307cd5..da2c5162e6e1 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java @@ -90,14 +90,20 @@ public class VirtualDeviceManagerService extends SystemService { new SparseArray<>(); /** - * Mapping from CDM association IDs to virtual devices. Only one virtual device is allowed for - * each CDM associated device. + * Mapping from device IDs to CameraAccessControllers. + */ + @GuardedBy("mVirtualDeviceManagerLock") + private final SparseArray<CameraAccessController> mCameraAccessControllersByDeviceId = + new SparseArray<>(); + + /** + * Mapping from device IDs to virtual devices. */ @GuardedBy("mVirtualDeviceManagerLock") private final SparseArray<VirtualDeviceImpl> mVirtualDevices = new SparseArray<>(); /** - * Mapping from CDM association IDs to app UIDs running on the corresponding virtual device. + * Mapping from device IDs to app UIDs running on the corresponding virtual device. */ @GuardedBy("mVirtualDeviceManagerLock") private final SparseArray<ArraySet<Integer>> mAppsOnVirtualDevices = new SparseArray<>(); @@ -160,7 +166,7 @@ public class VirtualDeviceManagerService extends SystemService { @GuardedBy("mVirtualDeviceManagerLock") private boolean isValidVirtualDeviceLocked(IVirtualDevice virtualDevice) { try { - return mVirtualDevices.contains(virtualDevice.getAssociationId()); + return mVirtualDevices.contains(virtualDevice.getDeviceId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -230,9 +236,9 @@ public class VirtualDeviceManagerService extends SystemService { } @VisibleForTesting - void notifyRunningAppsChanged(int associationId, ArraySet<Integer> uids) { + void notifyRunningAppsChanged(int deviceId, ArraySet<Integer> uids) { synchronized (mVirtualDeviceManagerLock) { - mAppsOnVirtualDevices.put(associationId, uids); + mAppsOnVirtualDevices.put(deviceId, uids); } mLocalService.onAppsOnVirtualDeviceChanged(); } @@ -240,7 +246,7 @@ public class VirtualDeviceManagerService extends SystemService { @VisibleForTesting void addVirtualDevice(VirtualDeviceImpl virtualDevice) { synchronized (mVirtualDeviceManagerLock) { - mVirtualDevices.put(virtualDevice.getAssociationId(), virtualDevice); + mVirtualDevices.put(virtualDevice.getDeviceId(), virtualDevice); } } @@ -268,60 +274,26 @@ public class VirtualDeviceManagerService extends SystemService { throw new IllegalArgumentException("No association with ID " + associationId); } synchronized (mVirtualDeviceManagerLock) { - if (mVirtualDevices.contains(associationId)) { - throw new IllegalStateException( - "Virtual device for association ID " + associationId - + " already exists"); - } final int userId = UserHandle.getUserId(callingUid); final CameraAccessController cameraAccessController = mCameraAccessControllers.get(userId); - final int uniqueId = sNextUniqueIndex.getAndIncrement(); - + final int deviceId = sNextUniqueIndex.getAndIncrement(); VirtualDeviceImpl virtualDevice = new VirtualDeviceImpl(getContext(), - associationInfo, token, callingUid, uniqueId, - new VirtualDeviceImpl.OnDeviceCloseListener() { - @Override - public void onClose(int associationId) { - synchronized (mVirtualDeviceManagerLock) { - VirtualDeviceImpl removedDevice = - mVirtualDevices.removeReturnOld(associationId); - if (removedDevice != null) { - Intent i = new Intent( - VirtualDeviceManager.ACTION_VIRTUAL_DEVICE_REMOVED); - i.putExtra( - VirtualDeviceManager.EXTRA_VIRTUAL_DEVICE_ID, - removedDevice.getDeviceId()); - i.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); - final long identity = Binder.clearCallingIdentity(); - try { - getContext().sendBroadcastAsUser(i, UserHandle.ALL); - } finally { - Binder.restoreCallingIdentity(identity); - } - } - mAppsOnVirtualDevices.remove(associationId); - if (cameraAccessController != null) { - cameraAccessController.stopObservingIfNeeded(); - } else { - Slog.w(TAG, "cameraAccessController not found for user " - + userId); - } - } - } - }, + associationInfo, token, callingUid, deviceId, + /* onDeviceCloseListener= */ this::onDeviceClosed, this, activityListener, runningUids -> { cameraAccessController.blockCameraAccessIfNeeded(runningUids); - notifyRunningAppsChanged(associationInfo.getId(), runningUids); + notifyRunningAppsChanged(deviceId, runningUids); }, params); if (cameraAccessController != null) { cameraAccessController.startObservingIfNeeded(); + mCameraAccessControllersByDeviceId.put(deviceId, cameraAccessController); } else { Slog.w(TAG, "cameraAccessController not found for user " + userId); } - mVirtualDevices.put(associationInfo.getId(), virtualDevice); + mVirtualDevices.put(deviceId, virtualDevice); return virtualDevice; } } @@ -338,7 +310,7 @@ public class VirtualDeviceManagerService extends SystemService { } VirtualDeviceImpl virtualDeviceImpl; synchronized (mVirtualDeviceManagerLock) { - virtualDeviceImpl = mVirtualDevices.get(virtualDevice.getAssociationId()); + virtualDeviceImpl = mVirtualDevices.get(virtualDevice.getDeviceId()); if (virtualDeviceImpl == null) { throw new SecurityException("Invalid VirtualDevice"); } @@ -419,8 +391,7 @@ public class VirtualDeviceManagerService extends SystemService { @Nullable private AssociationInfo getAssociationInfo(String packageName, int associationId) { final int callingUserId = getCallingUserHandle().getIdentifier(); - final List<AssociationInfo> associations = - mAllAssociations.get(callingUserId); + final List<AssociationInfo> associations = mAllAssociations.get(callingUserId); if (associations != null) { final int associationSize = associations.size(); for (int i = 0; i < associationSize; i++) { @@ -436,6 +407,29 @@ public class VirtualDeviceManagerService extends SystemService { return null; } + private void onDeviceClosed(int deviceId) { + synchronized (mVirtualDeviceManagerLock) { + mVirtualDevices.remove(deviceId); + Intent i = new Intent(VirtualDeviceManager.ACTION_VIRTUAL_DEVICE_REMOVED); + i.putExtra(VirtualDeviceManager.EXTRA_VIRTUAL_DEVICE_ID, deviceId); + i.setFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY); + final long identity = Binder.clearCallingIdentity(); + try { + getContext().sendBroadcastAsUser(i, UserHandle.ALL); + } finally { + Binder.restoreCallingIdentity(identity); + } + mAppsOnVirtualDevices.remove(deviceId); + final CameraAccessController cameraAccessController = + mCameraAccessControllersByDeviceId.removeReturnOld(deviceId); + if (cameraAccessController != null) { + cameraAccessController.stopObservingIfNeeded(); + } else { + Slog.w(TAG, "cameraAccessController not found for device Id " + deviceId); + } + } + } + @Override public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException { diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index b62937847ea9..045c757767e4 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -13577,9 +13577,10 @@ public class ActivityManagerService extends IActivityManager.Stub // updating their receivers to be exempt from this requirement until their receivers // are flagged. if (requireExplicitFlagForDynamicReceivers) { - if ("com.google.android.apps.messaging".equals(callerPackage)) { - // Note, a versionCode check for this package is not performed because it could - // cause breakage with a subsequent update outside the system image. + if ("com.shannon.imsservice".equals(callerPackage)) { + // Note, a versionCode check for this package is not performed because this + // package consumes the SecurityException, so it wouldn't be caught during + // presubmit. requireExplicitFlagForDynamicReceivers = false; } } diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java index 280256fb49d7..e5123eff9095 100644 --- a/services/core/java/com/android/server/am/UserController.java +++ b/services/core/java/com/android/server/am/UserController.java @@ -1571,12 +1571,6 @@ class UserController implements Handler.Callback { checkCallingHasOneOfThosePermissions("startUserOnSecondaryDisplay", MANAGE_USERS, INTERACT_ACROSS_USERS); - // DEFAULT_DISPLAY is used for the current foreground user only - // TODO(b/245939659): might need to move this check to UserVisibilityMediator to support - // passenger-only screens - Preconditions.checkArgument(displayId != Display.DEFAULT_DISPLAY, - "Cannot use DEFAULT_DISPLAY"); - try { return startUserNoChecks(userId, displayId, USER_START_MODE_BACKGROUND_VISIBLE, /* unlockListener= */ null); diff --git a/services/core/java/com/android/server/appop/AppOpsCheckingServiceImpl.java b/services/core/java/com/android/server/appop/AppOpsCheckingServiceImpl.java index 587fb0410bca..ac25f4edfdb7 100644 --- a/services/core/java/com/android/server/appop/AppOpsCheckingServiceImpl.java +++ b/services/core/java/com/android/server/appop/AppOpsCheckingServiceImpl.java @@ -20,7 +20,7 @@ import static android.app.AppOpsManager.OP_NONE; import static android.app.AppOpsManager.WATCH_FOREGROUND_CHANGES; import static android.app.AppOpsManager.opRestrictsRead; -import static com.android.server.appop.AppOpsServiceImpl.ModeCallback.ALL_OPS; +import static com.android.server.appop.AppOpsService.ModeCallback.ALL_OPS; import android.Manifest; import android.annotation.NonNull; @@ -85,8 +85,8 @@ public class AppOpsCheckingServiceImpl implements AppOpsCheckingServiceInterface AppOpsCheckingServiceImpl(PersistenceScheduler persistenceScheduler, - @NonNull Object lock, Handler handler, Context context, - SparseArray<int[]> switchedOps) { + @NonNull Object lock, Handler handler, Context context, + SparseArray<int[]> switchedOps) { this.mPersistenceScheduler = persistenceScheduler; this.mLock = lock; this.mHandler = handler; @@ -218,7 +218,7 @@ public class AppOpsCheckingServiceImpl implements AppOpsCheckingServiceInterface } @Override - public boolean arePackageModesDefault(@NonNull String packageMode, @UserIdInt int userId) { + public boolean arePackageModesDefault(String packageMode, @UserIdInt int userId) { synchronized (mLock) { ArrayMap<String, SparseIntArray> packageModes = mUserPackageModes.get(userId, null); if (packageModes == null) { @@ -490,16 +490,15 @@ public class AppOpsCheckingServiceImpl implements AppOpsCheckingServiceInterface } @Override - public SparseBooleanArray evalForegroundUidOps(int uid, - @Nullable SparseBooleanArray foregroundOps) { + public SparseBooleanArray evalForegroundUidOps(int uid, SparseBooleanArray foregroundOps) { synchronized (mLock) { return evalForegroundOps(mUidModes.get(uid), foregroundOps); } } @Override - public SparseBooleanArray evalForegroundPackageOps(@NonNull String packageName, - @Nullable SparseBooleanArray foregroundOps, @UserIdInt int userId) { + public SparseBooleanArray evalForegroundPackageOps(String packageName, + SparseBooleanArray foregroundOps, @UserIdInt int userId) { synchronized (mLock) { ArrayMap<String, SparseIntArray> packageModes = mUserPackageModes.get(userId, null); return evalForegroundOps(packageModes == null ? null : packageModes.get(packageName), @@ -538,8 +537,8 @@ public class AppOpsCheckingServiceImpl implements AppOpsCheckingServiceInterface } @Override - public boolean dumpListeners(int dumpOp, int dumpUid, @Nullable String dumpPackage, - @NonNull PrintWriter printWriter) { + public boolean dumpListeners(int dumpOp, int dumpUid, String dumpPackage, + PrintWriter printWriter) { boolean needSep = false; if (mOpModeWatchers.size() > 0) { boolean printedHeader = false; diff --git a/services/core/java/com/android/server/appop/AppOpsCheckingServiceInterface.java b/services/core/java/com/android/server/appop/AppOpsCheckingServiceInterface.java index ef3e3685401f..d8d0d48965ea 100644 --- a/services/core/java/com/android/server/appop/AppOpsCheckingServiceInterface.java +++ b/services/core/java/com/android/server/appop/AppOpsCheckingServiceInterface.java @@ -103,7 +103,7 @@ public interface AppOpsCheckingServiceInterface { * @param packageName package name. * @param userId user id associated with the package. */ - boolean arePackageModesDefault(@NonNull String packageName, @UserIdInt int userId); + boolean arePackageModesDefault(String packageName, @UserIdInt int userId); /** * Stop tracking app-op modes for all uid and packages. @@ -184,7 +184,7 @@ public interface AppOpsCheckingServiceInterface { * @param foregroundOps boolean array where app-ops that have MODE_FOREGROUND are marked true. * @return foregroundOps. */ - SparseBooleanArray evalForegroundUidOps(int uid, @Nullable SparseBooleanArray foregroundOps); + SparseBooleanArray evalForegroundUidOps(int uid, SparseBooleanArray foregroundOps); /** * Go over the list of app-ops for the package name and mark app-ops with MODE_FOREGROUND in @@ -194,8 +194,8 @@ public interface AppOpsCheckingServiceInterface { * @param userId user id associated with the package. * @return foregroundOps. */ - SparseBooleanArray evalForegroundPackageOps(@NonNull String packageName, - @Nullable SparseBooleanArray foregroundOps, @UserIdInt int userId); + SparseBooleanArray evalForegroundPackageOps(String packageName, + SparseBooleanArray foregroundOps, @UserIdInt int userId); /** * Dump op mode and package mode listeners and their details. @@ -205,6 +205,5 @@ public interface AppOpsCheckingServiceInterface { * @param dumpPackage if not null and if dumpOp is -1, dumps watchers for the package name. * @param printWriter writer to dump to. */ - boolean dumpListeners(int dumpOp, int dumpUid, @Nullable String dumpPackage, - @NonNull PrintWriter printWriter); + boolean dumpListeners(int dumpOp, int dumpUid, String dumpPackage, PrintWriter printWriter); } diff --git a/services/core/java/com/android/server/appop/AppOpsCheckingServiceTracingDecorator.java b/services/core/java/com/android/server/appop/AppOpsCheckingServiceTracingDecorator.java deleted file mode 100644 index 44360028704e..000000000000 --- a/services/core/java/com/android/server/appop/AppOpsCheckingServiceTracingDecorator.java +++ /dev/null @@ -1,279 +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.server.appop; - -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.annotation.UserIdInt; -import android.app.AppOpsManager; -import android.os.Trace; -import android.util.ArraySet; -import android.util.SparseBooleanArray; -import android.util.SparseIntArray; - -import java.io.PrintWriter; - -/** - * Surrounds all AppOpsCheckingServiceInterface method calls with Trace.traceBegin and - * Trace.traceEnd. These traces are used for performance testing. - */ -public class AppOpsCheckingServiceTracingDecorator implements AppOpsCheckingServiceInterface { - private static final long TRACE_TAG = Trace.TRACE_TAG_SYSTEM_SERVER; - private final AppOpsCheckingServiceInterface mService; - - AppOpsCheckingServiceTracingDecorator( - @NonNull AppOpsCheckingServiceInterface appOpsCheckingServiceInterface) { - mService = appOpsCheckingServiceInterface; - } - - @Override - public SparseIntArray getNonDefaultUidModes(int uid) { - Trace.traceBegin(TRACE_TAG, - "TaggedTracingAppOpsCheckingServiceInterfaceImpl#getNonDefaultUidModes"); - try { - return mService.getNonDefaultUidModes(uid); - } finally { - Trace.traceEnd(TRACE_TAG); - } - } - - @Override - public int getUidMode(int uid, int op) { - Trace.traceBegin(TRACE_TAG, "TaggedTracingAppOpsCheckingServiceInterfaceImpl#getUidMode"); - try { - return mService.getUidMode(uid, op); - } finally { - Trace.traceEnd(TRACE_TAG); - } - } - - @Override - public boolean setUidMode(int uid, int op, @AppOpsManager.Mode int mode) { - Trace.traceBegin(TRACE_TAG, "TaggedTracingAppOpsCheckingServiceInterfaceImpl#setUidMode"); - try { - return mService.setUidMode(uid, op, mode); - } finally { - Trace.traceEnd(TRACE_TAG); - } - } - - @Override - public int getPackageMode(@NonNull String packageName, int op, @UserIdInt int userId) { - Trace.traceBegin(TRACE_TAG, - "TaggedTracingAppOpsCheckingServiceInterfaceImpl#getPackageMode"); - try { - return mService.getPackageMode(packageName, op, userId); - } finally { - Trace.traceEnd(TRACE_TAG); - } - } - - @Override - public void setPackageMode(@NonNull String packageName, int op, @AppOpsManager.Mode int mode, - @UserIdInt int userId) { - Trace.traceBegin(TRACE_TAG, - "TaggedTracingAppOpsCheckingServiceInterfaceImpl#setPackageMode"); - try { - mService.setPackageMode(packageName, op, mode, userId); - } finally { - Trace.traceEnd(TRACE_TAG); - } - } - - @Override - public boolean removePackage(@NonNull String packageName, @UserIdInt int userId) { - Trace.traceBegin(TRACE_TAG, - "TaggedTracingAppOpsCheckingServiceInterfaceImpl#removePackage"); - try { - return mService.removePackage(packageName, userId); - } finally { - Trace.traceEnd(TRACE_TAG); - } - } - - @Override - public void removeUid(int uid) { - Trace.traceBegin(TRACE_TAG, - "TaggedTracingAppOpsCheckingServiceInterfaceImpl#removeUid"); - try { - mService.removeUid(uid); - } finally { - Trace.traceEnd(TRACE_TAG); - } - } - - @Override - public boolean areUidModesDefault(int uid) { - Trace.traceBegin(TRACE_TAG, - "TaggedTracingAppOpsCheckingServiceInterfaceImpl#areUidModesDefault"); - try { - return mService.areUidModesDefault(uid); - } finally { - Trace.traceEnd(TRACE_TAG); - } - } - - @Override - public boolean arePackageModesDefault(String packageName, @UserIdInt int userId) { - Trace.traceBegin(TRACE_TAG, - "TaggedTracingAppOpsCheckingServiceInterfaceImpl#arePackageModesDefault"); - try { - return mService.arePackageModesDefault(packageName, userId); - } finally { - Trace.traceEnd(TRACE_TAG); - } - } - - @Override - public void clearAllModes() { - Trace.traceBegin(TRACE_TAG, - "TaggedTracingAppOpsCheckingServiceInterfaceImpl#clearAllModes"); - try { - mService.clearAllModes(); - } finally { - Trace.traceEnd(TRACE_TAG); - } - } - - @Override - public void startWatchingOpModeChanged(@NonNull OnOpModeChangedListener changedListener, - int op) { - Trace.traceBegin(TRACE_TAG, - "TaggedTracingAppOpsCheckingServiceInterfaceImpl#startWatchingOpModeChanged"); - try { - mService.startWatchingOpModeChanged(changedListener, op); - } finally { - Trace.traceEnd(TRACE_TAG); - } - } - - @Override - public void startWatchingPackageModeChanged(@NonNull OnOpModeChangedListener changedListener, - @NonNull String packageName) { - Trace.traceBegin(TRACE_TAG, - "TaggedTracingAppOpsCheckingServiceInterfaceImpl#startWatchingPackageModeChanged"); - try { - mService.startWatchingPackageModeChanged(changedListener, packageName); - } finally { - Trace.traceEnd(TRACE_TAG); - } - } - - @Override - public void removeListener(@NonNull OnOpModeChangedListener changedListener) { - Trace.traceBegin(TRACE_TAG, - "TaggedTracingAppOpsCheckingServiceInterfaceImpl#removeListener"); - try { - mService.removeListener(changedListener); - } finally { - Trace.traceEnd(TRACE_TAG); - } - } - - @Override - public ArraySet<OnOpModeChangedListener> getOpModeChangedListeners(int op) { - Trace.traceBegin(TRACE_TAG, - "TaggedTracingAppOpsCheckingServiceInterfaceImpl#getOpModeChangedListeners"); - try { - return mService.getOpModeChangedListeners(op); - } finally { - Trace.traceEnd(TRACE_TAG); - } - } - - @Override - public ArraySet<OnOpModeChangedListener> getPackageModeChangedListeners( - @NonNull String packageName) { - Trace.traceBegin(TRACE_TAG, - "TaggedTracingAppOpsCheckingServiceInterfaceImpl#getPackageModeChangedListeners"); - try { - return mService.getPackageModeChangedListeners(packageName); - } finally { - Trace.traceEnd(TRACE_TAG); - } - } - - @Override - public void notifyWatchersOfChange(int op, int uid) { - Trace.traceBegin(TRACE_TAG, - "TaggedTracingAppOpsCheckingServiceInterfaceImpl#notifyWatchersOfChange"); - try { - mService.notifyWatchersOfChange(op, uid); - } finally { - Trace.traceEnd(TRACE_TAG); - } - } - - @Override - public void notifyOpChanged(@NonNull OnOpModeChangedListener changedListener, int op, int uid, - @Nullable String packageName) { - Trace.traceBegin(TRACE_TAG, - "TaggedTracingAppOpsCheckingServiceInterfaceImpl#notifyOpChanged"); - try { - mService.notifyOpChanged(changedListener, op, uid, packageName); - } finally { - Trace.traceEnd(TRACE_TAG); - } - } - - @Override - public void notifyOpChangedForAllPkgsInUid(int op, int uid, boolean onlyForeground, - @Nullable OnOpModeChangedListener callbackToIgnore) { - Trace.traceBegin(TRACE_TAG, - "TaggedTracingAppOpsCheckingServiceInterfaceImpl#notifyOpChangedForAllPkgsInUid"); - try { - mService.notifyOpChangedForAllPkgsInUid(op, uid, onlyForeground, callbackToIgnore); - } finally { - Trace.traceEnd(TRACE_TAG); - } - } - - @Override - public SparseBooleanArray evalForegroundUidOps(int uid, SparseBooleanArray foregroundOps) { - Trace.traceBegin(TRACE_TAG, - "TaggedTracingAppOpsCheckingServiceInterfaceImpl#evalForegroundUidOps"); - try { - return mService.evalForegroundUidOps(uid, foregroundOps); - } finally { - Trace.traceEnd(TRACE_TAG); - } - } - - @Override - public SparseBooleanArray evalForegroundPackageOps(String packageName, - SparseBooleanArray foregroundOps, @UserIdInt int userId) { - Trace.traceBegin(TRACE_TAG, - "TaggedTracingAppOpsCheckingServiceInterfaceImpl#evalForegroundPackageOps"); - try { - return mService.evalForegroundPackageOps(packageName, foregroundOps, userId); - } finally { - Trace.traceEnd(TRACE_TAG); - } - } - - @Override - public boolean dumpListeners(int dumpOp, int dumpUid, String dumpPackage, - PrintWriter printWriter) { - Trace.traceBegin(TRACE_TAG, - "TaggedTracingAppOpsCheckingServiceInterfaceImpl#dumpListeners"); - try { - return mService.dumpListeners(dumpOp, dumpUid, dumpPackage, printWriter); - } finally { - Trace.traceEnd(TRACE_TAG); - } - } -} diff --git a/services/core/java/com/android/server/appop/AppOpsRestrictionsImpl.java b/services/core/java/com/android/server/appop/AppOpsRestrictionsImpl.java index af5b07e0bffc..f51200f2bf0c 100644 --- a/services/core/java/com/android/server/appop/AppOpsRestrictionsImpl.java +++ b/services/core/java/com/android/server/appop/AppOpsRestrictionsImpl.java @@ -42,7 +42,7 @@ public class AppOpsRestrictionsImpl implements AppOpsRestrictions { private Context mContext; private Handler mHandler; - private AppOpsCheckingServiceInterface mAppOpsServiceInterface; + private AppOpsCheckingServiceInterface mAppOpsCheckingServiceInterface; // Map from (Object token) to (int code) to (boolean restricted) private final ArrayMap<Object, SparseBooleanArray> mGlobalRestrictions = new ArrayMap<>(); @@ -56,10 +56,10 @@ public class AppOpsRestrictionsImpl implements AppOpsRestrictions { mUserRestrictionExcludedPackageTags = new ArrayMap<>(); public AppOpsRestrictionsImpl(Context context, Handler handler, - AppOpsCheckingServiceInterface appOpsServiceInterface) { + AppOpsCheckingServiceInterface appOpsCheckingServiceInterface) { mContext = context; mHandler = handler; - mAppOpsServiceInterface = appOpsServiceInterface; + mAppOpsCheckingServiceInterface = appOpsCheckingServiceInterface; } @Override @@ -219,7 +219,7 @@ public class AppOpsRestrictionsImpl implements AppOpsRestrictions { int restrictedCodesSize = allUserRestrictedCodes.size(); for (int j = 0; j < restrictedCodesSize; j++) { int code = allUserRestrictedCodes.keyAt(j); - mHandler.post(() -> mAppOpsServiceInterface.notifyWatchersOfChange(code, UID_ANY)); + mHandler.post(() -> mAppOpsCheckingServiceInterface.notifyWatchersOfChange(code, UID_ANY)); } } diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java index 39338c6f43ad..934542291334 100644 --- a/services/core/java/com/android/server/appop/AppOpsService.java +++ b/services/core/java/com/android/server/appop/AppOpsService.java @@ -18,22 +18,56 @@ package com.android.server.appop; import static android.app.AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE; import static android.app.AppOpsManager.ATTRIBUTION_FLAG_TRUSTED; +import static android.app.AppOpsManager.CALL_BACK_ON_SWITCHED_OP; +import static android.app.AppOpsManager.FILTER_BY_ATTRIBUTION_TAG; +import static android.app.AppOpsManager.FILTER_BY_OP_NAMES; +import static android.app.AppOpsManager.FILTER_BY_PACKAGE_NAME; +import static android.app.AppOpsManager.FILTER_BY_UID; +import static android.app.AppOpsManager.HISTORY_FLAG_GET_ATTRIBUTION_CHAINS; +import static android.app.AppOpsManager.HistoricalOpsRequestFilter; +import static android.app.AppOpsManager.KEY_BG_STATE_SETTLE_TIME; +import static android.app.AppOpsManager.KEY_FG_SERVICE_STATE_SETTLE_TIME; +import static android.app.AppOpsManager.KEY_TOP_STATE_SETTLE_TIME; import static android.app.AppOpsManager.MODE_ALLOWED; import static android.app.AppOpsManager.MODE_DEFAULT; +import static android.app.AppOpsManager.MODE_ERRORED; +import static android.app.AppOpsManager.MODE_FOREGROUND; +import static android.app.AppOpsManager.MODE_IGNORED; +import static android.app.AppOpsManager.OP_CAMERA; import static android.app.AppOpsManager.OP_FLAGS_ALL; import static android.app.AppOpsManager.OP_FLAG_SELF; import static android.app.AppOpsManager.OP_FLAG_TRUSTED_PROXIED; import static android.app.AppOpsManager.OP_NONE; +import static android.app.AppOpsManager.OP_PLAY_AUDIO; +import static android.app.AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO; +import static android.app.AppOpsManager.OP_RECORD_AUDIO; +import static android.app.AppOpsManager.OP_RECORD_AUDIO_HOTWORD; +import static android.app.AppOpsManager.OP_SCHEDULE_EXACT_ALARM; +import static android.app.AppOpsManager.OP_VIBRATE; +import static android.app.AppOpsManager.OnOpStartedListener.START_TYPE_FAILED; +import static android.app.AppOpsManager.OnOpStartedListener.START_TYPE_STARTED; +import static android.app.AppOpsManager.OpEventProxyInfo; +import static android.app.AppOpsManager.RestrictionBypass; import static android.app.AppOpsManager.SAMPLING_STRATEGY_BOOT_TIME_SAMPLING; import static android.app.AppOpsManager.SAMPLING_STRATEGY_RARELY_USED; import static android.app.AppOpsManager.SAMPLING_STRATEGY_UNIFORM; import static android.app.AppOpsManager.SAMPLING_STRATEGY_UNIFORM_OPS; +import static android.app.AppOpsManager.SECURITY_EXCEPTION_ON_INVALID_ATTRIBUTION_TAG_CHANGE; import static android.app.AppOpsManager._NUM_OP; +import static android.app.AppOpsManager.extractFlagsFromKey; +import static android.app.AppOpsManager.extractUidStateFromKey; +import static android.app.AppOpsManager.modeToName; +import static android.app.AppOpsManager.opAllowSystemBypassRestriction; import static android.app.AppOpsManager.opRestrictsRead; +import static android.app.AppOpsManager.opToName; import static android.app.AppOpsManager.opToPublicName; +import static android.content.Intent.ACTION_PACKAGE_REMOVED; +import static android.content.Intent.EXTRA_REPLACING; import static android.content.pm.PermissionInfo.PROTECTION_DANGEROUS; import static android.content.pm.PermissionInfo.PROTECTION_FLAG_APPOP; +import static com.android.server.appop.AppOpsService.ModeCallback.ALL_OPS; + import android.Manifest; import android.annotation.NonNull; import android.annotation.Nullable; @@ -42,16 +76,21 @@ import android.app.ActivityManager; import android.app.ActivityManagerInternal; import android.app.AppGlobals; import android.app.AppOpsManager; +import android.app.AppOpsManager.AttributedOpEntry; import android.app.AppOpsManager.AttributionFlags; import android.app.AppOpsManager.HistoricalOps; +import android.app.AppOpsManager.Mode; +import android.app.AppOpsManager.OpEntry; import android.app.AppOpsManager.OpFlags; import android.app.AppOpsManagerInternal; import android.app.AppOpsManagerInternal.CheckOpsDelegate; import android.app.AsyncNotedAppOp; import android.app.RuntimeAppOpAccessMessage; import android.app.SyncNotedAppOp; +import android.app.admin.DevicePolicyManagerInternal; import android.content.AttributionSource; import android.content.BroadcastReceiver; +import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; @@ -59,11 +98,15 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; import android.content.pm.PermissionInfo; +import android.database.ContentObserver; import android.hardware.camera2.CameraDevice.CAMERA_AUDIO_RESTRICTION; +import android.net.Uri; import android.os.AsyncTask; import android.os.Binder; +import android.os.Build; import android.os.Bundle; import android.os.Handler; +import android.os.HandlerExecutor; import android.os.IBinder; import android.os.PackageTagsList; import android.os.Process; @@ -74,14 +117,22 @@ import android.os.ResultReceiver; import android.os.ServiceManager; import android.os.ShellCallback; import android.os.ShellCommand; +import android.os.SystemClock; import android.os.UserHandle; +import android.os.storage.StorageManagerInternal; +import android.permission.PermissionManager; +import android.provider.Settings; import android.util.ArrayMap; import android.util.ArraySet; +import android.util.AtomicFile; +import android.util.KeyValueListParser; import android.util.Pair; import android.util.Slog; import android.util.SparseArray; +import android.util.SparseBooleanArray; import android.util.SparseIntArray; import android.util.TimeUtils; +import android.util.Xml; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.Immutable; @@ -93,37 +144,61 @@ import com.android.internal.app.IAppOpsNotedCallback; import com.android.internal.app.IAppOpsService; import com.android.internal.app.IAppOpsStartedCallback; import com.android.internal.app.MessageSamplingConfig; +import com.android.internal.compat.IPlatformCompat; +import com.android.internal.os.Clock; +import com.android.internal.util.ArrayUtils; +import com.android.internal.util.DumpUtils; import com.android.internal.util.Preconditions; +import com.android.internal.util.XmlUtils; import com.android.internal.util.function.pooled.PooledLambda; +import com.android.modules.utils.TypedXmlPullParser; +import com.android.modules.utils.TypedXmlSerializer; import com.android.server.LocalServices; +import com.android.server.LockGuard; +import com.android.server.SystemServerInitThreadPool; import com.android.server.SystemServiceManager; import com.android.server.pm.PackageList; +import com.android.server.pm.UserManagerInternal; +import com.android.server.pm.permission.PermissionManagerServiceInternal; +import com.android.server.pm.pkg.AndroidPackage; +import com.android.server.pm.pkg.component.ParsedAttribution; import com.android.server.policy.AppOpsPolicy; +import dalvik.annotation.optimization.NeverCompile; + +import libcore.util.EmptyArray; + import org.json.JSONException; import org.json.JSONObject; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; import java.io.File; import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; +import java.text.SimpleDateFormat; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Scanner; +import java.util.Set; import java.util.concurrent.ThreadLocalRandom; import java.util.function.Consumer; -/** - * The system service component to {@link AppOpsManager}. - */ -public class AppOpsService extends IAppOpsService.Stub { - - private final AppOpsServiceInterface mAppOpsService; - +public class AppOpsService extends IAppOpsService.Stub implements PersistenceScheduler { static final String TAG = "AppOps"; static final boolean DEBUG = false; @@ -132,19 +207,74 @@ public class AppOpsService extends IAppOpsService.Stub { */ private final ArraySet<NoteOpTrace> mNoteOpCallerStacktraces = new ArraySet<>(); + /** + * Sentinel integer version to denote that there was no appops.xml found on boot. + * This will happen when a device boots with no existing userdata. + */ + private static final int NO_FILE_VERSION = -2; + + /** + * Sentinel integer version to denote that there was no version in the appops.xml found on boot. + * This means the file is coming from a build before versioning was added. + */ + private static final int NO_VERSION = -1; + + /** Increment by one every time and add the corresponding upgrade logic in + * {@link #upgradeLocked(int)} below. The first version was 1 */ + static final int CURRENT_VERSION = 2; + + /** + * This stores the version of appops.xml seen at boot. If this is smaller than + * {@link #CURRENT_VERSION}, then we will run {@link #upgradeLocked(int)} on startup. + */ + private int mVersionAtBoot = NO_FILE_VERSION; + + // Write at most every 30 minutes. + static final long WRITE_DELAY = DEBUG ? 1000 : 30*60*1000; + // Constant meaning that any UID should be matched when dispatching callbacks private static final int UID_ANY = -2; - private static final int MAX_UNFORWARDED_OPS = 10; + private static final int[] OPS_RESTRICTED_ON_SUSPEND = { + OP_PLAY_AUDIO, + OP_RECORD_AUDIO, + OP_CAMERA, + OP_VIBRATE, + }; + private static final int MAX_UNFORWARDED_OPS = 10; + private static final int MAX_UNUSED_POOLED_OBJECTS = 3; private static final int RARELY_USED_PACKAGES_INITIALIZATION_DELAY_MILLIS = 300000; final Context mContext; + final AtomicFile mFile; private final @Nullable File mNoteOpCallerStacktracesFile; final Handler mHandler; + /** + * Pool for {@link AttributedOp.OpEventProxyInfoPool} to avoid to constantly reallocate new + * objects + */ + @GuardedBy("this") + final AttributedOp.OpEventProxyInfoPool mOpEventProxyInfoPool = + new AttributedOp.OpEventProxyInfoPool(MAX_UNUSED_POOLED_OBJECTS); + + /** + * Pool for {@link AttributedOp.InProgressStartOpEventPool} to avoid to constantly reallocate + * new objects + */ + @GuardedBy("this") + final AttributedOp.InProgressStartOpEventPool mInProgressStartOpEventPool = + new AttributedOp.InProgressStartOpEventPool(mOpEventProxyInfoPool, + MAX_UNUSED_POOLED_OBJECTS); + private final AppOpsManagerInternalImpl mAppOpsManagerInternal = new AppOpsManagerInternalImpl(); + @Nullable private final DevicePolicyManagerInternal dpmi = + LocalServices.getService(DevicePolicyManagerInternal.class); + + private final IPlatformCompat mPlatformCompat = IPlatformCompat.Stub.asInterface( + ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE)); /** * Registered callbacks, called from {@link #collectAsyncNotedOp}. @@ -171,9 +301,54 @@ public class AppOpsService extends IAppOpsService.Stub { boolean mWriteNoteOpsScheduled; + boolean mWriteScheduled; + boolean mFastWriteScheduled; + final Runnable mWriteRunner = new Runnable() { + public void run() { + synchronized (AppOpsService.this) { + mWriteScheduled = false; + mFastWriteScheduled = false; + AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() { + @Override protected Void doInBackground(Void... params) { + writeState(); + return null; + } + }; + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[])null); + } + } + }; + + @GuardedBy("this") + @VisibleForTesting + final SparseArray<UidState> mUidStates = new SparseArray<>(); + + volatile @NonNull HistoricalRegistry mHistoricalRegistry = new HistoricalRegistry(this); + + /* + * These are app op restrictions imposed per user from various parties. + */ + private final ArrayMap<IBinder, ClientUserRestrictionState> mOpUserRestrictions = + new ArrayMap<>(); + + /* + * These are app op restrictions imposed globally from various parties within the system. + */ + private final ArrayMap<IBinder, ClientGlobalRestrictionState> mOpGlobalRestrictions = + new ArrayMap<>(); + + SparseIntArray mProfileOwners; + private volatile CheckOpsDelegateDispatcher mCheckOpsDelegateDispatcher = new CheckOpsDelegateDispatcher(/*policy*/ null, /*delegate*/ null); + /** + * Reverse lookup for {@link AppOpsManager#opToSwitch(int)}. Initialized once and never + * changed + */ + private final SparseArray<int[]> mSwitchedOps = new SparseArray<>(); + + private ActivityManagerInternal mActivityManagerInternal; /** Package sampled for message collection in the current session */ @GuardedBy("this") @@ -207,8 +382,546 @@ public class AppOpsService extends IAppOpsService.Stub { /** Package Manager internal. Access via {@link #getPackageManagerInternal()} */ private @Nullable PackageManagerInternal mPackageManagerInternal; + /** Interface for app-op modes.*/ + @VisibleForTesting + AppOpsCheckingServiceInterface mAppOpsCheckingService; + + /** Interface for app-op restrictions.*/ + @VisibleForTesting AppOpsRestrictions mAppOpsRestrictions; + + private AppOpsUidStateTracker mUidStateTracker; + + /** Hands the definition of foreground and uid states */ + @GuardedBy("this") + public AppOpsUidStateTracker getUidStateTracker() { + if (mUidStateTracker == null) { + mUidStateTracker = new AppOpsUidStateTrackerImpl( + LocalServices.getService(ActivityManagerInternal.class), + mHandler, + r -> { + synchronized (AppOpsService.this) { + r.run(); + } + }, + Clock.SYSTEM_CLOCK, mConstants); + + mUidStateTracker.addUidStateChangedCallback(new HandlerExecutor(mHandler), + this::onUidStateChanged); + } + return mUidStateTracker; + } + + /** + * All times are in milliseconds. These constants are kept synchronized with the system + * global Settings. Any access to this class or its fields should be done while + * holding the AppOpsService lock. + */ + final class Constants extends ContentObserver { + + /** + * How long we want for a drop in uid state from top to settle before applying it. + * @see Settings.Global#APP_OPS_CONSTANTS + * @see AppOpsManager#KEY_TOP_STATE_SETTLE_TIME + */ + public long TOP_STATE_SETTLE_TIME; + + /** + * How long we want for a drop in uid state from foreground to settle before applying it. + * @see Settings.Global#APP_OPS_CONSTANTS + * @see AppOpsManager#KEY_FG_SERVICE_STATE_SETTLE_TIME + */ + public long FG_SERVICE_STATE_SETTLE_TIME; + + /** + * How long we want for a drop in uid state from background to settle before applying it. + * @see Settings.Global#APP_OPS_CONSTANTS + * @see AppOpsManager#KEY_BG_STATE_SETTLE_TIME + */ + public long BG_STATE_SETTLE_TIME; + + private final KeyValueListParser mParser = new KeyValueListParser(','); + private ContentResolver mResolver; + + public Constants(Handler handler) { + super(handler); + updateConstants(); + } + + public void startMonitoring(ContentResolver resolver) { + mResolver = resolver; + mResolver.registerContentObserver( + Settings.Global.getUriFor(Settings.Global.APP_OPS_CONSTANTS), + false, this); + updateConstants(); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + updateConstants(); + } + + private void updateConstants() { + String value = mResolver != null ? Settings.Global.getString(mResolver, + Settings.Global.APP_OPS_CONSTANTS) : ""; + + synchronized (AppOpsService.this) { + try { + mParser.setString(value); + } catch (IllegalArgumentException e) { + // Failed to parse the settings string, log this and move on + // with defaults. + Slog.e(TAG, "Bad app ops settings", e); + } + TOP_STATE_SETTLE_TIME = mParser.getDurationMillis( + KEY_TOP_STATE_SETTLE_TIME, 5 * 1000L); + FG_SERVICE_STATE_SETTLE_TIME = mParser.getDurationMillis( + KEY_FG_SERVICE_STATE_SETTLE_TIME, 5 * 1000L); + BG_STATE_SETTLE_TIME = mParser.getDurationMillis( + KEY_BG_STATE_SETTLE_TIME, 1 * 1000L); + } + } + + void dump(PrintWriter pw) { + pw.println(" Settings:"); + + pw.print(" "); pw.print(KEY_TOP_STATE_SETTLE_TIME); pw.print("="); + TimeUtils.formatDuration(TOP_STATE_SETTLE_TIME, pw); + pw.println(); + pw.print(" "); pw.print(KEY_FG_SERVICE_STATE_SETTLE_TIME); pw.print("="); + TimeUtils.formatDuration(FG_SERVICE_STATE_SETTLE_TIME, pw); + pw.println(); + pw.print(" "); pw.print(KEY_BG_STATE_SETTLE_TIME); pw.print("="); + TimeUtils.formatDuration(BG_STATE_SETTLE_TIME, pw); + pw.println(); + } + } + + @VisibleForTesting + final Constants mConstants; + + @VisibleForTesting + final class UidState { + public final int uid; + + public ArrayMap<String, Ops> pkgOps; + + // true indicates there is an interested observer, false there isn't but it has such an op + //TODO: Move foregroundOps and hasForegroundWatchers into the AppOpsServiceInterface. + public SparseBooleanArray foregroundOps; + public boolean hasForegroundWatchers; + + public UidState(int uid) { + this.uid = uid; + } + + public void clear() { + mAppOpsCheckingService.removeUid(uid); + if (pkgOps != null) { + for (String packageName : pkgOps.keySet()) { + mAppOpsCheckingService.removePackage(packageName, UserHandle.getUserId(uid)); + } + } + pkgOps = null; + } + + public boolean isDefault() { + boolean areAllPackageModesDefault = true; + if (pkgOps != null) { + for (String packageName : pkgOps.keySet()) { + if (!mAppOpsCheckingService.arePackageModesDefault(packageName, + UserHandle.getUserId(uid))) { + areAllPackageModesDefault = false; + break; + } + } + } + return (pkgOps == null || pkgOps.isEmpty()) + && mAppOpsCheckingService.areUidModesDefault(uid) + && areAllPackageModesDefault; + } + + // Functions for uid mode access and manipulation. + public SparseIntArray getNonDefaultUidModes() { + return mAppOpsCheckingService.getNonDefaultUidModes(uid); + } + + public int getUidMode(int op) { + return mAppOpsCheckingService.getUidMode(uid, op); + } + + public boolean setUidMode(int op, int mode) { + return mAppOpsCheckingService.setUidMode(uid, op, mode); + } + + @SuppressWarnings("GuardedBy") + int evalMode(int op, int mode) { + return getUidStateTracker().evalMode(uid, op, mode); + } + + public void evalForegroundOps() { + foregroundOps = null; + foregroundOps = mAppOpsCheckingService.evalForegroundUidOps(uid, foregroundOps); + if (pkgOps != null) { + for (int i = pkgOps.size() - 1; i >= 0; i--) { + foregroundOps = mAppOpsCheckingService + .evalForegroundPackageOps(pkgOps.valueAt(i).packageName, foregroundOps, + UserHandle.getUserId(uid)); + } + } + hasForegroundWatchers = false; + if (foregroundOps != null) { + for (int i = 0; i < foregroundOps.size(); i++) { + if (foregroundOps.valueAt(i)) { + hasForegroundWatchers = true; + break; + } + } + } + } + + @SuppressWarnings("GuardedBy") + public int getState() { + return getUidStateTracker().getUidState(uid); + } + + @SuppressWarnings("GuardedBy") + public void dump(PrintWriter pw, long nowElapsed) { + getUidStateTracker().dumpUidState(pw, uid, nowElapsed); + } + } + + final static class Ops extends SparseArray<Op> { + final String packageName; + final UidState uidState; + + /** + * The restriction properties of the package. If {@code null} it could not have been read + * yet and has to be refreshed. + */ + @Nullable RestrictionBypass bypass; + + /** Lazily populated cache of attributionTags of this package */ + final @NonNull ArraySet<String> knownAttributionTags = new ArraySet<>(); + + /** + * Lazily populated cache of <b>valid</b> attributionTags of this package, a set smaller + * than or equal to {@link #knownAttributionTags}. + */ + final @NonNull ArraySet<String> validAttributionTags = new ArraySet<>(); + + Ops(String _packageName, UidState _uidState) { + packageName = _packageName; + uidState = _uidState; + } + } + + /** Returned from {@link #verifyAndGetBypass(int, String, String, String)}. */ + private static final class PackageVerificationResult { + + final RestrictionBypass bypass; + final boolean isAttributionTagValid; + + PackageVerificationResult(RestrictionBypass bypass, boolean isAttributionTagValid) { + this.bypass = bypass; + this.isAttributionTagValid = isAttributionTagValid; + } + } + + final class Op { + int op; + int uid; + final UidState uidState; + final @NonNull String packageName; + + /** attributionTag -> AttributedOp */ + final ArrayMap<String, AttributedOp> mAttributions = new ArrayMap<>(1); + + Op(UidState uidState, String packageName, int op, int uid) { + this.op = op; + this.uid = uid; + this.uidState = uidState; + this.packageName = packageName; + } + + @Mode int getMode() { + return mAppOpsCheckingService.getPackageMode(packageName, this.op, + UserHandle.getUserId(this.uid)); + } + void setMode(@Mode int mode) { + mAppOpsCheckingService.setPackageMode(packageName, this.op, mode, + UserHandle.getUserId(this.uid)); + } + + void removeAttributionsWithNoTime() { + for (int i = mAttributions.size() - 1; i >= 0; i--) { + if (!mAttributions.valueAt(i).hasAnyTime()) { + mAttributions.removeAt(i); + } + } + } + + private @NonNull AttributedOp getOrCreateAttribution(@NonNull Op parent, + @Nullable String attributionTag) { + AttributedOp attributedOp; + + attributedOp = mAttributions.get(attributionTag); + if (attributedOp == null) { + attributedOp = new AttributedOp(AppOpsService.this, attributionTag, parent); + mAttributions.put(attributionTag, attributedOp); + } + + return attributedOp; + } + + @NonNull OpEntry createEntryLocked() { + final int numAttributions = mAttributions.size(); + + final ArrayMap<String, AppOpsManager.AttributedOpEntry> attributionEntries = + new ArrayMap<>(numAttributions); + for (int i = 0; i < numAttributions; i++) { + attributionEntries.put(mAttributions.keyAt(i), + mAttributions.valueAt(i).createAttributedOpEntryLocked()); + } + + return new OpEntry(op, getMode(), attributionEntries); + } + + @NonNull OpEntry createSingleAttributionEntryLocked(@Nullable String attributionTag) { + final int numAttributions = mAttributions.size(); + + final ArrayMap<String, AttributedOpEntry> attributionEntries = new ArrayMap<>(1); + for (int i = 0; i < numAttributions; i++) { + if (Objects.equals(mAttributions.keyAt(i), attributionTag)) { + attributionEntries.put(mAttributions.keyAt(i), + mAttributions.valueAt(i).createAttributedOpEntryLocked()); + break; + } + } + + return new OpEntry(op, getMode(), attributionEntries); + } + + boolean isRunning() { + final int numAttributions = mAttributions.size(); + for (int i = 0; i < numAttributions; i++) { + if (mAttributions.valueAt(i).isRunning()) { + return true; + } + } + + return false; + } + } + + final ArrayMap<IBinder, ModeCallback> mModeWatchers = new ArrayMap<>(); + final ArrayMap<IBinder, SparseArray<ActiveCallback>> mActiveWatchers = new ArrayMap<>(); + final ArrayMap<IBinder, SparseArray<StartedCallback>> mStartedWatchers = new ArrayMap<>(); + final ArrayMap<IBinder, SparseArray<NotedCallback>> mNotedWatchers = new ArrayMap<>(); final AudioRestrictionManager mAudioRestrictionManager = new AudioRestrictionManager(); + final class ModeCallback extends OnOpModeChangedListener implements DeathRecipient { + /** If mWatchedOpCode==ALL_OPS notify for ops affected by the switch-op */ + public static final int ALL_OPS = -2; + + // Need to keep this only because stopWatchingMode needs an IAppOpsCallback. + // Otherwise we can just use the IBinder object. + private final IAppOpsCallback mCallback; + + ModeCallback(IAppOpsCallback callback, int watchingUid, int flags, int watchedOpCode, + int callingUid, int callingPid) { + super(watchingUid, flags, watchedOpCode, callingUid, callingPid); + this.mCallback = callback; + try { + mCallback.asBinder().linkToDeath(this, 0); + } catch (RemoteException e) { + /*ignored*/ + } + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(128); + sb.append("ModeCallback{"); + sb.append(Integer.toHexString(System.identityHashCode(this))); + sb.append(" watchinguid="); + UserHandle.formatUid(sb, getWatchingUid()); + sb.append(" flags=0x"); + sb.append(Integer.toHexString(getFlags())); + switch (getWatchedOpCode()) { + case OP_NONE: + break; + case ALL_OPS: + sb.append(" op=(all)"); + break; + default: + sb.append(" op="); + sb.append(opToName(getWatchedOpCode())); + break; + } + sb.append(" from uid="); + UserHandle.formatUid(sb, getCallingUid()); + sb.append(" pid="); + sb.append(getCallingPid()); + sb.append('}'); + return sb.toString(); + } + + void unlinkToDeath() { + mCallback.asBinder().unlinkToDeath(this, 0); + } + + @Override + public void binderDied() { + stopWatchingMode(mCallback); + } + + @Override + public void onOpModeChanged(int op, int uid, String packageName) throws RemoteException { + mCallback.opChanged(op, uid, packageName); + } + } + + final class ActiveCallback implements DeathRecipient { + final IAppOpsActiveCallback mCallback; + final int mWatchingUid; + final int mCallingUid; + final int mCallingPid; + + ActiveCallback(IAppOpsActiveCallback callback, int watchingUid, int callingUid, + int callingPid) { + mCallback = callback; + mWatchingUid = watchingUid; + mCallingUid = callingUid; + mCallingPid = callingPid; + try { + mCallback.asBinder().linkToDeath(this, 0); + } catch (RemoteException e) { + /*ignored*/ + } + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(128); + sb.append("ActiveCallback{"); + sb.append(Integer.toHexString(System.identityHashCode(this))); + sb.append(" watchinguid="); + UserHandle.formatUid(sb, mWatchingUid); + sb.append(" from uid="); + UserHandle.formatUid(sb, mCallingUid); + sb.append(" pid="); + sb.append(mCallingPid); + sb.append('}'); + return sb.toString(); + } + + void destroy() { + mCallback.asBinder().unlinkToDeath(this, 0); + } + + @Override + public void binderDied() { + stopWatchingActive(mCallback); + } + } + + final class StartedCallback implements DeathRecipient { + final IAppOpsStartedCallback mCallback; + final int mWatchingUid; + final int mCallingUid; + final int mCallingPid; + + StartedCallback(IAppOpsStartedCallback callback, int watchingUid, int callingUid, + int callingPid) { + mCallback = callback; + mWatchingUid = watchingUid; + mCallingUid = callingUid; + mCallingPid = callingPid; + try { + mCallback.asBinder().linkToDeath(this, 0); + } catch (RemoteException e) { + /*ignored*/ + } + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(128); + sb.append("StartedCallback{"); + sb.append(Integer.toHexString(System.identityHashCode(this))); + sb.append(" watchinguid="); + UserHandle.formatUid(sb, mWatchingUid); + sb.append(" from uid="); + UserHandle.formatUid(sb, mCallingUid); + sb.append(" pid="); + sb.append(mCallingPid); + sb.append('}'); + return sb.toString(); + } + + void destroy() { + mCallback.asBinder().unlinkToDeath(this, 0); + } + + @Override + public void binderDied() { + stopWatchingStarted(mCallback); + } + } + + final class NotedCallback implements DeathRecipient { + final IAppOpsNotedCallback mCallback; + final int mWatchingUid; + final int mCallingUid; + final int mCallingPid; + + NotedCallback(IAppOpsNotedCallback callback, int watchingUid, int callingUid, + int callingPid) { + mCallback = callback; + mWatchingUid = watchingUid; + mCallingUid = callingUid; + mCallingPid = callingPid; + try { + mCallback.asBinder().linkToDeath(this, 0); + } catch (RemoteException e) { + /*ignored*/ + } + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(128); + sb.append("NotedCallback{"); + sb.append(Integer.toHexString(System.identityHashCode(this))); + sb.append(" watchinguid="); + UserHandle.formatUid(sb, mWatchingUid); + sb.append(" from uid="); + UserHandle.formatUid(sb, mCallingUid); + sb.append(" pid="); + sb.append(mCallingPid); + sb.append('}'); + return sb.toString(); + } + + void destroy() { + mCallback.asBinder().unlinkToDeath(this, 0); + } + + @Override + public void binderDied() { + stopWatchingNoted(mCallback); + } + } + + /** + * Call {@link AttributedOp#onClientDeath attributedOp.onClientDeath(clientId)}. + */ + static void onClientDeath(@NonNull AttributedOp attributedOp, + @NonNull IBinder clientId) { + attributedOp.onClientDeath(clientId); + } + + /** * Loads the OpsValidation file results into a hashmap {@link #mNoteOpCallerStacktraces} * so that we do not log the same operation twice between instances @@ -233,12 +946,20 @@ public class AppOpsService extends IAppOpsService.Stub { } public AppOpsService(File storagePath, Handler handler, Context context) { - this(handler, context, new AppOpsServiceImpl(storagePath, handler, context)); - } + mContext = context; - @VisibleForTesting - public AppOpsService(Handler handler, Context context, - AppOpsServiceInterface appOpsServiceInterface) { + for (int switchedCode = 0; switchedCode < _NUM_OP; switchedCode++) { + int switchCode = AppOpsManager.opToSwitch(switchedCode); + mSwitchedOps.put(switchCode, + ArrayUtils.appendInt(mSwitchedOps.get(switchCode), switchedCode)); + } + mAppOpsCheckingService = + new AppOpsCheckingServiceImpl(this, this, handler, context, mSwitchedOps); + mAppOpsRestrictions = new AppOpsRestrictionsImpl(context, handler, + mAppOpsCheckingService); + + LockGuard.installLock(this, LockGuard.INDEX_APP_OPS); + mFile = new AtomicFile(storagePath, "appops"); if (AppOpsManager.NOTE_OP_COLLECTION_ENABLED) { mNoteOpCallerStacktracesFile = new File(SystemServiceManager.ensureSystemDir(), "noteOpStackTraces.json"); @@ -246,25 +967,189 @@ public class AppOpsService extends IAppOpsService.Stub { } else { mNoteOpCallerStacktracesFile = null; } - - mAppOpsService = appOpsServiceInterface; - mContext = context; mHandler = handler; + mConstants = new Constants(mHandler); + readState(); } - /** - * Publishes binder and local service. - */ public void publish() { ServiceManager.addService(Context.APP_OPS_SERVICE, asBinder()); LocalServices.addService(AppOpsManagerInternal.class, mAppOpsManagerInternal); } - /** - * Finishes boot sequence. - */ + /** Handler for work when packages are removed or updated */ + private BroadcastReceiver mOnPackageUpdatedReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + String pkgName = intent.getData().getEncodedSchemeSpecificPart(); + int uid = intent.getIntExtra(Intent.EXTRA_UID, Process.INVALID_UID); + + if (action.equals(ACTION_PACKAGE_REMOVED) && !intent.hasExtra(EXTRA_REPLACING)) { + synchronized (AppOpsService.this) { + UidState uidState = mUidStates.get(uid); + if (uidState == null || uidState.pkgOps == null) { + return; + } + mAppOpsCheckingService.removePackage(pkgName, UserHandle.getUserId(uid)); + Ops removedOps = uidState.pkgOps.remove(pkgName); + if (removedOps != null) { + scheduleFastWriteLocked(); + } + } + } else if (action.equals(Intent.ACTION_PACKAGE_REPLACED)) { + AndroidPackage pkg = getPackageManagerInternal().getPackage(pkgName); + if (pkg == null) { + return; + } + + ArrayMap<String, String> dstAttributionTags = new ArrayMap<>(); + ArraySet<String> attributionTags = new ArraySet<>(); + attributionTags.add(null); + if (pkg.getAttributions() != null) { + int numAttributions = pkg.getAttributions().size(); + for (int attributionNum = 0; attributionNum < numAttributions; + attributionNum++) { + ParsedAttribution attribution = pkg.getAttributions().get(attributionNum); + attributionTags.add(attribution.getTag()); + + int numInheritFrom = attribution.getInheritFrom().size(); + for (int inheritFromNum = 0; inheritFromNum < numInheritFrom; + inheritFromNum++) { + dstAttributionTags.put(attribution.getInheritFrom().get(inheritFromNum), + attribution.getTag()); + } + } + } + + synchronized (AppOpsService.this) { + UidState uidState = mUidStates.get(uid); + if (uidState == null || uidState.pkgOps == null) { + return; + } + + Ops ops = uidState.pkgOps.get(pkgName); + if (ops == null) { + return; + } + + // Reset cached package properties to re-initialize when needed + ops.bypass = null; + ops.knownAttributionTags.clear(); + + // Merge data collected for removed attributions into their successor + // attributions + int numOps = ops.size(); + for (int opNum = 0; opNum < numOps; opNum++) { + Op op = ops.valueAt(opNum); + + int numAttributions = op.mAttributions.size(); + for (int attributionNum = numAttributions - 1; attributionNum >= 0; + attributionNum--) { + String attributionTag = op.mAttributions.keyAt(attributionNum); + + if (attributionTags.contains(attributionTag)) { + // attribution still exist after upgrade + continue; + } + + String newAttributionTag = dstAttributionTags.get(attributionTag); + + AttributedOp newAttributedOp = op.getOrCreateAttribution(op, + newAttributionTag); + newAttributedOp.add(op.mAttributions.valueAt(attributionNum)); + op.mAttributions.removeAt(attributionNum); + + scheduleFastWriteLocked(); + } + } + } + } + } + }; + public void systemReady() { - mAppOpsService.systemReady(); + synchronized (this) { + upgradeLocked(mVersionAtBoot); + } + + mConstants.startMonitoring(mContext.getContentResolver()); + mHistoricalRegistry.systemReady(mContext.getContentResolver()); + + IntentFilter packageUpdateFilter = new IntentFilter(); + packageUpdateFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); + packageUpdateFilter.addAction(Intent.ACTION_PACKAGE_REPLACED); + packageUpdateFilter.addDataScheme("package"); + + mContext.registerReceiverAsUser(mOnPackageUpdatedReceiver, UserHandle.ALL, + packageUpdateFilter, null, null); + + synchronized (this) { + for (int uidNum = mUidStates.size() - 1; uidNum >= 0; uidNum--) { + int uid = mUidStates.keyAt(uidNum); + UidState uidState = mUidStates.valueAt(uidNum); + + String[] pkgsInUid = getPackagesForUid(uidState.uid); + if (ArrayUtils.isEmpty(pkgsInUid)) { + uidState.clear(); + mUidStates.removeAt(uidNum); + scheduleFastWriteLocked(); + continue; + } + + ArrayMap<String, Ops> pkgs = uidState.pkgOps; + if (pkgs == null) { + continue; + } + + int numPkgs = pkgs.size(); + for (int pkgNum = 0; pkgNum < numPkgs; pkgNum++) { + String pkg = pkgs.keyAt(pkgNum); + + String action; + if (!ArrayUtils.contains(pkgsInUid, pkg)) { + action = Intent.ACTION_PACKAGE_REMOVED; + } else { + action = Intent.ACTION_PACKAGE_REPLACED; + } + + SystemServerInitThreadPool.submit( + () -> mOnPackageUpdatedReceiver.onReceive(mContext, new Intent(action) + .setData(Uri.fromParts("package", pkg, null)) + .putExtra(Intent.EXTRA_UID, uid)), + "Update app-ops uidState in case package " + pkg + " changed"); + } + } + } + + final IntentFilter packageSuspendFilter = new IntentFilter(); + packageSuspendFilter.addAction(Intent.ACTION_PACKAGES_UNSUSPENDED); + packageSuspendFilter.addAction(Intent.ACTION_PACKAGES_SUSPENDED); + mContext.registerReceiverAsUser(new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + final int[] changedUids = intent.getIntArrayExtra(Intent.EXTRA_CHANGED_UID_LIST); + final String[] changedPkgs = intent.getStringArrayExtra( + Intent.EXTRA_CHANGED_PACKAGE_LIST); + for (int code : OPS_RESTRICTED_ON_SUSPEND) { + ArraySet<OnOpModeChangedListener> onModeChangedListeners; + synchronized (AppOpsService.this) { + onModeChangedListeners = + mAppOpsCheckingService.getOpModeChangedListeners(code); + if (onModeChangedListeners == null) { + continue; + } + } + for (int i = 0; i < changedUids.length; i++) { + final int changedUid = changedUids[i]; + final String changedPkg = changedPkgs[i]; + // We trust packagemanager to insert matching uid and packageNames in the + // extras + notifyOpChanged(onModeChangedListeners, code, changedUid, changedPkg); + } + } + } + }, UserHandle.ALL, packageSuspendFilter, null, null); final IntentFilter packageAddedFilter = new IntentFilter(); packageAddedFilter.addAction(Intent.ACTION_PACKAGE_ADDED); @@ -272,8 +1157,9 @@ public class AppOpsService extends IAppOpsService.Stub { mContext.registerReceiver(new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { + final Uri data = intent.getData(); - final String packageName = intent.getData().getSchemeSpecificPart(); + final String packageName = data.getSchemeSpecificPart(); PackageInfo pi = getPackageManagerInternal().getPackageInfo(packageName, PackageManager.GET_PERMISSIONS, Process.myUid(), mContext.getUserId()); if (isSamplingTarget(pi)) { @@ -308,6 +1194,8 @@ public class AppOpsService extends IAppOpsService.Stub { } } }); + + mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class); } /** @@ -322,18 +1210,132 @@ public class AppOpsService extends IAppOpsService.Stub { mCheckOpsDelegateDispatcher = new CheckOpsDelegateDispatcher(policy, delegate); } - /** - * Notify when a package is removed - */ public void packageRemoved(int uid, String packageName) { - mAppOpsService.packageRemoved(uid, packageName); + synchronized (this) { + UidState uidState = mUidStates.get(uid); + if (uidState == null) { + return; + } + + Ops removedOps = null; + + // Remove any package state if such. + if (uidState.pkgOps != null) { + removedOps = uidState.pkgOps.remove(packageName); + mAppOpsCheckingService.removePackage(packageName, UserHandle.getUserId(uid)); + } + + // If we just nuked the last package state check if the UID is valid. + if (removedOps != null && uidState.pkgOps.isEmpty() + && getPackagesForUid(uid).length <= 0) { + uidState.clear(); + mUidStates.remove(uid); + } + + if (removedOps != null) { + scheduleFastWriteLocked(); + + final int numOps = removedOps.size(); + for (int opNum = 0; opNum < numOps; opNum++) { + final Op op = removedOps.valueAt(opNum); + + final int numAttributions = op.mAttributions.size(); + for (int attributionNum = 0; attributionNum < numAttributions; + attributionNum++) { + AttributedOp attributedOp = op.mAttributions.valueAt(attributionNum); + + while (attributedOp.isRunning()) { + attributedOp.finished(attributedOp.mInProgressEvents.keyAt(0)); + } + while (attributedOp.isPaused()) { + attributedOp.finished(attributedOp.mPausedInProgressEvents.keyAt(0)); + } + } + } + } + } + + mHandler.post(PooledLambda.obtainRunnable(HistoricalRegistry::clearHistory, + mHistoricalRegistry, uid, packageName)); } - /** - * Notify when a uid is removed. - */ public void uidRemoved(int uid) { - mAppOpsService.uidRemoved(uid); + synchronized (this) { + if (mUidStates.indexOfKey(uid) >= 0) { + mUidStates.get(uid).clear(); + mUidStates.remove(uid); + scheduleFastWriteLocked(); + } + } + } + + // The callback method from ForegroundPolicyInterface + private void onUidStateChanged(int uid, int state, boolean foregroundModeMayChange) { + synchronized (this) { + UidState uidState = getUidStateLocked(uid, true); + + if (uidState != null && foregroundModeMayChange && uidState.hasForegroundWatchers) { + for (int fgi = uidState.foregroundOps.size() - 1; fgi >= 0; fgi--) { + if (!uidState.foregroundOps.valueAt(fgi)) { + continue; + } + final int code = uidState.foregroundOps.keyAt(fgi); + + if (uidState.getUidMode(code) != AppOpsManager.opToDefaultMode(code) + && uidState.getUidMode(code) == AppOpsManager.MODE_FOREGROUND) { + mHandler.sendMessage(PooledLambda.obtainMessage( + AppOpsService::notifyOpChangedForAllPkgsInUid, + this, code, uidState.uid, true, null)); + } else if (uidState.pkgOps != null) { + final ArraySet<OnOpModeChangedListener> listenerSet = + mAppOpsCheckingService.getOpModeChangedListeners(code); + if (listenerSet != null) { + for (int cbi = listenerSet.size() - 1; cbi >= 0; cbi--) { + final OnOpModeChangedListener listener = listenerSet.valueAt(cbi); + if ((listener.getFlags() + & AppOpsManager.WATCH_FOREGROUND_CHANGES) == 0 + || !listener.isWatchingUid(uidState.uid)) { + continue; + } + for (int pkgi = uidState.pkgOps.size() - 1; pkgi >= 0; pkgi--) { + final Op op = uidState.pkgOps.valueAt(pkgi).get(code); + if (op == null) { + continue; + } + if (op.getMode() == AppOpsManager.MODE_FOREGROUND) { + mHandler.sendMessage(PooledLambda.obtainMessage( + AppOpsService::notifyOpChanged, + this, listenerSet.valueAt(cbi), code, uidState.uid, + uidState.pkgOps.keyAt(pkgi))); + } + } + } + } + } + } + } + + if (uidState != null && uidState.pkgOps != null) { + int numPkgs = uidState.pkgOps.size(); + for (int pkgNum = 0; pkgNum < numPkgs; pkgNum++) { + Ops ops = uidState.pkgOps.valueAt(pkgNum); + + int numOps = ops.size(); + for (int opNum = 0; opNum < numOps; opNum++) { + Op op = ops.valueAt(opNum); + + int numAttributions = op.mAttributions.size(); + for (int attributionNum = 0; attributionNum < numAttributions; + attributionNum++) { + AttributedOp attributedOp = op.mAttributions.valueAt( + attributionNum); + + attributedOp.onUidStateChanged(state); + } + } + } + } + } } /** @@ -341,60 +1343,542 @@ public class AppOpsService extends IAppOpsService.Stub { */ public void updateUidProcState(int uid, int procState, @ActivityManager.ProcessCapability int capability) { - mAppOpsService.updateUidProcState(uid, procState, capability); + synchronized (this) { + getUidStateTracker().updateUidProcState(uid, procState, capability); + if (!mUidStates.contains(uid)) { + UidState uidState = new UidState(uid); + mUidStates.put(uid, uidState); + onUidStateChanged(uid, + AppOpsUidStateTracker.processStateToUidState(procState), false); + } + } } - /** - * Initiates shutdown. - */ public void shutdown() { - mAppOpsService.shutdown(); - + Slog.w(TAG, "Writing app ops before shutdown..."); + boolean doWrite = false; + synchronized (this) { + if (mWriteScheduled) { + mWriteScheduled = false; + mFastWriteScheduled = false; + mHandler.removeCallbacks(mWriteRunner); + doWrite = true; + } + } + if (doWrite) { + writeState(); + } if (AppOpsManager.NOTE_OP_COLLECTION_ENABLED && mWriteNoteOpsScheduled) { writeNoteOps(); } + + mHistoricalRegistry.shutdown(); + } + + private ArrayList<AppOpsManager.OpEntry> collectOps(Ops pkgOps, int[] ops) { + ArrayList<AppOpsManager.OpEntry> resOps = null; + if (ops == null) { + resOps = new ArrayList<>(); + for (int j=0; j<pkgOps.size(); j++) { + Op curOp = pkgOps.valueAt(j); + resOps.add(getOpEntryForResult(curOp)); + } + } else { + for (int j=0; j<ops.length; j++) { + Op curOp = pkgOps.get(ops[j]); + if (curOp != null) { + if (resOps == null) { + resOps = new ArrayList<>(); + } + resOps.add(getOpEntryForResult(curOp)); + } + } + } + return resOps; + } + + @Nullable + private ArrayList<AppOpsManager.OpEntry> collectUidOps(@NonNull UidState uidState, + @Nullable int[] ops) { + final SparseIntArray opModes = uidState.getNonDefaultUidModes(); + if (opModes == null) { + return null; + } + + int opModeCount = opModes.size(); + if (opModeCount == 0) { + return null; + } + ArrayList<AppOpsManager.OpEntry> resOps = null; + if (ops == null) { + resOps = new ArrayList<>(); + for (int i = 0; i < opModeCount; i++) { + int code = opModes.keyAt(i); + resOps.add(new OpEntry(code, opModes.get(code), Collections.emptyMap())); + } + } else { + for (int j=0; j<ops.length; j++) { + int code = ops[j]; + if (opModes.indexOfKey(code) >= 0) { + if (resOps == null) { + resOps = new ArrayList<>(); + } + resOps.add(new OpEntry(code, opModes.get(code), Collections.emptyMap())); + } + } + } + return resOps; + } + + private static @NonNull OpEntry getOpEntryForResult(@NonNull Op op) { + return op.createEntryLocked(); } @Override public List<AppOpsManager.PackageOps> getPackagesForOps(int[] ops) { - return mAppOpsService.getPackagesForOps(ops); + final int callingUid = Binder.getCallingUid(); + final boolean hasAllPackageAccess = mContext.checkPermission( + Manifest.permission.GET_APP_OPS_STATS, Binder.getCallingPid(), + Binder.getCallingUid(), null) == PackageManager.PERMISSION_GRANTED; + ArrayList<AppOpsManager.PackageOps> res = null; + synchronized (this) { + final int uidStateCount = mUidStates.size(); + for (int i = 0; i < uidStateCount; i++) { + UidState uidState = mUidStates.valueAt(i); + if (uidState.pkgOps == null || uidState.pkgOps.isEmpty()) { + continue; + } + ArrayMap<String, Ops> packages = uidState.pkgOps; + final int packageCount = packages.size(); + for (int j = 0; j < packageCount; j++) { + Ops pkgOps = packages.valueAt(j); + ArrayList<AppOpsManager.OpEntry> resOps = collectOps(pkgOps, ops); + if (resOps != null) { + if (res == null) { + res = new ArrayList<>(); + } + AppOpsManager.PackageOps resPackage = new AppOpsManager.PackageOps( + pkgOps.packageName, pkgOps.uidState.uid, resOps); + // Caller can always see their packages and with a permission all. + if (hasAllPackageAccess || callingUid == pkgOps.uidState.uid) { + res.add(resPackage); + } + } + } + } + } + return res; } @Override public List<AppOpsManager.PackageOps> getOpsForPackage(int uid, String packageName, int[] ops) { - return mAppOpsService.getOpsForPackage(uid, packageName, ops); + enforceGetAppOpsStatsPermissionIfNeeded(uid,packageName); + String resolvedPackageName = AppOpsManager.resolvePackageName(uid, packageName); + if (resolvedPackageName == null) { + return Collections.emptyList(); + } + synchronized (this) { + Ops pkgOps = getOpsLocked(uid, resolvedPackageName, null, false, null, + /* edit */ false); + if (pkgOps == null) { + return null; + } + ArrayList<AppOpsManager.OpEntry> resOps = collectOps(pkgOps, ops); + if (resOps == null) { + return null; + } + ArrayList<AppOpsManager.PackageOps> res = new ArrayList<AppOpsManager.PackageOps>(); + AppOpsManager.PackageOps resPackage = new AppOpsManager.PackageOps( + pkgOps.packageName, pkgOps.uidState.uid, resOps); + res.add(resPackage); + return res; + } + } + + private void enforceGetAppOpsStatsPermissionIfNeeded(int uid, String packageName) { + final int callingUid = Binder.getCallingUid(); + // We get to access everything + if (callingUid == Process.myPid()) { + return; + } + // Apps can access their own data + if (uid == callingUid && packageName != null + && checkPackage(uid, packageName) == MODE_ALLOWED) { + return; + } + // Otherwise, you need a permission... + mContext.enforcePermission(android.Manifest.permission.GET_APP_OPS_STATS, + Binder.getCallingPid(), callingUid, null); + } + + /** + * Verify that historical appop request arguments are valid. + */ + private void ensureHistoricalOpRequestIsValid(int uid, String packageName, + String attributionTag, List<String> opNames, int filter, long beginTimeMillis, + long endTimeMillis, int flags) { + if ((filter & FILTER_BY_UID) != 0) { + Preconditions.checkArgument(uid != Process.INVALID_UID); + } else { + Preconditions.checkArgument(uid == Process.INVALID_UID); + } + + if ((filter & FILTER_BY_PACKAGE_NAME) != 0) { + Objects.requireNonNull(packageName); + } else { + Preconditions.checkArgument(packageName == null); + } + + if ((filter & FILTER_BY_ATTRIBUTION_TAG) == 0) { + Preconditions.checkArgument(attributionTag == null); + } + + if ((filter & FILTER_BY_OP_NAMES) != 0) { + Objects.requireNonNull(opNames); + } else { + Preconditions.checkArgument(opNames == null); + } + + Preconditions.checkFlagsArgument(filter, + FILTER_BY_UID | FILTER_BY_PACKAGE_NAME | FILTER_BY_ATTRIBUTION_TAG + | FILTER_BY_OP_NAMES); + Preconditions.checkArgumentNonnegative(beginTimeMillis); + Preconditions.checkArgument(endTimeMillis > beginTimeMillis); + Preconditions.checkFlagsArgument(flags, OP_FLAGS_ALL); } @Override public void getHistoricalOps(int uid, String packageName, String attributionTag, List<String> opNames, int dataType, int filter, long beginTimeMillis, long endTimeMillis, int flags, RemoteCallback callback) { - mAppOpsService.getHistoricalOps(uid, packageName, attributionTag, opNames, - dataType, filter, beginTimeMillis, endTimeMillis, flags, callback); + PackageManager pm = mContext.getPackageManager(); + + ensureHistoricalOpRequestIsValid(uid, packageName, attributionTag, opNames, filter, + beginTimeMillis, endTimeMillis, flags); + Objects.requireNonNull(callback, "callback cannot be null"); + ActivityManagerInternal ami = LocalServices.getService(ActivityManagerInternal.class); + boolean isSelfRequest = (filter & FILTER_BY_UID) != 0 && uid == Binder.getCallingUid(); + if (!isSelfRequest) { + boolean isCallerInstrumented = + ami.getInstrumentationSourceUid(Binder.getCallingUid()) != Process.INVALID_UID; + boolean isCallerSystem = Binder.getCallingPid() == Process.myPid(); + boolean isCallerPermissionController; + try { + isCallerPermissionController = pm.getPackageUidAsUser( + mContext.getPackageManager().getPermissionControllerPackageName(), 0, + UserHandle.getUserId(Binder.getCallingUid())) + == Binder.getCallingUid(); + } catch (PackageManager.NameNotFoundException doesNotHappen) { + return; + } + + boolean doesCallerHavePermission = mContext.checkPermission( + android.Manifest.permission.GET_HISTORICAL_APP_OPS_STATS, + Binder.getCallingPid(), Binder.getCallingUid()) + == PackageManager.PERMISSION_GRANTED; + + if (!isCallerSystem && !isCallerInstrumented && !isCallerPermissionController + && !doesCallerHavePermission) { + mHandler.post(() -> callback.sendResult(new Bundle())); + return; + } + + mContext.enforcePermission(android.Manifest.permission.GET_APP_OPS_STATS, + Binder.getCallingPid(), Binder.getCallingUid(), "getHistoricalOps"); + } + + final String[] opNamesArray = (opNames != null) + ? opNames.toArray(new String[opNames.size()]) : null; + + Set<String> attributionChainExemptPackages = null; + if ((dataType & HISTORY_FLAG_GET_ATTRIBUTION_CHAINS) != 0) { + attributionChainExemptPackages = + PermissionManager.getIndicatorExemptedPackages(mContext); + } + + final String[] chainExemptPkgArray = attributionChainExemptPackages != null + ? attributionChainExemptPackages.toArray( + new String[attributionChainExemptPackages.size()]) : null; + + // Must not hold the appops lock + mHandler.post(PooledLambda.obtainRunnable(HistoricalRegistry::getHistoricalOps, + mHistoricalRegistry, uid, packageName, attributionTag, opNamesArray, dataType, + filter, beginTimeMillis, endTimeMillis, flags, chainExemptPkgArray, + callback).recycleOnUse()); } @Override public void getHistoricalOpsFromDiskRaw(int uid, String packageName, String attributionTag, List<String> opNames, int dataType, int filter, long beginTimeMillis, long endTimeMillis, int flags, RemoteCallback callback) { - mAppOpsService.getHistoricalOpsFromDiskRaw(uid, packageName, attributionTag, - opNames, dataType, filter, beginTimeMillis, endTimeMillis, flags, callback); + ensureHistoricalOpRequestIsValid(uid, packageName, attributionTag, opNames, filter, + beginTimeMillis, endTimeMillis, flags); + Objects.requireNonNull(callback, "callback cannot be null"); + + mContext.enforcePermission(Manifest.permission.MANAGE_APPOPS, + Binder.getCallingPid(), Binder.getCallingUid(), "getHistoricalOps"); + + final String[] opNamesArray = (opNames != null) + ? opNames.toArray(new String[opNames.size()]) : null; + + Set<String> attributionChainExemptPackages = null; + if ((dataType & HISTORY_FLAG_GET_ATTRIBUTION_CHAINS) != 0) { + attributionChainExemptPackages = + PermissionManager.getIndicatorExemptedPackages(mContext); + } + + final String[] chainExemptPkgArray = attributionChainExemptPackages != null + ? attributionChainExemptPackages.toArray( + new String[attributionChainExemptPackages.size()]) : null; + + // Must not hold the appops lock + mHandler.post(PooledLambda.obtainRunnable(HistoricalRegistry::getHistoricalOpsFromDiskRaw, + mHistoricalRegistry, uid, packageName, attributionTag, opNamesArray, dataType, + filter, beginTimeMillis, endTimeMillis, flags, chainExemptPkgArray, + callback).recycleOnUse()); } @Override public void reloadNonHistoricalState() { - mAppOpsService.reloadNonHistoricalState(); + mContext.enforcePermission(Manifest.permission.MANAGE_APPOPS, + Binder.getCallingPid(), Binder.getCallingUid(), "reloadNonHistoricalState"); + writeState(); + readState(); } @Override public List<AppOpsManager.PackageOps> getUidOps(int uid, int[] ops) { - return mAppOpsService.getUidOps(uid, ops); + mContext.enforcePermission(android.Manifest.permission.GET_APP_OPS_STATS, + Binder.getCallingPid(), Binder.getCallingUid(), null); + synchronized (this) { + UidState uidState = getUidStateLocked(uid, false); + if (uidState == null) { + return null; + } + ArrayList<AppOpsManager.OpEntry> resOps = collectUidOps(uidState, ops); + if (resOps == null) { + return null; + } + ArrayList<AppOpsManager.PackageOps> res = new ArrayList<AppOpsManager.PackageOps>(); + AppOpsManager.PackageOps resPackage = new AppOpsManager.PackageOps( + null, uidState.uid, resOps); + res.add(resPackage); + return res; + } + } + + private void pruneOpLocked(Op op, int uid, String packageName) { + op.removeAttributionsWithNoTime(); + + if (op.mAttributions.isEmpty()) { + Ops ops = getOpsLocked(uid, packageName, null, false, null, /* edit */ false); + if (ops != null) { + ops.remove(op.op); + op.setMode(AppOpsManager.opToDefaultMode(op.op)); + if (ops.size() <= 0) { + UidState uidState = ops.uidState; + ArrayMap<String, Ops> pkgOps = uidState.pkgOps; + if (pkgOps != null) { + pkgOps.remove(ops.packageName); + mAppOpsCheckingService.removePackage(ops.packageName, + UserHandle.getUserId(uidState.uid)); + if (pkgOps.isEmpty()) { + uidState.pkgOps = null; + } + if (uidState.isDefault()) { + uidState.clear(); + mUidStates.remove(uid); + } + } + } + } + } + } + + private void enforceManageAppOpsModes(int callingPid, int callingUid, int targetUid) { + if (callingPid == Process.myPid()) { + return; + } + final int callingUser = UserHandle.getUserId(callingUid); + synchronized (this) { + if (mProfileOwners != null && mProfileOwners.get(callingUser, -1) == callingUid) { + if (targetUid >= 0 && callingUser == UserHandle.getUserId(targetUid)) { + // Profile owners are allowed to change modes but only for apps + // within their user. + return; + } + } + } + mContext.enforcePermission(android.Manifest.permission.MANAGE_APP_OPS_MODES, + Binder.getCallingPid(), Binder.getCallingUid(), null); } @Override public void setUidMode(int code, int uid, int mode) { - mAppOpsService.setUidMode(code, uid, mode, null); + setUidMode(code, uid, mode, null); + } + + private void setUidMode(int code, int uid, int mode, + @Nullable IAppOpsCallback permissionPolicyCallback) { + if (DEBUG) { + Slog.i(TAG, "uid " + uid + " OP_" + opToName(code) + " := " + modeToName(mode) + + " by uid " + Binder.getCallingUid()); + } + + enforceManageAppOpsModes(Binder.getCallingPid(), Binder.getCallingUid(), uid); + verifyIncomingOp(code); + code = AppOpsManager.opToSwitch(code); + + if (permissionPolicyCallback == null) { + updatePermissionRevokedCompat(uid, code, mode); + } + + int previousMode; + synchronized (this) { + final int defaultMode = AppOpsManager.opToDefaultMode(code); + + UidState uidState = getUidStateLocked(uid, false); + if (uidState == null) { + if (mode == defaultMode) { + return; + } + uidState = new UidState(uid); + mUidStates.put(uid, uidState); + } + if (uidState.getUidMode(code) != AppOpsManager.opToDefaultMode(code)) { + previousMode = uidState.getUidMode(code); + } else { + // doesn't look right but is legacy behavior. + previousMode = MODE_DEFAULT; + } + + if (!uidState.setUidMode(code, mode)) { + return; + } + uidState.evalForegroundOps(); + if (mode != MODE_ERRORED && mode != previousMode) { + updateStartedOpModeForUidLocked(code, mode == MODE_IGNORED, uid); + } + } + + notifyOpChangedForAllPkgsInUid(code, uid, false, permissionPolicyCallback); + notifyOpChangedSync(code, uid, null, mode, previousMode); + } + + /** + * Notify that an op changed for all packages in an uid. + * + * @param code The op that changed + * @param uid The uid the op was changed for + * @param onlyForeground Only notify watchers that watch for foreground changes + */ + private void notifyOpChangedForAllPkgsInUid(int code, int uid, boolean onlyForeground, + @Nullable IAppOpsCallback callbackToIgnore) { + ModeCallback listenerToIgnore = callbackToIgnore != null + ? mModeWatchers.get(callbackToIgnore.asBinder()) : null; + mAppOpsCheckingService.notifyOpChangedForAllPkgsInUid(code, uid, onlyForeground, + listenerToIgnore); + } + + private void updatePermissionRevokedCompat(int uid, int switchCode, int mode) { + PackageManager packageManager = mContext.getPackageManager(); + if (packageManager == null) { + // This can only happen during early boot. At this time the permission state and appop + // state are in sync + return; + } + + String[] packageNames = packageManager.getPackagesForUid(uid); + if (ArrayUtils.isEmpty(packageNames)) { + return; + } + String packageName = packageNames[0]; + + int[] ops = mSwitchedOps.get(switchCode); + for (int code : ops) { + String permissionName = AppOpsManager.opToPermission(code); + if (permissionName == null) { + continue; + } + + if (packageManager.checkPermission(permissionName, packageName) + != PackageManager.PERMISSION_GRANTED) { + continue; + } + + PermissionInfo permissionInfo; + try { + permissionInfo = packageManager.getPermissionInfo(permissionName, 0); + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + continue; + } + + if (!permissionInfo.isRuntime()) { + continue; + } + + boolean supportsRuntimePermissions = getPackageManagerInternal() + .getUidTargetSdkVersion(uid) >= Build.VERSION_CODES.M; + + UserHandle user = UserHandle.getUserHandleForUid(uid); + boolean isRevokedCompat; + if (permissionInfo.backgroundPermission != null) { + if (packageManager.checkPermission(permissionInfo.backgroundPermission, packageName) + == PackageManager.PERMISSION_GRANTED) { + boolean isBackgroundRevokedCompat = mode != AppOpsManager.MODE_ALLOWED; + + if (isBackgroundRevokedCompat && supportsRuntimePermissions) { + Slog.w(TAG, "setUidMode() called with a mode inconsistent with runtime" + + " permission state, this is discouraged and you should revoke the" + + " runtime permission instead: uid=" + uid + ", switchCode=" + + switchCode + ", mode=" + mode + ", permission=" + + permissionInfo.backgroundPermission); + } + + final long identity = Binder.clearCallingIdentity(); + try { + packageManager.updatePermissionFlags(permissionInfo.backgroundPermission, + packageName, PackageManager.FLAG_PERMISSION_REVOKED_COMPAT, + isBackgroundRevokedCompat + ? PackageManager.FLAG_PERMISSION_REVOKED_COMPAT : 0, user); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + isRevokedCompat = mode != AppOpsManager.MODE_ALLOWED + && mode != AppOpsManager.MODE_FOREGROUND; + } else { + isRevokedCompat = mode != AppOpsManager.MODE_ALLOWED; + } + + if (isRevokedCompat && supportsRuntimePermissions) { + Slog.w(TAG, "setUidMode() called with a mode inconsistent with runtime" + + " permission state, this is discouraged and you should revoke the" + + " runtime permission instead: uid=" + uid + ", switchCode=" + + switchCode + ", mode=" + mode + ", permission=" + permissionName); + } + + final long identity = Binder.clearCallingIdentity(); + try { + packageManager.updatePermissionFlags(permissionName, packageName, + PackageManager.FLAG_PERMISSION_REVOKED_COMPAT, isRevokedCompat + ? PackageManager.FLAG_PERMISSION_REVOKED_COMPAT : 0, user); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + } + + private void notifyOpChangedSync(int code, int uid, @NonNull String packageName, int mode, + int previousMode) { + final StorageManagerInternal storageManagerInternal = + LocalServices.getService(StorageManagerInternal.class); + if (storageManagerInternal != null) { + storageManagerInternal.onAppOpsChanged(code, uid, packageName, mode, previousMode); + } } /** @@ -407,12 +1891,309 @@ public class AppOpsService extends IAppOpsService.Stub { */ @Override public void setMode(int code, int uid, @NonNull String packageName, int mode) { - mAppOpsService.setMode(code, uid, packageName, mode, null); + setMode(code, uid, packageName, mode, null); + } + + void setMode(int code, int uid, @NonNull String packageName, int mode, + @Nullable IAppOpsCallback permissionPolicyCallback) { + enforceManageAppOpsModes(Binder.getCallingPid(), Binder.getCallingUid(), uid); + verifyIncomingOp(code); + if (!isIncomingPackageValid(packageName, UserHandle.getUserId(uid))) { + return; + } + + ArraySet<OnOpModeChangedListener> repCbs = null; + code = AppOpsManager.opToSwitch(code); + + PackageVerificationResult pvr; + try { + pvr = verifyAndGetBypass(uid, packageName, null); + } catch (SecurityException e) { + Slog.e(TAG, "Cannot setMode", e); + return; + } + + int previousMode = MODE_DEFAULT; + synchronized (this) { + UidState uidState = getUidStateLocked(uid, false); + Op op = getOpLocked(code, uid, packageName, null, false, pvr.bypass, /* edit */ true); + if (op != null) { + if (op.getMode() != mode) { + previousMode = op.getMode(); + op.setMode(mode); + + if (uidState != null) { + uidState.evalForegroundOps(); + } + ArraySet<OnOpModeChangedListener> cbs = + mAppOpsCheckingService.getOpModeChangedListeners(code); + if (cbs != null) { + if (repCbs == null) { + repCbs = new ArraySet<>(); + } + repCbs.addAll(cbs); + } + cbs = mAppOpsCheckingService.getPackageModeChangedListeners(packageName); + if (cbs != null) { + if (repCbs == null) { + repCbs = new ArraySet<>(); + } + repCbs.addAll(cbs); + } + if (repCbs != null && permissionPolicyCallback != null) { + repCbs.remove(mModeWatchers.get(permissionPolicyCallback.asBinder())); + } + if (mode == AppOpsManager.opToDefaultMode(op.op)) { + // If going into the default mode, prune this op + // if there is nothing else interesting in it. + pruneOpLocked(op, uid, packageName); + } + scheduleFastWriteLocked(); + if (mode != MODE_ERRORED) { + updateStartedOpModeForUidLocked(code, mode == MODE_IGNORED, uid); + } + } + } + } + if (repCbs != null) { + mHandler.sendMessage(PooledLambda.obtainMessage( + AppOpsService::notifyOpChanged, + this, repCbs, code, uid, packageName)); + } + + notifyOpChangedSync(code, uid, packageName, mode, previousMode); + } + + private void notifyOpChanged(ArraySet<OnOpModeChangedListener> callbacks, int code, + int uid, String packageName) { + for (int i = 0; i < callbacks.size(); i++) { + final OnOpModeChangedListener callback = callbacks.valueAt(i); + notifyOpChanged(callback, code, uid, packageName); + } + } + + private void notifyOpChanged(OnOpModeChangedListener callback, int code, + int uid, String packageName) { + mAppOpsCheckingService.notifyOpChanged(callback, code, uid, packageName); + } + + private static ArrayList<ChangeRec> addChange(ArrayList<ChangeRec> reports, + int op, int uid, String packageName, int previousMode) { + boolean duplicate = false; + if (reports == null) { + reports = new ArrayList<>(); + } else { + final int reportCount = reports.size(); + for (int j = 0; j < reportCount; j++) { + ChangeRec report = reports.get(j); + if (report.op == op && report.pkg.equals(packageName)) { + duplicate = true; + break; + } + } + } + if (!duplicate) { + reports.add(new ChangeRec(op, uid, packageName, previousMode)); + } + + return reports; + } + + private static HashMap<OnOpModeChangedListener, ArrayList<ChangeRec>> addCallbacks( + HashMap<OnOpModeChangedListener, ArrayList<ChangeRec>> callbacks, + int op, int uid, String packageName, int previousMode, + ArraySet<OnOpModeChangedListener> cbs) { + if (cbs == null) { + return callbacks; + } + if (callbacks == null) { + callbacks = new HashMap<>(); + } + final int N = cbs.size(); + for (int i=0; i<N; i++) { + OnOpModeChangedListener cb = cbs.valueAt(i); + ArrayList<ChangeRec> reports = callbacks.get(cb); + ArrayList<ChangeRec> changed = addChange(reports, op, uid, packageName, previousMode); + if (changed != reports) { + callbacks.put(cb, changed); + } + } + return callbacks; + } + + static final class ChangeRec { + final int op; + final int uid; + final String pkg; + final int previous_mode; + + ChangeRec(int _op, int _uid, String _pkg, int _previous_mode) { + op = _op; + uid = _uid; + pkg = _pkg; + previous_mode = _previous_mode; + } } @Override public void resetAllModes(int reqUserId, String reqPackageName) { - mAppOpsService.resetAllModes(reqUserId, reqPackageName); + final int callingPid = Binder.getCallingPid(); + final int callingUid = Binder.getCallingUid(); + reqUserId = ActivityManager.handleIncomingUser(callingPid, callingUid, reqUserId, + true, true, "resetAllModes", null); + + int reqUid = -1; + if (reqPackageName != null) { + try { + reqUid = AppGlobals.getPackageManager().getPackageUid( + reqPackageName, PackageManager.MATCH_UNINSTALLED_PACKAGES, reqUserId); + } catch (RemoteException e) { + /* ignore - local call */ + } + } + + enforceManageAppOpsModes(callingPid, callingUid, reqUid); + + HashMap<OnOpModeChangedListener, ArrayList<ChangeRec>> callbacks = null; + ArrayList<ChangeRec> allChanges = new ArrayList<>(); + synchronized (this) { + boolean changed = false; + for (int i = mUidStates.size() - 1; i >= 0; i--) { + UidState uidState = mUidStates.valueAt(i); + + SparseIntArray opModes = uidState.getNonDefaultUidModes(); + if (opModes != null && (uidState.uid == reqUid || reqUid == -1)) { + final int uidOpCount = opModes.size(); + for (int j = uidOpCount - 1; j >= 0; j--) { + final int code = opModes.keyAt(j); + if (AppOpsManager.opAllowsReset(code)) { + int previousMode = opModes.valueAt(j); + uidState.setUidMode(code, AppOpsManager.opToDefaultMode(code)); + for (String packageName : getPackagesForUid(uidState.uid)) { + callbacks = addCallbacks(callbacks, code, uidState.uid, packageName, + previousMode, + mAppOpsCheckingService.getOpModeChangedListeners(code)); + callbacks = addCallbacks(callbacks, code, uidState.uid, packageName, + previousMode, mAppOpsCheckingService + .getPackageModeChangedListeners(packageName)); + + allChanges = addChange(allChanges, code, uidState.uid, + packageName, previousMode); + } + } + } + } + + if (uidState.pkgOps == null) { + continue; + } + + if (reqUserId != UserHandle.USER_ALL + && reqUserId != UserHandle.getUserId(uidState.uid)) { + // Skip any ops for a different user + continue; + } + + Map<String, Ops> packages = uidState.pkgOps; + Iterator<Map.Entry<String, Ops>> it = packages.entrySet().iterator(); + boolean uidChanged = false; + while (it.hasNext()) { + Map.Entry<String, Ops> ent = it.next(); + String packageName = ent.getKey(); + if (reqPackageName != null && !reqPackageName.equals(packageName)) { + // Skip any ops for a different package + continue; + } + Ops pkgOps = ent.getValue(); + for (int j=pkgOps.size()-1; j>=0; j--) { + Op curOp = pkgOps.valueAt(j); + if (shouldDeferResetOpToDpm(curOp.op)) { + deferResetOpToDpm(curOp.op, reqPackageName, reqUserId); + continue; + } + if (AppOpsManager.opAllowsReset(curOp.op) + && curOp.getMode() != AppOpsManager.opToDefaultMode(curOp.op)) { + int previousMode = curOp.getMode(); + curOp.setMode(AppOpsManager.opToDefaultMode(curOp.op)); + changed = true; + uidChanged = true; + final int uid = curOp.uidState.uid; + callbacks = addCallbacks(callbacks, curOp.op, uid, packageName, + previousMode, + mAppOpsCheckingService.getOpModeChangedListeners(curOp.op)); + callbacks = addCallbacks(callbacks, curOp.op, uid, packageName, + previousMode, mAppOpsCheckingService + .getPackageModeChangedListeners(packageName)); + + allChanges = addChange(allChanges, curOp.op, uid, packageName, + previousMode); + curOp.removeAttributionsWithNoTime(); + if (curOp.mAttributions.isEmpty()) { + pkgOps.removeAt(j); + } + } + } + if (pkgOps.size() == 0) { + it.remove(); + mAppOpsCheckingService.removePackage(packageName, + UserHandle.getUserId(uidState.uid)); + } + } + if (uidState.isDefault()) { + uidState.clear(); + mUidStates.remove(uidState.uid); + } + if (uidChanged) { + uidState.evalForegroundOps(); + } + } + + if (changed) { + scheduleFastWriteLocked(); + } + } + if (callbacks != null) { + for (Map.Entry<OnOpModeChangedListener, ArrayList<ChangeRec>> ent + : callbacks.entrySet()) { + OnOpModeChangedListener cb = ent.getKey(); + ArrayList<ChangeRec> reports = ent.getValue(); + for (int i=0; i<reports.size(); i++) { + ChangeRec rep = reports.get(i); + mHandler.sendMessage(PooledLambda.obtainMessage( + AppOpsService::notifyOpChanged, + this, cb, rep.op, rep.uid, rep.pkg)); + } + } + } + + int numChanges = allChanges.size(); + for (int i = 0; i < numChanges; i++) { + ChangeRec change = allChanges.get(i); + notifyOpChangedSync(change.op, change.uid, change.pkg, + AppOpsManager.opToDefaultMode(change.op), change.previous_mode); + } + } + + private boolean shouldDeferResetOpToDpm(int op) { + // TODO(b/174582385): avoid special-casing app-op resets by migrating app-op permission + // pre-grants to a role-based mechanism or another general-purpose mechanism. + return dpmi != null && dpmi.supportsResetOp(op); + } + + /** Assumes {@link #shouldDeferResetOpToDpm(int)} is true. */ + private void deferResetOpToDpm(int op, String packageName, @UserIdInt int userId) { + // TODO(b/174582385): avoid special-casing app-op resets by migrating app-op permission + // pre-grants to a role-based mechanism or another general-purpose mechanism. + dpmi.resetOp(op, packageName, userId); + } + + private void evalAllForegroundOpsLocked() { + for (int uidi = mUidStates.size() - 1; uidi >= 0; uidi--) { + final UidState uidState = mUidStates.valueAt(uidi); + if (uidState.foregroundOps != null) { + uidState.evalForegroundOps(); + } + } } @Override @@ -423,17 +2204,66 @@ public class AppOpsService extends IAppOpsService.Stub { @Override public void startWatchingModeWithFlags(int op, String packageName, int flags, IAppOpsCallback callback) { - mAppOpsService.startWatchingModeWithFlags(op, packageName, flags, callback); + int watchedUid = -1; + final int callingUid = Binder.getCallingUid(); + final int callingPid = Binder.getCallingPid(); + // TODO: should have a privileged permission to protect this. + // Also, if the caller has requested WATCH_FOREGROUND_CHANGES, should we require + // the USAGE_STATS permission since this can provide information about when an + // app is in the foreground? + Preconditions.checkArgumentInRange(op, AppOpsManager.OP_NONE, + AppOpsManager._NUM_OP - 1, "Invalid op code: " + op); + if (callback == null) { + return; + } + final boolean mayWatchPackageName = packageName != null + && !filterAppAccessUnlocked(packageName, UserHandle.getUserId(callingUid)); + synchronized (this) { + int switchOp = (op != AppOpsManager.OP_NONE) ? AppOpsManager.opToSwitch(op) : op; + + int notifiedOps; + if ((flags & CALL_BACK_ON_SWITCHED_OP) == 0) { + if (op == OP_NONE) { + notifiedOps = ALL_OPS; + } else { + notifiedOps = op; + } + } else { + notifiedOps = switchOp; + } + + ModeCallback cb = mModeWatchers.get(callback.asBinder()); + if (cb == null) { + cb = new ModeCallback(callback, watchedUid, flags, notifiedOps, callingUid, + callingPid); + mModeWatchers.put(callback.asBinder(), cb); + } + if (switchOp != AppOpsManager.OP_NONE) { + mAppOpsCheckingService.startWatchingOpModeChanged(cb, switchOp); + } + if (mayWatchPackageName) { + mAppOpsCheckingService.startWatchingPackageModeChanged(cb, packageName); + } + evalAllForegroundOpsLocked(); + } } @Override public void stopWatchingMode(IAppOpsCallback callback) { - mAppOpsService.stopWatchingMode(callback); + if (callback == null) { + return; + } + synchronized (this) { + ModeCallback cb = mModeWatchers.remove(callback.asBinder()); + if (cb != null) { + cb.unlinkToDeath(); + mAppOpsCheckingService.removeListener(cb); + } + + evalAllForegroundOpsLocked(); + } } - /** - * @return the current {@link CheckOpsDelegate}. - */ public CheckOpsDelegate getAppOpsServiceDelegate() { synchronized (AppOpsService.this) { final CheckOpsDelegateDispatcher dispatcher = mCheckOpsDelegateDispatcher; @@ -441,9 +2271,6 @@ public class AppOpsService extends IAppOpsService.Stub { } } - /** - * Sets the appops {@link CheckOpsDelegate} - */ public void setAppOpsServiceDelegate(CheckOpsDelegate delegate) { synchronized (AppOpsService.this) { final CheckOpsDelegateDispatcher oldDispatcher = mCheckOpsDelegateDispatcher; @@ -467,7 +2294,58 @@ public class AppOpsService extends IAppOpsService.Stub { private int checkOperationImpl(int code, int uid, String packageName, @Nullable String attributionTag, boolean raw) { - return mAppOpsService.checkOperation(code, uid, packageName, attributionTag, raw); + verifyIncomingOp(code); + if (!isIncomingPackageValid(packageName, UserHandle.getUserId(uid))) { + return AppOpsManager.opToDefaultMode(code); + } + + String resolvedPackageName = AppOpsManager.resolvePackageName(uid, packageName); + if (resolvedPackageName == null) { + return AppOpsManager.MODE_IGNORED; + } + return checkOperationUnchecked(code, uid, resolvedPackageName, attributionTag, raw); + } + + /** + * Get the mode of an app-op. + * + * @param code The code of the op + * @param uid The uid of the package the op belongs to + * @param packageName The package the op belongs to + * @param raw If the raw state of eval-ed state should be checked. + * + * @return The mode of the op + */ + private @Mode int checkOperationUnchecked(int code, int uid, @NonNull String packageName, + @Nullable String attributionTag, boolean raw) { + PackageVerificationResult pvr; + try { + pvr = verifyAndGetBypass(uid, packageName, null); + } catch (SecurityException e) { + Slog.e(TAG, "checkOperation", e); + return AppOpsManager.opToDefaultMode(code); + } + + if (isOpRestrictedDueToSuspend(code, packageName, uid)) { + return AppOpsManager.MODE_IGNORED; + } + synchronized (this) { + if (isOpRestrictedLocked(uid, code, packageName, attributionTag, pvr.bypass, true)) { + return AppOpsManager.MODE_IGNORED; + } + code = AppOpsManager.opToSwitch(code); + UidState uidState = getUidStateLocked(uid, false); + if (uidState != null + && uidState.getUidMode(code) != AppOpsManager.opToDefaultMode(code)) { + final int rawMode = uidState.getUidMode(code); + return raw ? rawMode : uidState.evalMode(code, rawMode); + } + Op op = getOpLocked(code, uid, packageName, null, false, pvr.bypass, /* edit */ false); + if (op == null) { + return AppOpsManager.opToDefaultMode(code); + } + return raw ? op.getMode() : op.uidState.evalMode(op.op, op.getMode()); + } } @Override @@ -487,8 +2365,7 @@ public class AppOpsService extends IAppOpsService.Stub { @Override public void setAudioRestriction(int code, int usage, int uid, int mode, String[] exceptionPackages) { - mAppOpsService.enforceManageAppOpsModes(Binder.getCallingPid(), - Binder.getCallingUid(), uid); + enforceManageAppOpsModes(Binder.getCallingPid(), Binder.getCallingUid(), uid); verifyIncomingUid(uid); verifyIncomingOp(code); @@ -496,35 +2373,58 @@ public class AppOpsService extends IAppOpsService.Stub { code, usage, uid, mode, exceptionPackages); mHandler.sendMessage(PooledLambda.obtainMessage( - AppOpsServiceInterface::notifyWatchersOfChange, mAppOpsService, code, - UID_ANY)); + AppOpsService::notifyWatchersOfChange, this, code, UID_ANY)); } @Override public void setCameraAudioRestriction(@CAMERA_AUDIO_RESTRICTION int mode) { - mAppOpsService.enforceManageAppOpsModes(Binder.getCallingPid(), - Binder.getCallingUid(), -1); + enforceManageAppOpsModes(Binder.getCallingPid(), Binder.getCallingUid(), -1); mAudioRestrictionManager.setCameraAudioRestriction(mode); mHandler.sendMessage(PooledLambda.obtainMessage( - AppOpsServiceInterface::notifyWatchersOfChange, mAppOpsService, + AppOpsService::notifyWatchersOfChange, this, AppOpsManager.OP_PLAY_AUDIO, UID_ANY)); mHandler.sendMessage(PooledLambda.obtainMessage( - AppOpsServiceInterface::notifyWatchersOfChange, mAppOpsService, + AppOpsService::notifyWatchersOfChange, this, AppOpsManager.OP_VIBRATE, UID_ANY)); } @Override public int checkPackage(int uid, String packageName) { - return mAppOpsService.checkPackage(uid, packageName); + Objects.requireNonNull(packageName); + try { + verifyAndGetBypass(uid, packageName, null); + // When the caller is the system, it's possible that the packageName is the special + // one (e.g., "root") which isn't actually existed. + if (resolveUid(packageName) == uid + || (isPackageExisted(packageName) + && !filterAppAccessUnlocked(packageName, UserHandle.getUserId(uid)))) { + return AppOpsManager.MODE_ALLOWED; + } + return AppOpsManager.MODE_ERRORED; + } catch (SecurityException ignored) { + return AppOpsManager.MODE_ERRORED; + } } private boolean isPackageExisted(String packageName) { return getPackageManagerInternal().getPackageStateInternal(packageName) != null; } + /** + * This method will check with PackageManager to determine if the package provided should + * be visible to the {@link Binder#getCallingUid()}. + * + * NOTE: This must not be called while synchronized on {@code this} to avoid dead locks + */ + private boolean filterAppAccessUnlocked(String packageName, int userId) { + final int callingUid = Binder.getCallingUid(); + return LocalServices.getService(PackageManagerInternal.class) + .filterAppAccess(packageName, callingUid, userId); + } + @Override public SyncNotedAppOp noteProxyOperation(int code, AttributionSource attributionSource, boolean shouldCollectAsyncNotedOp, String message, boolean shouldCollectMessage, @@ -570,20 +2470,13 @@ public class AppOpsService extends IAppOpsService.Stub { final int proxyFlags = isProxyTrusted ? AppOpsManager.OP_FLAG_TRUSTED_PROXY : AppOpsManager.OP_FLAG_UNTRUSTED_PROXY; - final int proxyReturn = mAppOpsService.noteOperationUnchecked(code, proxyUid, + final SyncNotedAppOp proxyReturn = noteOperationUnchecked(code, proxyUid, resolveProxyPackageName, proxyAttributionTag, Process.INVALID_UID, null, null, - proxyFlags); - if (proxyReturn != AppOpsManager.MODE_ALLOWED) { - return new SyncNotedAppOp(proxyReturn, code, proxiedAttributionTag, + proxyFlags, !isProxyTrusted, "proxy " + message, shouldCollectMessage); + if (proxyReturn.getOpMode() != AppOpsManager.MODE_ALLOWED) { + return new SyncNotedAppOp(proxyReturn.getOpMode(), code, proxiedAttributionTag, proxiedPackageName); } - if (shouldCollectAsyncNotedOp) { - boolean isProxyAttributionTagValid = mAppOpsService.isAttributionTagValid(proxyUid, - resolveProxyPackageName, proxyAttributionTag, null); - collectAsyncNotedOp(proxyUid, resolveProxyPackageName, code, - isProxyAttributionTagValid ? proxyAttributionTag : null, proxyFlags, - message, shouldCollectMessage); - } } String resolveProxiedPackageName = AppOpsManager.resolvePackageName(proxiedUid, @@ -595,32 +2488,9 @@ public class AppOpsService extends IAppOpsService.Stub { final int proxiedFlags = isProxyTrusted ? AppOpsManager.OP_FLAG_TRUSTED_PROXIED : AppOpsManager.OP_FLAG_UNTRUSTED_PROXIED; - final int result = mAppOpsService.noteOperationUnchecked(code, proxiedUid, - resolveProxiedPackageName, proxiedAttributionTag, proxyUid, resolveProxyPackageName, - proxyAttributionTag, proxiedFlags); - - boolean isProxiedAttributionTagValid = mAppOpsService.isAttributionTagValid(proxiedUid, - resolveProxiedPackageName, proxiedAttributionTag, resolveProxyPackageName); - if (shouldCollectAsyncNotedOp && result == AppOpsManager.MODE_ALLOWED) { - collectAsyncNotedOp(proxiedUid, resolveProxiedPackageName, code, - isProxiedAttributionTagValid ? proxiedAttributionTag : null, proxiedFlags, - message, shouldCollectMessage); - } - - - return new SyncNotedAppOp(result, code, - isProxiedAttributionTagValid ? proxiedAttributionTag : null, - resolveProxiedPackageName); - } - - private boolean isCallerAndAttributionTrusted(@NonNull AttributionSource attributionSource) { - if (attributionSource.getUid() != Binder.getCallingUid() - && attributionSource.isTrusted(mContext)) { - return true; - } - return mContext.checkPermission(android.Manifest.permission.UPDATE_APP_OPS_STATS, - Binder.getCallingPid(), Binder.getCallingUid(), null) - == PackageManager.PERMISSION_GRANTED; + return noteOperationUnchecked(code, proxiedUid, resolveProxiedPackageName, + proxiedAttributionTag, proxyUid, resolveProxyPackageName, proxyAttributionTag, + proxiedFlags, shouldCollectAsyncNotedOp, message, shouldCollectMessage); } @Override @@ -634,58 +2504,258 @@ public class AppOpsService extends IAppOpsService.Stub { private SyncNotedAppOp noteOperationImpl(int code, int uid, @Nullable String packageName, @Nullable String attributionTag, boolean shouldCollectAsyncNotedOp, @Nullable String message, boolean shouldCollectMessage) { + verifyIncomingUid(uid); + verifyIncomingOp(code); if (!isIncomingPackageValid(packageName, UserHandle.getUserId(uid))) { return new SyncNotedAppOp(AppOpsManager.MODE_ERRORED, code, attributionTag, packageName); } - int result = mAppOpsService.noteOperation(code, uid, packageName, - attributionTag, message); - String resolvedPackageName = AppOpsManager.resolvePackageName(uid, packageName); + if (resolvedPackageName == null) { + return new SyncNotedAppOp(AppOpsManager.MODE_IGNORED, code, attributionTag, + packageName); + } + return noteOperationUnchecked(code, uid, resolvedPackageName, attributionTag, + Process.INVALID_UID, null, null, AppOpsManager.OP_FLAG_SELF, + shouldCollectAsyncNotedOp, message, shouldCollectMessage); + } - boolean isAttributionTagValid = mAppOpsService.isAttributionTagValid(uid, - resolvedPackageName, attributionTag, null); - - if (shouldCollectAsyncNotedOp && result == MODE_ALLOWED) { - collectAsyncNotedOp(uid, resolvedPackageName, code, - isAttributionTagValid ? attributionTag : null, AppOpsManager.OP_FLAG_SELF, - message, shouldCollectMessage); + private SyncNotedAppOp noteOperationUnchecked(int code, int uid, @NonNull String packageName, + @Nullable String attributionTag, int proxyUid, String proxyPackageName, + @Nullable String proxyAttributionTag, @OpFlags int flags, + boolean shouldCollectAsyncNotedOp, @Nullable String message, + boolean shouldCollectMessage) { + PackageVerificationResult pvr; + try { + pvr = verifyAndGetBypass(uid, packageName, attributionTag, proxyPackageName); + boolean wasNull = attributionTag == null; + if (!pvr.isAttributionTagValid) { + attributionTag = null; + } + } catch (SecurityException e) { + Slog.e(TAG, "noteOperation", e); + return new SyncNotedAppOp(AppOpsManager.MODE_ERRORED, code, attributionTag, + packageName); } - return new SyncNotedAppOp(result, code, isAttributionTagValid ? attributionTag : null, - resolvedPackageName); + synchronized (this) { + final Ops ops = getOpsLocked(uid, packageName, attributionTag, + pvr.isAttributionTagValid, pvr.bypass, /* edit */ true); + if (ops == null) { + scheduleOpNotedIfNeededLocked(code, uid, packageName, attributionTag, flags, + AppOpsManager.MODE_IGNORED); + if (DEBUG) Slog.d(TAG, "noteOperation: no op for code " + code + " uid " + uid + + " package " + packageName + "flags: " + + AppOpsManager.flagsToString(flags)); + return new SyncNotedAppOp(AppOpsManager.MODE_ERRORED, code, attributionTag, + packageName); + } + final Op op = getOpLocked(ops, code, uid, true); + final AttributedOp attributedOp = op.getOrCreateAttribution(op, attributionTag); + if (attributedOp.isRunning()) { + Slog.w(TAG, "Noting op not finished: uid " + uid + " pkg " + packageName + " code " + + code + " startTime of in progress event=" + + attributedOp.mInProgressEvents.valueAt(0).getStartTime()); + } + + final int switchCode = AppOpsManager.opToSwitch(code); + final UidState uidState = ops.uidState; + if (isOpRestrictedLocked(uid, code, packageName, attributionTag, pvr.bypass, false)) { + attributedOp.rejected(uidState.getState(), flags); + scheduleOpNotedIfNeededLocked(code, uid, packageName, attributionTag, flags, + AppOpsManager.MODE_IGNORED); + return new SyncNotedAppOp(AppOpsManager.MODE_IGNORED, code, attributionTag, + packageName); + } + // If there is a non-default per UID policy (we set UID op mode only if + // non-default) it takes over, otherwise use the per package policy. + if (uidState.getUidMode(switchCode) != AppOpsManager.opToDefaultMode(switchCode)) { + final int uidMode = uidState.evalMode(code, uidState.getUidMode(switchCode)); + if (uidMode != AppOpsManager.MODE_ALLOWED) { + if (DEBUG) Slog.d(TAG, "noteOperation: uid reject #" + uidMode + " for code " + + switchCode + " (" + code + ") uid " + uid + " package " + + packageName + " flags: " + AppOpsManager.flagsToString(flags)); + attributedOp.rejected(uidState.getState(), flags); + scheduleOpNotedIfNeededLocked(code, uid, packageName, attributionTag, flags, + uidMode); + return new SyncNotedAppOp(uidMode, code, attributionTag, packageName); + } + } else { + final Op switchOp = switchCode != code ? getOpLocked(ops, switchCode, uid, true) + : op; + final int mode = switchOp.uidState.evalMode(switchOp.op, switchOp.getMode()); + if (mode != AppOpsManager.MODE_ALLOWED) { + if (DEBUG) Slog.d(TAG, "noteOperation: reject #" + mode + " for code " + + switchCode + " (" + code + ") uid " + uid + " package " + + packageName + " flags: " + AppOpsManager.flagsToString(flags)); + attributedOp.rejected(uidState.getState(), flags); + scheduleOpNotedIfNeededLocked(code, uid, packageName, attributionTag, flags, + mode); + return new SyncNotedAppOp(mode, code, attributionTag, packageName); + } + } + if (DEBUG) { + Slog.d(TAG, + "noteOperation: allowing code " + code + " uid " + uid + " package " + + packageName + (attributionTag == null ? "" + : "." + attributionTag) + " flags: " + + AppOpsManager.flagsToString(flags)); + } + scheduleOpNotedIfNeededLocked(code, uid, packageName, attributionTag, flags, + AppOpsManager.MODE_ALLOWED); + attributedOp.accessed(proxyUid, proxyPackageName, proxyAttributionTag, + uidState.getState(), + flags); + + if (shouldCollectAsyncNotedOp) { + collectAsyncNotedOp(uid, packageName, code, attributionTag, flags, message, + shouldCollectMessage); + } + + return new SyncNotedAppOp(AppOpsManager.MODE_ALLOWED, code, attributionTag, + packageName); + } } // TODO moltmann: Allow watching for attribution ops @Override public void startWatchingActive(int[] ops, IAppOpsActiveCallback callback) { - mAppOpsService.startWatchingActive(ops, callback); + int watchedUid = Process.INVALID_UID; + final int callingUid = Binder.getCallingUid(); + final int callingPid = Binder.getCallingPid(); + if (mContext.checkCallingOrSelfPermission(Manifest.permission.WATCH_APPOPS) + != PackageManager.PERMISSION_GRANTED) { + watchedUid = callingUid; + } + if (ops != null) { + Preconditions.checkArrayElementsInRange(ops, 0, + AppOpsManager._NUM_OP - 1, "Invalid op code in: " + Arrays.toString(ops)); + } + if (callback == null) { + return; + } + synchronized (this) { + SparseArray<ActiveCallback> callbacks = mActiveWatchers.get(callback.asBinder()); + if (callbacks == null) { + callbacks = new SparseArray<>(); + mActiveWatchers.put(callback.asBinder(), callbacks); + } + final ActiveCallback activeCallback = new ActiveCallback(callback, watchedUid, + callingUid, callingPid); + for (int op : ops) { + callbacks.put(op, activeCallback); + } + } } @Override public void stopWatchingActive(IAppOpsActiveCallback callback) { - mAppOpsService.stopWatchingActive(callback); + if (callback == null) { + return; + } + synchronized (this) { + final SparseArray<ActiveCallback> activeCallbacks = + mActiveWatchers.remove(callback.asBinder()); + if (activeCallbacks == null) { + return; + } + final int callbackCount = activeCallbacks.size(); + for (int i = 0; i < callbackCount; i++) { + activeCallbacks.valueAt(i).destroy(); + } + } } @Override public void startWatchingStarted(int[] ops, @NonNull IAppOpsStartedCallback callback) { - mAppOpsService.startWatchingStarted(ops, callback); + int watchedUid = Process.INVALID_UID; + final int callingUid = Binder.getCallingUid(); + final int callingPid = Binder.getCallingPid(); + if (mContext.checkCallingOrSelfPermission(Manifest.permission.WATCH_APPOPS) + != PackageManager.PERMISSION_GRANTED) { + watchedUid = callingUid; + } + + Preconditions.checkArgument(!ArrayUtils.isEmpty(ops), "Ops cannot be null or empty"); + Preconditions.checkArrayElementsInRange(ops, 0, AppOpsManager._NUM_OP - 1, + "Invalid op code in: " + Arrays.toString(ops)); + Objects.requireNonNull(callback, "Callback cannot be null"); + + synchronized (this) { + SparseArray<StartedCallback> callbacks = mStartedWatchers.get(callback.asBinder()); + if (callbacks == null) { + callbacks = new SparseArray<>(); + mStartedWatchers.put(callback.asBinder(), callbacks); + } + + final StartedCallback startedCallback = new StartedCallback(callback, watchedUid, + callingUid, callingPid); + for (int op : ops) { + callbacks.put(op, startedCallback); + } + } } @Override public void stopWatchingStarted(IAppOpsStartedCallback callback) { - mAppOpsService.stopWatchingStarted(callback); + Objects.requireNonNull(callback, "Callback cannot be null"); + + synchronized (this) { + final SparseArray<StartedCallback> startedCallbacks = + mStartedWatchers.remove(callback.asBinder()); + if (startedCallbacks == null) { + return; + } + + final int callbackCount = startedCallbacks.size(); + for (int i = 0; i < callbackCount; i++) { + startedCallbacks.valueAt(i).destroy(); + } + } } @Override public void startWatchingNoted(@NonNull int[] ops, @NonNull IAppOpsNotedCallback callback) { - mAppOpsService.startWatchingNoted(ops, callback); + int watchedUid = Process.INVALID_UID; + final int callingUid = Binder.getCallingUid(); + final int callingPid = Binder.getCallingPid(); + if (mContext.checkCallingOrSelfPermission(Manifest.permission.WATCH_APPOPS) + != PackageManager.PERMISSION_GRANTED) { + watchedUid = callingUid; + } + Preconditions.checkArgument(!ArrayUtils.isEmpty(ops), "Ops cannot be null or empty"); + Preconditions.checkArrayElementsInRange(ops, 0, AppOpsManager._NUM_OP - 1, + "Invalid op code in: " + Arrays.toString(ops)); + Objects.requireNonNull(callback, "Callback cannot be null"); + synchronized (this) { + SparseArray<NotedCallback> callbacks = mNotedWatchers.get(callback.asBinder()); + if (callbacks == null) { + callbacks = new SparseArray<>(); + mNotedWatchers.put(callback.asBinder(), callbacks); + } + final NotedCallback notedCallback = new NotedCallback(callback, watchedUid, + callingUid, callingPid); + for (int op : ops) { + callbacks.put(op, notedCallback); + } + } } @Override public void stopWatchingNoted(IAppOpsNotedCallback callback) { - mAppOpsService.stopWatchingNoted(callback); + Objects.requireNonNull(callback, "Callback cannot be null"); + synchronized (this) { + final SparseArray<NotedCallback> notedCallbacks = + mNotedWatchers.remove(callback.asBinder()); + if (notedCallbacks == null) { + return; + } + final int callbackCount = notedCallbacks.size(); + for (int i = 0; i < callbackCount; i++) { + notedCallbacks.valueAt(i).destroy(); + } + } } /** @@ -772,7 +2842,7 @@ public class AppOpsService extends IAppOpsService.Stub { int uid = Binder.getCallingUid(); Pair<String, Integer> key = getAsyncNotedOpsKey(packageName, uid); - mAppOpsService.verifyPackage(uid, packageName); + verifyAndGetBypass(uid, packageName, null); synchronized (this) { RemoteCallbackList<IAppOpsAsyncNotedCallback> callbacks = mAsyncOpWatchers.get(key); @@ -802,7 +2872,7 @@ public class AppOpsService extends IAppOpsService.Stub { int uid = Binder.getCallingUid(); Pair<String, Integer> key = getAsyncNotedOpsKey(packageName, uid); - mAppOpsService.verifyPackage(uid, packageName); + verifyAndGetBypass(uid, packageName, null); synchronized (this) { RemoteCallbackList<IAppOpsAsyncNotedCallback> callbacks = mAsyncOpWatchers.get(key); @@ -821,7 +2891,7 @@ public class AppOpsService extends IAppOpsService.Stub { int uid = Binder.getCallingUid(); - mAppOpsService.verifyPackage(uid, packageName); + verifyAndGetBypass(uid, packageName, null); synchronized (this) { return mUnforwardedAsyncNotedOps.remove(getAsyncNotedOpsKey(packageName, uid)); @@ -844,49 +2914,54 @@ public class AppOpsService extends IAppOpsService.Stub { boolean startIfModeDefault, boolean shouldCollectAsyncNotedOp, @NonNull String message, boolean shouldCollectMessage, @AttributionFlags int attributionFlags, int attributionChainId) { + verifyIncomingUid(uid); + verifyIncomingOp(code); if (!isIncomingPackageValid(packageName, UserHandle.getUserId(uid))) { return new SyncNotedAppOp(AppOpsManager.MODE_ERRORED, code, attributionTag, packageName); } - int result = mAppOpsService.startOperation(clientId, code, uid, packageName, - attributionTag, startIfModeDefault, message, - attributionFlags, attributionChainId); - String resolvedPackageName = AppOpsManager.resolvePackageName(uid, packageName); - - boolean isAttributionTagValid = mAppOpsService.isAttributionTagValid(uid, - resolvedPackageName, attributionTag, null); - - if (shouldCollectAsyncNotedOp && result == MODE_ALLOWED) { - collectAsyncNotedOp(uid, resolvedPackageName, code, - isAttributionTagValid ? attributionTag : null, AppOpsManager.OP_FLAG_SELF, - message, shouldCollectMessage); + if (resolvedPackageName == null) { + return new SyncNotedAppOp(AppOpsManager.MODE_IGNORED, code, attributionTag, + packageName); } - return new SyncNotedAppOp(result, code, isAttributionTagValid ? attributionTag : null, - resolvedPackageName); + // As a special case for OP_RECORD_AUDIO_HOTWORD, which we use only for attribution + // purposes and not as a check, also make sure that the caller is allowed to access + // the data gated by OP_RECORD_AUDIO. + // + // TODO: Revert this change before Android 12. + if (code == OP_RECORD_AUDIO_HOTWORD || code == OP_RECEIVE_AMBIENT_TRIGGER_AUDIO) { + int result = checkOperation(OP_RECORD_AUDIO, uid, packageName); + if (result != AppOpsManager.MODE_ALLOWED) { + return new SyncNotedAppOp(result, code, attributionTag, packageName); + } + } + return startOperationUnchecked(clientId, code, uid, packageName, attributionTag, + Process.INVALID_UID, null, null, OP_FLAG_SELF, startIfModeDefault, + shouldCollectAsyncNotedOp, message, shouldCollectMessage, attributionFlags, + attributionChainId, /*dryRun*/ false); } @Override - public SyncNotedAppOp startProxyOperation(IBinder clientId, int code, + public SyncNotedAppOp startProxyOperation(@NonNull IBinder clientId, int code, @NonNull AttributionSource attributionSource, boolean startIfModeDefault, boolean shouldCollectAsyncNotedOp, String message, boolean shouldCollectMessage, boolean skipProxyOperation, @AttributionFlags int proxyAttributionFlags, @AttributionFlags int proxiedAttributionFlags, int attributionChainId) { - return mCheckOpsDelegateDispatcher.startProxyOperation(clientId, code, - attributionSource, startIfModeDefault, shouldCollectAsyncNotedOp, message, - shouldCollectMessage, skipProxyOperation, proxyAttributionFlags, - proxiedAttributionFlags, attributionChainId); + return mCheckOpsDelegateDispatcher.startProxyOperation(clientId, code, attributionSource, + startIfModeDefault, shouldCollectAsyncNotedOp, message, shouldCollectMessage, + skipProxyOperation, proxyAttributionFlags, proxiedAttributionFlags, + attributionChainId); } - private SyncNotedAppOp startProxyOperationImpl(IBinder clientId, int code, + private SyncNotedAppOp startProxyOperationImpl(@NonNull IBinder clientId, int code, @NonNull AttributionSource attributionSource, boolean startIfModeDefault, boolean shouldCollectAsyncNotedOp, String message, boolean shouldCollectMessage, boolean skipProxyOperation, @AttributionFlags int proxyAttributionFlags, @AttributionFlags int proxiedAttributionFlags, int attributionChainId) { - final int proxyUid = attributionSource.getUid(); final String proxyPackageName = attributionSource.getPackageName(); final String proxyAttributionTag = attributionSource.getAttributionTag(); @@ -934,66 +3009,145 @@ public class AppOpsService extends IAppOpsService.Stub { if (!skipProxyOperation) { // Test if the proxied operation will succeed before starting the proxy operation - final int testProxiedOp = mAppOpsService.startOperationUnchecked(clientId, code, + final SyncNotedAppOp testProxiedOp = startOperationUnchecked(clientId, code, proxiedUid, resolvedProxiedPackageName, proxiedAttributionTag, proxyUid, resolvedProxyPackageName, proxyAttributionTag, proxiedFlags, startIfModeDefault, + shouldCollectAsyncNotedOp, message, shouldCollectMessage, proxiedAttributionFlags, attributionChainId, /*dryRun*/ true); - - boolean isTestProxiedAttributionTagValid = - mAppOpsService.isAttributionTagValid(proxiedUid, resolvedProxiedPackageName, - proxiedAttributionTag, resolvedProxyPackageName); - - if (!shouldStartForMode(testProxiedOp, startIfModeDefault)) { - return new SyncNotedAppOp(testProxiedOp, code, - isTestProxiedAttributionTagValid ? proxiedAttributionTag : null, - resolvedProxiedPackageName); + if (!shouldStartForMode(testProxiedOp.getOpMode(), startIfModeDefault)) { + return testProxiedOp; } final int proxyFlags = isProxyTrusted ? AppOpsManager.OP_FLAG_TRUSTED_PROXY : AppOpsManager.OP_FLAG_UNTRUSTED_PROXY; - final int proxyAppOp = mAppOpsService.startOperationUnchecked(clientId, code, proxyUid, + final SyncNotedAppOp proxyAppOp = startOperationUnchecked(clientId, code, proxyUid, resolvedProxyPackageName, proxyAttributionTag, Process.INVALID_UID, null, null, - proxyFlags, startIfModeDefault, proxyAttributionFlags, attributionChainId, + proxyFlags, startIfModeDefault, !isProxyTrusted, "proxy " + message, + shouldCollectMessage, proxyAttributionFlags, attributionChainId, /*dryRun*/ false); + if (!shouldStartForMode(proxyAppOp.getOpMode(), startIfModeDefault)) { + return proxyAppOp; + } + } - boolean isProxyAttributionTagValid = mAppOpsService.isAttributionTagValid(proxyUid, - resolvedProxyPackageName, proxyAttributionTag, null); + return startOperationUnchecked(clientId, code, proxiedUid, resolvedProxiedPackageName, + proxiedAttributionTag, proxyUid, resolvedProxyPackageName, proxyAttributionTag, + proxiedFlags, startIfModeDefault, shouldCollectAsyncNotedOp, message, + shouldCollectMessage, proxiedAttributionFlags, attributionChainId, + /*dryRun*/ false); + } - if (!shouldStartForMode(proxyAppOp, startIfModeDefault)) { - return new SyncNotedAppOp(proxyAppOp, code, - isProxyAttributionTagValid ? proxyAttributionTag : null, - resolvedProxyPackageName); - } + private boolean shouldStartForMode(int mode, boolean startIfModeDefault) { + return (mode == MODE_ALLOWED || (mode == MODE_DEFAULT && startIfModeDefault)); + } - if (shouldCollectAsyncNotedOp) { - collectAsyncNotedOp(proxyUid, resolvedProxyPackageName, code, - isProxyAttributionTagValid ? proxyAttributionTag : null, proxyFlags, - message, shouldCollectMessage); + private SyncNotedAppOp startOperationUnchecked(IBinder clientId, int code, int uid, + @NonNull String packageName, @Nullable String attributionTag, int proxyUid, + String proxyPackageName, @Nullable String proxyAttributionTag, @OpFlags int flags, + boolean startIfModeDefault, boolean shouldCollectAsyncNotedOp, @Nullable String message, + boolean shouldCollectMessage, @AttributionFlags int attributionFlags, + int attributionChainId, boolean dryRun) { + PackageVerificationResult pvr; + try { + pvr = verifyAndGetBypass(uid, packageName, attributionTag, proxyPackageName); + if (!pvr.isAttributionTagValid) { + attributionTag = null; } + } catch (SecurityException e) { + Slog.e(TAG, "startOperation", e); + return new SyncNotedAppOp(AppOpsManager.MODE_ERRORED, code, attributionTag, + packageName); } - final int proxiedAppOp = mAppOpsService.startOperationUnchecked(clientId, code, proxiedUid, - resolvedProxiedPackageName, proxiedAttributionTag, proxyUid, - resolvedProxyPackageName, proxyAttributionTag, proxiedFlags, startIfModeDefault, - proxiedAttributionFlags, attributionChainId,/*dryRun*/ false); - - boolean isProxiedAttributionTagValid = mAppOpsService.isAttributionTagValid(proxiedUid, - resolvedProxiedPackageName, proxiedAttributionTag, resolvedProxyPackageName); + boolean isRestricted = false; + int startType = START_TYPE_FAILED; + synchronized (this) { + final Ops ops = getOpsLocked(uid, packageName, attributionTag, + pvr.isAttributionTagValid, pvr.bypass, /* edit */ true); + if (ops == null) { + if (!dryRun) { + scheduleOpStartedIfNeededLocked(code, uid, packageName, attributionTag, + flags, AppOpsManager.MODE_IGNORED, startType, attributionFlags, + attributionChainId); + } + if (DEBUG) Slog.d(TAG, "startOperation: no op for code " + code + " uid " + uid + + " package " + packageName + " flags: " + + AppOpsManager.flagsToString(flags)); + return new SyncNotedAppOp(AppOpsManager.MODE_ERRORED, code, attributionTag, + packageName); + } + final Op op = getOpLocked(ops, code, uid, true); + final AttributedOp attributedOp = op.getOrCreateAttribution(op, attributionTag); + final UidState uidState = ops.uidState; + isRestricted = isOpRestrictedLocked(uid, code, packageName, attributionTag, pvr.bypass, + false); + final int switchCode = AppOpsManager.opToSwitch(code); + // If there is a non-default per UID policy (we set UID op mode only if + // non-default) it takes over, otherwise use the per package policy. + if (uidState.getUidMode(switchCode) != AppOpsManager.opToDefaultMode(switchCode)) { + final int uidMode = uidState.evalMode(code, uidState.getUidMode(switchCode)); + if (!shouldStartForMode(uidMode, startIfModeDefault)) { + if (DEBUG) { + Slog.d(TAG, "startOperation: uid reject #" + uidMode + " for code " + + switchCode + " (" + code + ") uid " + uid + " package " + + packageName + " flags: " + AppOpsManager.flagsToString(flags)); + } + if (!dryRun) { + attributedOp.rejected(uidState.getState(), flags); + scheduleOpStartedIfNeededLocked(code, uid, packageName, attributionTag, + flags, uidMode, startType, attributionFlags, attributionChainId); + } + return new SyncNotedAppOp(uidMode, code, attributionTag, packageName); + } + } else { + final Op switchOp = switchCode != code ? getOpLocked(ops, switchCode, uid, true) + : op; + final int mode = switchOp.uidState.evalMode(switchOp.op, switchOp.getMode()); + if (mode != AppOpsManager.MODE_ALLOWED + && (!startIfModeDefault || mode != MODE_DEFAULT)) { + if (DEBUG) Slog.d(TAG, "startOperation: reject #" + mode + " for code " + + switchCode + " (" + code + ") uid " + uid + " package " + + packageName + " flags: " + AppOpsManager.flagsToString(flags)); + if (!dryRun) { + attributedOp.rejected(uidState.getState(), flags); + scheduleOpStartedIfNeededLocked(code, uid, packageName, attributionTag, + flags, mode, startType, attributionFlags, attributionChainId); + } + return new SyncNotedAppOp(mode, code, attributionTag, packageName); + } + } + if (DEBUG) Slog.d(TAG, "startOperation: allowing code " + code + " uid " + uid + + " package " + packageName + " restricted: " + isRestricted + + " flags: " + AppOpsManager.flagsToString(flags)); + if (!dryRun) { + try { + if (isRestricted) { + attributedOp.createPaused(clientId, proxyUid, proxyPackageName, + proxyAttributionTag, uidState.getState(), flags, + attributionFlags, attributionChainId); + } else { + attributedOp.started(clientId, proxyUid, proxyPackageName, + proxyAttributionTag, uidState.getState(), flags, + attributionFlags, attributionChainId); + startType = START_TYPE_STARTED; + } + } catch (RemoteException e) { + throw new RuntimeException(e); + } + scheduleOpStartedIfNeededLocked(code, uid, packageName, attributionTag, flags, + isRestricted ? MODE_IGNORED : MODE_ALLOWED, startType, attributionFlags, + attributionChainId); + } + } - if (shouldCollectAsyncNotedOp && proxiedAppOp == MODE_ALLOWED) { - collectAsyncNotedOp(proxyUid, resolvedProxiedPackageName, code, - isProxiedAttributionTagValid ? proxiedAttributionTag : null, - proxiedAttributionFlags, message, shouldCollectMessage); + if (shouldCollectAsyncNotedOp && !dryRun && !isRestricted) { + collectAsyncNotedOp(uid, packageName, code, attributionTag, AppOpsManager.OP_FLAG_SELF, + message, shouldCollectMessage); } - return new SyncNotedAppOp(proxiedAppOp, code, - isProxiedAttributionTagValid ? proxiedAttributionTag : null, - resolvedProxiedPackageName); - } - - private boolean shouldStartForMode(int mode, boolean startIfModeDefault) { - return (mode == MODE_ALLOWED || (mode == MODE_DEFAULT && startIfModeDefault)); + return new SyncNotedAppOp(isRestricted ? MODE_IGNORED : MODE_ALLOWED, code, attributionTag, + packageName); } @Override @@ -1005,11 +3159,22 @@ public class AppOpsService extends IAppOpsService.Stub { private void finishOperationImpl(IBinder clientId, int code, int uid, String packageName, String attributionTag) { - mAppOpsService.finishOperation(clientId, code, uid, packageName, attributionTag); + verifyIncomingUid(uid); + verifyIncomingOp(code); + if (!isIncomingPackageValid(packageName, UserHandle.getUserId(uid))) { + return; + } + + String resolvedPackageName = AppOpsManager.resolvePackageName(uid, packageName); + if (resolvedPackageName == null) { + return; + } + + finishOperationUnchecked(clientId, code, uid, resolvedPackageName, attributionTag); } @Override - public void finishProxyOperation(IBinder clientId, int code, + public void finishProxyOperation(@NonNull IBinder clientId, int code, @NonNull AttributionSource attributionSource, boolean skipProxyOperation) { mCheckOpsDelegateDispatcher.finishProxyOperation(clientId, code, attributionSource, skipProxyOperation); @@ -1041,8 +3206,8 @@ public class AppOpsService extends IAppOpsService.Stub { } if (!skipProxyOperation) { - mAppOpsService.finishOperationUnchecked(clientId, code, proxyUid, - resolvedProxyPackageName, proxyAttributionTag); + finishOperationUnchecked(clientId, code, proxyUid, resolvedProxyPackageName, + proxyAttributionTag); } String resolvedProxiedPackageName = AppOpsManager.resolvePackageName(proxiedUid, @@ -1051,12 +3216,209 @@ public class AppOpsService extends IAppOpsService.Stub { return null; } - mAppOpsService.finishOperationUnchecked(clientId, code, proxiedUid, - resolvedProxiedPackageName, proxiedAttributionTag); + finishOperationUnchecked(clientId, code, proxiedUid, resolvedProxiedPackageName, + proxiedAttributionTag); return null; } + private void finishOperationUnchecked(IBinder clientId, int code, int uid, String packageName, + String attributionTag) { + PackageVerificationResult pvr; + try { + pvr = verifyAndGetBypass(uid, packageName, attributionTag); + if (!pvr.isAttributionTagValid) { + attributionTag = null; + } + } catch (SecurityException e) { + Slog.e(TAG, "Cannot finishOperation", e); + return; + } + + synchronized (this) { + Op op = getOpLocked(code, uid, packageName, attributionTag, pvr.isAttributionTagValid, + pvr.bypass, /* edit */ true); + if (op == null) { + Slog.e(TAG, "Operation not found: uid=" + uid + " pkg=" + packageName + "(" + + attributionTag + ") op=" + AppOpsManager.opToName(code)); + return; + } + final AttributedOp attributedOp = op.mAttributions.get(attributionTag); + if (attributedOp == null) { + Slog.e(TAG, "Attribution not found: uid=" + uid + " pkg=" + packageName + "(" + + attributionTag + ") op=" + AppOpsManager.opToName(code)); + return; + } + + if (attributedOp.isRunning() || attributedOp.isPaused()) { + attributedOp.finished(clientId); + } else { + Slog.e(TAG, "Operation not started: uid=" + uid + " pkg=" + packageName + "(" + + attributionTag + ") op=" + AppOpsManager.opToName(code)); + } + } + } + + void scheduleOpActiveChangedIfNeededLocked(int code, int uid, @NonNull + String packageName, @Nullable String attributionTag, boolean active, @AttributionFlags + int attributionFlags, int attributionChainId) { + ArraySet<ActiveCallback> dispatchedCallbacks = null; + final int callbackListCount = mActiveWatchers.size(); + for (int i = 0; i < callbackListCount; i++) { + final SparseArray<ActiveCallback> callbacks = mActiveWatchers.valueAt(i); + ActiveCallback callback = callbacks.get(code); + if (callback != null) { + if (callback.mWatchingUid >= 0 && callback.mWatchingUid != uid) { + continue; + } + if (dispatchedCallbacks == null) { + dispatchedCallbacks = new ArraySet<>(); + } + dispatchedCallbacks.add(callback); + } + } + if (dispatchedCallbacks == null) { + return; + } + mHandler.sendMessage(PooledLambda.obtainMessage( + AppOpsService::notifyOpActiveChanged, + this, dispatchedCallbacks, code, uid, packageName, attributionTag, active, + attributionFlags, attributionChainId)); + } + + private void notifyOpActiveChanged(ArraySet<ActiveCallback> callbacks, + int code, int uid, @NonNull String packageName, @Nullable String attributionTag, + boolean active, @AttributionFlags int attributionFlags, int attributionChainId) { + // There are features watching for mode changes such as window manager + // and location manager which are in our process. The callbacks in these + // features may require permissions our remote caller does not have. + final long identity = Binder.clearCallingIdentity(); + try { + final int callbackCount = callbacks.size(); + for (int i = 0; i < callbackCount; i++) { + final ActiveCallback callback = callbacks.valueAt(i); + try { + if (shouldIgnoreCallback(code, callback.mCallingPid, callback.mCallingUid)) { + continue; + } + callback.mCallback.opActiveChanged(code, uid, packageName, attributionTag, + active, attributionFlags, attributionChainId); + } catch (RemoteException e) { + /* do nothing */ + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + void scheduleOpStartedIfNeededLocked(int code, int uid, String pkgName, + String attributionTag, @OpFlags int flags, @Mode int result, + @AppOpsManager.OnOpStartedListener.StartedType int startedType, + @AttributionFlags int attributionFlags, int attributionChainId) { + ArraySet<StartedCallback> dispatchedCallbacks = null; + final int callbackListCount = mStartedWatchers.size(); + for (int i = 0; i < callbackListCount; i++) { + final SparseArray<StartedCallback> callbacks = mStartedWatchers.valueAt(i); + + StartedCallback callback = callbacks.get(code); + if (callback != null) { + if (callback.mWatchingUid >= 0 && callback.mWatchingUid != uid) { + continue; + } + + if (dispatchedCallbacks == null) { + dispatchedCallbacks = new ArraySet<>(); + } + dispatchedCallbacks.add(callback); + } + } + + if (dispatchedCallbacks == null) { + return; + } + + mHandler.sendMessage(PooledLambda.obtainMessage( + AppOpsService::notifyOpStarted, + this, dispatchedCallbacks, code, uid, pkgName, attributionTag, flags, + result, startedType, attributionFlags, attributionChainId)); + } + + private void notifyOpStarted(ArraySet<StartedCallback> callbacks, + int code, int uid, String packageName, String attributionTag, @OpFlags int flags, + @Mode int result, @AppOpsManager.OnOpStartedListener.StartedType int startedType, + @AttributionFlags int attributionFlags, int attributionChainId) { + final long identity = Binder.clearCallingIdentity(); + try { + final int callbackCount = callbacks.size(); + for (int i = 0; i < callbackCount; i++) { + final StartedCallback callback = callbacks.valueAt(i); + try { + if (shouldIgnoreCallback(code, callback.mCallingPid, callback.mCallingUid)) { + continue; + } + callback.mCallback.opStarted(code, uid, packageName, attributionTag, flags, + result, startedType, attributionFlags, attributionChainId); + } catch (RemoteException e) { + /* do nothing */ + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + private void scheduleOpNotedIfNeededLocked(int code, int uid, String packageName, + String attributionTag, @OpFlags int flags, @Mode int result) { + ArraySet<NotedCallback> dispatchedCallbacks = null; + final int callbackListCount = mNotedWatchers.size(); + for (int i = 0; i < callbackListCount; i++) { + final SparseArray<NotedCallback> callbacks = mNotedWatchers.valueAt(i); + final NotedCallback callback = callbacks.get(code); + if (callback != null) { + if (callback.mWatchingUid >= 0 && callback.mWatchingUid != uid) { + continue; + } + if (dispatchedCallbacks == null) { + dispatchedCallbacks = new ArraySet<>(); + } + dispatchedCallbacks.add(callback); + } + } + if (dispatchedCallbacks == null) { + return; + } + mHandler.sendMessage(PooledLambda.obtainMessage( + AppOpsService::notifyOpChecked, + this, dispatchedCallbacks, code, uid, packageName, attributionTag, flags, + result)); + } + + private void notifyOpChecked(ArraySet<NotedCallback> callbacks, + int code, int uid, String packageName, String attributionTag, @OpFlags int flags, + @Mode int result) { + // There are features watching for checks in our process. The callbacks in + // these features may require permissions our remote caller does not have. + final long identity = Binder.clearCallingIdentity(); + try { + final int callbackCount = callbacks.size(); + for (int i = 0; i < callbackCount; i++) { + final NotedCallback callback = callbacks.valueAt(i); + try { + if (shouldIgnoreCallback(code, callback.mCallingPid, callback.mCallingUid)) { + continue; + } + callback.mCallback.opNoted(code, uid, packageName, attributionTag, flags, + result); + } catch (RemoteException e) { + /* do nothing */ + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + @Override public int permissionToOpCode(String permission) { if (permission == null) { @@ -1114,6 +3476,13 @@ public class AppOpsService extends IAppOpsService.Stub { Binder.getCallingPid(), Binder.getCallingUid(), null); } + private boolean shouldIgnoreCallback(int op, int watcherPid, int watcherUid) { + // If it's a restricted read op, ignore it if watcher doesn't have manage ops permission, + // as watcher should not use this to signal if the value is changed. + return opRestrictsRead(op) && mContext.checkPermission(Manifest.permission.MANAGE_APPOPS, + watcherPid, watcherUid) != PackageManager.PERMISSION_GRANTED; + } + private void verifyIncomingOp(int op) { if (op >= 0 && op < AppOpsManager._NUM_OP) { // Enforce manage appops permission if it's a restricted read op. @@ -1154,6 +3523,35 @@ public class AppOpsService extends IAppOpsService.Stub { || resolveUid(resolvedPackage) != Process.INVALID_UID; } + private boolean isCallerAndAttributionTrusted(@NonNull AttributionSource attributionSource) { + if (attributionSource.getUid() != Binder.getCallingUid() + && attributionSource.isTrusted(mContext)) { + return true; + } + return mContext.checkPermission(android.Manifest.permission.UPDATE_APP_OPS_STATS, + Binder.getCallingPid(), Binder.getCallingUid(), null) + == PackageManager.PERMISSION_GRANTED; + } + + private @Nullable UidState getUidStateLocked(int uid, boolean edit) { + UidState uidState = mUidStates.get(uid); + if (uidState == null) { + if (!edit) { + return null; + } + uidState = new UidState(uid); + mUidStates.put(uid, uidState); + } + + return uidState; + } + + private void updateAppWidgetVisibility(SparseArray<String> uidPackageNames, boolean visible) { + synchronized (this) { + getUidStateTracker().updateAppWidgetVisibility(uidPackageNames, visible); + } + } + /** * @return {@link PackageManagerInternal} */ @@ -1165,6 +3563,801 @@ public class AppOpsService extends IAppOpsService.Stub { return mPackageManagerInternal; } + /** + * Create a restriction description matching the properties of the package. + * + * @param pkg The package to create the restriction description for + * + * @return The restriction matching the package + */ + private RestrictionBypass getBypassforPackage(@NonNull AndroidPackage pkg) { + return new RestrictionBypass(pkg.getUid() == Process.SYSTEM_UID, pkg.isPrivileged(), + mContext.checkPermission(android.Manifest.permission + .EXEMPT_FROM_AUDIO_RECORD_RESTRICTIONS, -1, pkg.getUid()) + == PackageManager.PERMISSION_GRANTED); + } + + /** + * @see #verifyAndGetBypass(int, String, String, String) + */ + private @NonNull PackageVerificationResult verifyAndGetBypass(int uid, String packageName, + @Nullable String attributionTag) { + return verifyAndGetBypass(uid, packageName, attributionTag, null); + } + + /** + * Verify that package belongs to uid and return the {@link RestrictionBypass bypass + * description} for the package, along with a boolean indicating whether the attribution tag is + * valid. + * + * @param uid The uid the package belongs to + * @param packageName The package the might belong to the uid + * @param attributionTag attribution tag or {@code null} if no need to verify + * @param proxyPackageName The proxy package, from which the attribution tag is to be pulled + * + * @return PackageVerificationResult containing {@link RestrictionBypass} and whether the + * attribution tag is valid + */ + private @NonNull PackageVerificationResult verifyAndGetBypass(int uid, String packageName, + @Nullable String attributionTag, @Nullable String proxyPackageName) { + if (uid == Process.ROOT_UID) { + // For backwards compatibility, don't check package name for root UID. + return new PackageVerificationResult(null, + /* isAttributionTagValid */ true); + } + if (Process.isSdkSandboxUid(uid)) { + // SDK sandbox processes run in their own UID range, but their associated + // UID for checks should always be the UID of the package implementing SDK sandbox + // service. + // TODO: We will need to modify the callers of this function instead, so + // modifications and checks against the app ops state are done with the + // correct UID. + try { + final PackageManager pm = mContext.getPackageManager(); + final String supplementalPackageName = pm.getSdkSandboxPackageName(); + if (Objects.equals(packageName, supplementalPackageName)) { + uid = pm.getPackageUidAsUser(supplementalPackageName, + PackageManager.PackageInfoFlags.of(0), UserHandle.getUserId(uid)); + } + } catch (PackageManager.NameNotFoundException e) { + // Shouldn't happen for the supplemental package + e.printStackTrace(); + } + } + + + // Do not check if uid/packageName/attributionTag is already known. + synchronized (this) { + UidState uidState = mUidStates.get(uid); + if (uidState != null && uidState.pkgOps != null) { + Ops ops = uidState.pkgOps.get(packageName); + + if (ops != null && (attributionTag == null || ops.knownAttributionTags.contains( + attributionTag)) && ops.bypass != null) { + return new PackageVerificationResult(ops.bypass, + ops.validAttributionTags.contains(attributionTag)); + } + } + } + + int callingUid = Binder.getCallingUid(); + + // Allow any attribution tag for resolvable uids + int pkgUid; + if (Objects.equals(packageName, "com.android.shell")) { + // Special case for the shell which is a package but should be able + // to bypass app attribution tag restrictions. + pkgUid = Process.SHELL_UID; + } else { + pkgUid = resolveUid(packageName); + } + if (pkgUid != Process.INVALID_UID) { + if (pkgUid != UserHandle.getAppId(uid)) { + Slog.e(TAG, "Bad call made by uid " + callingUid + ". " + + "Package \"" + packageName + "\" does not belong to uid " + uid + "."); + String otherUidMessage = DEBUG ? " but it is really " + pkgUid : " but it is not"; + throw new SecurityException("Specified package \"" + packageName + "\" under uid " + + UserHandle.getAppId(uid) + otherUidMessage); + } + return new PackageVerificationResult(RestrictionBypass.UNRESTRICTED, + /* isAttributionTagValid */ true); + } + + int userId = UserHandle.getUserId(uid); + RestrictionBypass bypass = null; + boolean isAttributionTagValid = false; + + final long ident = Binder.clearCallingIdentity(); + try { + PackageManagerInternal pmInt = LocalServices.getService(PackageManagerInternal.class); + AndroidPackage pkg = pmInt.getPackage(packageName); + if (pkg != null) { + isAttributionTagValid = isAttributionInPackage(pkg, attributionTag); + pkgUid = UserHandle.getUid(userId, UserHandle.getAppId(pkg.getUid())); + bypass = getBypassforPackage(pkg); + } + if (!isAttributionTagValid) { + AndroidPackage proxyPkg = proxyPackageName != null + ? pmInt.getPackage(proxyPackageName) : null; + // Re-check in proxy. + isAttributionTagValid = isAttributionInPackage(proxyPkg, attributionTag); + String msg; + if (pkg != null && isAttributionTagValid) { + msg = "attributionTag " + attributionTag + " declared in manifest of the proxy" + + " package " + proxyPackageName + ", this is not advised"; + } else if (pkg != null) { + msg = "attributionTag " + attributionTag + " not declared in manifest of " + + packageName; + } else { + msg = "package " + packageName + " not found, can't check for " + + "attributionTag " + attributionTag; + } + + try { + if (!mPlatformCompat.isChangeEnabledByPackageName( + SECURITY_EXCEPTION_ON_INVALID_ATTRIBUTION_TAG_CHANGE, packageName, + userId) || !mPlatformCompat.isChangeEnabledByUid( + SECURITY_EXCEPTION_ON_INVALID_ATTRIBUTION_TAG_CHANGE, + callingUid)) { + // Do not override tags if overriding is not enabled for this package + isAttributionTagValid = true; + } + Slog.e(TAG, msg); + } catch (RemoteException neverHappens) { + } + } + } finally { + Binder.restoreCallingIdentity(ident); + } + + if (pkgUid != uid) { + Slog.e(TAG, "Bad call made by uid " + callingUid + ". " + + "Package \"" + packageName + "\" does not belong to uid " + uid + "."); + String otherUidMessage = DEBUG ? " but it is really " + pkgUid : " but it is not"; + throw new SecurityException("Specified package \"" + packageName + "\" under uid " + uid + + otherUidMessage); + } + + return new PackageVerificationResult(bypass, isAttributionTagValid); + } + + private boolean isAttributionInPackage(@Nullable AndroidPackage pkg, + @Nullable String attributionTag) { + if (pkg == null) { + return false; + } else if (attributionTag == null) { + return true; + } + if (pkg.getAttributions() != null) { + int numAttributions = pkg.getAttributions().size(); + for (int i = 0; i < numAttributions; i++) { + if (pkg.getAttributions().get(i).getTag().equals(attributionTag)) { + return true; + } + } + } + + return false; + } + + /** + * Get (and potentially create) ops. + * + * @param uid The uid the package belongs to + * @param packageName The name of the package + * @param attributionTag attribution tag + * @param isAttributionTagValid whether the given attribution tag is valid + * @param bypass When to bypass certain op restrictions (can be null if edit == false) + * @param edit If an ops does not exist, create the ops? + + * @return The ops + */ + private Ops getOpsLocked(int uid, String packageName, @Nullable String attributionTag, + boolean isAttributionTagValid, @Nullable RestrictionBypass bypass, boolean edit) { + UidState uidState = getUidStateLocked(uid, edit); + if (uidState == null) { + return null; + } + + if (uidState.pkgOps == null) { + if (!edit) { + return null; + } + uidState.pkgOps = new ArrayMap<>(); + } + + Ops ops = uidState.pkgOps.get(packageName); + if (ops == null) { + if (!edit) { + return null; + } + ops = new Ops(packageName, uidState); + uidState.pkgOps.put(packageName, ops); + } + + if (edit) { + if (bypass != null) { + ops.bypass = bypass; + } + + if (attributionTag != null) { + ops.knownAttributionTags.add(attributionTag); + if (isAttributionTagValid) { + ops.validAttributionTags.add(attributionTag); + } else { + ops.validAttributionTags.remove(attributionTag); + } + } + } + + return ops; + } + + @Override + public void scheduleWriteLocked() { + if (!mWriteScheduled) { + mWriteScheduled = true; + mHandler.postDelayed(mWriteRunner, WRITE_DELAY); + } + } + + @Override + public void scheduleFastWriteLocked() { + if (!mFastWriteScheduled) { + mWriteScheduled = true; + mFastWriteScheduled = true; + mHandler.removeCallbacks(mWriteRunner); + mHandler.postDelayed(mWriteRunner, 10*1000); + } + } + + /** + * Get the state of an op for a uid. + * + * @param code The code of the op + * @param uid The uid the of the package + * @param packageName The package name for which to get the state for + * @param attributionTag The attribution tag + * @param isAttributionTagValid Whether the given attribution tag is valid + * @param bypass When to bypass certain op restrictions (can be null if edit == false) + * @param edit Iff {@code true} create the {@link Op} object if not yet created + * + * @return The {@link Op state} of the op + */ + private @Nullable Op getOpLocked(int code, int uid, @NonNull String packageName, + @Nullable String attributionTag, boolean isAttributionTagValid, + @Nullable RestrictionBypass bypass, boolean edit) { + Ops ops = getOpsLocked(uid, packageName, attributionTag, isAttributionTagValid, bypass, + edit); + if (ops == null) { + return null; + } + return getOpLocked(ops, code, uid, edit); + } + + private Op getOpLocked(Ops ops, int code, int uid, boolean edit) { + Op op = ops.get(code); + if (op == null) { + if (!edit) { + return null; + } + op = new Op(ops.uidState, ops.packageName, code, uid); + ops.put(code, op); + } + if (edit) { + scheduleWriteLocked(); + } + return op; + } + + private boolean isOpRestrictedDueToSuspend(int code, String packageName, int uid) { + if (!ArrayUtils.contains(OPS_RESTRICTED_ON_SUSPEND, code)) { + return false; + } + final PackageManagerInternal pmi = LocalServices.getService(PackageManagerInternal.class); + return pmi.isPackageSuspended(packageName, UserHandle.getUserId(uid)); + } + + private boolean isOpRestrictedLocked(int uid, int code, String packageName, + String attributionTag, @Nullable RestrictionBypass appBypass, boolean isCheckOp) { + int restrictionSetCount = mOpGlobalRestrictions.size(); + + for (int i = 0; i < restrictionSetCount; i++) { + ClientGlobalRestrictionState restrictionState = mOpGlobalRestrictions.valueAt(i); + if (restrictionState.hasRestriction(code)) { + return true; + } + } + + int userHandle = UserHandle.getUserId(uid); + restrictionSetCount = mOpUserRestrictions.size(); + + for (int i = 0; i < restrictionSetCount; i++) { + // For each client, check that the given op is not restricted, or that the given + // package is exempt from the restriction. + ClientUserRestrictionState restrictionState = mOpUserRestrictions.valueAt(i); + if (restrictionState.hasRestriction(code, packageName, attributionTag, userHandle, + isCheckOp)) { + RestrictionBypass opBypass = opAllowSystemBypassRestriction(code); + if (opBypass != null) { + // If we are the system, bypass user restrictions for certain codes + synchronized (this) { + if (opBypass.isSystemUid && appBypass != null && appBypass.isSystemUid) { + return false; + } + if (opBypass.isPrivileged && appBypass != null && appBypass.isPrivileged) { + return false; + } + if (opBypass.isRecordAudioRestrictionExcept && appBypass != null + && appBypass.isRecordAudioRestrictionExcept) { + return false; + } + } + } + return true; + } + } + return false; + } + + void readState() { + synchronized (mFile) { + synchronized (this) { + FileInputStream stream; + try { + stream = mFile.openRead(); + } catch (FileNotFoundException e) { + Slog.i(TAG, "No existing app ops " + mFile.getBaseFile() + "; starting empty"); + return; + } + boolean success = false; + mUidStates.clear(); + mAppOpsCheckingService.clearAllModes(); + try { + TypedXmlPullParser parser = Xml.resolvePullParser(stream); + int type; + while ((type = parser.next()) != XmlPullParser.START_TAG + && type != XmlPullParser.END_DOCUMENT) { + ; + } + + if (type != XmlPullParser.START_TAG) { + throw new IllegalStateException("no start tag found"); + } + + mVersionAtBoot = parser.getAttributeInt(null, "v", NO_VERSION); + + int outerDepth = parser.getDepth(); + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { + continue; + } + + String tagName = parser.getName(); + if (tagName.equals("pkg")) { + readPackage(parser); + } else if (tagName.equals("uid")) { + readUidOps(parser); + } else { + Slog.w(TAG, "Unknown element under <app-ops>: " + + parser.getName()); + XmlUtils.skipCurrentTag(parser); + } + } + success = true; + } catch (IllegalStateException e) { + Slog.w(TAG, "Failed parsing " + e); + } catch (NullPointerException e) { + Slog.w(TAG, "Failed parsing " + e); + } catch (NumberFormatException e) { + Slog.w(TAG, "Failed parsing " + e); + } catch (XmlPullParserException e) { + Slog.w(TAG, "Failed parsing " + e); + } catch (IOException e) { + Slog.w(TAG, "Failed parsing " + e); + } catch (IndexOutOfBoundsException e) { + Slog.w(TAG, "Failed parsing " + e); + } finally { + if (!success) { + mUidStates.clear(); + mAppOpsCheckingService.clearAllModes(); + } + try { + stream.close(); + } catch (IOException e) { + } + } + } + } + } + + @VisibleForTesting + @GuardedBy("this") + void upgradeRunAnyInBackgroundLocked() { + for (int i = 0; i < mUidStates.size(); i++) { + final UidState uidState = mUidStates.valueAt(i); + if (uidState == null) { + continue; + } + SparseIntArray opModes = uidState.getNonDefaultUidModes(); + if (opModes != null) { + final int idx = opModes.indexOfKey(AppOpsManager.OP_RUN_IN_BACKGROUND); + if (idx >= 0) { + uidState.setUidMode(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, + opModes.valueAt(idx)); + } + } + if (uidState.pkgOps == null) { + continue; + } + boolean changed = false; + for (int j = 0; j < uidState.pkgOps.size(); j++) { + Ops ops = uidState.pkgOps.valueAt(j); + if (ops != null) { + final Op op = ops.get(AppOpsManager.OP_RUN_IN_BACKGROUND); + if (op != null && op.getMode() != AppOpsManager.opToDefaultMode(op.op)) { + final Op copy = new Op(op.uidState, op.packageName, + AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, uidState.uid); + copy.setMode(op.getMode()); + ops.put(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, copy); + changed = true; + } + } + } + if (changed) { + uidState.evalForegroundOps(); + } + } + } + + /** + * The interpretation of the default mode - MODE_DEFAULT - for OP_SCHEDULE_EXACT_ALARM is + * changing. Simultaneously, we want to change this op's mode from MODE_DEFAULT to MODE_ALLOWED + * for already installed apps. For newer apps, it will stay as MODE_DEFAULT. + */ + @VisibleForTesting + @GuardedBy("this") + void upgradeScheduleExactAlarmLocked() { + final PermissionManagerServiceInternal pmsi = LocalServices.getService( + PermissionManagerServiceInternal.class); + final UserManagerInternal umi = LocalServices.getService(UserManagerInternal.class); + final PackageManagerInternal pmi = getPackageManagerInternal(); + + final String[] packagesDeclaringPermission = pmsi.getAppOpPermissionPackages( + AppOpsManager.opToPermission(OP_SCHEDULE_EXACT_ALARM)); + final int[] userIds = umi.getUserIds(); + + for (final String pkg : packagesDeclaringPermission) { + for (int userId : userIds) { + final int uid = pmi.getPackageUid(pkg, 0, userId); + + UidState uidState = mUidStates.get(uid); + if (uidState == null) { + uidState = new UidState(uid); + mUidStates.put(uid, uidState); + } + final int oldMode = uidState.getUidMode(OP_SCHEDULE_EXACT_ALARM); + if (oldMode == AppOpsManager.opToDefaultMode(OP_SCHEDULE_EXACT_ALARM)) { + uidState.setUidMode(OP_SCHEDULE_EXACT_ALARM, MODE_ALLOWED); + } + } + // This appop is meant to be controlled at a uid level. So we leave package modes as + // they are. + } + } + + private void upgradeLocked(int oldVersion) { + if (oldVersion >= CURRENT_VERSION) { + return; + } + Slog.d(TAG, "Upgrading app-ops xml from version " + oldVersion + " to " + CURRENT_VERSION); + switch (oldVersion) { + case NO_VERSION: + upgradeRunAnyInBackgroundLocked(); + // fall through + case 1: + upgradeScheduleExactAlarmLocked(); + // fall through + case 2: + // for future upgrades + } + scheduleFastWriteLocked(); + } + + private void readUidOps(TypedXmlPullParser parser) throws NumberFormatException, + XmlPullParserException, IOException { + final int uid = parser.getAttributeInt(null, "n"); + int outerDepth = parser.getDepth(); + int type; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { + continue; + } + + String tagName = parser.getName(); + if (tagName.equals("op")) { + final int code = parser.getAttributeInt(null, "n"); + final int mode = parser.getAttributeInt(null, "m"); + setUidMode(code, uid, mode); + } else { + Slog.w(TAG, "Unknown element under <uid-ops>: " + + parser.getName()); + XmlUtils.skipCurrentTag(parser); + } + } + } + + private void readPackage(TypedXmlPullParser parser) + throws NumberFormatException, XmlPullParserException, IOException { + String pkgName = parser.getAttributeValue(null, "n"); + int outerDepth = parser.getDepth(); + int type; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { + continue; + } + + String tagName = parser.getName(); + if (tagName.equals("uid")) { + readUid(parser, pkgName); + } else { + Slog.w(TAG, "Unknown element under <pkg>: " + + parser.getName()); + XmlUtils.skipCurrentTag(parser); + } + } + } + + private void readUid(TypedXmlPullParser parser, String pkgName) + throws NumberFormatException, XmlPullParserException, IOException { + int uid = parser.getAttributeInt(null, "n"); + final UidState uidState = getUidStateLocked(uid, true); + int outerDepth = parser.getDepth(); + int type; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { + continue; + } + String tagName = parser.getName(); + if (tagName.equals("op")) { + readOp(parser, uidState, pkgName); + } else { + Slog.w(TAG, "Unknown element under <pkg>: " + + parser.getName()); + XmlUtils.skipCurrentTag(parser); + } + } + uidState.evalForegroundOps(); + } + + private void readAttributionOp(TypedXmlPullParser parser, @NonNull Op parent, + @Nullable String attribution) + throws NumberFormatException, IOException, XmlPullParserException { + final AttributedOp attributedOp = parent.getOrCreateAttribution(parent, attribution); + + final long key = parser.getAttributeLong(null, "n"); + final int uidState = extractUidStateFromKey(key); + final int opFlags = extractFlagsFromKey(key); + + final long accessTime = parser.getAttributeLong(null, "t", 0); + final long rejectTime = parser.getAttributeLong(null, "r", 0); + final long accessDuration = parser.getAttributeLong(null, "d", -1); + final String proxyPkg = XmlUtils.readStringAttribute(parser, "pp"); + final int proxyUid = parser.getAttributeInt(null, "pu", Process.INVALID_UID); + final String proxyAttributionTag = XmlUtils.readStringAttribute(parser, "pc"); + + if (accessTime > 0) { + attributedOp.accessed(accessTime, accessDuration, proxyUid, proxyPkg, + proxyAttributionTag, uidState, opFlags); + } + if (rejectTime > 0) { + attributedOp.rejected(rejectTime, uidState, opFlags); + } + } + + private void readOp(TypedXmlPullParser parser, + @NonNull UidState uidState, @NonNull String pkgName) + throws NumberFormatException, XmlPullParserException, IOException { + int opCode = parser.getAttributeInt(null, "n"); + Op op = new Op(uidState, pkgName, opCode, uidState.uid); + + final int mode = parser.getAttributeInt(null, "m", AppOpsManager.opToDefaultMode(op.op)); + op.setMode(mode); + + int outerDepth = parser.getDepth(); + int type; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { + if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { + continue; + } + String tagName = parser.getName(); + if (tagName.equals("st")) { + readAttributionOp(parser, op, XmlUtils.readStringAttribute(parser, "id")); + } else { + Slog.w(TAG, "Unknown element under <op>: " + + parser.getName()); + XmlUtils.skipCurrentTag(parser); + } + } + + if (uidState.pkgOps == null) { + uidState.pkgOps = new ArrayMap<>(); + } + Ops ops = uidState.pkgOps.get(pkgName); + if (ops == null) { + ops = new Ops(pkgName, uidState); + uidState.pkgOps.put(pkgName, ops); + } + ops.put(op.op, op); + } + + void writeState() { + synchronized (mFile) { + FileOutputStream stream; + try { + stream = mFile.startWrite(); + } catch (IOException e) { + Slog.w(TAG, "Failed to write state: " + e); + return; + } + + List<AppOpsManager.PackageOps> allOps = getPackagesForOps(null); + + try { + TypedXmlSerializer out = Xml.resolveSerializer(stream); + out.startDocument(null, true); + out.startTag(null, "app-ops"); + out.attributeInt(null, "v", CURRENT_VERSION); + + SparseArray<SparseIntArray> uidStatesClone; + synchronized (this) { + uidStatesClone = new SparseArray<>(mUidStates.size()); + + final int uidStateCount = mUidStates.size(); + for (int uidStateNum = 0; uidStateNum < uidStateCount; uidStateNum++) { + UidState uidState = mUidStates.valueAt(uidStateNum); + int uid = mUidStates.keyAt(uidStateNum); + + SparseIntArray opModes = uidState.getNonDefaultUidModes(); + if (opModes != null && opModes.size() > 0) { + uidStatesClone.put(uid, opModes); + } + } + } + + final int uidStateCount = uidStatesClone.size(); + for (int uidStateNum = 0; uidStateNum < uidStateCount; uidStateNum++) { + SparseIntArray opModes = uidStatesClone.valueAt(uidStateNum); + if (opModes != null && opModes.size() > 0) { + out.startTag(null, "uid"); + out.attributeInt(null, "n", uidStatesClone.keyAt(uidStateNum)); + final int opCount = opModes.size(); + for (int opCountNum = 0; opCountNum < opCount; opCountNum++) { + final int op = opModes.keyAt(opCountNum); + final int mode = opModes.valueAt(opCountNum); + out.startTag(null, "op"); + out.attributeInt(null, "n", op); + out.attributeInt(null, "m", mode); + out.endTag(null, "op"); + } + out.endTag(null, "uid"); + } + } + + if (allOps != null) { + String lastPkg = null; + for (int i=0; i<allOps.size(); i++) { + AppOpsManager.PackageOps pkg = allOps.get(i); + if (!Objects.equals(pkg.getPackageName(), lastPkg)) { + if (lastPkg != null) { + out.endTag(null, "pkg"); + } + lastPkg = pkg.getPackageName(); + if (lastPkg != null) { + out.startTag(null, "pkg"); + out.attribute(null, "n", lastPkg); + } + } + out.startTag(null, "uid"); + out.attributeInt(null, "n", pkg.getUid()); + List<AppOpsManager.OpEntry> ops = pkg.getOps(); + for (int j=0; j<ops.size(); j++) { + AppOpsManager.OpEntry op = ops.get(j); + out.startTag(null, "op"); + out.attributeInt(null, "n", op.getOp()); + if (op.getMode() != AppOpsManager.opToDefaultMode(op.getOp())) { + out.attributeInt(null, "m", op.getMode()); + } + + for (String attributionTag : op.getAttributedOpEntries().keySet()) { + final AttributedOpEntry attribution = + op.getAttributedOpEntries().get(attributionTag); + + final ArraySet<Long> keys = attribution.collectKeys(); + + final int keyCount = keys.size(); + for (int k = 0; k < keyCount; k++) { + final long key = keys.valueAt(k); + + final int uidState = AppOpsManager.extractUidStateFromKey(key); + final int flags = AppOpsManager.extractFlagsFromKey(key); + + final long accessTime = attribution.getLastAccessTime(uidState, + uidState, flags); + final long rejectTime = attribution.getLastRejectTime(uidState, + uidState, flags); + final long accessDuration = attribution.getLastDuration( + uidState, uidState, flags); + // Proxy information for rejections is not backed up + final OpEventProxyInfo proxy = attribution.getLastProxyInfo( + uidState, uidState, flags); + + if (accessTime <= 0 && rejectTime <= 0 && accessDuration <= 0 + && proxy == null) { + continue; + } + + String proxyPkg = null; + String proxyAttributionTag = null; + int proxyUid = Process.INVALID_UID; + if (proxy != null) { + proxyPkg = proxy.getPackageName(); + proxyAttributionTag = proxy.getAttributionTag(); + proxyUid = proxy.getUid(); + } + + out.startTag(null, "st"); + if (attributionTag != null) { + out.attribute(null, "id", attributionTag); + } + out.attributeLong(null, "n", key); + if (accessTime > 0) { + out.attributeLong(null, "t", accessTime); + } + if (rejectTime > 0) { + out.attributeLong(null, "r", rejectTime); + } + if (accessDuration > 0) { + out.attributeLong(null, "d", accessDuration); + } + if (proxyPkg != null) { + out.attribute(null, "pp", proxyPkg); + } + if (proxyAttributionTag != null) { + out.attribute(null, "pc", proxyAttributionTag); + } + if (proxyUid >= 0) { + out.attributeInt(null, "pu", proxyUid); + } + out.endTag(null, "st"); + } + } + + out.endTag(null, "op"); + } + out.endTag(null, "uid"); + } + if (lastPkg != null) { + out.endTag(null, "pkg"); + } + } + + out.endTag(null, "app-ops"); + out.endDocument(); + mFile.finishWrite(stream); + } catch (IOException e) { + Slog.w(TAG, "Failed to write state, restoring backup.", e); + mFile.failWrite(stream); + } + } + mHistoricalRegistry.writeAndClearDiscreteHistory(); + } + static class Shell extends ShellCommand { final IAppOpsService mInterface; final AppOpsService mInternal; @@ -1178,6 +4371,7 @@ public class AppOpsService extends IAppOpsService.Stub { int mode; int packageUid; int nonpackageUid; + final static Binder sBinder = new Binder(); IBinder mToken; boolean targetsUid; @@ -1198,7 +4392,7 @@ public class AppOpsService extends IAppOpsService.Stub { dumpCommandHelp(pw); } - static int strOpToOp(String op, PrintWriter err) { + static private int strOpToOp(String op, PrintWriter err) { try { return AppOpsManager.strOpToOp(op); } catch (IllegalArgumentException e) { @@ -1395,24 +4589,6 @@ public class AppOpsService extends IAppOpsService.Stub { pw.println(" not specified, the current user is assumed."); } - @Override - protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { - mAppOpsService.dump(fd, pw, args); - - pw.println(); - if (mCheckOpsDelegateDispatcher.mPolicy != null - && mCheckOpsDelegateDispatcher.mPolicy instanceof AppOpsPolicy) { - AppOpsPolicy policy = (AppOpsPolicy) mCheckOpsDelegateDispatcher.mPolicy; - policy.dumpTags(pw); - } else { - pw.println(" AppOps policy not set."); - } - - if (mAudioRestrictionManager.hasActiveRestrictions()) { - pw.println(); - mAudioRestrictionManager.dump(pw); - } - } static int onShellCommand(Shell shell, String cmd) { if (cmd == null) { return shell.handleDefaultCommands(cmd); @@ -1616,12 +4792,14 @@ public class AppOpsService extends IAppOpsService.Stub { return 0; } case "write-settings": { - shell.mInternal.mAppOpsService - .enforceManageAppOpsModes(Binder.getCallingPid(), + shell.mInternal.enforceManageAppOpsModes(Binder.getCallingPid(), Binder.getCallingUid(), -1); final long token = Binder.clearCallingIdentity(); try { - shell.mInternal.mAppOpsService.writeState(); + synchronized (shell.mInternal) { + shell.mInternal.mHandler.removeCallbacks(shell.mInternal.mWriteRunner); + } + shell.mInternal.writeState(); pw.println("Current settings written."); } finally { Binder.restoreCallingIdentity(token); @@ -1629,12 +4807,11 @@ public class AppOpsService extends IAppOpsService.Stub { return 0; } case "read-settings": { - shell.mInternal.mAppOpsService - .enforceManageAppOpsModes(Binder.getCallingPid(), - Binder.getCallingUid(), -1); + shell.mInternal.enforceManageAppOpsModes(Binder.getCallingPid(), + Binder.getCallingUid(), -1); final long token = Binder.clearCallingIdentity(); try { - shell.mInternal.mAppOpsService.readState(); + shell.mInternal.readState(); pw.println("Last settings read."); } finally { Binder.restoreCallingIdentity(token); @@ -1680,70 +4857,877 @@ public class AppOpsService extends IAppOpsService.Stub { return -1; } + private void dumpHelp(PrintWriter pw) { + pw.println("AppOps service (appops) dump options:"); + pw.println(" -h"); + pw.println(" Print this help text."); + pw.println(" --op [OP]"); + pw.println(" Limit output to data associated with the given app op code."); + pw.println(" --mode [MODE]"); + pw.println(" Limit output to data associated with the given app op mode."); + pw.println(" --package [PACKAGE]"); + pw.println(" Limit output to data associated with the given package name."); + pw.println(" --attributionTag [attributionTag]"); + pw.println(" Limit output to data associated with the given attribution tag."); + pw.println(" --include-discrete [n]"); + pw.println(" Include discrete ops limited to n per dimension. Use zero for no limit."); + pw.println(" --watchers"); + pw.println(" Only output the watcher sections."); + pw.println(" --history"); + pw.println(" Only output history."); + pw.println(" --uid-state-changes"); + pw.println(" Include logs about uid state changes."); + } + + private void dumpStatesLocked(@NonNull PrintWriter pw, @Nullable String filterAttributionTag, + @HistoricalOpsRequestFilter int filter, long nowElapsed, @NonNull Op op, long now, + @NonNull SimpleDateFormat sdf, @NonNull Date date, @NonNull String prefix) { + final int numAttributions = op.mAttributions.size(); + for (int i = 0; i < numAttributions; i++) { + if ((filter & FILTER_BY_ATTRIBUTION_TAG) != 0 && !Objects.equals( + op.mAttributions.keyAt(i), filterAttributionTag)) { + continue; + } + + pw.print(prefix + op.mAttributions.keyAt(i) + "=[\n"); + dumpStatesLocked(pw, nowElapsed, op, op.mAttributions.keyAt(i), now, sdf, date, + prefix + " "); + pw.print(prefix + "]\n"); + } + } + + private void dumpStatesLocked(@NonNull PrintWriter pw, long nowElapsed, @NonNull Op op, + @Nullable String attributionTag, long now, @NonNull SimpleDateFormat sdf, + @NonNull Date date, @NonNull String prefix) { + + final AttributedOpEntry entry = op.createSingleAttributionEntryLocked( + attributionTag).getAttributedOpEntries().get(attributionTag); + + final ArraySet<Long> keys = entry.collectKeys(); + + final int keyCount = keys.size(); + for (int k = 0; k < keyCount; k++) { + final long key = keys.valueAt(k); + + final int uidState = AppOpsManager.extractUidStateFromKey(key); + final int flags = AppOpsManager.extractFlagsFromKey(key); + + final long accessTime = entry.getLastAccessTime(uidState, uidState, flags); + final long rejectTime = entry.getLastRejectTime(uidState, uidState, flags); + final long accessDuration = entry.getLastDuration(uidState, uidState, flags); + final OpEventProxyInfo proxy = entry.getLastProxyInfo(uidState, uidState, flags); + + String proxyPkg = null; + String proxyAttributionTag = null; + int proxyUid = Process.INVALID_UID; + if (proxy != null) { + proxyPkg = proxy.getPackageName(); + proxyAttributionTag = proxy.getAttributionTag(); + proxyUid = proxy.getUid(); + } + + if (accessTime > 0) { + pw.print(prefix); + pw.print("Access: "); + pw.print(AppOpsManager.keyToString(key)); + pw.print(" "); + date.setTime(accessTime); + pw.print(sdf.format(date)); + pw.print(" ("); + TimeUtils.formatDuration(accessTime - now, pw); + pw.print(")"); + if (accessDuration > 0) { + pw.print(" duration="); + TimeUtils.formatDuration(accessDuration, pw); + } + if (proxyUid >= 0) { + pw.print(" proxy["); + pw.print("uid="); + pw.print(proxyUid); + pw.print(", pkg="); + pw.print(proxyPkg); + pw.print(", attributionTag="); + pw.print(proxyAttributionTag); + pw.print("]"); + } + pw.println(); + } + + if (rejectTime > 0) { + pw.print(prefix); + pw.print("Reject: "); + pw.print(AppOpsManager.keyToString(key)); + date.setTime(rejectTime); + pw.print(sdf.format(date)); + pw.print(" ("); + TimeUtils.formatDuration(rejectTime - now, pw); + pw.print(")"); + if (proxyUid >= 0) { + pw.print(" proxy["); + pw.print("uid="); + pw.print(proxyUid); + pw.print(", pkg="); + pw.print(proxyPkg); + pw.print(", attributionTag="); + pw.print(proxyAttributionTag); + pw.print("]"); + } + pw.println(); + } + } + + final AttributedOp attributedOp = op.mAttributions.get(attributionTag); + if (attributedOp.isRunning()) { + long earliestElapsedTime = Long.MAX_VALUE; + long maxNumStarts = 0; + int numInProgressEvents = attributedOp.mInProgressEvents.size(); + for (int i = 0; i < numInProgressEvents; i++) { + AttributedOp.InProgressStartOpEvent event = + attributedOp.mInProgressEvents.valueAt(i); + + earliestElapsedTime = Math.min(earliestElapsedTime, event.getStartElapsedTime()); + maxNumStarts = Math.max(maxNumStarts, event.mNumUnfinishedStarts); + } + + pw.print(prefix + "Running start at: "); + TimeUtils.formatDuration(nowElapsed - earliestElapsedTime, pw); + pw.println(); + + if (maxNumStarts > 1) { + pw.print(prefix + "startNesting="); + pw.println(maxNumStarts); + } + } + } + + @NeverCompile // Avoid size overhead of debugging code. + @Override + protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + if (!DumpUtils.checkDumpAndUsageStatsPermission(mContext, TAG, pw)) return; + + int dumpOp = OP_NONE; + String dumpPackage = null; + String dumpAttributionTag = null; + int dumpUid = Process.INVALID_UID; + int dumpMode = -1; + boolean dumpWatchers = false; + // TODO ntmyren: Remove the dumpHistory and dumpFilter + boolean dumpHistory = false; + boolean includeDiscreteOps = false; + boolean dumpUidStateChangeLogs = false; + int nDiscreteOps = 10; + @HistoricalOpsRequestFilter int dumpFilter = 0; + boolean dumpAll = false; + + if (args != null) { + for (int i = 0; i < args.length; i++) { + String arg = args[i]; + if ("-h".equals(arg)) { + dumpHelp(pw); + return; + } else if ("-a".equals(arg)) { + // dump all data + dumpAll = true; + } else if ("--op".equals(arg)) { + i++; + if (i >= args.length) { + pw.println("No argument for --op option"); + return; + } + dumpOp = Shell.strOpToOp(args[i], pw); + dumpFilter |= FILTER_BY_OP_NAMES; + if (dumpOp < 0) { + return; + } + } else if ("--package".equals(arg)) { + i++; + if (i >= args.length) { + pw.println("No argument for --package option"); + return; + } + dumpPackage = args[i]; + dumpFilter |= FILTER_BY_PACKAGE_NAME; + try { + dumpUid = AppGlobals.getPackageManager().getPackageUid(dumpPackage, + PackageManager.MATCH_KNOWN_PACKAGES | PackageManager.MATCH_INSTANT, + 0); + } catch (RemoteException e) { + } + if (dumpUid < 0) { + pw.println("Unknown package: " + dumpPackage); + return; + } + dumpUid = UserHandle.getAppId(dumpUid); + dumpFilter |= FILTER_BY_UID; + } else if ("--attributionTag".equals(arg)) { + i++; + if (i >= args.length) { + pw.println("No argument for --attributionTag option"); + return; + } + dumpAttributionTag = args[i]; + dumpFilter |= FILTER_BY_ATTRIBUTION_TAG; + } else if ("--mode".equals(arg)) { + i++; + if (i >= args.length) { + pw.println("No argument for --mode option"); + return; + } + dumpMode = Shell.strModeToMode(args[i], pw); + if (dumpMode < 0) { + return; + } + } else if ("--watchers".equals(arg)) { + dumpWatchers = true; + } else if ("--include-discrete".equals(arg)) { + i++; + if (i >= args.length) { + pw.println("No argument for --include-discrete option"); + return; + } + try { + nDiscreteOps = Integer.valueOf(args[i]); + } catch (NumberFormatException e) { + pw.println("Wrong parameter: " + args[i]); + return; + } + includeDiscreteOps = true; + } else if ("--history".equals(arg)) { + dumpHistory = true; + } else if (arg.length() > 0 && arg.charAt(0) == '-') { + pw.println("Unknown option: " + arg); + return; + } else if ("--uid-state-changes".equals(arg)) { + dumpUidStateChangeLogs = true; + } else { + pw.println("Unknown command: " + arg); + return; + } + } + } + + final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); + final Date date = new Date(); + synchronized (this) { + pw.println("Current AppOps Service state:"); + if (!dumpHistory && !dumpWatchers) { + mConstants.dump(pw); + } + pw.println(); + final long now = System.currentTimeMillis(); + final long nowElapsed = SystemClock.elapsedRealtime(); + final long nowUptime = SystemClock.uptimeMillis(); + boolean needSep = false; + if (dumpFilter == 0 && dumpMode < 0 && mProfileOwners != null && !dumpWatchers + && !dumpHistory) { + pw.println(" Profile owners:"); + for (int poi = 0; poi < mProfileOwners.size(); poi++) { + pw.print(" User #"); + pw.print(mProfileOwners.keyAt(poi)); + pw.print(": "); + UserHandle.formatUid(pw, mProfileOwners.valueAt(poi)); + pw.println(); + } + pw.println(); + } + + if (!dumpHistory) { + needSep |= mAppOpsCheckingService.dumpListeners(dumpOp, dumpUid, dumpPackage, pw); + } + + if (mModeWatchers.size() > 0 && dumpOp < 0 && !dumpHistory) { + boolean printedHeader = false; + for (int i = 0; i < mModeWatchers.size(); i++) { + final ModeCallback cb = mModeWatchers.valueAt(i); + if (dumpPackage != null + && dumpUid != UserHandle.getAppId(cb.getWatchingUid())) { + continue; + } + needSep = true; + if (!printedHeader) { + pw.println(" All op mode watchers:"); + printedHeader = true; + } + pw.print(" "); + pw.print(Integer.toHexString(System.identityHashCode(mModeWatchers.keyAt(i)))); + pw.print(": "); pw.println(cb); + } + } + if (mActiveWatchers.size() > 0 && dumpMode < 0) { + needSep = true; + boolean printedHeader = false; + for (int watcherNum = 0; watcherNum < mActiveWatchers.size(); watcherNum++) { + final SparseArray<ActiveCallback> activeWatchers = + mActiveWatchers.valueAt(watcherNum); + if (activeWatchers.size() <= 0) { + continue; + } + final ActiveCallback cb = activeWatchers.valueAt(0); + if (dumpOp >= 0 && activeWatchers.indexOfKey(dumpOp) < 0) { + continue; + } + if (dumpPackage != null + && dumpUid != UserHandle.getAppId(cb.mWatchingUid)) { + continue; + } + if (!printedHeader) { + pw.println(" All op active watchers:"); + printedHeader = true; + } + pw.print(" "); + pw.print(Integer.toHexString(System.identityHashCode( + mActiveWatchers.keyAt(watcherNum)))); + pw.println(" ->"); + pw.print(" ["); + final int opCount = activeWatchers.size(); + for (int opNum = 0; opNum < opCount; opNum++) { + if (opNum > 0) { + pw.print(' '); + } + pw.print(AppOpsManager.opToName(activeWatchers.keyAt(opNum))); + if (opNum < opCount - 1) { + pw.print(','); + } + } + pw.println("]"); + pw.print(" "); + pw.println(cb); + } + } + if (mStartedWatchers.size() > 0 && dumpMode < 0) { + needSep = true; + boolean printedHeader = false; + + final int watchersSize = mStartedWatchers.size(); + for (int watcherNum = 0; watcherNum < watchersSize; watcherNum++) { + final SparseArray<StartedCallback> startedWatchers = + mStartedWatchers.valueAt(watcherNum); + if (startedWatchers.size() <= 0) { + continue; + } + + final StartedCallback cb = startedWatchers.valueAt(0); + if (dumpOp >= 0 && startedWatchers.indexOfKey(dumpOp) < 0) { + continue; + } + + if (dumpPackage != null + && dumpUid != UserHandle.getAppId(cb.mWatchingUid)) { + continue; + } + + if (!printedHeader) { + pw.println(" All op started watchers:"); + printedHeader = true; + } + + pw.print(" "); + pw.print(Integer.toHexString(System.identityHashCode( + mStartedWatchers.keyAt(watcherNum)))); + pw.println(" ->"); + + pw.print(" ["); + final int opCount = startedWatchers.size(); + for (int opNum = 0; opNum < opCount; opNum++) { + if (opNum > 0) { + pw.print(' '); + } + + pw.print(AppOpsManager.opToName(startedWatchers.keyAt(opNum))); + if (opNum < opCount - 1) { + pw.print(','); + } + } + pw.println("]"); + + pw.print(" "); + pw.println(cb); + } + } + if (mNotedWatchers.size() > 0 && dumpMode < 0) { + needSep = true; + boolean printedHeader = false; + for (int watcherNum = 0; watcherNum < mNotedWatchers.size(); watcherNum++) { + final SparseArray<NotedCallback> notedWatchers = + mNotedWatchers.valueAt(watcherNum); + if (notedWatchers.size() <= 0) { + continue; + } + final NotedCallback cb = notedWatchers.valueAt(0); + if (dumpOp >= 0 && notedWatchers.indexOfKey(dumpOp) < 0) { + continue; + } + if (dumpPackage != null + && dumpUid != UserHandle.getAppId(cb.mWatchingUid)) { + continue; + } + if (!printedHeader) { + pw.println(" All op noted watchers:"); + printedHeader = true; + } + pw.print(" "); + pw.print(Integer.toHexString(System.identityHashCode( + mNotedWatchers.keyAt(watcherNum)))); + pw.println(" ->"); + pw.print(" ["); + final int opCount = notedWatchers.size(); + for (int opNum = 0; opNum < opCount; opNum++) { + if (opNum > 0) { + pw.print(' '); + } + pw.print(AppOpsManager.opToName(notedWatchers.keyAt(opNum))); + if (opNum < opCount - 1) { + pw.print(','); + } + } + pw.println("]"); + pw.print(" "); + pw.println(cb); + } + } + if (mAudioRestrictionManager.hasActiveRestrictions() && dumpOp < 0 + && dumpPackage != null && dumpMode < 0 && !dumpWatchers) { + needSep = mAudioRestrictionManager.dump(pw) || needSep; + } + if (needSep) { + pw.println(); + } + for (int i=0; i<mUidStates.size(); i++) { + UidState uidState = mUidStates.valueAt(i); + final SparseIntArray opModes = uidState.getNonDefaultUidModes(); + final ArrayMap<String, Ops> pkgOps = uidState.pkgOps; + + if (dumpWatchers || dumpHistory) { + continue; + } + if (dumpOp >= 0 || dumpPackage != null || dumpMode >= 0) { + boolean hasOp = dumpOp < 0 || (opModes != null + && opModes.indexOfKey(dumpOp) >= 0); + boolean hasPackage = dumpPackage == null || dumpUid == mUidStates.keyAt(i); + boolean hasMode = dumpMode < 0; + if (!hasMode && opModes != null) { + for (int opi = 0; !hasMode && opi < opModes.size(); opi++) { + if (opModes.valueAt(opi) == dumpMode) { + hasMode = true; + } + } + } + if (pkgOps != null) { + for (int pkgi = 0; + (!hasOp || !hasPackage || !hasMode) && pkgi < pkgOps.size(); + pkgi++) { + Ops ops = pkgOps.valueAt(pkgi); + if (!hasOp && ops != null && ops.indexOfKey(dumpOp) >= 0) { + hasOp = true; + } + if (!hasMode) { + for (int opi = 0; !hasMode && opi < ops.size(); opi++) { + if (ops.valueAt(opi).getMode() == dumpMode) { + hasMode = true; + } + } + } + if (!hasPackage && dumpPackage.equals(ops.packageName)) { + hasPackage = true; + } + } + } + if (uidState.foregroundOps != null && !hasOp) { + if (uidState.foregroundOps.indexOfKey(dumpOp) > 0) { + hasOp = true; + } + } + if (!hasOp || !hasPackage || !hasMode) { + continue; + } + } + + pw.print(" Uid "); UserHandle.formatUid(pw, uidState.uid); pw.println(":"); + uidState.dump(pw, nowElapsed); + if (uidState.foregroundOps != null && (dumpMode < 0 + || dumpMode == AppOpsManager.MODE_FOREGROUND)) { + pw.println(" foregroundOps:"); + for (int j = 0; j < uidState.foregroundOps.size(); j++) { + if (dumpOp >= 0 && dumpOp != uidState.foregroundOps.keyAt(j)) { + continue; + } + pw.print(" "); + pw.print(AppOpsManager.opToName(uidState.foregroundOps.keyAt(j))); + pw.print(": "); + pw.println(uidState.foregroundOps.valueAt(j) ? "WATCHER" : "SILENT"); + } + pw.print(" hasForegroundWatchers="); + pw.println(uidState.hasForegroundWatchers); + } + needSep = true; + + if (opModes != null) { + final int opModeCount = opModes.size(); + for (int j = 0; j < opModeCount; j++) { + final int code = opModes.keyAt(j); + final int mode = opModes.valueAt(j); + if (dumpOp >= 0 && dumpOp != code) { + continue; + } + if (dumpMode >= 0 && dumpMode != mode) { + continue; + } + pw.print(" "); pw.print(AppOpsManager.opToName(code)); + pw.print(": mode="); pw.println(AppOpsManager.modeToName(mode)); + } + } + + if (pkgOps == null) { + continue; + } + + for (int pkgi = 0; pkgi < pkgOps.size(); pkgi++) { + final Ops ops = pkgOps.valueAt(pkgi); + if (dumpPackage != null && !dumpPackage.equals(ops.packageName)) { + continue; + } + boolean printedPackage = false; + for (int j=0; j<ops.size(); j++) { + final Op op = ops.valueAt(j); + final int opCode = op.op; + if (dumpOp >= 0 && dumpOp != opCode) { + continue; + } + if (dumpMode >= 0 && dumpMode != op.getMode()) { + continue; + } + if (!printedPackage) { + pw.print(" Package "); pw.print(ops.packageName); pw.println(":"); + printedPackage = true; + } + pw.print(" "); pw.print(AppOpsManager.opToName(opCode)); + pw.print(" ("); pw.print(AppOpsManager.modeToName(op.getMode())); + final int switchOp = AppOpsManager.opToSwitch(opCode); + if (switchOp != opCode) { + pw.print(" / switch "); + pw.print(AppOpsManager.opToName(switchOp)); + final Op switchObj = ops.get(switchOp); + int mode = switchObj == null + ? AppOpsManager.opToDefaultMode(switchOp) : switchObj.getMode(); + pw.print("="); pw.print(AppOpsManager.modeToName(mode)); + } + pw.println("): "); + dumpStatesLocked(pw, dumpAttributionTag, dumpFilter, nowElapsed, op, now, + sdf, date, " "); + } + } + } + if (needSep) { + pw.println(); + } + + boolean showUserRestrictions = !(dumpMode < 0 && !dumpWatchers && !dumpHistory); + mAppOpsRestrictions.dumpRestrictions(pw, dumpOp, dumpPackage, showUserRestrictions); + + if (!dumpHistory && !dumpWatchers) { + pw.println(); + if (mCheckOpsDelegateDispatcher.mPolicy != null + && mCheckOpsDelegateDispatcher.mPolicy instanceof AppOpsPolicy) { + AppOpsPolicy policy = (AppOpsPolicy) mCheckOpsDelegateDispatcher.mPolicy; + policy.dumpTags(pw); + } else { + pw.println(" AppOps policy not set."); + } + } + + if (dumpAll || dumpUidStateChangeLogs) { + pw.println(); + pw.println("Uid State Changes Event Log:"); + getUidStateTracker().dumpEvents(pw); + } + } + + // Must not hold the appops lock + if (dumpHistory && !dumpWatchers) { + mHistoricalRegistry.dump(" ", pw, dumpUid, dumpPackage, dumpAttributionTag, dumpOp, + dumpFilter); + } + if (includeDiscreteOps) { + pw.println("Discrete accesses: "); + mHistoricalRegistry.dumpDiscreteData(pw, dumpUid, dumpPackage, dumpAttributionTag, + dumpFilter, dumpOp, sdf, date, " ", nDiscreteOps); + } + } + @Override public void setUserRestrictions(Bundle restrictions, IBinder token, int userHandle) { - mAppOpsService.setUserRestrictions(restrictions, token, userHandle); + checkSystemUid("setUserRestrictions"); + Objects.requireNonNull(restrictions); + Objects.requireNonNull(token); + for (int i = 0; i < AppOpsManager._NUM_OP; i++) { + String restriction = AppOpsManager.opToRestriction(i); + if (restriction != null) { + setUserRestrictionNoCheck(i, restrictions.getBoolean(restriction, false), token, + userHandle, null); + } + } } @Override public void setUserRestriction(int code, boolean restricted, IBinder token, int userHandle, PackageTagsList excludedPackageTags) { - mAppOpsService.setUserRestriction(code, restricted, token, userHandle, - excludedPackageTags); + if (Binder.getCallingPid() != Process.myPid()) { + mContext.enforcePermission(Manifest.permission.MANAGE_APP_OPS_RESTRICTIONS, + Binder.getCallingPid(), Binder.getCallingUid(), null); + } + if (userHandle != UserHandle.getCallingUserId()) { + if (mContext.checkCallingOrSelfPermission(Manifest.permission + .INTERACT_ACROSS_USERS_FULL) != PackageManager.PERMISSION_GRANTED + && mContext.checkCallingOrSelfPermission(Manifest.permission + .INTERACT_ACROSS_USERS) != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("Need INTERACT_ACROSS_USERS_FULL or" + + " INTERACT_ACROSS_USERS to interact cross user "); + } + } + verifyIncomingOp(code); + Objects.requireNonNull(token); + setUserRestrictionNoCheck(code, restricted, token, userHandle, excludedPackageTags); + } + + private void setUserRestrictionNoCheck(int code, boolean restricted, IBinder token, + int userHandle, PackageTagsList excludedPackageTags) { + synchronized (AppOpsService.this) { + ClientUserRestrictionState restrictionState = mOpUserRestrictions.get(token); + + if (restrictionState == null) { + try { + restrictionState = new ClientUserRestrictionState(token); + } catch (RemoteException e) { + return; + } + mOpUserRestrictions.put(token, restrictionState); + } + + if (restrictionState.setRestriction(code, restricted, excludedPackageTags, + userHandle)) { + mHandler.sendMessage(PooledLambda.obtainMessage( + AppOpsService::notifyWatchersOfChange, this, code, UID_ANY)); + mHandler.sendMessage(PooledLambda.obtainMessage( + AppOpsService::updateStartedOpModeForUser, this, code, restricted, + userHandle)); + } + + if (restrictionState.isDefault()) { + mOpUserRestrictions.remove(token); + restrictionState.destroy(); + } + } + } + + private void updateStartedOpModeForUser(int code, boolean restricted, int userId) { + synchronized (AppOpsService.this) { + int numUids = mUidStates.size(); + for (int uidNum = 0; uidNum < numUids; uidNum++) { + int uid = mUidStates.keyAt(uidNum); + if (userId != UserHandle.USER_ALL && UserHandle.getUserId(uid) != userId) { + continue; + } + updateStartedOpModeForUidLocked(code, restricted, uid); + } + } + } + + private void updateStartedOpModeForUidLocked(int code, boolean restricted, int uid) { + UidState uidState = mUidStates.get(uid); + if (uidState == null || uidState.pkgOps == null) { + return; + } + + int numPkgOps = uidState.pkgOps.size(); + for (int pkgNum = 0; pkgNum < numPkgOps; pkgNum++) { + Ops ops = uidState.pkgOps.valueAt(pkgNum); + Op op = ops != null ? ops.get(code) : null; + if (op == null || (op.getMode() != MODE_ALLOWED && op.getMode() != MODE_FOREGROUND)) { + continue; + } + int numAttrTags = op.mAttributions.size(); + for (int attrNum = 0; attrNum < numAttrTags; attrNum++) { + AttributedOp attrOp = op.mAttributions.valueAt(attrNum); + if (restricted && attrOp.isRunning()) { + attrOp.pause(); + } else if (attrOp.isPaused()) { + attrOp.resume(); + } + } + } + } + + private void notifyWatchersOfChange(int code, int uid) { + final ArraySet<OnOpModeChangedListener> modeChangedListenerSet; + synchronized (this) { + modeChangedListenerSet = mAppOpsCheckingService.getOpModeChangedListeners(code); + if (modeChangedListenerSet == null) { + return; + } + } + + notifyOpChanged(modeChangedListenerSet, code, uid, null); } @Override public void removeUser(int userHandle) throws RemoteException { - mAppOpsService.removeUser(userHandle); + checkSystemUid("removeUser"); + synchronized (AppOpsService.this) { + final int tokenCount = mOpUserRestrictions.size(); + for (int i = tokenCount - 1; i >= 0; i--) { + ClientUserRestrictionState opRestrictions = mOpUserRestrictions.valueAt(i); + opRestrictions.removeUser(userHandle); + } + removeUidsForUserLocked(userHandle); + } } @Override public boolean isOperationActive(int code, int uid, String packageName) { - return mAppOpsService.isOperationActive(code, uid, packageName); + if (Binder.getCallingUid() != uid) { + if (mContext.checkCallingOrSelfPermission(Manifest.permission.WATCH_APPOPS) + != PackageManager.PERMISSION_GRANTED) { + return false; + } + } + verifyIncomingOp(code); + if (!isIncomingPackageValid(packageName, UserHandle.getUserId(uid))) { + return false; + } + + final String resolvedPackageName = AppOpsManager.resolvePackageName(uid, packageName); + if (resolvedPackageName == null) { + return false; + } + // TODO moltmann: Allow to check for attribution op activeness + synchronized (AppOpsService.this) { + Ops pkgOps = getOpsLocked(uid, resolvedPackageName, null, false, null, false); + if (pkgOps == null) { + return false; + } + + Op op = pkgOps.get(code); + if (op == null) { + return false; + } + + return op.isRunning(); + } } @Override public boolean isProxying(int op, @NonNull String proxyPackageName, @NonNull String proxyAttributionTag, int proxiedUid, @NonNull String proxiedPackageName) { - return mAppOpsService.isProxying(op, proxyPackageName, proxyAttributionTag, - proxiedUid, proxiedPackageName); + Objects.requireNonNull(proxyPackageName); + Objects.requireNonNull(proxiedPackageName); + final long callingUid = Binder.getCallingUid(); + final long identity = Binder.clearCallingIdentity(); + try { + final List<AppOpsManager.PackageOps> packageOps = getOpsForPackage(proxiedUid, + proxiedPackageName, new int[] {op}); + if (packageOps == null || packageOps.isEmpty()) { + return false; + } + final List<OpEntry> opEntries = packageOps.get(0).getOps(); + if (opEntries.isEmpty()) { + return false; + } + final OpEntry opEntry = opEntries.get(0); + if (!opEntry.isRunning()) { + return false; + } + final OpEventProxyInfo proxyInfo = opEntry.getLastProxyInfo( + OP_FLAG_TRUSTED_PROXIED | AppOpsManager.OP_FLAG_UNTRUSTED_PROXIED); + return proxyInfo != null && callingUid == proxyInfo.getUid() + && proxyPackageName.equals(proxyInfo.getPackageName()) + && Objects.equals(proxyAttributionTag, proxyInfo.getAttributionTag()); + } finally { + Binder.restoreCallingIdentity(identity); + } } @Override public void resetPackageOpsNoHistory(@NonNull String packageName) { - mAppOpsService.resetPackageOpsNoHistory(packageName); + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_APPOPS, + "resetPackageOpsNoHistory"); + synchronized (AppOpsService.this) { + final int uid = mPackageManagerInternal.getPackageUid(packageName, 0, + UserHandle.getCallingUserId()); + if (uid == Process.INVALID_UID) { + return; + } + UidState uidState = mUidStates.get(uid); + if (uidState == null || uidState.pkgOps == null) { + return; + } + Ops removedOps = uidState.pkgOps.remove(packageName); + mAppOpsCheckingService.removePackage(packageName, UserHandle.getUserId(uid)); + if (removedOps != null) { + scheduleFastWriteLocked(); + } + } } @Override public void setHistoryParameters(@AppOpsManager.HistoricalMode int mode, long baseSnapshotInterval, int compressionStep) { - mAppOpsService.setHistoryParameters(mode, baseSnapshotInterval, compressionStep); + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_APPOPS, + "setHistoryParameters"); + // Must not hold the appops lock + mHistoricalRegistry.setHistoryParameters(mode, baseSnapshotInterval, compressionStep); } @Override public void offsetHistory(long offsetMillis) { - mAppOpsService.offsetHistory(offsetMillis); + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_APPOPS, + "offsetHistory"); + // Must not hold the appops lock + mHistoricalRegistry.offsetHistory(offsetMillis); + mHistoricalRegistry.offsetDiscreteHistory(offsetMillis); } @Override public void addHistoricalOps(HistoricalOps ops) { - mAppOpsService.addHistoricalOps(ops); + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_APPOPS, + "addHistoricalOps"); + // Must not hold the appops lock + mHistoricalRegistry.addHistoricalOps(ops); } @Override public void resetHistoryParameters() { - mAppOpsService.resetHistoryParameters(); + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_APPOPS, + "resetHistoryParameters"); + // Must not hold the appops lock + mHistoricalRegistry.resetHistoryParameters(); } @Override public void clearHistory() { - mAppOpsService.clearHistory(); + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_APPOPS, + "clearHistory"); + // Must not hold the appops lock + mHistoricalRegistry.clearAllHistory(); } @Override public void rebootHistory(long offlineDurationMillis) { - mAppOpsService.rebootHistory(offlineDurationMillis); + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_APPOPS, + "rebootHistory"); + + Preconditions.checkArgument(offlineDurationMillis >= 0); + + // Must not hold the appops lock + mHistoricalRegistry.shutdown(); + + if (offlineDurationMillis > 0) { + SystemClock.sleep(offlineDurationMillis); + } + + mHistoricalRegistry = new HistoricalRegistry(mHistoricalRegistry); + mHistoricalRegistry.systemReady(mContext.getContentResolver()); + mHistoricalRegistry.persistPendingHistory(); } /** @@ -1998,6 +5982,24 @@ public class AppOpsService extends IAppOpsService.Stub { return false; } + @GuardedBy("this") + private void removeUidsForUserLocked(int userHandle) { + for (int i = mUidStates.size() - 1; i >= 0; --i) { + final int uid = mUidStates.keyAt(i); + if (UserHandle.getUserId(uid) == userHandle) { + mUidStates.valueAt(i).clear(); + mUidStates.removeAt(i); + } + } + } + + private void checkSystemUid(String function) { + int uid = Binder.getCallingUid(); + if (uid != Process.SYSTEM_UID) { + throw new SecurityException(function + " must by called by the system"); + } + } + private static int resolveUid(String packageName) { if (packageName == null) { return Process.INVALID_UID; @@ -2018,43 +6020,184 @@ public class AppOpsService extends IAppOpsService.Stub { return Process.INVALID_UID; } + private static String[] getPackagesForUid(int uid) { + String[] packageNames = null; + + // Very early during boot the package manager is not yet or not yet fully started. At this + // time there are no packages yet. + if (AppGlobals.getPackageManager() != null) { + try { + packageNames = AppGlobals.getPackageManager().getPackagesForUid(uid); + } catch (RemoteException e) { + /* ignore - local call */ + } + } + if (packageNames == null) { + return EmptyArray.STRING; + } + return packageNames; + } + + private final class ClientUserRestrictionState implements DeathRecipient { + private final IBinder token; + + ClientUserRestrictionState(IBinder token) + throws RemoteException { + token.linkToDeath(this, 0); + this.token = token; + } + + public boolean setRestriction(int code, boolean restricted, + PackageTagsList excludedPackageTags, int userId) { + return mAppOpsRestrictions.setUserRestriction(token, userId, code, + restricted, excludedPackageTags); + } + + public boolean hasRestriction(int code, String packageName, String attributionTag, + int userId, boolean isCheckOp) { + return mAppOpsRestrictions.getUserRestriction(token, userId, code, packageName, + attributionTag, isCheckOp); + } + + public void removeUser(int userId) { + mAppOpsRestrictions.clearUserRestrictions(token, userId); + } + + public boolean isDefault() { + return !mAppOpsRestrictions.hasUserRestrictions(token); + } + + @Override + public void binderDied() { + synchronized (AppOpsService.this) { + mAppOpsRestrictions.clearUserRestrictions(token); + mOpUserRestrictions.remove(token); + destroy(); + } + } + + public void destroy() { + token.unlinkToDeath(this, 0); + } + } + + private final class ClientGlobalRestrictionState implements DeathRecipient { + final IBinder mToken; + + ClientGlobalRestrictionState(IBinder token) + throws RemoteException { + token.linkToDeath(this, 0); + this.mToken = token; + } + + boolean setRestriction(int code, boolean restricted) { + return mAppOpsRestrictions.setGlobalRestriction(mToken, code, restricted); + } + + boolean hasRestriction(int code) { + return mAppOpsRestrictions.getGlobalRestriction(mToken, code); + } + + boolean isDefault() { + return !mAppOpsRestrictions.hasGlobalRestrictions(mToken); + } + + @Override + public void binderDied() { + mAppOpsRestrictions.clearGlobalRestrictions(mToken); + mOpGlobalRestrictions.remove(mToken); + destroy(); + } + + void destroy() { + mToken.unlinkToDeath(this, 0); + } + } + private final class AppOpsManagerInternalImpl extends AppOpsManagerInternal { @Override public void setDeviceAndProfileOwners(SparseIntArray owners) { - AppOpsService.this.mAppOpsService.setDeviceAndProfileOwners(owners); + synchronized (AppOpsService.this) { + mProfileOwners = owners; + } } @Override public void updateAppWidgetVisibility(SparseArray<String> uidPackageNames, boolean visible) { - AppOpsService.this.mAppOpsService - .updateAppWidgetVisibility(uidPackageNames, visible); + AppOpsService.this.updateAppWidgetVisibility(uidPackageNames, visible); } @Override public void setUidModeFromPermissionPolicy(int code, int uid, int mode, @Nullable IAppOpsCallback callback) { - AppOpsService.this.mAppOpsService.setUidMode(code, uid, mode, callback); + setUidMode(code, uid, mode, callback); } @Override public void setModeFromPermissionPolicy(int code, int uid, @NonNull String packageName, int mode, @Nullable IAppOpsCallback callback) { - AppOpsService.this.mAppOpsService - .setMode(code, uid, packageName, mode, callback); + setMode(code, uid, packageName, mode, callback); } @Override public void setGlobalRestriction(int code, boolean restricted, IBinder token) { - AppOpsService.this.mAppOpsService - .setGlobalRestriction(code, restricted, token); + if (Binder.getCallingPid() != Process.myPid()) { + // TODO instead of this enforcement put in AppOpsManagerInternal + throw new SecurityException("Only the system can set global restrictions"); + } + + synchronized (AppOpsService.this) { + ClientGlobalRestrictionState restrictionState = mOpGlobalRestrictions.get(token); + + if (restrictionState == null) { + try { + restrictionState = new ClientGlobalRestrictionState(token); + } catch (RemoteException e) { + return; + } + mOpGlobalRestrictions.put(token, restrictionState); + } + + if (restrictionState.setRestriction(code, restricted)) { + mHandler.sendMessage(PooledLambda.obtainMessage( + AppOpsService::notifyWatchersOfChange, AppOpsService.this, code, + UID_ANY)); + mHandler.sendMessage(PooledLambda.obtainMessage( + AppOpsService::updateStartedOpModeForUser, AppOpsService.this, + code, restricted, UserHandle.USER_ALL)); + } + + if (restrictionState.isDefault()) { + mOpGlobalRestrictions.remove(token); + restrictionState.destroy(); + } + } } @Override public int getOpRestrictionCount(int code, UserHandle user, String pkg, String attributionTag) { - return AppOpsService.this.mAppOpsService - .getOpRestrictionCount(code, user, pkg, attributionTag); + int number = 0; + synchronized (AppOpsService.this) { + int numRestrictions = mOpUserRestrictions.size(); + for (int i = 0; i < numRestrictions; i++) { + if (mOpUserRestrictions.valueAt(i) + .hasRestriction(code, pkg, attributionTag, user.getIdentifier(), + false)) { + number++; + } + } + + numRestrictions = mOpGlobalRestrictions.size(); + for (int i = 0; i < numRestrictions; i++) { + if (mOpGlobalRestrictions.valueAt(i).hasRestriction(code)) { + number++; + } + } + } + + return number; } } @@ -2350,7 +6493,7 @@ public class AppOpsService extends IAppOpsService.Stub { attributionFlags, attributionChainId, AppOpsService.this::startOperationImpl); } - public SyncNotedAppOp startProxyOperation(IBinder clientId, int code, + public SyncNotedAppOp startProxyOperation(@NonNull IBinder clientId, int code, @NonNull AttributionSource attributionSource, boolean startIfModeDefault, boolean shouldCollectAsyncNotedOp, String message, boolean shouldCollectMessage, boolean skipProxyOperation, @AttributionFlags int proxyAttributionFlags, @@ -2380,7 +6523,7 @@ public class AppOpsService extends IAppOpsService.Stub { proxyAttributionFlags, proxiedAttributionFlags, attributionChainId); } - private SyncNotedAppOp startDelegateProxyOperationImpl(IBinder clientId, int code, + private SyncNotedAppOp startDelegateProxyOperationImpl(@NonNull IBinder clientId, int code, @NonNull AttributionSource attributionSource, boolean startIfModeDefault, boolean shouldCollectAsyncNotedOp, String message, boolean shouldCollectMessage, boolean skipProxyOperation, @AttributionFlags int proxyAttributionFlags, @@ -2414,7 +6557,7 @@ public class AppOpsService extends IAppOpsService.Stub { AppOpsService.this::finishOperationImpl); } - public void finishProxyOperation(IBinder clientId, int code, + public void finishProxyOperation(@NonNull IBinder clientId, int code, @NonNull AttributionSource attributionSource, boolean skipProxyOperation) { if (mPolicy != null) { if (mCheckOpsDelegate != null) { @@ -2432,7 +6575,7 @@ public class AppOpsService extends IAppOpsService.Stub { } } - private Void finishDelegateProxyOperationImpl(IBinder clientId, int code, + private Void finishDelegateProxyOperationImpl(@NonNull IBinder clientId, int code, @NonNull AttributionSource attributionSource, boolean skipProxyOperation) { mCheckOpsDelegate.finishProxyOperation(clientId, code, attributionSource, skipProxyOperation, AppOpsService.this::finishProxyOperationImpl); diff --git a/services/core/java/com/android/server/appop/AppOpsServiceImpl.java b/services/core/java/com/android/server/appop/AppOpsServiceImpl.java deleted file mode 100644 index c3d2717ce795..000000000000 --- a/services/core/java/com/android/server/appop/AppOpsServiceImpl.java +++ /dev/null @@ -1,4742 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server.appop; - -import static android.app.AppOpsManager.AttributedOpEntry; -import static android.app.AppOpsManager.AttributionFlags; -import static android.app.AppOpsManager.CALL_BACK_ON_SWITCHED_OP; -import static android.app.AppOpsManager.FILTER_BY_ATTRIBUTION_TAG; -import static android.app.AppOpsManager.FILTER_BY_OP_NAMES; -import static android.app.AppOpsManager.FILTER_BY_PACKAGE_NAME; -import static android.app.AppOpsManager.FILTER_BY_UID; -import static android.app.AppOpsManager.HISTORY_FLAG_GET_ATTRIBUTION_CHAINS; -import static android.app.AppOpsManager.HistoricalOps; -import static android.app.AppOpsManager.HistoricalOpsRequestFilter; -import static android.app.AppOpsManager.KEY_BG_STATE_SETTLE_TIME; -import static android.app.AppOpsManager.KEY_FG_SERVICE_STATE_SETTLE_TIME; -import static android.app.AppOpsManager.KEY_TOP_STATE_SETTLE_TIME; -import static android.app.AppOpsManager.MODE_ALLOWED; -import static android.app.AppOpsManager.MODE_DEFAULT; -import static android.app.AppOpsManager.MODE_ERRORED; -import static android.app.AppOpsManager.MODE_FOREGROUND; -import static android.app.AppOpsManager.MODE_IGNORED; -import static android.app.AppOpsManager.Mode; -import static android.app.AppOpsManager.OP_CAMERA; -import static android.app.AppOpsManager.OP_FLAGS_ALL; -import static android.app.AppOpsManager.OP_FLAG_SELF; -import static android.app.AppOpsManager.OP_FLAG_TRUSTED_PROXIED; -import static android.app.AppOpsManager.OP_NONE; -import static android.app.AppOpsManager.OP_PLAY_AUDIO; -import static android.app.AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO; -import static android.app.AppOpsManager.OP_RECORD_AUDIO; -import static android.app.AppOpsManager.OP_RECORD_AUDIO_HOTWORD; -import static android.app.AppOpsManager.OP_SCHEDULE_EXACT_ALARM; -import static android.app.AppOpsManager.OP_VIBRATE; -import static android.app.AppOpsManager.OnOpStartedListener.START_TYPE_FAILED; -import static android.app.AppOpsManager.OnOpStartedListener.START_TYPE_STARTED; -import static android.app.AppOpsManager.OpEntry; -import static android.app.AppOpsManager.OpEventProxyInfo; -import static android.app.AppOpsManager.OpFlags; -import static android.app.AppOpsManager.RestrictionBypass; -import static android.app.AppOpsManager.SECURITY_EXCEPTION_ON_INVALID_ATTRIBUTION_TAG_CHANGE; -import static android.app.AppOpsManager._NUM_OP; -import static android.app.AppOpsManager.extractFlagsFromKey; -import static android.app.AppOpsManager.extractUidStateFromKey; -import static android.app.AppOpsManager.modeToName; -import static android.app.AppOpsManager.opAllowSystemBypassRestriction; -import static android.app.AppOpsManager.opRestrictsRead; -import static android.app.AppOpsManager.opToName; -import static android.content.Intent.ACTION_PACKAGE_REMOVED; -import static android.content.Intent.EXTRA_REPLACING; - -import static com.android.server.appop.AppOpsServiceImpl.ModeCallback.ALL_OPS; - -import android.Manifest; -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.annotation.UserIdInt; -import android.app.ActivityManager; -import android.app.ActivityManagerInternal; -import android.app.AppGlobals; -import android.app.AppOpsManager; -import android.app.admin.DevicePolicyManagerInternal; -import android.content.BroadcastReceiver; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.pm.PackageManager; -import android.content.pm.PackageManagerInternal; -import android.content.pm.PermissionInfo; -import android.database.ContentObserver; -import android.net.Uri; -import android.os.AsyncTask; -import android.os.Binder; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.os.HandlerExecutor; -import android.os.IBinder; -import android.os.IBinder.DeathRecipient; -import android.os.PackageTagsList; -import android.os.Process; -import android.os.RemoteCallback; -import android.os.RemoteException; -import android.os.ServiceManager; -import android.os.SystemClock; -import android.os.UserHandle; -import android.os.storage.StorageManagerInternal; -import android.permission.PermissionManager; -import android.provider.Settings; -import android.util.ArrayMap; -import android.util.ArraySet; -import android.util.AtomicFile; -import android.util.KeyValueListParser; -import android.util.Slog; -import android.util.SparseArray; -import android.util.SparseBooleanArray; -import android.util.SparseIntArray; -import android.util.TimeUtils; -import android.util.Xml; - -import com.android.internal.annotations.GuardedBy; -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.app.IAppOpsActiveCallback; -import com.android.internal.app.IAppOpsCallback; -import com.android.internal.app.IAppOpsNotedCallback; -import com.android.internal.app.IAppOpsStartedCallback; -import com.android.internal.compat.IPlatformCompat; -import com.android.internal.os.Clock; -import com.android.internal.util.ArrayUtils; -import com.android.internal.util.DumpUtils; -import com.android.internal.util.Preconditions; -import com.android.internal.util.XmlUtils; -import com.android.internal.util.function.pooled.PooledLambda; -import com.android.modules.utils.TypedXmlPullParser; -import com.android.modules.utils.TypedXmlSerializer; -import com.android.server.LocalServices; -import com.android.server.LockGuard; -import com.android.server.SystemServerInitThreadPool; -import com.android.server.pm.UserManagerInternal; -import com.android.server.pm.permission.PermissionManagerServiceInternal; -import com.android.server.pm.pkg.AndroidPackage; -import com.android.server.pm.pkg.component.ParsedAttribution; - -import dalvik.annotation.optimization.NeverCompile; - -import libcore.util.EmptyArray; - -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; - -import java.io.File; -import java.io.FileDescriptor; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.PrintWriter; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; - -class AppOpsServiceImpl implements AppOpsServiceInterface { - static final String TAG = "AppOps"; - static final boolean DEBUG = false; - - /** - * Sentinel integer version to denote that there was no appops.xml found on boot. - * This will happen when a device boots with no existing userdata. - */ - private static final int NO_FILE_VERSION = -2; - - /** - * Sentinel integer version to denote that there was no version in the appops.xml found on boot. - * This means the file is coming from a build before versioning was added. - */ - private static final int NO_VERSION = -1; - - /** - * Increment by one every time and add the corresponding upgrade logic in - * {@link #upgradeLocked(int)} below. The first version was 1. - */ - @VisibleForTesting - static final int CURRENT_VERSION = 2; - - /** - * This stores the version of appops.xml seen at boot. If this is smaller than - * {@link #CURRENT_VERSION}, then we will run {@link #upgradeLocked(int)} on startup. - */ - private int mVersionAtBoot = NO_FILE_VERSION; - - // Write at most every 30 minutes. - static final long WRITE_DELAY = DEBUG ? 1000 : 30 * 60 * 1000; - - // Constant meaning that any UID should be matched when dispatching callbacks - private static final int UID_ANY = -2; - - private static final int[] OPS_RESTRICTED_ON_SUSPEND = { - OP_PLAY_AUDIO, - OP_RECORD_AUDIO, - OP_CAMERA, - OP_VIBRATE, - }; - private static final int MAX_UNUSED_POOLED_OBJECTS = 3; - - final Context mContext; - final AtomicFile mFile; - final Handler mHandler; - - /** - * Pool for {@link AttributedOp.OpEventProxyInfoPool} to avoid to constantly reallocate new - * objects - */ - @GuardedBy("this") - final AttributedOp.OpEventProxyInfoPool mOpEventProxyInfoPool = - new AttributedOp.OpEventProxyInfoPool(MAX_UNUSED_POOLED_OBJECTS); - - /** - * Pool for {@link AttributedOp.InProgressStartOpEventPool} to avoid to constantly reallocate - * new objects - */ - @GuardedBy("this") - final AttributedOp.InProgressStartOpEventPool mInProgressStartOpEventPool = - new AttributedOp.InProgressStartOpEventPool(mOpEventProxyInfoPool, - MAX_UNUSED_POOLED_OBJECTS); - @Nullable - private final DevicePolicyManagerInternal dpmi = - LocalServices.getService(DevicePolicyManagerInternal.class); - - private final IPlatformCompat mPlatformCompat = IPlatformCompat.Stub.asInterface( - ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE)); - - boolean mWriteScheduled; - boolean mFastWriteScheduled; - final Runnable mWriteRunner = new Runnable() { - public void run() { - synchronized (AppOpsServiceImpl.this) { - mWriteScheduled = false; - mFastWriteScheduled = false; - AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() { - @Override - protected Void doInBackground(Void... params) { - writeState(); - return null; - } - }; - task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null); - } - } - }; - - @GuardedBy("this") - @VisibleForTesting - final SparseArray<UidState> mUidStates = new SparseArray<>(); - - volatile @NonNull HistoricalRegistry mHistoricalRegistry = new HistoricalRegistry(this); - - /* - * These are app op restrictions imposed per user from various parties. - */ - private final ArrayMap<IBinder, ClientUserRestrictionState> mOpUserRestrictions = - new ArrayMap<>(); - - /* - * These are app op restrictions imposed globally from various parties within the system. - */ - private final ArrayMap<IBinder, ClientGlobalRestrictionState> mOpGlobalRestrictions = - new ArrayMap<>(); - - SparseIntArray mProfileOwners; - - /** - * Reverse lookup for {@link AppOpsManager#opToSwitch(int)}. Initialized once and never - * changed - */ - private final SparseArray<int[]> mSwitchedOps = new SparseArray<>(); - - /** - * Package Manager internal. Access via {@link #getPackageManagerInternal()} - */ - private @Nullable PackageManagerInternal mPackageManagerInternal; - - /** - * Interface for app-op modes. - */ - @VisibleForTesting - AppOpsCheckingServiceInterface mAppOpsServiceInterface; - - /** - * Interface for app-op restrictions. - */ - @VisibleForTesting - AppOpsRestrictions mAppOpsRestrictions; - - private AppOpsUidStateTracker mUidStateTracker; - - /** - * Hands the definition of foreground and uid states - */ - @GuardedBy("this") - public AppOpsUidStateTracker getUidStateTracker() { - if (mUidStateTracker == null) { - mUidStateTracker = new AppOpsUidStateTrackerImpl( - LocalServices.getService(ActivityManagerInternal.class), - mHandler, - r -> { - synchronized (AppOpsServiceImpl.this) { - r.run(); - } - }, - Clock.SYSTEM_CLOCK, mConstants); - - mUidStateTracker.addUidStateChangedCallback(new HandlerExecutor(mHandler), - this::onUidStateChanged); - } - return mUidStateTracker; - } - - /** - * All times are in milliseconds. These constants are kept synchronized with the system - * global Settings. Any access to this class or its fields should be done while - * holding the AppOpsService lock. - */ - final class Constants extends ContentObserver { - - /** - * How long we want for a drop in uid state from top to settle before applying it. - * - * @see Settings.Global#APP_OPS_CONSTANTS - * @see AppOpsManager#KEY_TOP_STATE_SETTLE_TIME - */ - public long TOP_STATE_SETTLE_TIME; - - /** - * How long we want for a drop in uid state from foreground to settle before applying it. - * - * @see Settings.Global#APP_OPS_CONSTANTS - * @see AppOpsManager#KEY_FG_SERVICE_STATE_SETTLE_TIME - */ - public long FG_SERVICE_STATE_SETTLE_TIME; - - /** - * How long we want for a drop in uid state from background to settle before applying it. - * - * @see Settings.Global#APP_OPS_CONSTANTS - * @see AppOpsManager#KEY_BG_STATE_SETTLE_TIME - */ - public long BG_STATE_SETTLE_TIME; - - private final KeyValueListParser mParser = new KeyValueListParser(','); - private ContentResolver mResolver; - - Constants(Handler handler) { - super(handler); - updateConstants(); - } - - public void startMonitoring(ContentResolver resolver) { - mResolver = resolver; - mResolver.registerContentObserver( - Settings.Global.getUriFor(Settings.Global.APP_OPS_CONSTANTS), - false, this); - updateConstants(); - } - - @Override - public void onChange(boolean selfChange, Uri uri) { - updateConstants(); - } - - private void updateConstants() { - String value = mResolver != null ? Settings.Global.getString(mResolver, - Settings.Global.APP_OPS_CONSTANTS) : ""; - - synchronized (AppOpsServiceImpl.this) { - try { - mParser.setString(value); - } catch (IllegalArgumentException e) { - // Failed to parse the settings string, log this and move on - // with defaults. - Slog.e(TAG, "Bad app ops settings", e); - } - TOP_STATE_SETTLE_TIME = mParser.getDurationMillis( - KEY_TOP_STATE_SETTLE_TIME, 5 * 1000L); - FG_SERVICE_STATE_SETTLE_TIME = mParser.getDurationMillis( - KEY_FG_SERVICE_STATE_SETTLE_TIME, 5 * 1000L); - BG_STATE_SETTLE_TIME = mParser.getDurationMillis( - KEY_BG_STATE_SETTLE_TIME, 1 * 1000L); - } - } - - void dump(PrintWriter pw) { - pw.println(" Settings:"); - - pw.print(" "); - pw.print(KEY_TOP_STATE_SETTLE_TIME); - pw.print("="); - TimeUtils.formatDuration(TOP_STATE_SETTLE_TIME, pw); - pw.println(); - pw.print(" "); - pw.print(KEY_FG_SERVICE_STATE_SETTLE_TIME); - pw.print("="); - TimeUtils.formatDuration(FG_SERVICE_STATE_SETTLE_TIME, pw); - pw.println(); - pw.print(" "); - pw.print(KEY_BG_STATE_SETTLE_TIME); - pw.print("="); - TimeUtils.formatDuration(BG_STATE_SETTLE_TIME, pw); - pw.println(); - } - } - - @VisibleForTesting - final Constants mConstants; - - @VisibleForTesting - final class UidState { - public final int uid; - - public ArrayMap<String, Ops> pkgOps; - - // true indicates there is an interested observer, false there isn't but it has such an op - //TODO: Move foregroundOps and hasForegroundWatchers into the AppOpsServiceInterface. - public SparseBooleanArray foregroundOps; - public boolean hasForegroundWatchers; - - public UidState(int uid) { - this.uid = uid; - } - - public void clear() { - mAppOpsServiceInterface.removeUid(uid); - if (pkgOps != null) { - for (String packageName : pkgOps.keySet()) { - mAppOpsServiceInterface.removePackage(packageName, UserHandle.getUserId(uid)); - } - } - pkgOps = null; - } - - public boolean isDefault() { - boolean areAllPackageModesDefault = true; - if (pkgOps != null) { - for (String packageName : pkgOps.keySet()) { - if (!mAppOpsServiceInterface.arePackageModesDefault(packageName, - UserHandle.getUserId(uid))) { - areAllPackageModesDefault = false; - break; - } - } - } - return (pkgOps == null || pkgOps.isEmpty()) - && mAppOpsServiceInterface.areUidModesDefault(uid) - && areAllPackageModesDefault; - } - - // Functions for uid mode access and manipulation. - public SparseIntArray getNonDefaultUidModes() { - return mAppOpsServiceInterface.getNonDefaultUidModes(uid); - } - - public int getUidMode(int op) { - return mAppOpsServiceInterface.getUidMode(uid, op); - } - - public boolean setUidMode(int op, int mode) { - return mAppOpsServiceInterface.setUidMode(uid, op, mode); - } - - @SuppressWarnings("GuardedBy") - int evalMode(int op, int mode) { - return getUidStateTracker().evalMode(uid, op, mode); - } - - public void evalForegroundOps() { - foregroundOps = null; - foregroundOps = mAppOpsServiceInterface.evalForegroundUidOps(uid, foregroundOps); - if (pkgOps != null) { - for (int i = pkgOps.size() - 1; i >= 0; i--) { - foregroundOps = mAppOpsServiceInterface - .evalForegroundPackageOps(pkgOps.valueAt(i).packageName, - foregroundOps, - UserHandle.getUserId(uid)); - } - } - hasForegroundWatchers = false; - if (foregroundOps != null) { - for (int i = 0; i < foregroundOps.size(); i++) { - if (foregroundOps.valueAt(i)) { - hasForegroundWatchers = true; - break; - } - } - } - } - - @SuppressWarnings("GuardedBy") - public int getState() { - return getUidStateTracker().getUidState(uid); - } - - @SuppressWarnings("GuardedBy") - public void dump(PrintWriter pw, long nowElapsed) { - getUidStateTracker().dumpUidState(pw, uid, nowElapsed); - } - } - - static final class Ops extends SparseArray<Op> { - final String packageName; - final UidState uidState; - - /** - * The restriction properties of the package. If {@code null} it could not have been read - * yet and has to be refreshed. - */ - @Nullable RestrictionBypass bypass; - - /** Lazily populated cache of attributionTags of this package */ - final @NonNull ArraySet<String> knownAttributionTags = new ArraySet<>(); - - /** - * Lazily populated cache of <b>valid</b> attributionTags of this package, a set smaller - * than or equal to {@link #knownAttributionTags}. - */ - final @NonNull ArraySet<String> validAttributionTags = new ArraySet<>(); - - Ops(String _packageName, UidState _uidState) { - packageName = _packageName; - uidState = _uidState; - } - } - - /** Returned from {@link #verifyAndGetBypass(int, String, String, String)}. */ - private static final class PackageVerificationResult { - - final RestrictionBypass bypass; - final boolean isAttributionTagValid; - - PackageVerificationResult(RestrictionBypass bypass, boolean isAttributionTagValid) { - this.bypass = bypass; - this.isAttributionTagValid = isAttributionTagValid; - } - } - - final class Op { - int op; - int uid; - final UidState uidState; - final @NonNull String packageName; - - /** attributionTag -> AttributedOp */ - final ArrayMap<String, AttributedOp> mAttributions = new ArrayMap<>(1); - - Op(UidState uidState, String packageName, int op, int uid) { - this.op = op; - this.uid = uid; - this.uidState = uidState; - this.packageName = packageName; - } - - @Mode int getMode() { - return mAppOpsServiceInterface.getPackageMode(packageName, this.op, - UserHandle.getUserId(this.uid)); - } - - void setMode(@Mode int mode) { - mAppOpsServiceInterface.setPackageMode(packageName, this.op, mode, - UserHandle.getUserId(this.uid)); - } - - void removeAttributionsWithNoTime() { - for (int i = mAttributions.size() - 1; i >= 0; i--) { - if (!mAttributions.valueAt(i).hasAnyTime()) { - mAttributions.removeAt(i); - } - } - } - - private @NonNull AttributedOp getOrCreateAttribution(@NonNull Op parent, - @Nullable String attributionTag) { - AttributedOp attributedOp; - - attributedOp = mAttributions.get(attributionTag); - if (attributedOp == null) { - attributedOp = new AttributedOp(AppOpsServiceImpl.this, attributionTag, - parent); - mAttributions.put(attributionTag, attributedOp); - } - - return attributedOp; - } - - @NonNull - OpEntry createEntryLocked() { - final int numAttributions = mAttributions.size(); - - final ArrayMap<String, AppOpsManager.AttributedOpEntry> attributionEntries = - new ArrayMap<>(numAttributions); - for (int i = 0; i < numAttributions; i++) { - attributionEntries.put(mAttributions.keyAt(i), - mAttributions.valueAt(i).createAttributedOpEntryLocked()); - } - - return new OpEntry(op, getMode(), attributionEntries); - } - - @NonNull - OpEntry createSingleAttributionEntryLocked(@Nullable String attributionTag) { - final int numAttributions = mAttributions.size(); - - final ArrayMap<String, AttributedOpEntry> attributionEntries = new ArrayMap<>(1); - for (int i = 0; i < numAttributions; i++) { - if (Objects.equals(mAttributions.keyAt(i), attributionTag)) { - attributionEntries.put(mAttributions.keyAt(i), - mAttributions.valueAt(i).createAttributedOpEntryLocked()); - break; - } - } - - return new OpEntry(op, getMode(), attributionEntries); - } - - boolean isRunning() { - final int numAttributions = mAttributions.size(); - for (int i = 0; i < numAttributions; i++) { - if (mAttributions.valueAt(i).isRunning()) { - return true; - } - } - - return false; - } - } - - final ArrayMap<IBinder, ModeCallback> mModeWatchers = new ArrayMap<>(); - final ArrayMap<IBinder, SparseArray<ActiveCallback>> mActiveWatchers = new ArrayMap<>(); - final ArrayMap<IBinder, SparseArray<StartedCallback>> mStartedWatchers = new ArrayMap<>(); - final ArrayMap<IBinder, SparseArray<NotedCallback>> mNotedWatchers = new ArrayMap<>(); - - final class ModeCallback extends OnOpModeChangedListener implements DeathRecipient { - /** If mWatchedOpCode==ALL_OPS notify for ops affected by the switch-op */ - public static final int ALL_OPS = -2; - - // Need to keep this only because stopWatchingMode needs an IAppOpsCallback. - // Otherwise we can just use the IBinder object. - private final IAppOpsCallback mCallback; - - ModeCallback(IAppOpsCallback callback, int watchingUid, int flags, int watchedOpCode, - int callingUid, int callingPid) { - super(watchingUid, flags, watchedOpCode, callingUid, callingPid); - this.mCallback = callback; - try { - mCallback.asBinder().linkToDeath(this, 0); - } catch (RemoteException e) { - /*ignored*/ - } - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(128); - sb.append("ModeCallback{"); - sb.append(Integer.toHexString(System.identityHashCode(this))); - sb.append(" watchinguid="); - UserHandle.formatUid(sb, getWatchingUid()); - sb.append(" flags=0x"); - sb.append(Integer.toHexString(getFlags())); - switch (getWatchedOpCode()) { - case OP_NONE: - break; - case ALL_OPS: - sb.append(" op=(all)"); - break; - default: - sb.append(" op="); - sb.append(opToName(getWatchedOpCode())); - break; - } - sb.append(" from uid="); - UserHandle.formatUid(sb, getCallingUid()); - sb.append(" pid="); - sb.append(getCallingPid()); - sb.append('}'); - return sb.toString(); - } - - void unlinkToDeath() { - mCallback.asBinder().unlinkToDeath(this, 0); - } - - @Override - public void binderDied() { - stopWatchingMode(mCallback); - } - - @Override - public void onOpModeChanged(int op, int uid, String packageName) throws RemoteException { - mCallback.opChanged(op, uid, packageName); - } - } - - final class ActiveCallback implements DeathRecipient { - final IAppOpsActiveCallback mCallback; - final int mWatchingUid; - final int mCallingUid; - final int mCallingPid; - - ActiveCallback(IAppOpsActiveCallback callback, int watchingUid, int callingUid, - int callingPid) { - mCallback = callback; - mWatchingUid = watchingUid; - mCallingUid = callingUid; - mCallingPid = callingPid; - try { - mCallback.asBinder().linkToDeath(this, 0); - } catch (RemoteException e) { - /*ignored*/ - } - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(128); - sb.append("ActiveCallback{"); - sb.append(Integer.toHexString(System.identityHashCode(this))); - sb.append(" watchinguid="); - UserHandle.formatUid(sb, mWatchingUid); - sb.append(" from uid="); - UserHandle.formatUid(sb, mCallingUid); - sb.append(" pid="); - sb.append(mCallingPid); - sb.append('}'); - return sb.toString(); - } - - void destroy() { - mCallback.asBinder().unlinkToDeath(this, 0); - } - - @Override - public void binderDied() { - stopWatchingActive(mCallback); - } - } - - final class StartedCallback implements DeathRecipient { - final IAppOpsStartedCallback mCallback; - final int mWatchingUid; - final int mCallingUid; - final int mCallingPid; - - StartedCallback(IAppOpsStartedCallback callback, int watchingUid, int callingUid, - int callingPid) { - mCallback = callback; - mWatchingUid = watchingUid; - mCallingUid = callingUid; - mCallingPid = callingPid; - try { - mCallback.asBinder().linkToDeath(this, 0); - } catch (RemoteException e) { - /*ignored*/ - } - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(128); - sb.append("StartedCallback{"); - sb.append(Integer.toHexString(System.identityHashCode(this))); - sb.append(" watchinguid="); - UserHandle.formatUid(sb, mWatchingUid); - sb.append(" from uid="); - UserHandle.formatUid(sb, mCallingUid); - sb.append(" pid="); - sb.append(mCallingPid); - sb.append('}'); - return sb.toString(); - } - - void destroy() { - mCallback.asBinder().unlinkToDeath(this, 0); - } - - @Override - public void binderDied() { - stopWatchingStarted(mCallback); - } - } - - final class NotedCallback implements DeathRecipient { - final IAppOpsNotedCallback mCallback; - final int mWatchingUid; - final int mCallingUid; - final int mCallingPid; - - NotedCallback(IAppOpsNotedCallback callback, int watchingUid, int callingUid, - int callingPid) { - mCallback = callback; - mWatchingUid = watchingUid; - mCallingUid = callingUid; - mCallingPid = callingPid; - try { - mCallback.asBinder().linkToDeath(this, 0); - } catch (RemoteException e) { - /*ignored*/ - } - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(128); - sb.append("NotedCallback{"); - sb.append(Integer.toHexString(System.identityHashCode(this))); - sb.append(" watchinguid="); - UserHandle.formatUid(sb, mWatchingUid); - sb.append(" from uid="); - UserHandle.formatUid(sb, mCallingUid); - sb.append(" pid="); - sb.append(mCallingPid); - sb.append('}'); - return sb.toString(); - } - - void destroy() { - mCallback.asBinder().unlinkToDeath(this, 0); - } - - @Override - public void binderDied() { - stopWatchingNoted(mCallback); - } - } - - /** - * Call {@link AttributedOp#onClientDeath attributedOp.onClientDeath(clientId)}. - */ - static void onClientDeath(@NonNull AttributedOp attributedOp, - @NonNull IBinder clientId) { - attributedOp.onClientDeath(clientId); - } - - AppOpsServiceImpl(File storagePath, Handler handler, Context context) { - mContext = context; - - for (int switchedCode = 0; switchedCode < _NUM_OP; switchedCode++) { - int switchCode = AppOpsManager.opToSwitch(switchedCode); - mSwitchedOps.put(switchCode, - ArrayUtils.appendInt(mSwitchedOps.get(switchCode), switchedCode)); - } - mAppOpsServiceInterface = new AppOpsCheckingServiceTracingDecorator( - new AppOpsCheckingServiceImpl(this, this, handler, context, mSwitchedOps)); - mAppOpsRestrictions = new AppOpsRestrictionsImpl(context, handler, - mAppOpsServiceInterface); - - LockGuard.installLock(this, LockGuard.INDEX_APP_OPS); - mFile = new AtomicFile(storagePath, "appops"); - - mHandler = handler; - mConstants = new Constants(mHandler); - readState(); - } - - /** - * Handler for work when packages are removed or updated - */ - private BroadcastReceiver mOnPackageUpdatedReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - String pkgName = intent.getData().getEncodedSchemeSpecificPart(); - int uid = intent.getIntExtra(Intent.EXTRA_UID, Process.INVALID_UID); - - if (action.equals(ACTION_PACKAGE_REMOVED) && !intent.hasExtra(EXTRA_REPLACING)) { - synchronized (AppOpsServiceImpl.this) { - UidState uidState = mUidStates.get(uid); - if (uidState == null || uidState.pkgOps == null) { - return; - } - mAppOpsServiceInterface.removePackage(pkgName, UserHandle.getUserId(uid)); - Ops removedOps = uidState.pkgOps.remove(pkgName); - if (removedOps != null) { - scheduleFastWriteLocked(); - } - } - } else if (action.equals(Intent.ACTION_PACKAGE_REPLACED)) { - AndroidPackage pkg = getPackageManagerInternal().getPackage(pkgName); - if (pkg == null) { - return; - } - - ArrayMap<String, String> dstAttributionTags = new ArrayMap<>(); - ArraySet<String> attributionTags = new ArraySet<>(); - attributionTags.add(null); - if (pkg.getAttributions() != null) { - int numAttributions = pkg.getAttributions().size(); - for (int attributionNum = 0; attributionNum < numAttributions; - attributionNum++) { - ParsedAttribution attribution = pkg.getAttributions().get(attributionNum); - attributionTags.add(attribution.getTag()); - - int numInheritFrom = attribution.getInheritFrom().size(); - for (int inheritFromNum = 0; inheritFromNum < numInheritFrom; - inheritFromNum++) { - dstAttributionTags.put(attribution.getInheritFrom().get(inheritFromNum), - attribution.getTag()); - } - } - } - - synchronized (AppOpsServiceImpl.this) { - UidState uidState = mUidStates.get(uid); - if (uidState == null || uidState.pkgOps == null) { - return; - } - - Ops ops = uidState.pkgOps.get(pkgName); - if (ops == null) { - return; - } - - // Reset cached package properties to re-initialize when needed - ops.bypass = null; - ops.knownAttributionTags.clear(); - - // Merge data collected for removed attributions into their successor - // attributions - int numOps = ops.size(); - for (int opNum = 0; opNum < numOps; opNum++) { - Op op = ops.valueAt(opNum); - - int numAttributions = op.mAttributions.size(); - for (int attributionNum = numAttributions - 1; attributionNum >= 0; - attributionNum--) { - String attributionTag = op.mAttributions.keyAt(attributionNum); - - if (attributionTags.contains(attributionTag)) { - // attribution still exist after upgrade - continue; - } - - String newAttributionTag = dstAttributionTags.get(attributionTag); - - AttributedOp newAttributedOp = op.getOrCreateAttribution(op, - newAttributionTag); - newAttributedOp.add(op.mAttributions.valueAt(attributionNum)); - op.mAttributions.removeAt(attributionNum); - - scheduleFastWriteLocked(); - } - } - } - } - } - }; - - @Override - public void systemReady() { - synchronized (this) { - upgradeLocked(mVersionAtBoot); - } - - mConstants.startMonitoring(mContext.getContentResolver()); - mHistoricalRegistry.systemReady(mContext.getContentResolver()); - - IntentFilter packageUpdateFilter = new IntentFilter(); - packageUpdateFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); - packageUpdateFilter.addAction(Intent.ACTION_PACKAGE_REPLACED); - packageUpdateFilter.addDataScheme("package"); - - mContext.registerReceiverAsUser(mOnPackageUpdatedReceiver, UserHandle.ALL, - packageUpdateFilter, null, null); - - synchronized (this) { - for (int uidNum = mUidStates.size() - 1; uidNum >= 0; uidNum--) { - int uid = mUidStates.keyAt(uidNum); - UidState uidState = mUidStates.valueAt(uidNum); - - String[] pkgsInUid = getPackagesForUid(uidState.uid); - if (ArrayUtils.isEmpty(pkgsInUid)) { - uidState.clear(); - mUidStates.removeAt(uidNum); - scheduleFastWriteLocked(); - continue; - } - - ArrayMap<String, Ops> pkgs = uidState.pkgOps; - if (pkgs == null) { - continue; - } - - int numPkgs = pkgs.size(); - for (int pkgNum = 0; pkgNum < numPkgs; pkgNum++) { - String pkg = pkgs.keyAt(pkgNum); - - String action; - if (!ArrayUtils.contains(pkgsInUid, pkg)) { - action = Intent.ACTION_PACKAGE_REMOVED; - } else { - action = Intent.ACTION_PACKAGE_REPLACED; - } - - SystemServerInitThreadPool.submit( - () -> mOnPackageUpdatedReceiver.onReceive(mContext, new Intent(action) - .setData(Uri.fromParts("package", pkg, null)) - .putExtra(Intent.EXTRA_UID, uid)), - "Update app-ops uidState in case package " + pkg + " changed"); - } - } - } - - final IntentFilter packageSuspendFilter = new IntentFilter(); - packageSuspendFilter.addAction(Intent.ACTION_PACKAGES_UNSUSPENDED); - packageSuspendFilter.addAction(Intent.ACTION_PACKAGES_SUSPENDED); - mContext.registerReceiverAsUser(new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - final int[] changedUids = intent.getIntArrayExtra(Intent.EXTRA_CHANGED_UID_LIST); - final String[] changedPkgs = intent.getStringArrayExtra( - Intent.EXTRA_CHANGED_PACKAGE_LIST); - for (int code : OPS_RESTRICTED_ON_SUSPEND) { - ArraySet<OnOpModeChangedListener> onModeChangedListeners; - synchronized (AppOpsServiceImpl.this) { - onModeChangedListeners = - mAppOpsServiceInterface.getOpModeChangedListeners(code); - if (onModeChangedListeners == null) { - continue; - } - } - for (int i = 0; i < changedUids.length; i++) { - final int changedUid = changedUids[i]; - final String changedPkg = changedPkgs[i]; - // We trust packagemanager to insert matching uid and packageNames in the - // extras - notifyOpChanged(onModeChangedListeners, code, changedUid, changedPkg); - } - } - } - }, UserHandle.ALL, packageSuspendFilter, null, null); - } - - @Override - public void packageRemoved(int uid, String packageName) { - synchronized (this) { - UidState uidState = mUidStates.get(uid); - if (uidState == null) { - return; - } - - Ops removedOps = null; - - // Remove any package state if such. - if (uidState.pkgOps != null) { - removedOps = uidState.pkgOps.remove(packageName); - mAppOpsServiceInterface.removePackage(packageName, UserHandle.getUserId(uid)); - } - - // If we just nuked the last package state check if the UID is valid. - if (removedOps != null && uidState.pkgOps.isEmpty() - && getPackagesForUid(uid).length <= 0) { - uidState.clear(); - mUidStates.remove(uid); - } - - if (removedOps != null) { - scheduleFastWriteLocked(); - - final int numOps = removedOps.size(); - for (int opNum = 0; opNum < numOps; opNum++) { - final Op op = removedOps.valueAt(opNum); - - final int numAttributions = op.mAttributions.size(); - for (int attributionNum = 0; attributionNum < numAttributions; - attributionNum++) { - AttributedOp attributedOp = op.mAttributions.valueAt(attributionNum); - - while (attributedOp.isRunning()) { - attributedOp.finished(attributedOp.mInProgressEvents.keyAt(0)); - } - while (attributedOp.isPaused()) { - attributedOp.finished(attributedOp.mPausedInProgressEvents.keyAt(0)); - } - } - } - } - } - - mHandler.post(PooledLambda.obtainRunnable(HistoricalRegistry::clearHistory, - mHistoricalRegistry, uid, packageName)); - } - - @Override - public void uidRemoved(int uid) { - synchronized (this) { - if (mUidStates.indexOfKey(uid) >= 0) { - mUidStates.get(uid).clear(); - mUidStates.remove(uid); - scheduleFastWriteLocked(); - } - } - } - - // The callback method from ForegroundPolicyInterface - private void onUidStateChanged(int uid, int state, boolean foregroundModeMayChange) { - synchronized (this) { - UidState uidState = getUidStateLocked(uid, true); - - if (uidState != null && foregroundModeMayChange && uidState.hasForegroundWatchers) { - for (int fgi = uidState.foregroundOps.size() - 1; fgi >= 0; fgi--) { - if (!uidState.foregroundOps.valueAt(fgi)) { - continue; - } - final int code = uidState.foregroundOps.keyAt(fgi); - - if (uidState.getUidMode(code) != AppOpsManager.opToDefaultMode(code) - && uidState.getUidMode(code) == AppOpsManager.MODE_FOREGROUND) { - mHandler.sendMessage(PooledLambda.obtainMessage( - AppOpsServiceImpl::notifyOpChangedForAllPkgsInUid, - this, code, uidState.uid, true, null)); - } else if (uidState.pkgOps != null) { - final ArraySet<OnOpModeChangedListener> listenerSet = - mAppOpsServiceInterface.getOpModeChangedListeners(code); - if (listenerSet != null) { - for (int cbi = listenerSet.size() - 1; cbi >= 0; cbi--) { - final OnOpModeChangedListener listener = listenerSet.valueAt(cbi); - if ((listener.getFlags() - & AppOpsManager.WATCH_FOREGROUND_CHANGES) == 0 - || !listener.isWatchingUid(uidState.uid)) { - continue; - } - for (int pkgi = uidState.pkgOps.size() - 1; pkgi >= 0; pkgi--) { - final Op op = uidState.pkgOps.valueAt(pkgi).get(code); - if (op == null) { - continue; - } - if (op.getMode() == AppOpsManager.MODE_FOREGROUND) { - mHandler.sendMessage(PooledLambda.obtainMessage( - AppOpsServiceImpl::notifyOpChanged, - this, listenerSet.valueAt(cbi), code, uidState.uid, - uidState.pkgOps.keyAt(pkgi))); - } - } - } - } - } - } - } - - if (uidState != null && uidState.pkgOps != null) { - int numPkgs = uidState.pkgOps.size(); - for (int pkgNum = 0; pkgNum < numPkgs; pkgNum++) { - Ops ops = uidState.pkgOps.valueAt(pkgNum); - - int numOps = ops.size(); - for (int opNum = 0; opNum < numOps; opNum++) { - Op op = ops.valueAt(opNum); - - int numAttributions = op.mAttributions.size(); - for (int attributionNum = 0; attributionNum < numAttributions; - attributionNum++) { - AttributedOp attributedOp = op.mAttributions.valueAt( - attributionNum); - - attributedOp.onUidStateChanged(state); - } - } - } - } - } - } - - /** - * Notify the proc state or capability has changed for a certain UID. - */ - @Override - public void updateUidProcState(int uid, int procState, - @ActivityManager.ProcessCapability int capability) { - synchronized (this) { - getUidStateTracker().updateUidProcState(uid, procState, capability); - if (!mUidStates.contains(uid)) { - UidState uidState = new UidState(uid); - mUidStates.put(uid, uidState); - onUidStateChanged(uid, - AppOpsUidStateTracker.processStateToUidState(procState), false); - } - } - } - - @Override - public void shutdown() { - Slog.w(TAG, "Writing app ops before shutdown..."); - boolean doWrite = false; - synchronized (this) { - if (mWriteScheduled) { - mWriteScheduled = false; - mFastWriteScheduled = false; - mHandler.removeCallbacks(mWriteRunner); - doWrite = true; - } - } - if (doWrite) { - writeState(); - } - - mHistoricalRegistry.shutdown(); - } - - private ArrayList<AppOpsManager.OpEntry> collectOps(Ops pkgOps, int[] ops) { - ArrayList<AppOpsManager.OpEntry> resOps = null; - if (ops == null) { - resOps = new ArrayList<>(); - for (int j = 0; j < pkgOps.size(); j++) { - Op curOp = pkgOps.valueAt(j); - resOps.add(getOpEntryForResult(curOp)); - } - } else { - for (int j = 0; j < ops.length; j++) { - Op curOp = pkgOps.get(ops[j]); - if (curOp != null) { - if (resOps == null) { - resOps = new ArrayList<>(); - } - resOps.add(getOpEntryForResult(curOp)); - } - } - } - return resOps; - } - - @Nullable - private ArrayList<AppOpsManager.OpEntry> collectUidOps(@NonNull UidState uidState, - @Nullable int[] ops) { - final SparseIntArray opModes = uidState.getNonDefaultUidModes(); - if (opModes == null) { - return null; - } - - int opModeCount = opModes.size(); - if (opModeCount == 0) { - return null; - } - ArrayList<AppOpsManager.OpEntry> resOps = null; - if (ops == null) { - resOps = new ArrayList<>(); - for (int i = 0; i < opModeCount; i++) { - int code = opModes.keyAt(i); - resOps.add(new OpEntry(code, opModes.get(code), Collections.emptyMap())); - } - } else { - for (int j = 0; j < ops.length; j++) { - int code = ops[j]; - if (opModes.indexOfKey(code) >= 0) { - if (resOps == null) { - resOps = new ArrayList<>(); - } - resOps.add(new OpEntry(code, opModes.get(code), Collections.emptyMap())); - } - } - } - return resOps; - } - - private static @NonNull OpEntry getOpEntryForResult(@NonNull Op op) { - return op.createEntryLocked(); - } - - @Override - public List<AppOpsManager.PackageOps> getPackagesForOps(int[] ops) { - final int callingUid = Binder.getCallingUid(); - final boolean hasAllPackageAccess = mContext.checkPermission( - Manifest.permission.GET_APP_OPS_STATS, Binder.getCallingPid(), - Binder.getCallingUid(), null) == PackageManager.PERMISSION_GRANTED; - ArrayList<AppOpsManager.PackageOps> res = null; - synchronized (this) { - final int uidStateCount = mUidStates.size(); - for (int i = 0; i < uidStateCount; i++) { - UidState uidState = mUidStates.valueAt(i); - if (uidState.pkgOps == null || uidState.pkgOps.isEmpty()) { - continue; - } - ArrayMap<String, Ops> packages = uidState.pkgOps; - final int packageCount = packages.size(); - for (int j = 0; j < packageCount; j++) { - Ops pkgOps = packages.valueAt(j); - ArrayList<AppOpsManager.OpEntry> resOps = collectOps(pkgOps, ops); - if (resOps != null) { - if (res == null) { - res = new ArrayList<>(); - } - AppOpsManager.PackageOps resPackage = new AppOpsManager.PackageOps( - pkgOps.packageName, pkgOps.uidState.uid, resOps); - // Caller can always see their packages and with a permission all. - if (hasAllPackageAccess || callingUid == pkgOps.uidState.uid) { - res.add(resPackage); - } - } - } - } - } - return res; - } - - @Override - public List<AppOpsManager.PackageOps> getOpsForPackage(int uid, String packageName, - int[] ops) { - enforceGetAppOpsStatsPermissionIfNeeded(uid, packageName); - String resolvedPackageName = AppOpsManager.resolvePackageName(uid, packageName); - if (resolvedPackageName == null) { - return Collections.emptyList(); - } - synchronized (this) { - Ops pkgOps = getOpsLocked(uid, resolvedPackageName, null, false, null, - /* edit */ false); - if (pkgOps == null) { - return null; - } - ArrayList<AppOpsManager.OpEntry> resOps = collectOps(pkgOps, ops); - if (resOps == null) { - return null; - } - ArrayList<AppOpsManager.PackageOps> res = new ArrayList<>(); - AppOpsManager.PackageOps resPackage = new AppOpsManager.PackageOps( - pkgOps.packageName, pkgOps.uidState.uid, resOps); - res.add(resPackage); - return res; - } - } - - private void enforceGetAppOpsStatsPermissionIfNeeded(int uid, String packageName) { - final int callingUid = Binder.getCallingUid(); - // We get to access everything - if (callingUid == Process.myPid()) { - return; - } - // Apps can access their own data - if (uid == callingUid && packageName != null - && checkPackage(uid, packageName) == MODE_ALLOWED) { - return; - } - // Otherwise, you need a permission... - mContext.enforcePermission(android.Manifest.permission.GET_APP_OPS_STATS, - Binder.getCallingPid(), callingUid, null); - } - - /** - * Verify that historical appop request arguments are valid. - */ - private void ensureHistoricalOpRequestIsValid(int uid, String packageName, - String attributionTag, List<String> opNames, int filter, long beginTimeMillis, - long endTimeMillis, int flags) { - if ((filter & FILTER_BY_UID) != 0) { - Preconditions.checkArgument(uid != Process.INVALID_UID); - } else { - Preconditions.checkArgument(uid == Process.INVALID_UID); - } - - if ((filter & FILTER_BY_PACKAGE_NAME) != 0) { - Objects.requireNonNull(packageName); - } else { - Preconditions.checkArgument(packageName == null); - } - - if ((filter & FILTER_BY_ATTRIBUTION_TAG) == 0) { - Preconditions.checkArgument(attributionTag == null); - } - - if ((filter & FILTER_BY_OP_NAMES) != 0) { - Objects.requireNonNull(opNames); - } else { - Preconditions.checkArgument(opNames == null); - } - - Preconditions.checkFlagsArgument(filter, - FILTER_BY_UID | FILTER_BY_PACKAGE_NAME | FILTER_BY_ATTRIBUTION_TAG - | FILTER_BY_OP_NAMES); - Preconditions.checkArgumentNonnegative(beginTimeMillis); - Preconditions.checkArgument(endTimeMillis > beginTimeMillis); - Preconditions.checkFlagsArgument(flags, OP_FLAGS_ALL); - } - - @Override - public void getHistoricalOps(int uid, String packageName, String attributionTag, - List<String> opNames, int dataType, int filter, long beginTimeMillis, - long endTimeMillis, int flags, RemoteCallback callback) { - PackageManager pm = mContext.getPackageManager(); - - ensureHistoricalOpRequestIsValid(uid, packageName, attributionTag, opNames, filter, - beginTimeMillis, endTimeMillis, flags); - Objects.requireNonNull(callback, "callback cannot be null"); - ActivityManagerInternal ami = LocalServices.getService(ActivityManagerInternal.class); - boolean isSelfRequest = (filter & FILTER_BY_UID) != 0 && uid == Binder.getCallingUid(); - if (!isSelfRequest) { - boolean isCallerInstrumented = - ami.getInstrumentationSourceUid(Binder.getCallingUid()) != Process.INVALID_UID; - boolean isCallerSystem = Binder.getCallingPid() == Process.myPid(); - boolean isCallerPermissionController; - try { - isCallerPermissionController = pm.getPackageUidAsUser( - mContext.getPackageManager().getPermissionControllerPackageName(), 0, - UserHandle.getUserId(Binder.getCallingUid())) - == Binder.getCallingUid(); - } catch (PackageManager.NameNotFoundException doesNotHappen) { - return; - } - - boolean doesCallerHavePermission = mContext.checkPermission( - android.Manifest.permission.GET_HISTORICAL_APP_OPS_STATS, - Binder.getCallingPid(), Binder.getCallingUid()) - == PackageManager.PERMISSION_GRANTED; - - if (!isCallerSystem && !isCallerInstrumented && !isCallerPermissionController - && !doesCallerHavePermission) { - mHandler.post(() -> callback.sendResult(new Bundle())); - return; - } - - mContext.enforcePermission(android.Manifest.permission.GET_APP_OPS_STATS, - Binder.getCallingPid(), Binder.getCallingUid(), "getHistoricalOps"); - } - - final String[] opNamesArray = (opNames != null) - ? opNames.toArray(new String[opNames.size()]) : null; - - Set<String> attributionChainExemptPackages = null; - if ((dataType & HISTORY_FLAG_GET_ATTRIBUTION_CHAINS) != 0) { - attributionChainExemptPackages = - PermissionManager.getIndicatorExemptedPackages(mContext); - } - - final String[] chainExemptPkgArray = attributionChainExemptPackages != null - ? attributionChainExemptPackages.toArray( - new String[attributionChainExemptPackages.size()]) : null; - - // Must not hold the appops lock - mHandler.post(PooledLambda.obtainRunnable(HistoricalRegistry::getHistoricalOps, - mHistoricalRegistry, uid, packageName, attributionTag, opNamesArray, dataType, - filter, beginTimeMillis, endTimeMillis, flags, chainExemptPkgArray, - callback).recycleOnUse()); - } - - @Override - public void getHistoricalOpsFromDiskRaw(int uid, String packageName, String attributionTag, - List<String> opNames, int dataType, int filter, long beginTimeMillis, - long endTimeMillis, int flags, RemoteCallback callback) { - ensureHistoricalOpRequestIsValid(uid, packageName, attributionTag, opNames, filter, - beginTimeMillis, endTimeMillis, flags); - Objects.requireNonNull(callback, "callback cannot be null"); - - mContext.enforcePermission(Manifest.permission.MANAGE_APPOPS, - Binder.getCallingPid(), Binder.getCallingUid(), "getHistoricalOps"); - - final String[] opNamesArray = (opNames != null) - ? opNames.toArray(new String[opNames.size()]) : null; - - Set<String> attributionChainExemptPackages = null; - if ((dataType & HISTORY_FLAG_GET_ATTRIBUTION_CHAINS) != 0) { - attributionChainExemptPackages = - PermissionManager.getIndicatorExemptedPackages(mContext); - } - - final String[] chainExemptPkgArray = attributionChainExemptPackages != null - ? attributionChainExemptPackages.toArray( - new String[attributionChainExemptPackages.size()]) : null; - - // Must not hold the appops lock - mHandler.post(PooledLambda.obtainRunnable(HistoricalRegistry::getHistoricalOpsFromDiskRaw, - mHistoricalRegistry, uid, packageName, attributionTag, opNamesArray, dataType, - filter, beginTimeMillis, endTimeMillis, flags, chainExemptPkgArray, - callback).recycleOnUse()); - } - - @Override - public void reloadNonHistoricalState() { - mContext.enforcePermission(Manifest.permission.MANAGE_APPOPS, - Binder.getCallingPid(), Binder.getCallingUid(), "reloadNonHistoricalState"); - writeState(); - readState(); - } - - @Override - public List<AppOpsManager.PackageOps> getUidOps(int uid, int[] ops) { - mContext.enforcePermission(android.Manifest.permission.GET_APP_OPS_STATS, - Binder.getCallingPid(), Binder.getCallingUid(), null); - synchronized (this) { - UidState uidState = getUidStateLocked(uid, false); - if (uidState == null) { - return null; - } - ArrayList<AppOpsManager.OpEntry> resOps = collectUidOps(uidState, ops); - if (resOps == null) { - return null; - } - ArrayList<AppOpsManager.PackageOps> res = new ArrayList<AppOpsManager.PackageOps>(); - AppOpsManager.PackageOps resPackage = new AppOpsManager.PackageOps( - null, uidState.uid, resOps); - res.add(resPackage); - return res; - } - } - - private void pruneOpLocked(Op op, int uid, String packageName) { - op.removeAttributionsWithNoTime(); - - if (op.mAttributions.isEmpty()) { - Ops ops = getOpsLocked(uid, packageName, null, false, null, /* edit */ false); - if (ops != null) { - ops.remove(op.op); - op.setMode(AppOpsManager.opToDefaultMode(op.op)); - if (ops.size() <= 0) { - UidState uidState = ops.uidState; - ArrayMap<String, Ops> pkgOps = uidState.pkgOps; - if (pkgOps != null) { - pkgOps.remove(ops.packageName); - mAppOpsServiceInterface.removePackage(ops.packageName, - UserHandle.getUserId(uidState.uid)); - if (pkgOps.isEmpty()) { - uidState.pkgOps = null; - } - if (uidState.isDefault()) { - uidState.clear(); - mUidStates.remove(uid); - } - } - } - } - } - } - - @Override - public void enforceManageAppOpsModes(int callingPid, int callingUid, int targetUid) { - if (callingPid == Process.myPid()) { - return; - } - final int callingUser = UserHandle.getUserId(callingUid); - synchronized (this) { - if (mProfileOwners != null && mProfileOwners.get(callingUser, -1) == callingUid) { - if (targetUid >= 0 && callingUser == UserHandle.getUserId(targetUid)) { - // Profile owners are allowed to change modes but only for apps - // within their user. - return; - } - } - } - mContext.enforcePermission(android.Manifest.permission.MANAGE_APP_OPS_MODES, - Binder.getCallingPid(), Binder.getCallingUid(), null); - } - - @Override - public void setUidMode(int code, int uid, int mode, - @Nullable IAppOpsCallback permissionPolicyCallback) { - if (DEBUG) { - Slog.i(TAG, "uid " + uid + " OP_" + opToName(code) + " := " + modeToName(mode) - + " by uid " + Binder.getCallingUid()); - } - - enforceManageAppOpsModes(Binder.getCallingPid(), Binder.getCallingUid(), uid); - verifyIncomingOp(code); - code = AppOpsManager.opToSwitch(code); - - if (permissionPolicyCallback == null) { - updatePermissionRevokedCompat(uid, code, mode); - } - - int previousMode; - synchronized (this) { - final int defaultMode = AppOpsManager.opToDefaultMode(code); - - UidState uidState = getUidStateLocked(uid, false); - if (uidState == null) { - if (mode == defaultMode) { - return; - } - uidState = new UidState(uid); - mUidStates.put(uid, uidState); - } - if (uidState.getUidMode(code) != AppOpsManager.opToDefaultMode(code)) { - previousMode = uidState.getUidMode(code); - } else { - // doesn't look right but is legacy behavior. - previousMode = MODE_DEFAULT; - } - - if (!uidState.setUidMode(code, mode)) { - return; - } - uidState.evalForegroundOps(); - if (mode != MODE_ERRORED && mode != previousMode) { - updateStartedOpModeForUidLocked(code, mode == MODE_IGNORED, uid); - } - } - - notifyOpChangedForAllPkgsInUid(code, uid, false, permissionPolicyCallback); - notifyOpChangedSync(code, uid, null, mode, previousMode); - } - - /** - * Notify that an op changed for all packages in an uid. - * - * @param code The op that changed - * @param uid The uid the op was changed for - * @param onlyForeground Only notify watchers that watch for foreground changes - */ - private void notifyOpChangedForAllPkgsInUid(int code, int uid, boolean onlyForeground, - @Nullable IAppOpsCallback callbackToIgnore) { - ModeCallback listenerToIgnore = callbackToIgnore != null - ? mModeWatchers.get(callbackToIgnore.asBinder()) : null; - mAppOpsServiceInterface.notifyOpChangedForAllPkgsInUid(code, uid, onlyForeground, - listenerToIgnore); - } - - private void updatePermissionRevokedCompat(int uid, int switchCode, int mode) { - PackageManager packageManager = mContext.getPackageManager(); - if (packageManager == null) { - // This can only happen during early boot. At this time the permission state and appop - // state are in sync - return; - } - - String[] packageNames = packageManager.getPackagesForUid(uid); - if (ArrayUtils.isEmpty(packageNames)) { - return; - } - String packageName = packageNames[0]; - - int[] ops = mSwitchedOps.get(switchCode); - for (int code : ops) { - String permissionName = AppOpsManager.opToPermission(code); - if (permissionName == null) { - continue; - } - - if (packageManager.checkPermission(permissionName, packageName) - != PackageManager.PERMISSION_GRANTED) { - continue; - } - - PermissionInfo permissionInfo; - try { - permissionInfo = packageManager.getPermissionInfo(permissionName, 0); - } catch (PackageManager.NameNotFoundException e) { - e.printStackTrace(); - continue; - } - - if (!permissionInfo.isRuntime()) { - continue; - } - - boolean supportsRuntimePermissions = getPackageManagerInternal() - .getUidTargetSdkVersion(uid) >= Build.VERSION_CODES.M; - - UserHandle user = UserHandle.getUserHandleForUid(uid); - boolean isRevokedCompat; - if (permissionInfo.backgroundPermission != null) { - if (packageManager.checkPermission(permissionInfo.backgroundPermission, packageName) - == PackageManager.PERMISSION_GRANTED) { - boolean isBackgroundRevokedCompat = mode != AppOpsManager.MODE_ALLOWED; - - if (isBackgroundRevokedCompat && supportsRuntimePermissions) { - Slog.w(TAG, "setUidMode() called with a mode inconsistent with runtime" - + " permission state, this is discouraged and you should revoke the" - + " runtime permission instead: uid=" + uid + ", switchCode=" - + switchCode + ", mode=" + mode + ", permission=" - + permissionInfo.backgroundPermission); - } - - final long identity = Binder.clearCallingIdentity(); - try { - packageManager.updatePermissionFlags(permissionInfo.backgroundPermission, - packageName, PackageManager.FLAG_PERMISSION_REVOKED_COMPAT, - isBackgroundRevokedCompat - ? PackageManager.FLAG_PERMISSION_REVOKED_COMPAT : 0, user); - } finally { - Binder.restoreCallingIdentity(identity); - } - } - - isRevokedCompat = mode != AppOpsManager.MODE_ALLOWED - && mode != AppOpsManager.MODE_FOREGROUND; - } else { - isRevokedCompat = mode != AppOpsManager.MODE_ALLOWED; - } - - if (isRevokedCompat && supportsRuntimePermissions) { - Slog.w(TAG, "setUidMode() called with a mode inconsistent with runtime" - + " permission state, this is discouraged and you should revoke the" - + " runtime permission instead: uid=" + uid + ", switchCode=" - + switchCode + ", mode=" + mode + ", permission=" + permissionName); - } - - final long identity = Binder.clearCallingIdentity(); - try { - packageManager.updatePermissionFlags(permissionName, packageName, - PackageManager.FLAG_PERMISSION_REVOKED_COMPAT, isRevokedCompat - ? PackageManager.FLAG_PERMISSION_REVOKED_COMPAT : 0, user); - } finally { - Binder.restoreCallingIdentity(identity); - } - } - } - - private void notifyOpChangedSync(int code, int uid, @NonNull String packageName, int mode, - int previousMode) { - final StorageManagerInternal storageManagerInternal = - LocalServices.getService(StorageManagerInternal.class); - if (storageManagerInternal != null) { - storageManagerInternal.onAppOpsChanged(code, uid, packageName, mode, previousMode); - } - } - - @Override - public void setMode(int code, int uid, @NonNull String packageName, int mode, - @Nullable IAppOpsCallback permissionPolicyCallback) { - enforceManageAppOpsModes(Binder.getCallingPid(), Binder.getCallingUid(), uid); - verifyIncomingOp(code); - if (!isIncomingPackageValid(packageName, UserHandle.getUserId(uid))) { - return; - } - - ArraySet<OnOpModeChangedListener> repCbs = null; - code = AppOpsManager.opToSwitch(code); - - PackageVerificationResult pvr; - try { - pvr = verifyAndGetBypass(uid, packageName, null); - } catch (SecurityException e) { - Slog.e(TAG, "Cannot setMode", e); - return; - } - - int previousMode = MODE_DEFAULT; - synchronized (this) { - UidState uidState = getUidStateLocked(uid, false); - Op op = getOpLocked(code, uid, packageName, null, false, pvr.bypass, /* edit */ true); - if (op != null) { - if (op.getMode() != mode) { - previousMode = op.getMode(); - op.setMode(mode); - - if (uidState != null) { - uidState.evalForegroundOps(); - } - ArraySet<OnOpModeChangedListener> cbs = - mAppOpsServiceInterface.getOpModeChangedListeners(code); - if (cbs != null) { - if (repCbs == null) { - repCbs = new ArraySet<>(); - } - repCbs.addAll(cbs); - } - cbs = mAppOpsServiceInterface.getPackageModeChangedListeners(packageName); - if (cbs != null) { - if (repCbs == null) { - repCbs = new ArraySet<>(); - } - repCbs.addAll(cbs); - } - if (repCbs != null && permissionPolicyCallback != null) { - repCbs.remove(mModeWatchers.get(permissionPolicyCallback.asBinder())); - } - if (mode == AppOpsManager.opToDefaultMode(op.op)) { - // If going into the default mode, prune this op - // if there is nothing else interesting in it. - pruneOpLocked(op, uid, packageName); - } - scheduleFastWriteLocked(); - if (mode != MODE_ERRORED) { - updateStartedOpModeForUidLocked(code, mode == MODE_IGNORED, uid); - } - } - } - } - if (repCbs != null) { - mHandler.sendMessage(PooledLambda.obtainMessage( - AppOpsServiceImpl::notifyOpChanged, - this, repCbs, code, uid, packageName)); - } - - notifyOpChangedSync(code, uid, packageName, mode, previousMode); - } - - private void notifyOpChanged(ArraySet<OnOpModeChangedListener> callbacks, int code, - int uid, String packageName) { - for (int i = 0; i < callbacks.size(); i++) { - final OnOpModeChangedListener callback = callbacks.valueAt(i); - notifyOpChanged(callback, code, uid, packageName); - } - } - - private void notifyOpChanged(OnOpModeChangedListener callback, int code, - int uid, String packageName) { - mAppOpsServiceInterface.notifyOpChanged(callback, code, uid, packageName); - } - - private static ArrayList<ChangeRec> addChange(ArrayList<ChangeRec> reports, - int op, int uid, String packageName, int previousMode) { - boolean duplicate = false; - if (reports == null) { - reports = new ArrayList<>(); - } else { - final int reportCount = reports.size(); - for (int j = 0; j < reportCount; j++) { - ChangeRec report = reports.get(j); - if (report.op == op && report.pkg.equals(packageName)) { - duplicate = true; - break; - } - } - } - if (!duplicate) { - reports.add(new ChangeRec(op, uid, packageName, previousMode)); - } - - return reports; - } - - private static HashMap<OnOpModeChangedListener, ArrayList<ChangeRec>> addCallbacks( - HashMap<OnOpModeChangedListener, ArrayList<ChangeRec>> callbacks, - int op, int uid, String packageName, int previousMode, - ArraySet<OnOpModeChangedListener> cbs) { - if (cbs == null) { - return callbacks; - } - if (callbacks == null) { - callbacks = new HashMap<>(); - } - final int N = cbs.size(); - for (int i=0; i<N; i++) { - OnOpModeChangedListener cb = cbs.valueAt(i); - ArrayList<ChangeRec> reports = callbacks.get(cb); - ArrayList<ChangeRec> changed = addChange(reports, op, uid, packageName, previousMode); - if (changed != reports) { - callbacks.put(cb, changed); - } - } - return callbacks; - } - - static final class ChangeRec { - final int op; - final int uid; - final String pkg; - final int previous_mode; - - ChangeRec(int _op, int _uid, String _pkg, int _previous_mode) { - op = _op; - uid = _uid; - pkg = _pkg; - previous_mode = _previous_mode; - } - } - - @Override - public void resetAllModes(int reqUserId, String reqPackageName) { - final int callingPid = Binder.getCallingPid(); - final int callingUid = Binder.getCallingUid(); - reqUserId = ActivityManager.handleIncomingUser(callingPid, callingUid, reqUserId, - true, true, "resetAllModes", null); - - int reqUid = -1; - if (reqPackageName != null) { - try { - reqUid = AppGlobals.getPackageManager().getPackageUid( - reqPackageName, PackageManager.MATCH_UNINSTALLED_PACKAGES, reqUserId); - } catch (RemoteException e) { - /* ignore - local call */ - } - } - - enforceManageAppOpsModes(callingPid, callingUid, reqUid); - - HashMap<OnOpModeChangedListener, ArrayList<ChangeRec>> callbacks = null; - ArrayList<ChangeRec> allChanges = new ArrayList<>(); - synchronized (this) { - boolean changed = false; - for (int i = mUidStates.size() - 1; i >= 0; i--) { - UidState uidState = mUidStates.valueAt(i); - - SparseIntArray opModes = uidState.getNonDefaultUidModes(); - if (opModes != null && (uidState.uid == reqUid || reqUid == -1)) { - final int uidOpCount = opModes.size(); - for (int j = uidOpCount - 1; j >= 0; j--) { - final int code = opModes.keyAt(j); - if (AppOpsManager.opAllowsReset(code)) { - int previousMode = opModes.valueAt(j); - uidState.setUidMode(code, AppOpsManager.opToDefaultMode(code)); - for (String packageName : getPackagesForUid(uidState.uid)) { - callbacks = addCallbacks(callbacks, code, uidState.uid, - packageName, previousMode, - mAppOpsServiceInterface.getOpModeChangedListeners(code)); - callbacks = addCallbacks(callbacks, code, uidState.uid, - packageName, previousMode, mAppOpsServiceInterface - .getPackageModeChangedListeners(packageName)); - - allChanges = addChange(allChanges, code, uidState.uid, - packageName, previousMode); - } - } - } - } - - if (uidState.pkgOps == null) { - continue; - } - - if (reqUserId != UserHandle.USER_ALL - && reqUserId != UserHandle.getUserId(uidState.uid)) { - // Skip any ops for a different user - continue; - } - - Map<String, Ops> packages = uidState.pkgOps; - Iterator<Map.Entry<String, Ops>> it = packages.entrySet().iterator(); - boolean uidChanged = false; - while (it.hasNext()) { - Map.Entry<String, Ops> ent = it.next(); - String packageName = ent.getKey(); - if (reqPackageName != null && !reqPackageName.equals(packageName)) { - // Skip any ops for a different package - continue; - } - Ops pkgOps = ent.getValue(); - for (int j=pkgOps.size()-1; j>=0; j--) { - Op curOp = pkgOps.valueAt(j); - if (shouldDeferResetOpToDpm(curOp.op)) { - deferResetOpToDpm(curOp.op, reqPackageName, reqUserId); - continue; - } - if (AppOpsManager.opAllowsReset(curOp.op) - && curOp.getMode() != AppOpsManager.opToDefaultMode(curOp.op)) { - int previousMode = curOp.getMode(); - curOp.setMode(AppOpsManager.opToDefaultMode(curOp.op)); - changed = true; - uidChanged = true; - final int uid = curOp.uidState.uid; - callbacks = addCallbacks(callbacks, curOp.op, uid, packageName, - previousMode, - mAppOpsServiceInterface.getOpModeChangedListeners(curOp.op)); - callbacks = addCallbacks(callbacks, curOp.op, uid, packageName, - previousMode, mAppOpsServiceInterface - .getPackageModeChangedListeners(packageName)); - - allChanges = addChange(allChanges, curOp.op, uid, packageName, - previousMode); - curOp.removeAttributionsWithNoTime(); - if (curOp.mAttributions.isEmpty()) { - pkgOps.removeAt(j); - } - } - } - if (pkgOps.size() == 0) { - it.remove(); - mAppOpsServiceInterface.removePackage(packageName, - UserHandle.getUserId(uidState.uid)); - } - } - if (uidState.isDefault()) { - uidState.clear(); - mUidStates.remove(uidState.uid); - } - if (uidChanged) { - uidState.evalForegroundOps(); - } - } - - if (changed) { - scheduleFastWriteLocked(); - } - } - if (callbacks != null) { - for (Map.Entry<OnOpModeChangedListener, ArrayList<ChangeRec>> ent - : callbacks.entrySet()) { - OnOpModeChangedListener cb = ent.getKey(); - ArrayList<ChangeRec> reports = ent.getValue(); - for (int i=0; i<reports.size(); i++) { - ChangeRec rep = reports.get(i); - mHandler.sendMessage(PooledLambda.obtainMessage( - AppOpsServiceImpl::notifyOpChanged, - this, cb, rep.op, rep.uid, rep.pkg)); - } - } - } - - int numChanges = allChanges.size(); - for (int i = 0; i < numChanges; i++) { - ChangeRec change = allChanges.get(i); - notifyOpChangedSync(change.op, change.uid, change.pkg, - AppOpsManager.opToDefaultMode(change.op), change.previous_mode); - } - } - - private boolean shouldDeferResetOpToDpm(int op) { - // TODO(b/174582385): avoid special-casing app-op resets by migrating app-op permission - // pre-grants to a role-based mechanism or another general-purpose mechanism. - return dpmi != null && dpmi.supportsResetOp(op); - } - - /** Assumes {@link #shouldDeferResetOpToDpm(int)} is true. */ - private void deferResetOpToDpm(int op, String packageName, @UserIdInt int userId) { - // TODO(b/174582385): avoid special-casing app-op resets by migrating app-op permission - // pre-grants to a role-based mechanism or another general-purpose mechanism. - dpmi.resetOp(op, packageName, userId); - } - - private void evalAllForegroundOpsLocked() { - for (int uidi = mUidStates.size() - 1; uidi >= 0; uidi--) { - final UidState uidState = mUidStates.valueAt(uidi); - if (uidState.foregroundOps != null) { - uidState.evalForegroundOps(); - } - } - } - - @Override - public void startWatchingModeWithFlags(int op, String packageName, int flags, - IAppOpsCallback callback) { - int watchedUid = -1; - final int callingUid = Binder.getCallingUid(); - final int callingPid = Binder.getCallingPid(); - // TODO: should have a privileged permission to protect this. - // Also, if the caller has requested WATCH_FOREGROUND_CHANGES, should we require - // the USAGE_STATS permission since this can provide information about when an - // app is in the foreground? - Preconditions.checkArgumentInRange(op, AppOpsManager.OP_NONE, - AppOpsManager._NUM_OP - 1, "Invalid op code: " + op); - if (callback == null) { - return; - } - final boolean mayWatchPackageName = packageName != null - && !filterAppAccessUnlocked(packageName, UserHandle.getUserId(callingUid)); - synchronized (this) { - int switchOp = (op != AppOpsManager.OP_NONE) ? AppOpsManager.opToSwitch(op) : op; - - int notifiedOps; - if ((flags & CALL_BACK_ON_SWITCHED_OP) == 0) { - if (op == OP_NONE) { - notifiedOps = ALL_OPS; - } else { - notifiedOps = op; - } - } else { - notifiedOps = switchOp; - } - - ModeCallback cb = mModeWatchers.get(callback.asBinder()); - if (cb == null) { - cb = new ModeCallback(callback, watchedUid, flags, notifiedOps, callingUid, - callingPid); - mModeWatchers.put(callback.asBinder(), cb); - } - if (switchOp != AppOpsManager.OP_NONE) { - mAppOpsServiceInterface.startWatchingOpModeChanged(cb, switchOp); - } - if (mayWatchPackageName) { - mAppOpsServiceInterface.startWatchingPackageModeChanged(cb, packageName); - } - evalAllForegroundOpsLocked(); - } - } - - @Override - public void stopWatchingMode(IAppOpsCallback callback) { - if (callback == null) { - return; - } - synchronized (this) { - ModeCallback cb = mModeWatchers.remove(callback.asBinder()); - if (cb != null) { - cb.unlinkToDeath(); - mAppOpsServiceInterface.removeListener(cb); - } - - evalAllForegroundOpsLocked(); - } - } - - @Override - public int checkOperation(int code, int uid, String packageName, - @Nullable String attributionTag, boolean raw) { - verifyIncomingOp(code); - if (!isIncomingPackageValid(packageName, UserHandle.getUserId(uid))) { - return AppOpsManager.opToDefaultMode(code); - } - - String resolvedPackageName = AppOpsManager.resolvePackageName(uid, packageName); - if (resolvedPackageName == null) { - return AppOpsManager.MODE_IGNORED; - } - return checkOperationUnchecked(code, uid, resolvedPackageName, attributionTag, raw); - } - - /** - * Get the mode of an app-op. - * - * @param code The code of the op - * @param uid The uid of the package the op belongs to - * @param packageName The package the op belongs to - * @param raw If the raw state of eval-ed state should be checked. - * @return The mode of the op - */ - private @Mode int checkOperationUnchecked(int code, int uid, @NonNull String packageName, - @Nullable String attributionTag, boolean raw) { - PackageVerificationResult pvr; - try { - pvr = verifyAndGetBypass(uid, packageName, null); - } catch (SecurityException e) { - Slog.e(TAG, "checkOperation", e); - return AppOpsManager.opToDefaultMode(code); - } - - if (isOpRestrictedDueToSuspend(code, packageName, uid)) { - return AppOpsManager.MODE_IGNORED; - } - synchronized (this) { - if (isOpRestrictedLocked(uid, code, packageName, attributionTag, pvr.bypass, true)) { - return AppOpsManager.MODE_IGNORED; - } - code = AppOpsManager.opToSwitch(code); - UidState uidState = getUidStateLocked(uid, false); - if (uidState != null - && uidState.getUidMode(code) != AppOpsManager.opToDefaultMode(code)) { - final int rawMode = uidState.getUidMode(code); - return raw ? rawMode : uidState.evalMode(code, rawMode); - } - Op op = getOpLocked(code, uid, packageName, null, false, pvr.bypass, /* edit */ false); - if (op == null) { - return AppOpsManager.opToDefaultMode(code); - } - return raw ? op.getMode() : op.uidState.evalMode(op.op, op.getMode()); - } - } - - @Override - public int checkPackage(int uid, String packageName) { - Objects.requireNonNull(packageName); - try { - verifyAndGetBypass(uid, packageName, null); - // When the caller is the system, it's possible that the packageName is the special - // one (e.g., "root") which isn't actually existed. - if (resolveUid(packageName) == uid - || (isPackageExisted(packageName) - && !filterAppAccessUnlocked(packageName, UserHandle.getUserId(uid)))) { - return AppOpsManager.MODE_ALLOWED; - } - return AppOpsManager.MODE_ERRORED; - } catch (SecurityException ignored) { - return AppOpsManager.MODE_ERRORED; - } - } - - private boolean isPackageExisted(String packageName) { - return getPackageManagerInternal().getPackageStateInternal(packageName) != null; - } - - /** - * This method will check with PackageManager to determine if the package provided should - * be visible to the {@link Binder#getCallingUid()}. - * - * NOTE: This must not be called while synchronized on {@code this} to avoid dead locks - */ - private boolean filterAppAccessUnlocked(String packageName, int userId) { - final int callingUid = Binder.getCallingUid(); - return LocalServices.getService(PackageManagerInternal.class) - .filterAppAccess(packageName, callingUid, userId); - } - - @Override - public int noteOperation(int code, int uid, @Nullable String packageName, - @Nullable String attributionTag, @Nullable String message) { - verifyIncomingUid(uid); - verifyIncomingOp(code); - if (!isIncomingPackageValid(packageName, UserHandle.getUserId(uid))) { - return AppOpsManager.MODE_ERRORED; - } - - String resolvedPackageName = AppOpsManager.resolvePackageName(uid, packageName); - if (resolvedPackageName == null) { - return AppOpsManager.MODE_IGNORED; - } - return noteOperationUnchecked(code, uid, resolvedPackageName, attributionTag, - Process.INVALID_UID, null, null, AppOpsManager.OP_FLAG_SELF); - } - - @Override - public int noteOperationUnchecked(int code, int uid, @NonNull String packageName, - @Nullable String attributionTag, int proxyUid, String proxyPackageName, - @Nullable String proxyAttributionTag, @OpFlags int flags) { - PackageVerificationResult pvr; - try { - pvr = verifyAndGetBypass(uid, packageName, attributionTag, proxyPackageName); - if (!pvr.isAttributionTagValid) { - attributionTag = null; - } - } catch (SecurityException e) { - Slog.e(TAG, "noteOperation", e); - return AppOpsManager.MODE_ERRORED; - } - - synchronized (this) { - final Ops ops = getOpsLocked(uid, packageName, attributionTag, - pvr.isAttributionTagValid, pvr.bypass, /* edit */ true); - if (ops == null) { - scheduleOpNotedIfNeededLocked(code, uid, packageName, attributionTag, flags, - AppOpsManager.MODE_IGNORED); - if (DEBUG) { - Slog.d(TAG, "noteOperation: no op for code " + code + " uid " + uid - + " package " + packageName + "flags: " - + AppOpsManager.flagsToString(flags)); - } - return AppOpsManager.MODE_ERRORED; - } - final Op op = getOpLocked(ops, code, uid, true); - final AttributedOp attributedOp = op.getOrCreateAttribution(op, attributionTag); - if (attributedOp.isRunning()) { - Slog.w(TAG, "Noting op not finished: uid " + uid + " pkg " + packageName + " code " - + code + " startTime of in progress event=" - + attributedOp.mInProgressEvents.valueAt(0).getStartTime()); - } - - final int switchCode = AppOpsManager.opToSwitch(code); - final UidState uidState = ops.uidState; - if (isOpRestrictedLocked(uid, code, packageName, attributionTag, pvr.bypass, false)) { - attributedOp.rejected(uidState.getState(), flags); - scheduleOpNotedIfNeededLocked(code, uid, packageName, attributionTag, flags, - AppOpsManager.MODE_IGNORED); - return AppOpsManager.MODE_IGNORED; - } - // If there is a non-default per UID policy (we set UID op mode only if - // non-default) it takes over, otherwise use the per package policy. - if (uidState.getUidMode(switchCode) != AppOpsManager.opToDefaultMode(switchCode)) { - final int uidMode = uidState.evalMode(code, uidState.getUidMode(switchCode)); - if (uidMode != AppOpsManager.MODE_ALLOWED) { - if (DEBUG) { - Slog.d(TAG, "noteOperation: uid reject #" + uidMode + " for code " - + switchCode + " (" + code + ") uid " + uid + " package " - + packageName + " flags: " + AppOpsManager.flagsToString(flags)); - } - attributedOp.rejected(uidState.getState(), flags); - scheduleOpNotedIfNeededLocked(code, uid, packageName, attributionTag, flags, - uidMode); - return uidMode; - } - } else { - final Op switchOp = switchCode != code ? getOpLocked(ops, switchCode, uid, true) - : op; - final int mode = switchOp.uidState.evalMode(switchOp.op, switchOp.getMode()); - if (mode != AppOpsManager.MODE_ALLOWED) { - if (DEBUG) { - Slog.d(TAG, "noteOperation: reject #" + mode + " for code " - + switchCode + " (" + code + ") uid " + uid + " package " - + packageName + " flags: " + AppOpsManager.flagsToString(flags)); - } - attributedOp.rejected(uidState.getState(), flags); - scheduleOpNotedIfNeededLocked(code, uid, packageName, attributionTag, flags, - mode); - return mode; - } - } - if (DEBUG) { - Slog.d(TAG, - "noteOperation: allowing code " + code + " uid " + uid + " package " - + packageName + (attributionTag == null ? "" - : "." + attributionTag) + " flags: " - + AppOpsManager.flagsToString(flags)); - } - scheduleOpNotedIfNeededLocked(code, uid, packageName, attributionTag, flags, - AppOpsManager.MODE_ALLOWED); - attributedOp.accessed(proxyUid, proxyPackageName, proxyAttributionTag, - uidState.getState(), - flags); - - return AppOpsManager.MODE_ALLOWED; - } - } - - @Override - public boolean isAttributionTagValid(int uid, @NonNull String packageName, - @Nullable String attributionTag, - @Nullable String proxyPackageName) { - try { - return verifyAndGetBypass(uid, packageName, attributionTag, proxyPackageName) - .isAttributionTagValid; - } catch (SecurityException ignored) { - // We don't want to throw, this exception will be handled in the (c/n/s)Operation calls - // when they need the bypass object. - return false; - } - } - - // TODO moltmann: Allow watching for attribution ops - @Override - public void startWatchingActive(int[] ops, IAppOpsActiveCallback callback) { - int watchedUid = Process.INVALID_UID; - final int callingUid = Binder.getCallingUid(); - final int callingPid = Binder.getCallingPid(); - if (mContext.checkCallingOrSelfPermission(Manifest.permission.WATCH_APPOPS) - != PackageManager.PERMISSION_GRANTED) { - watchedUid = callingUid; - } - if (ops != null) { - Preconditions.checkArrayElementsInRange(ops, 0, - AppOpsManager._NUM_OP - 1, "Invalid op code in: " + Arrays.toString(ops)); - } - if (callback == null) { - return; - } - synchronized (this) { - SparseArray<ActiveCallback> callbacks = mActiveWatchers.get(callback.asBinder()); - if (callbacks == null) { - callbacks = new SparseArray<>(); - mActiveWatchers.put(callback.asBinder(), callbacks); - } - final ActiveCallback activeCallback = new ActiveCallback(callback, watchedUid, - callingUid, callingPid); - for (int op : ops) { - callbacks.put(op, activeCallback); - } - } - } - - @Override - public void stopWatchingActive(IAppOpsActiveCallback callback) { - if (callback == null) { - return; - } - synchronized (this) { - final SparseArray<ActiveCallback> activeCallbacks = - mActiveWatchers.remove(callback.asBinder()); - if (activeCallbacks == null) { - return; - } - final int callbackCount = activeCallbacks.size(); - for (int i = 0; i < callbackCount; i++) { - activeCallbacks.valueAt(i).destroy(); - } - } - } - - @Override - public void startWatchingStarted(int[] ops, @NonNull IAppOpsStartedCallback callback) { - int watchedUid = Process.INVALID_UID; - final int callingUid = Binder.getCallingUid(); - final int callingPid = Binder.getCallingPid(); - if (mContext.checkCallingOrSelfPermission(Manifest.permission.WATCH_APPOPS) - != PackageManager.PERMISSION_GRANTED) { - watchedUid = callingUid; - } - - Preconditions.checkArgument(!ArrayUtils.isEmpty(ops), "Ops cannot be null or empty"); - Preconditions.checkArrayElementsInRange(ops, 0, AppOpsManager._NUM_OP - 1, - "Invalid op code in: " + Arrays.toString(ops)); - Objects.requireNonNull(callback, "Callback cannot be null"); - - synchronized (this) { - SparseArray<StartedCallback> callbacks = mStartedWatchers.get(callback.asBinder()); - if (callbacks == null) { - callbacks = new SparseArray<>(); - mStartedWatchers.put(callback.asBinder(), callbacks); - } - - final StartedCallback startedCallback = new StartedCallback(callback, watchedUid, - callingUid, callingPid); - for (int op : ops) { - callbacks.put(op, startedCallback); - } - } - } - - @Override - public void stopWatchingStarted(IAppOpsStartedCallback callback) { - Objects.requireNonNull(callback, "Callback cannot be null"); - - synchronized (this) { - final SparseArray<StartedCallback> startedCallbacks = - mStartedWatchers.remove(callback.asBinder()); - if (startedCallbacks == null) { - return; - } - - final int callbackCount = startedCallbacks.size(); - for (int i = 0; i < callbackCount; i++) { - startedCallbacks.valueAt(i).destroy(); - } - } - } - - @Override - public void startWatchingNoted(@NonNull int[] ops, @NonNull IAppOpsNotedCallback callback) { - int watchedUid = Process.INVALID_UID; - final int callingUid = Binder.getCallingUid(); - final int callingPid = Binder.getCallingPid(); - if (mContext.checkCallingOrSelfPermission(Manifest.permission.WATCH_APPOPS) - != PackageManager.PERMISSION_GRANTED) { - watchedUid = callingUid; - } - Preconditions.checkArgument(!ArrayUtils.isEmpty(ops), "Ops cannot be null or empty"); - Preconditions.checkArrayElementsInRange(ops, 0, AppOpsManager._NUM_OP - 1, - "Invalid op code in: " + Arrays.toString(ops)); - Objects.requireNonNull(callback, "Callback cannot be null"); - synchronized (this) { - SparseArray<NotedCallback> callbacks = mNotedWatchers.get(callback.asBinder()); - if (callbacks == null) { - callbacks = new SparseArray<>(); - mNotedWatchers.put(callback.asBinder(), callbacks); - } - final NotedCallback notedCallback = new NotedCallback(callback, watchedUid, - callingUid, callingPid); - for (int op : ops) { - callbacks.put(op, notedCallback); - } - } - } - - @Override - public void stopWatchingNoted(IAppOpsNotedCallback callback) { - Objects.requireNonNull(callback, "Callback cannot be null"); - synchronized (this) { - final SparseArray<NotedCallback> notedCallbacks = - mNotedWatchers.remove(callback.asBinder()); - if (notedCallbacks == null) { - return; - } - final int callbackCount = notedCallbacks.size(); - for (int i = 0; i < callbackCount; i++) { - notedCallbacks.valueAt(i).destroy(); - } - } - } - - @Override - public int startOperation(@NonNull IBinder clientId, int code, int uid, - @Nullable String packageName, @Nullable String attributionTag, - boolean startIfModeDefault, @NonNull String message, - @AttributionFlags int attributionFlags, int attributionChainId) { - verifyIncomingUid(uid); - verifyIncomingOp(code); - if (!isIncomingPackageValid(packageName, UserHandle.getUserId(uid))) { - return AppOpsManager.MODE_ERRORED; - } - - String resolvedPackageName = AppOpsManager.resolvePackageName(uid, packageName); - if (resolvedPackageName == null) { - return AppOpsManager.MODE_IGNORED; - } - - // As a special case for OP_RECORD_AUDIO_HOTWORD, which we use only for attribution - // purposes and not as a check, also make sure that the caller is allowed to access - // the data gated by OP_RECORD_AUDIO. - // - // TODO: Revert this change before Android 12. - if (code == OP_RECORD_AUDIO_HOTWORD || code == OP_RECEIVE_AMBIENT_TRIGGER_AUDIO) { - int result = checkOperation(OP_RECORD_AUDIO, uid, packageName, null, false); - if (result != AppOpsManager.MODE_ALLOWED) { - return result; - } - } - return startOperationUnchecked(clientId, code, uid, packageName, attributionTag, - Process.INVALID_UID, null, null, OP_FLAG_SELF, startIfModeDefault, - attributionFlags, attributionChainId, /*dryRun*/ false); - } - - private boolean shouldStartForMode(int mode, boolean startIfModeDefault) { - return (mode == MODE_ALLOWED || (mode == MODE_DEFAULT && startIfModeDefault)); - } - - @Override - public int startOperationUnchecked(IBinder clientId, int code, int uid, - @NonNull String packageName, @Nullable String attributionTag, int proxyUid, - String proxyPackageName, @Nullable String proxyAttributionTag, @OpFlags int flags, - boolean startIfModeDefault, @AttributionFlags int attributionFlags, - int attributionChainId, boolean dryRun) { - PackageVerificationResult pvr; - try { - pvr = verifyAndGetBypass(uid, packageName, attributionTag, proxyPackageName); - if (!pvr.isAttributionTagValid) { - attributionTag = null; - } - } catch (SecurityException e) { - Slog.e(TAG, "startOperation", e); - return AppOpsManager.MODE_ERRORED; - } - - boolean isRestricted; - int startType = START_TYPE_FAILED; - synchronized (this) { - final Ops ops = getOpsLocked(uid, packageName, attributionTag, - pvr.isAttributionTagValid, pvr.bypass, /* edit */ true); - if (ops == null) { - if (!dryRun) { - scheduleOpStartedIfNeededLocked(code, uid, packageName, attributionTag, - flags, AppOpsManager.MODE_IGNORED, startType, attributionFlags, - attributionChainId); - } - if (DEBUG) { - Slog.d(TAG, "startOperation: no op for code " + code + " uid " + uid - + " package " + packageName + " flags: " - + AppOpsManager.flagsToString(flags)); - } - return AppOpsManager.MODE_ERRORED; - } - final Op op = getOpLocked(ops, code, uid, true); - final AttributedOp attributedOp = op.getOrCreateAttribution(op, attributionTag); - final UidState uidState = ops.uidState; - isRestricted = isOpRestrictedLocked(uid, code, packageName, attributionTag, pvr.bypass, - false); - final int switchCode = AppOpsManager.opToSwitch(code); - // If there is a non-default per UID policy (we set UID op mode only if - // non-default) it takes over, otherwise use the per package policy. - if (uidState.getUidMode(switchCode) != AppOpsManager.opToDefaultMode(switchCode)) { - final int uidMode = uidState.evalMode(code, uidState.getUidMode(switchCode)); - if (!shouldStartForMode(uidMode, startIfModeDefault)) { - if (DEBUG) { - Slog.d(TAG, "startOperation: uid reject #" + uidMode + " for code " - + switchCode + " (" + code + ") uid " + uid + " package " - + packageName + " flags: " + AppOpsManager.flagsToString(flags)); - } - if (!dryRun) { - attributedOp.rejected(uidState.getState(), flags); - scheduleOpStartedIfNeededLocked(code, uid, packageName, attributionTag, - flags, uidMode, startType, attributionFlags, attributionChainId); - } - return uidMode; - } - } else { - final Op switchOp = switchCode != code ? getOpLocked(ops, switchCode, uid, true) - : op; - final int mode = switchOp.uidState.evalMode(switchOp.op, switchOp.getMode()); - if (!shouldStartForMode(mode, startIfModeDefault)) { - if (DEBUG) { - Slog.d(TAG, "startOperation: reject #" + mode + " for code " - + switchCode + " (" + code + ") uid " + uid + " package " - + packageName + " flags: " + AppOpsManager.flagsToString(flags)); - } - if (!dryRun) { - attributedOp.rejected(uidState.getState(), flags); - scheduleOpStartedIfNeededLocked(code, uid, packageName, attributionTag, - flags, mode, startType, attributionFlags, attributionChainId); - } - return mode; - } - } - if (DEBUG) { - Slog.d(TAG, "startOperation: allowing code " + code + " uid " + uid - + " package " + packageName + " restricted: " + isRestricted - + " flags: " + AppOpsManager.flagsToString(flags)); - } - if (!dryRun) { - try { - if (isRestricted) { - attributedOp.createPaused(clientId, proxyUid, proxyPackageName, - proxyAttributionTag, uidState.getState(), flags, - attributionFlags, attributionChainId); - } else { - attributedOp.started(clientId, proxyUid, proxyPackageName, - proxyAttributionTag, uidState.getState(), flags, - attributionFlags, attributionChainId); - startType = START_TYPE_STARTED; - } - } catch (RemoteException e) { - throw new RuntimeException(e); - } - scheduleOpStartedIfNeededLocked(code, uid, packageName, attributionTag, flags, - isRestricted ? MODE_IGNORED : MODE_ALLOWED, startType, attributionFlags, - attributionChainId); - } - } - - // Possible bug? The raw mode could have been MODE_DEFAULT to reach here. - return isRestricted ? MODE_IGNORED : MODE_ALLOWED; - } - - @Override - public void finishOperation(IBinder clientId, int code, int uid, String packageName, - String attributionTag) { - verifyIncomingUid(uid); - verifyIncomingOp(code); - if (!isIncomingPackageValid(packageName, UserHandle.getUserId(uid))) { - return; - } - - String resolvedPackageName = AppOpsManager.resolvePackageName(uid, packageName); - if (resolvedPackageName == null) { - return; - } - - finishOperationUnchecked(clientId, code, uid, resolvedPackageName, attributionTag); - } - - @Override - public void finishOperationUnchecked(IBinder clientId, int code, int uid, String packageName, - String attributionTag) { - PackageVerificationResult pvr; - try { - pvr = verifyAndGetBypass(uid, packageName, attributionTag); - if (!pvr.isAttributionTagValid) { - attributionTag = null; - } - } catch (SecurityException e) { - Slog.e(TAG, "Cannot finishOperation", e); - return; - } - - synchronized (this) { - Op op = getOpLocked(code, uid, packageName, attributionTag, pvr.isAttributionTagValid, - pvr.bypass, /* edit */ true); - if (op == null) { - Slog.e(TAG, "Operation not found: uid=" + uid + " pkg=" + packageName + "(" - + attributionTag + ") op=" + AppOpsManager.opToName(code)); - return; - } - final AttributedOp attributedOp = op.mAttributions.get(attributionTag); - if (attributedOp == null) { - Slog.e(TAG, "Attribution not found: uid=" + uid + " pkg=" + packageName + "(" - + attributionTag + ") op=" + AppOpsManager.opToName(code)); - return; - } - - if (attributedOp.isRunning() || attributedOp.isPaused()) { - attributedOp.finished(clientId); - } else { - Slog.e(TAG, "Operation not started: uid=" + uid + " pkg=" + packageName + "(" - + attributionTag + ") op=" + AppOpsManager.opToName(code)); - } - } - } - - void scheduleOpActiveChangedIfNeededLocked(int code, int uid, @NonNull String packageName, - @Nullable String attributionTag, boolean active, - @AttributionFlags int attributionFlags, int attributionChainId) { - ArraySet<ActiveCallback> dispatchedCallbacks = null; - final int callbackListCount = mActiveWatchers.size(); - for (int i = 0; i < callbackListCount; i++) { - final SparseArray<ActiveCallback> callbacks = mActiveWatchers.valueAt(i); - ActiveCallback callback = callbacks.get(code); - if (callback != null) { - if (callback.mWatchingUid >= 0 && callback.mWatchingUid != uid) { - continue; - } - if (dispatchedCallbacks == null) { - dispatchedCallbacks = new ArraySet<>(); - } - dispatchedCallbacks.add(callback); - } - } - if (dispatchedCallbacks == null) { - return; - } - mHandler.sendMessage(PooledLambda.obtainMessage( - AppOpsServiceImpl::notifyOpActiveChanged, - this, dispatchedCallbacks, code, uid, packageName, attributionTag, active, - attributionFlags, attributionChainId)); - } - - private void notifyOpActiveChanged(ArraySet<ActiveCallback> callbacks, - int code, int uid, @NonNull String packageName, @Nullable String attributionTag, - boolean active, @AttributionFlags int attributionFlags, int attributionChainId) { - // There are features watching for mode changes such as window manager - // and location manager which are in our process. The callbacks in these - // features may require permissions our remote caller does not have. - final long identity = Binder.clearCallingIdentity(); - try { - final int callbackCount = callbacks.size(); - for (int i = 0; i < callbackCount; i++) { - final ActiveCallback callback = callbacks.valueAt(i); - try { - if (shouldIgnoreCallback(code, callback.mCallingPid, callback.mCallingUid)) { - continue; - } - callback.mCallback.opActiveChanged(code, uid, packageName, attributionTag, - active, attributionFlags, attributionChainId); - } catch (RemoteException e) { - /* do nothing */ - } - } - } finally { - Binder.restoreCallingIdentity(identity); - } - } - - void scheduleOpStartedIfNeededLocked(int code, int uid, String pkgName, - String attributionTag, @OpFlags int flags, @Mode int result, - @AppOpsManager.OnOpStartedListener.StartedType int startedType, - @AttributionFlags int attributionFlags, int attributionChainId) { - ArraySet<StartedCallback> dispatchedCallbacks = null; - final int callbackListCount = mStartedWatchers.size(); - for (int i = 0; i < callbackListCount; i++) { - final SparseArray<StartedCallback> callbacks = mStartedWatchers.valueAt(i); - - StartedCallback callback = callbacks.get(code); - if (callback != null) { - if (callback.mWatchingUid >= 0 && callback.mWatchingUid != uid) { - continue; - } - - if (dispatchedCallbacks == null) { - dispatchedCallbacks = new ArraySet<>(); - } - dispatchedCallbacks.add(callback); - } - } - - if (dispatchedCallbacks == null) { - return; - } - - mHandler.sendMessage(PooledLambda.obtainMessage( - AppOpsServiceImpl::notifyOpStarted, - this, dispatchedCallbacks, code, uid, pkgName, attributionTag, flags, - result, startedType, attributionFlags, attributionChainId)); - } - - private void notifyOpStarted(ArraySet<StartedCallback> callbacks, - int code, int uid, String packageName, String attributionTag, @OpFlags int flags, - @Mode int result, @AppOpsManager.OnOpStartedListener.StartedType int startedType, - @AttributionFlags int attributionFlags, int attributionChainId) { - final long identity = Binder.clearCallingIdentity(); - try { - final int callbackCount = callbacks.size(); - for (int i = 0; i < callbackCount; i++) { - final StartedCallback callback = callbacks.valueAt(i); - try { - if (shouldIgnoreCallback(code, callback.mCallingPid, callback.mCallingUid)) { - continue; - } - callback.mCallback.opStarted(code, uid, packageName, attributionTag, flags, - result, startedType, attributionFlags, attributionChainId); - } catch (RemoteException e) { - /* do nothing */ - } - } - } finally { - Binder.restoreCallingIdentity(identity); - } - } - - private void scheduleOpNotedIfNeededLocked(int code, int uid, String packageName, - String attributionTag, @OpFlags int flags, @Mode int result) { - ArraySet<NotedCallback> dispatchedCallbacks = null; - final int callbackListCount = mNotedWatchers.size(); - for (int i = 0; i < callbackListCount; i++) { - final SparseArray<NotedCallback> callbacks = mNotedWatchers.valueAt(i); - final NotedCallback callback = callbacks.get(code); - if (callback != null) { - if (callback.mWatchingUid >= 0 && callback.mWatchingUid != uid) { - continue; - } - if (dispatchedCallbacks == null) { - dispatchedCallbacks = new ArraySet<>(); - } - dispatchedCallbacks.add(callback); - } - } - if (dispatchedCallbacks == null) { - return; - } - mHandler.sendMessage(PooledLambda.obtainMessage( - AppOpsServiceImpl::notifyOpChecked, - this, dispatchedCallbacks, code, uid, packageName, attributionTag, flags, - result)); - } - - private void notifyOpChecked(ArraySet<NotedCallback> callbacks, - int code, int uid, String packageName, String attributionTag, @OpFlags int flags, - @Mode int result) { - // There are features watching for checks in our process. The callbacks in - // these features may require permissions our remote caller does not have. - final long identity = Binder.clearCallingIdentity(); - try { - final int callbackCount = callbacks.size(); - for (int i = 0; i < callbackCount; i++) { - final NotedCallback callback = callbacks.valueAt(i); - try { - if (shouldIgnoreCallback(code, callback.mCallingPid, callback.mCallingUid)) { - continue; - } - callback.mCallback.opNoted(code, uid, packageName, attributionTag, flags, - result); - } catch (RemoteException e) { - /* do nothing */ - } - } - } finally { - Binder.restoreCallingIdentity(identity); - } - } - - private void verifyIncomingUid(int uid) { - if (uid == Binder.getCallingUid()) { - return; - } - if (Binder.getCallingPid() == Process.myPid()) { - return; - } - mContext.enforcePermission(android.Manifest.permission.UPDATE_APP_OPS_STATS, - Binder.getCallingPid(), Binder.getCallingUid(), null); - } - - private boolean shouldIgnoreCallback(int op, int watcherPid, int watcherUid) { - // If it's a restricted read op, ignore it if watcher doesn't have manage ops permission, - // as watcher should not use this to signal if the value is changed. - return opRestrictsRead(op) && mContext.checkPermission(Manifest.permission.MANAGE_APPOPS, - watcherPid, watcherUid) != PackageManager.PERMISSION_GRANTED; - } - - private void verifyIncomingOp(int op) { - if (op >= 0 && op < AppOpsManager._NUM_OP) { - // Enforce manage appops permission if it's a restricted read op. - if (opRestrictsRead(op)) { - mContext.enforcePermission(Manifest.permission.MANAGE_APPOPS, - Binder.getCallingPid(), Binder.getCallingUid(), "verifyIncomingOp"); - } - return; - } - throw new IllegalArgumentException("Bad operation #" + op); - } - - private boolean isIncomingPackageValid(@Nullable String packageName, @UserIdInt int userId) { - final int callingUid = Binder.getCallingUid(); - // Handle the special UIDs that don't have actual packages (audioserver, cameraserver, etc). - if (packageName == null || isSpecialPackage(callingUid, packageName)) { - return true; - } - - // If the package doesn't exist, #verifyAndGetBypass would throw a SecurityException in - // the end. Although that exception would be caught and return, we could make it return - // early. - if (!isPackageExisted(packageName)) { - return false; - } - - if (getPackageManagerInternal().filterAppAccess(packageName, callingUid, userId)) { - Slog.w(TAG, packageName + " not found from " + callingUid); - return false; - } - - return true; - } - - private boolean isSpecialPackage(int callingUid, @Nullable String packageName) { - final String resolvedPackage = AppOpsManager.resolvePackageName(callingUid, packageName); - return callingUid == Process.SYSTEM_UID - || resolveUid(resolvedPackage) != Process.INVALID_UID; - } - - private @Nullable UidState getUidStateLocked(int uid, boolean edit) { - UidState uidState = mUidStates.get(uid); - if (uidState == null) { - if (!edit) { - return null; - } - uidState = new UidState(uid); - mUidStates.put(uid, uidState); - } - - return uidState; - } - - @Override - public void updateAppWidgetVisibility(SparseArray<String> uidPackageNames, boolean visible) { - synchronized (this) { - getUidStateTracker().updateAppWidgetVisibility(uidPackageNames, visible); - } - } - - /** - * @return {@link PackageManagerInternal} - */ - private @NonNull PackageManagerInternal getPackageManagerInternal() { - if (mPackageManagerInternal == null) { - mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class); - } - - return mPackageManagerInternal; - } - - @Override - public void verifyPackage(int uid, String packageName) { - verifyAndGetBypass(uid, packageName, null); - } - - /** - * Create a restriction description matching the properties of the package. - * - * @param pkg The package to create the restriction description for - * @return The restriction matching the package - */ - private RestrictionBypass getBypassforPackage(@NonNull AndroidPackage pkg) { - return new RestrictionBypass(pkg.getUid() == Process.SYSTEM_UID, pkg.isPrivileged(), - mContext.checkPermission(android.Manifest.permission - .EXEMPT_FROM_AUDIO_RECORD_RESTRICTIONS, -1, pkg.getUid()) - == PackageManager.PERMISSION_GRANTED); - } - - /** - * @see #verifyAndGetBypass(int, String, String, String) - */ - private @NonNull PackageVerificationResult verifyAndGetBypass(int uid, String packageName, - @Nullable String attributionTag) { - return verifyAndGetBypass(uid, packageName, attributionTag, null); - } - - /** - * Verify that package belongs to uid and return the {@link RestrictionBypass bypass - * description} for the package, along with a boolean indicating whether the attribution tag is - * valid. - * - * @param uid The uid the package belongs to - * @param packageName The package the might belong to the uid - * @param attributionTag attribution tag or {@code null} if no need to verify - * @param proxyPackageName The proxy package, from which the attribution tag is to be pulled - * @return PackageVerificationResult containing {@link RestrictionBypass} and whether the - * attribution tag is valid - */ - private @NonNull PackageVerificationResult verifyAndGetBypass(int uid, String packageName, - @Nullable String attributionTag, @Nullable String proxyPackageName) { - if (uid == Process.ROOT_UID) { - // For backwards compatibility, don't check package name for root UID. - return new PackageVerificationResult(null, - /* isAttributionTagValid */ true); - } - if (Process.isSdkSandboxUid(uid)) { - // SDK sandbox processes run in their own UID range, but their associated - // UID for checks should always be the UID of the package implementing SDK sandbox - // service. - // TODO: We will need to modify the callers of this function instead, so - // modifications and checks against the app ops state are done with the - // correct UID. - try { - final PackageManager pm = mContext.getPackageManager(); - final String supplementalPackageName = pm.getSdkSandboxPackageName(); - if (Objects.equals(packageName, supplementalPackageName)) { - uid = pm.getPackageUidAsUser(supplementalPackageName, - PackageManager.PackageInfoFlags.of(0), UserHandle.getUserId(uid)); - } - } catch (PackageManager.NameNotFoundException e) { - // Shouldn't happen for the supplemental package - e.printStackTrace(); - } - } - - - // Do not check if uid/packageName/attributionTag is already known. - synchronized (this) { - UidState uidState = mUidStates.get(uid); - if (uidState != null && uidState.pkgOps != null) { - Ops ops = uidState.pkgOps.get(packageName); - - if (ops != null && (attributionTag == null || ops.knownAttributionTags.contains( - attributionTag)) && ops.bypass != null) { - return new PackageVerificationResult(ops.bypass, - ops.validAttributionTags.contains(attributionTag)); - } - } - } - - int callingUid = Binder.getCallingUid(); - - // Allow any attribution tag for resolvable uids - int pkgUid; - if (Objects.equals(packageName, "com.android.shell")) { - // Special case for the shell which is a package but should be able - // to bypass app attribution tag restrictions. - pkgUid = Process.SHELL_UID; - } else { - pkgUid = resolveUid(packageName); - } - if (pkgUid != Process.INVALID_UID) { - if (pkgUid != UserHandle.getAppId(uid)) { - Slog.e(TAG, "Bad call made by uid " + callingUid + ". " - + "Package \"" + packageName + "\" does not belong to uid " + uid + "."); - String otherUidMessage = DEBUG ? " but it is really " + pkgUid : " but it is not"; - throw new SecurityException("Specified package \"" + packageName + "\" under uid " - + UserHandle.getAppId(uid) + otherUidMessage); - } - return new PackageVerificationResult(RestrictionBypass.UNRESTRICTED, - /* isAttributionTagValid */ true); - } - - int userId = UserHandle.getUserId(uid); - RestrictionBypass bypass = null; - boolean isAttributionTagValid = false; - - final long ident = Binder.clearCallingIdentity(); - try { - PackageManagerInternal pmInt = LocalServices.getService(PackageManagerInternal.class); - AndroidPackage pkg = pmInt.getPackage(packageName); - if (pkg != null) { - isAttributionTagValid = isAttributionInPackage(pkg, attributionTag); - pkgUid = UserHandle.getUid(userId, UserHandle.getAppId(pkg.getUid())); - bypass = getBypassforPackage(pkg); - } - if (!isAttributionTagValid) { - AndroidPackage proxyPkg = proxyPackageName != null - ? pmInt.getPackage(proxyPackageName) : null; - // Re-check in proxy. - isAttributionTagValid = isAttributionInPackage(proxyPkg, attributionTag); - String msg; - if (pkg != null && isAttributionTagValid) { - msg = "attributionTag " + attributionTag + " declared in manifest of the proxy" - + " package " + proxyPackageName + ", this is not advised"; - } else if (pkg != null) { - msg = "attributionTag " + attributionTag + " not declared in manifest of " - + packageName; - } else { - msg = "package " + packageName + " not found, can't check for " - + "attributionTag " + attributionTag; - } - - try { - if (!mPlatformCompat.isChangeEnabledByPackageName( - SECURITY_EXCEPTION_ON_INVALID_ATTRIBUTION_TAG_CHANGE, packageName, - userId) || !mPlatformCompat.isChangeEnabledByUid( - SECURITY_EXCEPTION_ON_INVALID_ATTRIBUTION_TAG_CHANGE, - callingUid)) { - // Do not override tags if overriding is not enabled for this package - isAttributionTagValid = true; - } - Slog.e(TAG, msg); - } catch (RemoteException neverHappens) { - } - } - } finally { - Binder.restoreCallingIdentity(ident); - } - - if (pkgUid != uid) { - Slog.e(TAG, "Bad call made by uid " + callingUid + ". " - + "Package \"" + packageName + "\" does not belong to uid " + uid + "."); - String otherUidMessage = DEBUG ? " but it is really " + pkgUid : " but it is not"; - throw new SecurityException("Specified package \"" + packageName + "\" under uid " + uid - + otherUidMessage); - } - - return new PackageVerificationResult(bypass, isAttributionTagValid); - } - - private boolean isAttributionInPackage(@Nullable AndroidPackage pkg, - @Nullable String attributionTag) { - if (pkg == null) { - return false; - } else if (attributionTag == null) { - return true; - } - if (pkg.getAttributions() != null) { - int numAttributions = pkg.getAttributions().size(); - for (int i = 0; i < numAttributions; i++) { - if (pkg.getAttributions().get(i).getTag().equals(attributionTag)) { - return true; - } - } - } - - return false; - } - - /** - * Get (and potentially create) ops. - * - * @param uid The uid the package belongs to - * @param packageName The name of the package - * @param attributionTag attribution tag - * @param isAttributionTagValid whether the given attribution tag is valid - * @param bypass When to bypass certain op restrictions (can be null if edit - * == false) - * @param edit If an ops does not exist, create the ops? - * @return The ops - */ - private Ops getOpsLocked(int uid, String packageName, @Nullable String attributionTag, - boolean isAttributionTagValid, @Nullable RestrictionBypass bypass, boolean edit) { - UidState uidState = getUidStateLocked(uid, edit); - if (uidState == null) { - return null; - } - - if (uidState.pkgOps == null) { - if (!edit) { - return null; - } - uidState.pkgOps = new ArrayMap<>(); - } - - Ops ops = uidState.pkgOps.get(packageName); - if (ops == null) { - if (!edit) { - return null; - } - ops = new Ops(packageName, uidState); - uidState.pkgOps.put(packageName, ops); - } - - if (edit) { - if (bypass != null) { - ops.bypass = bypass; - } - - if (attributionTag != null) { - ops.knownAttributionTags.add(attributionTag); - if (isAttributionTagValid) { - ops.validAttributionTags.add(attributionTag); - } else { - ops.validAttributionTags.remove(attributionTag); - } - } - } - - return ops; - } - - @Override - public void scheduleWriteLocked() { - if (!mWriteScheduled) { - mWriteScheduled = true; - mHandler.postDelayed(mWriteRunner, WRITE_DELAY); - } - } - - @Override - public void scheduleFastWriteLocked() { - if (!mFastWriteScheduled) { - mWriteScheduled = true; - mFastWriteScheduled = true; - mHandler.removeCallbacks(mWriteRunner); - mHandler.postDelayed(mWriteRunner, 10 * 1000); - } - } - - /** - * Get the state of an op for a uid. - * - * @param code The code of the op - * @param uid The uid the of the package - * @param packageName The package name for which to get the state for - * @param attributionTag The attribution tag - * @param isAttributionTagValid Whether the given attribution tag is valid - * @param bypass When to bypass certain op restrictions (can be null if edit - * == false) - * @param edit Iff {@code true} create the {@link Op} object if not yet created - * @return The {@link Op state} of the op - */ - private @Nullable Op getOpLocked(int code, int uid, @NonNull String packageName, - @Nullable String attributionTag, boolean isAttributionTagValid, - @Nullable RestrictionBypass bypass, boolean edit) { - Ops ops = getOpsLocked(uid, packageName, attributionTag, isAttributionTagValid, bypass, - edit); - if (ops == null) { - return null; - } - return getOpLocked(ops, code, uid, edit); - } - - private Op getOpLocked(Ops ops, int code, int uid, boolean edit) { - Op op = ops.get(code); - if (op == null) { - if (!edit) { - return null; - } - op = new Op(ops.uidState, ops.packageName, code, uid); - ops.put(code, op); - } - if (edit) { - scheduleWriteLocked(); - } - return op; - } - - private boolean isOpRestrictedDueToSuspend(int code, String packageName, int uid) { - if (!ArrayUtils.contains(OPS_RESTRICTED_ON_SUSPEND, code)) { - return false; - } - final PackageManagerInternal pmi = LocalServices.getService(PackageManagerInternal.class); - return pmi.isPackageSuspended(packageName, UserHandle.getUserId(uid)); - } - - private boolean isOpRestrictedLocked(int uid, int code, String packageName, - String attributionTag, @Nullable RestrictionBypass appBypass, boolean isCheckOp) { - int restrictionSetCount = mOpGlobalRestrictions.size(); - - for (int i = 0; i < restrictionSetCount; i++) { - ClientGlobalRestrictionState restrictionState = mOpGlobalRestrictions.valueAt(i); - if (restrictionState.hasRestriction(code)) { - return true; - } - } - - int userHandle = UserHandle.getUserId(uid); - restrictionSetCount = mOpUserRestrictions.size(); - - for (int i = 0; i < restrictionSetCount; i++) { - // For each client, check that the given op is not restricted, or that the given - // package is exempt from the restriction. - ClientUserRestrictionState restrictionState = mOpUserRestrictions.valueAt(i); - if (restrictionState.hasRestriction(code, packageName, attributionTag, userHandle, - isCheckOp)) { - RestrictionBypass opBypass = opAllowSystemBypassRestriction(code); - if (opBypass != null) { - // If we are the system, bypass user restrictions for certain codes - synchronized (this) { - if (opBypass.isSystemUid && appBypass != null && appBypass.isSystemUid) { - return false; - } - if (opBypass.isPrivileged && appBypass != null && appBypass.isPrivileged) { - return false; - } - if (opBypass.isRecordAudioRestrictionExcept && appBypass != null - && appBypass.isRecordAudioRestrictionExcept) { - return false; - } - } - } - return true; - } - } - return false; - } - - @Override - public void readState() { - synchronized (mFile) { - synchronized (this) { - FileInputStream stream; - try { - stream = mFile.openRead(); - } catch (FileNotFoundException e) { - Slog.i(TAG, "No existing app ops " + mFile.getBaseFile() + "; starting empty"); - return; - } - boolean success = false; - mUidStates.clear(); - mAppOpsServiceInterface.clearAllModes(); - try { - TypedXmlPullParser parser = Xml.resolvePullParser(stream); - int type; - while ((type = parser.next()) != XmlPullParser.START_TAG - && type != XmlPullParser.END_DOCUMENT) { - // Parse next until we reach the start or end - } - - if (type != XmlPullParser.START_TAG) { - throw new IllegalStateException("no start tag found"); - } - - mVersionAtBoot = parser.getAttributeInt(null, "v", NO_VERSION); - - int outerDepth = parser.getDepth(); - while ((type = parser.next()) != XmlPullParser.END_DOCUMENT - && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { - if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { - continue; - } - - String tagName = parser.getName(); - if (tagName.equals("pkg")) { - readPackage(parser); - } else if (tagName.equals("uid")) { - readUidOps(parser); - } else { - Slog.w(TAG, "Unknown element under <app-ops>: " - + parser.getName()); - XmlUtils.skipCurrentTag(parser); - } - } - success = true; - } catch (IllegalStateException e) { - Slog.w(TAG, "Failed parsing " + e); - } catch (NullPointerException e) { - Slog.w(TAG, "Failed parsing " + e); - } catch (NumberFormatException e) { - Slog.w(TAG, "Failed parsing " + e); - } catch (XmlPullParserException e) { - Slog.w(TAG, "Failed parsing " + e); - } catch (IOException e) { - Slog.w(TAG, "Failed parsing " + e); - } catch (IndexOutOfBoundsException e) { - Slog.w(TAG, "Failed parsing " + e); - } finally { - if (!success) { - mUidStates.clear(); - mAppOpsServiceInterface.clearAllModes(); - } - try { - stream.close(); - } catch (IOException e) { - } - } - } - } - } - - @VisibleForTesting - @GuardedBy("this") - void upgradeRunAnyInBackgroundLocked() { - for (int i = 0; i < mUidStates.size(); i++) { - final UidState uidState = mUidStates.valueAt(i); - if (uidState == null) { - continue; - } - SparseIntArray opModes = uidState.getNonDefaultUidModes(); - if (opModes != null) { - final int idx = opModes.indexOfKey(AppOpsManager.OP_RUN_IN_BACKGROUND); - if (idx >= 0) { - uidState.setUidMode(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, - opModes.valueAt(idx)); - } - } - if (uidState.pkgOps == null) { - continue; - } - boolean changed = false; - for (int j = 0; j < uidState.pkgOps.size(); j++) { - Ops ops = uidState.pkgOps.valueAt(j); - if (ops != null) { - final Op op = ops.get(AppOpsManager.OP_RUN_IN_BACKGROUND); - if (op != null && op.getMode() != AppOpsManager.opToDefaultMode(op.op)) { - final Op copy = new Op(op.uidState, op.packageName, - AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, uidState.uid); - copy.setMode(op.getMode()); - ops.put(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, copy); - changed = true; - } - } - } - if (changed) { - uidState.evalForegroundOps(); - } - } - } - - /** - * The interpretation of the default mode - MODE_DEFAULT - for OP_SCHEDULE_EXACT_ALARM is - * changing. Simultaneously, we want to change this op's mode from MODE_DEFAULT to MODE_ALLOWED - * for already installed apps. For newer apps, it will stay as MODE_DEFAULT. - */ - @VisibleForTesting - @GuardedBy("this") - void upgradeScheduleExactAlarmLocked() { - final PermissionManagerServiceInternal pmsi = LocalServices.getService( - PermissionManagerServiceInternal.class); - final UserManagerInternal umi = LocalServices.getService(UserManagerInternal.class); - final PackageManagerInternal pmi = getPackageManagerInternal(); - - final String[] packagesDeclaringPermission = pmsi.getAppOpPermissionPackages( - AppOpsManager.opToPermission(OP_SCHEDULE_EXACT_ALARM)); - final int[] userIds = umi.getUserIds(); - - for (final String pkg : packagesDeclaringPermission) { - for (int userId : userIds) { - final int uid = pmi.getPackageUid(pkg, 0, userId); - - UidState uidState = mUidStates.get(uid); - if (uidState == null) { - uidState = new UidState(uid); - mUidStates.put(uid, uidState); - } - final int oldMode = uidState.getUidMode(OP_SCHEDULE_EXACT_ALARM); - if (oldMode == AppOpsManager.opToDefaultMode(OP_SCHEDULE_EXACT_ALARM)) { - uidState.setUidMode(OP_SCHEDULE_EXACT_ALARM, MODE_ALLOWED); - } - } - // This appop is meant to be controlled at a uid level. So we leave package modes as - // they are. - } - } - - @GuardedBy("this") - private void upgradeLocked(int oldVersion) { - if (oldVersion == NO_FILE_VERSION || oldVersion >= CURRENT_VERSION) { - return; - } - Slog.d(TAG, "Upgrading app-ops xml from version " + oldVersion + " to " + CURRENT_VERSION); - switch (oldVersion) { - case NO_VERSION: - upgradeRunAnyInBackgroundLocked(); - // fall through - case 1: - upgradeScheduleExactAlarmLocked(); - // fall through - case 2: - // for future upgrades - } - scheduleFastWriteLocked(); - } - - private void readUidOps(TypedXmlPullParser parser) throws NumberFormatException, - XmlPullParserException, IOException { - final int uid = parser.getAttributeInt(null, "n"); - int outerDepth = parser.getDepth(); - int type; - while ((type = parser.next()) != XmlPullParser.END_DOCUMENT - && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { - if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { - continue; - } - - String tagName = parser.getName(); - if (tagName.equals("op")) { - final int code = parser.getAttributeInt(null, "n"); - final int mode = parser.getAttributeInt(null, "m"); - setUidMode(code, uid, mode, null); - } else { - Slog.w(TAG, "Unknown element under <uid-ops>: " - + parser.getName()); - XmlUtils.skipCurrentTag(parser); - } - } - } - - private void readPackage(TypedXmlPullParser parser) - throws NumberFormatException, XmlPullParserException, IOException { - String pkgName = parser.getAttributeValue(null, "n"); - int outerDepth = parser.getDepth(); - int type; - while ((type = parser.next()) != XmlPullParser.END_DOCUMENT - && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { - if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { - continue; - } - - String tagName = parser.getName(); - if (tagName.equals("uid")) { - readUid(parser, pkgName); - } else { - Slog.w(TAG, "Unknown element under <pkg>: " - + parser.getName()); - XmlUtils.skipCurrentTag(parser); - } - } - } - - private void readUid(TypedXmlPullParser parser, String pkgName) - throws NumberFormatException, XmlPullParserException, IOException { - int uid = parser.getAttributeInt(null, "n"); - final UidState uidState = getUidStateLocked(uid, true); - int outerDepth = parser.getDepth(); - int type; - while ((type = parser.next()) != XmlPullParser.END_DOCUMENT - && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { - if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { - continue; - } - String tagName = parser.getName(); - if (tagName.equals("op")) { - readOp(parser, uidState, pkgName); - } else { - Slog.w(TAG, "Unknown element under <pkg>: " - + parser.getName()); - XmlUtils.skipCurrentTag(parser); - } - } - uidState.evalForegroundOps(); - } - - private void readAttributionOp(TypedXmlPullParser parser, @NonNull Op parent, - @Nullable String attribution) - throws NumberFormatException, IOException, XmlPullParserException { - final AttributedOp attributedOp = parent.getOrCreateAttribution(parent, attribution); - - final long key = parser.getAttributeLong(null, "n"); - final int uidState = extractUidStateFromKey(key); - final int opFlags = extractFlagsFromKey(key); - - final long accessTime = parser.getAttributeLong(null, "t", 0); - final long rejectTime = parser.getAttributeLong(null, "r", 0); - final long accessDuration = parser.getAttributeLong(null, "d", -1); - final String proxyPkg = XmlUtils.readStringAttribute(parser, "pp"); - final int proxyUid = parser.getAttributeInt(null, "pu", Process.INVALID_UID); - final String proxyAttributionTag = XmlUtils.readStringAttribute(parser, "pc"); - - if (accessTime > 0) { - attributedOp.accessed(accessTime, accessDuration, proxyUid, proxyPkg, - proxyAttributionTag, uidState, opFlags); - } - if (rejectTime > 0) { - attributedOp.rejected(rejectTime, uidState, opFlags); - } - } - - private void readOp(TypedXmlPullParser parser, - @NonNull UidState uidState, @NonNull String pkgName) - throws NumberFormatException, XmlPullParserException, IOException { - int opCode = parser.getAttributeInt(null, "n"); - Op op = new Op(uidState, pkgName, opCode, uidState.uid); - - final int mode = parser.getAttributeInt(null, "m", AppOpsManager.opToDefaultMode(op.op)); - op.setMode(mode); - - int outerDepth = parser.getDepth(); - int type; - while ((type = parser.next()) != XmlPullParser.END_DOCUMENT - && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { - if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { - continue; - } - String tagName = parser.getName(); - if (tagName.equals("st")) { - readAttributionOp(parser, op, XmlUtils.readStringAttribute(parser, "id")); - } else { - Slog.w(TAG, "Unknown element under <op>: " - + parser.getName()); - XmlUtils.skipCurrentTag(parser); - } - } - - if (uidState.pkgOps == null) { - uidState.pkgOps = new ArrayMap<>(); - } - Ops ops = uidState.pkgOps.get(pkgName); - if (ops == null) { - ops = new Ops(pkgName, uidState); - uidState.pkgOps.put(pkgName, ops); - } - ops.put(op.op, op); - } - - @Override - public void writeState() { - synchronized (mFile) { - FileOutputStream stream; - try { - stream = mFile.startWrite(); - } catch (IOException e) { - Slog.w(TAG, "Failed to write state: " + e); - return; - } - - List<AppOpsManager.PackageOps> allOps = getPackagesForOps(null); - - try { - TypedXmlSerializer out = Xml.resolveSerializer(stream); - out.startDocument(null, true); - out.startTag(null, "app-ops"); - out.attributeInt(null, "v", CURRENT_VERSION); - - SparseArray<SparseIntArray> uidStatesClone; - synchronized (this) { - uidStatesClone = new SparseArray<>(mUidStates.size()); - - final int uidStateCount = mUidStates.size(); - for (int uidStateNum = 0; uidStateNum < uidStateCount; uidStateNum++) { - UidState uidState = mUidStates.valueAt(uidStateNum); - int uid = mUidStates.keyAt(uidStateNum); - - SparseIntArray opModes = uidState.getNonDefaultUidModes(); - if (opModes != null && opModes.size() > 0) { - uidStatesClone.put(uid, opModes); - } - } - } - - final int uidStateCount = uidStatesClone.size(); - for (int uidStateNum = 0; uidStateNum < uidStateCount; uidStateNum++) { - SparseIntArray opModes = uidStatesClone.valueAt(uidStateNum); - if (opModes != null && opModes.size() > 0) { - out.startTag(null, "uid"); - out.attributeInt(null, "n", uidStatesClone.keyAt(uidStateNum)); - final int opCount = opModes.size(); - for (int opCountNum = 0; opCountNum < opCount; opCountNum++) { - final int op = opModes.keyAt(opCountNum); - final int mode = opModes.valueAt(opCountNum); - out.startTag(null, "op"); - out.attributeInt(null, "n", op); - out.attributeInt(null, "m", mode); - out.endTag(null, "op"); - } - out.endTag(null, "uid"); - } - } - - if (allOps != null) { - String lastPkg = null; - for (int i = 0; i < allOps.size(); i++) { - AppOpsManager.PackageOps pkg = allOps.get(i); - if (!Objects.equals(pkg.getPackageName(), lastPkg)) { - if (lastPkg != null) { - out.endTag(null, "pkg"); - } - lastPkg = pkg.getPackageName(); - if (lastPkg != null) { - out.startTag(null, "pkg"); - out.attribute(null, "n", lastPkg); - } - } - out.startTag(null, "uid"); - out.attributeInt(null, "n", pkg.getUid()); - List<AppOpsManager.OpEntry> ops = pkg.getOps(); - for (int j = 0; j < ops.size(); j++) { - AppOpsManager.OpEntry op = ops.get(j); - out.startTag(null, "op"); - out.attributeInt(null, "n", op.getOp()); - if (op.getMode() != AppOpsManager.opToDefaultMode(op.getOp())) { - out.attributeInt(null, "m", op.getMode()); - } - - for (String attributionTag : op.getAttributedOpEntries().keySet()) { - final AttributedOpEntry attribution = - op.getAttributedOpEntries().get(attributionTag); - - final ArraySet<Long> keys = attribution.collectKeys(); - - final int keyCount = keys.size(); - for (int k = 0; k < keyCount; k++) { - final long key = keys.valueAt(k); - - final int uidState = AppOpsManager.extractUidStateFromKey(key); - final int flags = AppOpsManager.extractFlagsFromKey(key); - - final long accessTime = attribution.getLastAccessTime(uidState, - uidState, flags); - final long rejectTime = attribution.getLastRejectTime(uidState, - uidState, flags); - final long accessDuration = attribution.getLastDuration( - uidState, uidState, flags); - // Proxy information for rejections is not backed up - final OpEventProxyInfo proxy = attribution.getLastProxyInfo( - uidState, uidState, flags); - - if (accessTime <= 0 && rejectTime <= 0 && accessDuration <= 0 - && proxy == null) { - continue; - } - - String proxyPkg = null; - String proxyAttributionTag = null; - int proxyUid = Process.INVALID_UID; - if (proxy != null) { - proxyPkg = proxy.getPackageName(); - proxyAttributionTag = proxy.getAttributionTag(); - proxyUid = proxy.getUid(); - } - - out.startTag(null, "st"); - if (attributionTag != null) { - out.attribute(null, "id", attributionTag); - } - out.attributeLong(null, "n", key); - if (accessTime > 0) { - out.attributeLong(null, "t", accessTime); - } - if (rejectTime > 0) { - out.attributeLong(null, "r", rejectTime); - } - if (accessDuration > 0) { - out.attributeLong(null, "d", accessDuration); - } - if (proxyPkg != null) { - out.attribute(null, "pp", proxyPkg); - } - if (proxyAttributionTag != null) { - out.attribute(null, "pc", proxyAttributionTag); - } - if (proxyUid >= 0) { - out.attributeInt(null, "pu", proxyUid); - } - out.endTag(null, "st"); - } - } - - out.endTag(null, "op"); - } - out.endTag(null, "uid"); - } - if (lastPkg != null) { - out.endTag(null, "pkg"); - } - } - - out.endTag(null, "app-ops"); - out.endDocument(); - mFile.finishWrite(stream); - } catch (IOException e) { - Slog.w(TAG, "Failed to write state, restoring backup.", e); - mFile.failWrite(stream); - } - } - mHistoricalRegistry.writeAndClearDiscreteHistory(); - } - - private void dumpHelp(PrintWriter pw) { - pw.println("AppOps service (appops) dump options:"); - pw.println(" -h"); - pw.println(" Print this help text."); - pw.println(" --op [OP]"); - pw.println(" Limit output to data associated with the given app op code."); - pw.println(" --mode [MODE]"); - pw.println(" Limit output to data associated with the given app op mode."); - pw.println(" --package [PACKAGE]"); - pw.println(" Limit output to data associated with the given package name."); - pw.println(" --attributionTag [attributionTag]"); - pw.println(" Limit output to data associated with the given attribution tag."); - pw.println(" --include-discrete [n]"); - pw.println(" Include discrete ops limited to n per dimension. Use zero for no limit."); - pw.println(" --watchers"); - pw.println(" Only output the watcher sections."); - pw.println(" --history"); - pw.println(" Only output history."); - pw.println(" --uid-state-changes"); - pw.println(" Include logs about uid state changes."); - } - - private void dumpStatesLocked(@NonNull PrintWriter pw, @Nullable String filterAttributionTag, - @HistoricalOpsRequestFilter int filter, long nowElapsed, @NonNull Op op, long now, - @NonNull SimpleDateFormat sdf, @NonNull Date date, @NonNull String prefix) { - final int numAttributions = op.mAttributions.size(); - for (int i = 0; i < numAttributions; i++) { - if ((filter & FILTER_BY_ATTRIBUTION_TAG) != 0 && !Objects.equals( - op.mAttributions.keyAt(i), filterAttributionTag)) { - continue; - } - - pw.print(prefix + op.mAttributions.keyAt(i) + "=[\n"); - dumpStatesLocked(pw, nowElapsed, op, op.mAttributions.keyAt(i), now, sdf, date, - prefix + " "); - pw.print(prefix + "]\n"); - } - } - - private void dumpStatesLocked(@NonNull PrintWriter pw, long nowElapsed, @NonNull Op op, - @Nullable String attributionTag, long now, @NonNull SimpleDateFormat sdf, - @NonNull Date date, @NonNull String prefix) { - - final AttributedOpEntry entry = op.createSingleAttributionEntryLocked( - attributionTag).getAttributedOpEntries().get(attributionTag); - - final ArraySet<Long> keys = entry.collectKeys(); - - final int keyCount = keys.size(); - for (int k = 0; k < keyCount; k++) { - final long key = keys.valueAt(k); - - final int uidState = AppOpsManager.extractUidStateFromKey(key); - final int flags = AppOpsManager.extractFlagsFromKey(key); - - final long accessTime = entry.getLastAccessTime(uidState, uidState, flags); - final long rejectTime = entry.getLastRejectTime(uidState, uidState, flags); - final long accessDuration = entry.getLastDuration(uidState, uidState, flags); - final OpEventProxyInfo proxy = entry.getLastProxyInfo(uidState, uidState, flags); - - String proxyPkg = null; - String proxyAttributionTag = null; - int proxyUid = Process.INVALID_UID; - if (proxy != null) { - proxyPkg = proxy.getPackageName(); - proxyAttributionTag = proxy.getAttributionTag(); - proxyUid = proxy.getUid(); - } - - if (accessTime > 0) { - pw.print(prefix); - pw.print("Access: "); - pw.print(AppOpsManager.keyToString(key)); - pw.print(" "); - date.setTime(accessTime); - pw.print(sdf.format(date)); - pw.print(" ("); - TimeUtils.formatDuration(accessTime - now, pw); - pw.print(")"); - if (accessDuration > 0) { - pw.print(" duration="); - TimeUtils.formatDuration(accessDuration, pw); - } - if (proxyUid >= 0) { - pw.print(" proxy["); - pw.print("uid="); - pw.print(proxyUid); - pw.print(", pkg="); - pw.print(proxyPkg); - pw.print(", attributionTag="); - pw.print(proxyAttributionTag); - pw.print("]"); - } - pw.println(); - } - - if (rejectTime > 0) { - pw.print(prefix); - pw.print("Reject: "); - pw.print(AppOpsManager.keyToString(key)); - date.setTime(rejectTime); - pw.print(sdf.format(date)); - pw.print(" ("); - TimeUtils.formatDuration(rejectTime - now, pw); - pw.print(")"); - if (proxyUid >= 0) { - pw.print(" proxy["); - pw.print("uid="); - pw.print(proxyUid); - pw.print(", pkg="); - pw.print(proxyPkg); - pw.print(", attributionTag="); - pw.print(proxyAttributionTag); - pw.print("]"); - } - pw.println(); - } - } - - final AttributedOp attributedOp = op.mAttributions.get(attributionTag); - if (attributedOp.isRunning()) { - long earliestElapsedTime = Long.MAX_VALUE; - long maxNumStarts = 0; - int numInProgressEvents = attributedOp.mInProgressEvents.size(); - for (int i = 0; i < numInProgressEvents; i++) { - AttributedOp.InProgressStartOpEvent event = - attributedOp.mInProgressEvents.valueAt(i); - - earliestElapsedTime = Math.min(earliestElapsedTime, event.getStartElapsedTime()); - maxNumStarts = Math.max(maxNumStarts, event.mNumUnfinishedStarts); - } - - pw.print(prefix + "Running start at: "); - TimeUtils.formatDuration(nowElapsed - earliestElapsedTime, pw); - pw.println(); - - if (maxNumStarts > 1) { - pw.print(prefix + "startNesting="); - pw.println(maxNumStarts); - } - } - } - - @NeverCompile // Avoid size overhead of debugging code. - @Override - public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { - if (!DumpUtils.checkDumpAndUsageStatsPermission(mContext, TAG, pw)) return; - - int dumpOp = OP_NONE; - String dumpPackage = null; - String dumpAttributionTag = null; - int dumpUid = Process.INVALID_UID; - int dumpMode = -1; - boolean dumpWatchers = false; - // TODO ntmyren: Remove the dumpHistory and dumpFilter - boolean dumpHistory = false; - boolean includeDiscreteOps = false; - boolean dumpUidStateChangeLogs = false; - int nDiscreteOps = 10; - @HistoricalOpsRequestFilter int dumpFilter = 0; - boolean dumpAll = false; - - if (args != null) { - for (int i = 0; i < args.length; i++) { - String arg = args[i]; - if ("-h".equals(arg)) { - dumpHelp(pw); - return; - } else if ("-a".equals(arg)) { - // dump all data - dumpAll = true; - } else if ("--op".equals(arg)) { - i++; - if (i >= args.length) { - pw.println("No argument for --op option"); - return; - } - dumpOp = AppOpsService.Shell.strOpToOp(args[i], pw); - dumpFilter |= FILTER_BY_OP_NAMES; - if (dumpOp < 0) { - return; - } - } else if ("--package".equals(arg)) { - i++; - if (i >= args.length) { - pw.println("No argument for --package option"); - return; - } - dumpPackage = args[i]; - dumpFilter |= FILTER_BY_PACKAGE_NAME; - try { - dumpUid = AppGlobals.getPackageManager().getPackageUid(dumpPackage, - PackageManager.MATCH_KNOWN_PACKAGES | PackageManager.MATCH_INSTANT, - 0); - } catch (RemoteException e) { - } - if (dumpUid < 0) { - pw.println("Unknown package: " + dumpPackage); - return; - } - dumpUid = UserHandle.getAppId(dumpUid); - dumpFilter |= FILTER_BY_UID; - } else if ("--attributionTag".equals(arg)) { - i++; - if (i >= args.length) { - pw.println("No argument for --attributionTag option"); - return; - } - dumpAttributionTag = args[i]; - dumpFilter |= FILTER_BY_ATTRIBUTION_TAG; - } else if ("--mode".equals(arg)) { - i++; - if (i >= args.length) { - pw.println("No argument for --mode option"); - return; - } - dumpMode = AppOpsService.Shell.strModeToMode(args[i], pw); - if (dumpMode < 0) { - return; - } - } else if ("--watchers".equals(arg)) { - dumpWatchers = true; - } else if ("--include-discrete".equals(arg)) { - i++; - if (i >= args.length) { - pw.println("No argument for --include-discrete option"); - return; - } - try { - nDiscreteOps = Integer.valueOf(args[i]); - } catch (NumberFormatException e) { - pw.println("Wrong parameter: " + args[i]); - return; - } - includeDiscreteOps = true; - } else if ("--history".equals(arg)) { - dumpHistory = true; - } else if (arg.length() > 0 && arg.charAt(0) == '-') { - pw.println("Unknown option: " + arg); - return; - } else if ("--uid-state-changes".equals(arg)) { - dumpUidStateChangeLogs = true; - } else { - pw.println("Unknown command: " + arg); - return; - } - } - } - - final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); - final Date date = new Date(); - synchronized (this) { - pw.println("Current AppOps Service state:"); - if (!dumpHistory && !dumpWatchers) { - mConstants.dump(pw); - } - pw.println(); - final long now = System.currentTimeMillis(); - final long nowElapsed = SystemClock.elapsedRealtime(); - boolean needSep = false; - if (dumpFilter == 0 && dumpMode < 0 && mProfileOwners != null && !dumpWatchers - && !dumpHistory) { - pw.println(" Profile owners:"); - for (int poi = 0; poi < mProfileOwners.size(); poi++) { - pw.print(" User #"); - pw.print(mProfileOwners.keyAt(poi)); - pw.print(": "); - UserHandle.formatUid(pw, mProfileOwners.valueAt(poi)); - pw.println(); - } - pw.println(); - } - - if (!dumpHistory) { - needSep |= mAppOpsServiceInterface.dumpListeners(dumpOp, dumpUid, dumpPackage, pw); - } - - if (mModeWatchers.size() > 0 && dumpOp < 0 && !dumpHistory) { - boolean printedHeader = false; - for (int i = 0; i < mModeWatchers.size(); i++) { - final ModeCallback cb = mModeWatchers.valueAt(i); - if (dumpPackage != null - && dumpUid != UserHandle.getAppId(cb.getWatchingUid())) { - continue; - } - needSep = true; - if (!printedHeader) { - pw.println(" All op mode watchers:"); - printedHeader = true; - } - pw.print(" "); - pw.print(Integer.toHexString(System.identityHashCode(mModeWatchers.keyAt(i)))); - pw.print(": "); - pw.println(cb); - } - } - if (mActiveWatchers.size() > 0 && dumpMode < 0) { - needSep = true; - boolean printedHeader = false; - for (int watcherNum = 0; watcherNum < mActiveWatchers.size(); watcherNum++) { - final SparseArray<ActiveCallback> activeWatchers = - mActiveWatchers.valueAt(watcherNum); - if (activeWatchers.size() <= 0) { - continue; - } - final ActiveCallback cb = activeWatchers.valueAt(0); - if (dumpOp >= 0 && activeWatchers.indexOfKey(dumpOp) < 0) { - continue; - } - if (dumpPackage != null - && dumpUid != UserHandle.getAppId(cb.mWatchingUid)) { - continue; - } - if (!printedHeader) { - pw.println(" All op active watchers:"); - printedHeader = true; - } - pw.print(" "); - pw.print(Integer.toHexString(System.identityHashCode( - mActiveWatchers.keyAt(watcherNum)))); - pw.println(" ->"); - pw.print(" ["); - final int opCount = activeWatchers.size(); - for (int opNum = 0; opNum < opCount; opNum++) { - if (opNum > 0) { - pw.print(' '); - } - pw.print(AppOpsManager.opToName(activeWatchers.keyAt(opNum))); - if (opNum < opCount - 1) { - pw.print(','); - } - } - pw.println("]"); - pw.print(" "); - pw.println(cb); - } - } - if (mStartedWatchers.size() > 0 && dumpMode < 0) { - needSep = true; - boolean printedHeader = false; - - final int watchersSize = mStartedWatchers.size(); - for (int watcherNum = 0; watcherNum < watchersSize; watcherNum++) { - final SparseArray<StartedCallback> startedWatchers = - mStartedWatchers.valueAt(watcherNum); - if (startedWatchers.size() <= 0) { - continue; - } - - final StartedCallback cb = startedWatchers.valueAt(0); - if (dumpOp >= 0 && startedWatchers.indexOfKey(dumpOp) < 0) { - continue; - } - - if (dumpPackage != null - && dumpUid != UserHandle.getAppId(cb.mWatchingUid)) { - continue; - } - - if (!printedHeader) { - pw.println(" All op started watchers:"); - printedHeader = true; - } - - pw.print(" "); - pw.print(Integer.toHexString(System.identityHashCode( - mStartedWatchers.keyAt(watcherNum)))); - pw.println(" ->"); - - pw.print(" ["); - final int opCount = startedWatchers.size(); - for (int opNum = 0; opNum < opCount; opNum++) { - if (opNum > 0) { - pw.print(' '); - } - - pw.print(AppOpsManager.opToName(startedWatchers.keyAt(opNum))); - if (opNum < opCount - 1) { - pw.print(','); - } - } - pw.println("]"); - - pw.print(" "); - pw.println(cb); - } - } - if (mNotedWatchers.size() > 0 && dumpMode < 0) { - needSep = true; - boolean printedHeader = false; - for (int watcherNum = 0; watcherNum < mNotedWatchers.size(); watcherNum++) { - final SparseArray<NotedCallback> notedWatchers = - mNotedWatchers.valueAt(watcherNum); - if (notedWatchers.size() <= 0) { - continue; - } - final NotedCallback cb = notedWatchers.valueAt(0); - if (dumpOp >= 0 && notedWatchers.indexOfKey(dumpOp) < 0) { - continue; - } - if (dumpPackage != null - && dumpUid != UserHandle.getAppId(cb.mWatchingUid)) { - continue; - } - if (!printedHeader) { - pw.println(" All op noted watchers:"); - printedHeader = true; - } - pw.print(" "); - pw.print(Integer.toHexString(System.identityHashCode( - mNotedWatchers.keyAt(watcherNum)))); - pw.println(" ->"); - pw.print(" ["); - final int opCount = notedWatchers.size(); - for (int opNum = 0; opNum < opCount; opNum++) { - if (opNum > 0) { - pw.print(' '); - } - pw.print(AppOpsManager.opToName(notedWatchers.keyAt(opNum))); - if (opNum < opCount - 1) { - pw.print(','); - } - } - pw.println("]"); - pw.print(" "); - pw.println(cb); - } - } - if (needSep) { - pw.println(); - } - for (int i = 0; i < mUidStates.size(); i++) { - UidState uidState = mUidStates.valueAt(i); - final SparseIntArray opModes = uidState.getNonDefaultUidModes(); - final ArrayMap<String, Ops> pkgOps = uidState.pkgOps; - - if (dumpWatchers || dumpHistory) { - continue; - } - if (dumpOp >= 0 || dumpPackage != null || dumpMode >= 0) { - boolean hasOp = dumpOp < 0 || (opModes != null - && opModes.indexOfKey(dumpOp) >= 0); - boolean hasPackage = dumpPackage == null || dumpUid == mUidStates.keyAt(i); - boolean hasMode = dumpMode < 0; - if (!hasMode && opModes != null) { - for (int opi = 0; !hasMode && opi < opModes.size(); opi++) { - if (opModes.valueAt(opi) == dumpMode) { - hasMode = true; - } - } - } - if (pkgOps != null) { - for (int pkgi = 0; - (!hasOp || !hasPackage || !hasMode) && pkgi < pkgOps.size(); - pkgi++) { - Ops ops = pkgOps.valueAt(pkgi); - if (!hasOp && ops != null && ops.indexOfKey(dumpOp) >= 0) { - hasOp = true; - } - if (!hasMode) { - for (int opi = 0; !hasMode && opi < ops.size(); opi++) { - if (ops.valueAt(opi).getMode() == dumpMode) { - hasMode = true; - } - } - } - if (!hasPackage && dumpPackage.equals(ops.packageName)) { - hasPackage = true; - } - } - } - if (uidState.foregroundOps != null && !hasOp) { - if (uidState.foregroundOps.indexOfKey(dumpOp) > 0) { - hasOp = true; - } - } - if (!hasOp || !hasPackage || !hasMode) { - continue; - } - } - - pw.print(" Uid "); - UserHandle.formatUid(pw, uidState.uid); - pw.println(":"); - uidState.dump(pw, nowElapsed); - if (uidState.foregroundOps != null && (dumpMode < 0 - || dumpMode == AppOpsManager.MODE_FOREGROUND)) { - pw.println(" foregroundOps:"); - for (int j = 0; j < uidState.foregroundOps.size(); j++) { - if (dumpOp >= 0 && dumpOp != uidState.foregroundOps.keyAt(j)) { - continue; - } - pw.print(" "); - pw.print(AppOpsManager.opToName(uidState.foregroundOps.keyAt(j))); - pw.print(": "); - pw.println(uidState.foregroundOps.valueAt(j) ? "WATCHER" : "SILENT"); - } - pw.print(" hasForegroundWatchers="); - pw.println(uidState.hasForegroundWatchers); - } - needSep = true; - - if (opModes != null) { - final int opModeCount = opModes.size(); - for (int j = 0; j < opModeCount; j++) { - final int code = opModes.keyAt(j); - final int mode = opModes.valueAt(j); - if (dumpOp >= 0 && dumpOp != code) { - continue; - } - if (dumpMode >= 0 && dumpMode != mode) { - continue; - } - pw.print(" "); - pw.print(AppOpsManager.opToName(code)); - pw.print(": mode="); - pw.println(AppOpsManager.modeToName(mode)); - } - } - - if (pkgOps == null) { - continue; - } - - for (int pkgi = 0; pkgi < pkgOps.size(); pkgi++) { - final Ops ops = pkgOps.valueAt(pkgi); - if (dumpPackage != null && !dumpPackage.equals(ops.packageName)) { - continue; - } - boolean printedPackage = false; - for (int j = 0; j < ops.size(); j++) { - final Op op = ops.valueAt(j); - final int opCode = op.op; - if (dumpOp >= 0 && dumpOp != opCode) { - continue; - } - if (dumpMode >= 0 && dumpMode != op.getMode()) { - continue; - } - if (!printedPackage) { - pw.print(" Package "); - pw.print(ops.packageName); - pw.println(":"); - printedPackage = true; - } - pw.print(" "); - pw.print(AppOpsManager.opToName(opCode)); - pw.print(" ("); - pw.print(AppOpsManager.modeToName(op.getMode())); - final int switchOp = AppOpsManager.opToSwitch(opCode); - if (switchOp != opCode) { - pw.print(" / switch "); - pw.print(AppOpsManager.opToName(switchOp)); - final Op switchObj = ops.get(switchOp); - int mode = switchObj == null - ? AppOpsManager.opToDefaultMode(switchOp) : switchObj.getMode(); - pw.print("="); - pw.print(AppOpsManager.modeToName(mode)); - } - pw.println("): "); - dumpStatesLocked(pw, dumpAttributionTag, dumpFilter, nowElapsed, op, now, - sdf, date, " "); - } - } - } - if (needSep) { - pw.println(); - } - - boolean showUserRestrictions = !(dumpMode < 0 && !dumpWatchers && !dumpHistory); - mAppOpsRestrictions.dumpRestrictions(pw, dumpOp, dumpPackage, showUserRestrictions); - - if (dumpAll || dumpUidStateChangeLogs) { - pw.println(); - pw.println("Uid State Changes Event Log:"); - getUidStateTracker().dumpEvents(pw); - } - } - - // Must not hold the appops lock - if (dumpHistory && !dumpWatchers) { - mHistoricalRegistry.dump(" ", pw, dumpUid, dumpPackage, dumpAttributionTag, dumpOp, - dumpFilter); - } - if (includeDiscreteOps) { - pw.println("Discrete accesses: "); - mHistoricalRegistry.dumpDiscreteData(pw, dumpUid, dumpPackage, dumpAttributionTag, - dumpFilter, dumpOp, sdf, date, " ", nDiscreteOps); - } - } - - @Override - public void setUserRestrictions(Bundle restrictions, IBinder token, int userHandle) { - checkSystemUid("setUserRestrictions"); - Objects.requireNonNull(restrictions); - Objects.requireNonNull(token); - for (int i = 0; i < AppOpsManager._NUM_OP; i++) { - String restriction = AppOpsManager.opToRestriction(i); - if (restriction != null) { - setUserRestrictionNoCheck(i, restrictions.getBoolean(restriction, false), token, - userHandle, null); - } - } - } - - @Override - public void setUserRestriction(int code, boolean restricted, IBinder token, int userHandle, - PackageTagsList excludedPackageTags) { - if (Binder.getCallingPid() != Process.myPid()) { - mContext.enforcePermission(Manifest.permission.MANAGE_APP_OPS_RESTRICTIONS, - Binder.getCallingPid(), Binder.getCallingUid(), null); - } - if (userHandle != UserHandle.getCallingUserId()) { - if (mContext.checkCallingOrSelfPermission(Manifest.permission - .INTERACT_ACROSS_USERS_FULL) != PackageManager.PERMISSION_GRANTED - && mContext.checkCallingOrSelfPermission(Manifest.permission - .INTERACT_ACROSS_USERS) != PackageManager.PERMISSION_GRANTED) { - throw new SecurityException("Need INTERACT_ACROSS_USERS_FULL or" - + " INTERACT_ACROSS_USERS to interact cross user "); - } - } - verifyIncomingOp(code); - Objects.requireNonNull(token); - setUserRestrictionNoCheck(code, restricted, token, userHandle, excludedPackageTags); - } - - private void setUserRestrictionNoCheck(int code, boolean restricted, IBinder token, - int userHandle, PackageTagsList excludedPackageTags) { - synchronized (AppOpsServiceImpl.this) { - ClientUserRestrictionState restrictionState = mOpUserRestrictions.get(token); - - if (restrictionState == null) { - try { - restrictionState = new ClientUserRestrictionState(token); - } catch (RemoteException e) { - return; - } - mOpUserRestrictions.put(token, restrictionState); - } - - if (restrictionState.setRestriction(code, restricted, excludedPackageTags, - userHandle)) { - mHandler.sendMessage(PooledLambda.obtainMessage( - AppOpsServiceImpl::notifyWatchersOfChange, this, code, UID_ANY)); - mHandler.sendMessage(PooledLambda.obtainMessage( - AppOpsServiceImpl::updateStartedOpModeForUser, this, code, - restricted, userHandle)); - } - - if (restrictionState.isDefault()) { - mOpUserRestrictions.remove(token); - restrictionState.destroy(); - } - } - } - - @Override - public void setGlobalRestriction(int code, boolean restricted, IBinder token) { - if (Binder.getCallingPid() != Process.myPid()) { - throw new SecurityException("Only the system can set global restrictions"); - } - - synchronized (this) { - ClientGlobalRestrictionState restrictionState = mOpGlobalRestrictions.get(token); - - if (restrictionState == null) { - try { - restrictionState = new ClientGlobalRestrictionState(token); - } catch (RemoteException e) { - return; - } - mOpGlobalRestrictions.put(token, restrictionState); - } - - if (restrictionState.setRestriction(code, restricted)) { - mHandler.sendMessage(PooledLambda.obtainMessage( - AppOpsServiceImpl::notifyWatchersOfChange, this, code, UID_ANY)); - mHandler.sendMessage(PooledLambda.obtainMessage( - AppOpsServiceImpl::updateStartedOpModeForUser, this, code, - restricted, UserHandle.USER_ALL)); - } - - if (restrictionState.isDefault()) { - mOpGlobalRestrictions.remove(token); - restrictionState.destroy(); - } - } - } - - @Override - public int getOpRestrictionCount(int code, UserHandle user, String pkg, - String attributionTag) { - int number = 0; - synchronized (this) { - int numRestrictions = mOpUserRestrictions.size(); - for (int i = 0; i < numRestrictions; i++) { - if (mOpUserRestrictions.valueAt(i) - .hasRestriction(code, pkg, attributionTag, user.getIdentifier(), - false)) { - number++; - } - } - - numRestrictions = mOpGlobalRestrictions.size(); - for (int i = 0; i < numRestrictions; i++) { - if (mOpGlobalRestrictions.valueAt(i).hasRestriction(code)) { - number++; - } - } - } - - return number; - } - - private void updateStartedOpModeForUser(int code, boolean restricted, int userId) { - synchronized (AppOpsServiceImpl.this) { - int numUids = mUidStates.size(); - for (int uidNum = 0; uidNum < numUids; uidNum++) { - int uid = mUidStates.keyAt(uidNum); - if (userId != UserHandle.USER_ALL && UserHandle.getUserId(uid) != userId) { - continue; - } - updateStartedOpModeForUidLocked(code, restricted, uid); - } - } - } - - private void updateStartedOpModeForUidLocked(int code, boolean restricted, int uid) { - UidState uidState = mUidStates.get(uid); - if (uidState == null || uidState.pkgOps == null) { - return; - } - - int numPkgOps = uidState.pkgOps.size(); - for (int pkgNum = 0; pkgNum < numPkgOps; pkgNum++) { - Ops ops = uidState.pkgOps.valueAt(pkgNum); - Op op = ops != null ? ops.get(code) : null; - if (op == null || (op.getMode() != MODE_ALLOWED && op.getMode() != MODE_FOREGROUND)) { - continue; - } - int numAttrTags = op.mAttributions.size(); - for (int attrNum = 0; attrNum < numAttrTags; attrNum++) { - AttributedOp attrOp = op.mAttributions.valueAt(attrNum); - if (restricted && attrOp.isRunning()) { - attrOp.pause(); - } else if (attrOp.isPaused()) { - attrOp.resume(); - } - } - } - } - - @Override - public void notifyWatchersOfChange(int code, int uid) { - final ArraySet<OnOpModeChangedListener> modeChangedListenerSet; - synchronized (this) { - modeChangedListenerSet = mAppOpsServiceInterface.getOpModeChangedListeners(code); - if (modeChangedListenerSet == null) { - return; - } - } - - notifyOpChanged(modeChangedListenerSet, code, uid, null); - } - - @Override - public void removeUser(int userHandle) throws RemoteException { - checkSystemUid("removeUser"); - synchronized (AppOpsServiceImpl.this) { - final int tokenCount = mOpUserRestrictions.size(); - for (int i = tokenCount - 1; i >= 0; i--) { - ClientUserRestrictionState opRestrictions = mOpUserRestrictions.valueAt(i); - opRestrictions.removeUser(userHandle); - } - removeUidsForUserLocked(userHandle); - } - } - - @Override - public boolean isOperationActive(int code, int uid, String packageName) { - if (Binder.getCallingUid() != uid) { - if (mContext.checkCallingOrSelfPermission(Manifest.permission.WATCH_APPOPS) - != PackageManager.PERMISSION_GRANTED) { - return false; - } - } - verifyIncomingOp(code); - if (!isIncomingPackageValid(packageName, UserHandle.getUserId(uid))) { - return false; - } - - final String resolvedPackageName = AppOpsManager.resolvePackageName(uid, packageName); - if (resolvedPackageName == null) { - return false; - } - // TODO moltmann: Allow to check for attribution op activeness - synchronized (AppOpsServiceImpl.this) { - Ops pkgOps = getOpsLocked(uid, resolvedPackageName, null, false, null, false); - if (pkgOps == null) { - return false; - } - - Op op = pkgOps.get(code); - if (op == null) { - return false; - } - - return op.isRunning(); - } - } - - @Override - public boolean isProxying(int op, @NonNull String proxyPackageName, - @NonNull String proxyAttributionTag, int proxiedUid, - @NonNull String proxiedPackageName) { - Objects.requireNonNull(proxyPackageName); - Objects.requireNonNull(proxiedPackageName); - final long callingUid = Binder.getCallingUid(); - final long identity = Binder.clearCallingIdentity(); - try { - final List<AppOpsManager.PackageOps> packageOps = getOpsForPackage(proxiedUid, - proxiedPackageName, new int[]{op}); - if (packageOps == null || packageOps.isEmpty()) { - return false; - } - final List<OpEntry> opEntries = packageOps.get(0).getOps(); - if (opEntries.isEmpty()) { - return false; - } - final OpEntry opEntry = opEntries.get(0); - if (!opEntry.isRunning()) { - return false; - } - final OpEventProxyInfo proxyInfo = opEntry.getLastProxyInfo( - OP_FLAG_TRUSTED_PROXIED | AppOpsManager.OP_FLAG_UNTRUSTED_PROXIED); - return proxyInfo != null && callingUid == proxyInfo.getUid() - && proxyPackageName.equals(proxyInfo.getPackageName()) - && Objects.equals(proxyAttributionTag, proxyInfo.getAttributionTag()); - } finally { - Binder.restoreCallingIdentity(identity); - } - } - - @Override - public void resetPackageOpsNoHistory(@NonNull String packageName) { - mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_APPOPS, - "resetPackageOpsNoHistory"); - synchronized (AppOpsServiceImpl.this) { - final int uid = mPackageManagerInternal.getPackageUid(packageName, 0, - UserHandle.getCallingUserId()); - if (uid == Process.INVALID_UID) { - return; - } - UidState uidState = mUidStates.get(uid); - if (uidState == null || uidState.pkgOps == null) { - return; - } - Ops removedOps = uidState.pkgOps.remove(packageName); - mAppOpsServiceInterface.removePackage(packageName, UserHandle.getUserId(uid)); - if (removedOps != null) { - scheduleFastWriteLocked(); - } - } - } - - @Override - public void setHistoryParameters(@AppOpsManager.HistoricalMode int mode, - long baseSnapshotInterval, int compressionStep) { - mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_APPOPS, - "setHistoryParameters"); - // Must not hold the appops lock - mHistoricalRegistry.setHistoryParameters(mode, baseSnapshotInterval, compressionStep); - } - - @Override - public void offsetHistory(long offsetMillis) { - mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_APPOPS, - "offsetHistory"); - // Must not hold the appops lock - mHistoricalRegistry.offsetHistory(offsetMillis); - mHistoricalRegistry.offsetDiscreteHistory(offsetMillis); - } - - @Override - public void addHistoricalOps(HistoricalOps ops) { - mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_APPOPS, - "addHistoricalOps"); - // Must not hold the appops lock - mHistoricalRegistry.addHistoricalOps(ops); - } - - @Override - public void resetHistoryParameters() { - mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_APPOPS, - "resetHistoryParameters"); - // Must not hold the appops lock - mHistoricalRegistry.resetHistoryParameters(); - } - - @Override - public void clearHistory() { - mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_APPOPS, - "clearHistory"); - // Must not hold the appops lock - mHistoricalRegistry.clearAllHistory(); - } - - @Override - public void rebootHistory(long offlineDurationMillis) { - mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_APPOPS, - "rebootHistory"); - - Preconditions.checkArgument(offlineDurationMillis >= 0); - - // Must not hold the appops lock - mHistoricalRegistry.shutdown(); - - if (offlineDurationMillis > 0) { - SystemClock.sleep(offlineDurationMillis); - } - - mHistoricalRegistry = new HistoricalRegistry(mHistoricalRegistry); - mHistoricalRegistry.systemReady(mContext.getContentResolver()); - mHistoricalRegistry.persistPendingHistory(); - } - - @GuardedBy("this") - private void removeUidsForUserLocked(int userHandle) { - for (int i = mUidStates.size() - 1; i >= 0; --i) { - final int uid = mUidStates.keyAt(i); - if (UserHandle.getUserId(uid) == userHandle) { - mUidStates.valueAt(i).clear(); - mUidStates.removeAt(i); - } - } - } - - private void checkSystemUid(String function) { - int uid = Binder.getCallingUid(); - if (uid != Process.SYSTEM_UID) { - throw new SecurityException(function + " must by called by the system"); - } - } - - private static int resolveUid(String packageName) { - if (packageName == null) { - return Process.INVALID_UID; - } - switch (packageName) { - case "root": - return Process.ROOT_UID; - case "shell": - case "dumpstate": - return Process.SHELL_UID; - case "media": - return Process.MEDIA_UID; - case "audioserver": - return Process.AUDIOSERVER_UID; - case "cameraserver": - return Process.CAMERASERVER_UID; - } - return Process.INVALID_UID; - } - - private static String[] getPackagesForUid(int uid) { - String[] packageNames = null; - - // Very early during boot the package manager is not yet or not yet fully started. At this - // time there are no packages yet. - if (AppGlobals.getPackageManager() != null) { - try { - packageNames = AppGlobals.getPackageManager().getPackagesForUid(uid); - } catch (RemoteException e) { - /* ignore - local call */ - } - } - if (packageNames == null) { - return EmptyArray.STRING; - } - return packageNames; - } - - private final class ClientUserRestrictionState implements DeathRecipient { - private final IBinder mToken; - - ClientUserRestrictionState(IBinder token) - throws RemoteException { - token.linkToDeath(this, 0); - this.mToken = token; - } - - public boolean setRestriction(int code, boolean restricted, - PackageTagsList excludedPackageTags, int userId) { - return mAppOpsRestrictions.setUserRestriction(mToken, userId, code, - restricted, excludedPackageTags); - } - - public boolean hasRestriction(int code, String packageName, String attributionTag, - int userId, boolean isCheckOp) { - return mAppOpsRestrictions.getUserRestriction(mToken, userId, code, packageName, - attributionTag, isCheckOp); - } - - public void removeUser(int userId) { - mAppOpsRestrictions.clearUserRestrictions(mToken, userId); - } - - public boolean isDefault() { - return !mAppOpsRestrictions.hasUserRestrictions(mToken); - } - - @Override - public void binderDied() { - synchronized (AppOpsServiceImpl.this) { - mAppOpsRestrictions.clearUserRestrictions(mToken); - mOpUserRestrictions.remove(mToken); - destroy(); - } - } - - public void destroy() { - mToken.unlinkToDeath(this, 0); - } - } - - private final class ClientGlobalRestrictionState implements DeathRecipient { - final IBinder mToken; - - ClientGlobalRestrictionState(IBinder token) - throws RemoteException { - token.linkToDeath(this, 0); - this.mToken = token; - } - - boolean setRestriction(int code, boolean restricted) { - return mAppOpsRestrictions.setGlobalRestriction(mToken, code, restricted); - } - - boolean hasRestriction(int code) { - return mAppOpsRestrictions.getGlobalRestriction(mToken, code); - } - - boolean isDefault() { - return !mAppOpsRestrictions.hasGlobalRestrictions(mToken); - } - - @Override - public void binderDied() { - mAppOpsRestrictions.clearGlobalRestrictions(mToken); - mOpGlobalRestrictions.remove(mToken); - destroy(); - } - - void destroy() { - mToken.unlinkToDeath(this, 0); - } - } - - @Override - public void setDeviceAndProfileOwners(SparseIntArray owners) { - synchronized (this) { - mProfileOwners = owners; - } - } -} diff --git a/services/core/java/com/android/server/appop/AppOpsServiceInterface.java b/services/core/java/com/android/server/appop/AppOpsServiceInterface.java deleted file mode 100644 index 8420fcbd346f..000000000000 --- a/services/core/java/com/android/server/appop/AppOpsServiceInterface.java +++ /dev/null @@ -1,494 +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.server.appop; - -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.app.ActivityManager; -import android.app.AppOpsManager; -import android.content.AttributionSource; -import android.os.Bundle; -import android.os.IBinder; -import android.os.PackageTagsList; -import android.os.RemoteCallback; -import android.os.RemoteException; -import android.os.UserHandle; -import android.util.SparseArray; -import android.util.SparseIntArray; - -import com.android.internal.app.IAppOpsActiveCallback; -import com.android.internal.app.IAppOpsCallback; -import com.android.internal.app.IAppOpsNotedCallback; -import com.android.internal.app.IAppOpsStartedCallback; - -import dalvik.annotation.optimization.NeverCompile; - -import java.io.FileDescriptor; -import java.io.PrintWriter; -import java.util.List; - -/** - * - */ -public interface AppOpsServiceInterface extends PersistenceScheduler { - - /** - * - */ - void systemReady(); - - /** - * - */ - void shutdown(); - - /** - * - * @param uid - * @param packageName - */ - void verifyPackage(int uid, String packageName); - - /** - * - * @param op - * @param packageName - * @param flags - * @param callback - */ - void startWatchingModeWithFlags(int op, String packageName, int flags, - IAppOpsCallback callback); - - /** - * - * @param callback - */ - void stopWatchingMode(IAppOpsCallback callback); - - /** - * - * @param ops - * @param callback - */ - void startWatchingActive(int[] ops, IAppOpsActiveCallback callback); - - /** - * - * @param callback - */ - void stopWatchingActive(IAppOpsActiveCallback callback); - - /** - * - * @param ops - * @param callback - */ - void startWatchingStarted(int[] ops, @NonNull IAppOpsStartedCallback callback); - - /** - * - * @param callback - */ - void stopWatchingStarted(IAppOpsStartedCallback callback); - - /** - * - * @param ops - * @param callback - */ - void startWatchingNoted(@NonNull int[] ops, @NonNull IAppOpsNotedCallback callback); - - /** - * - * @param callback - */ - void stopWatchingNoted(IAppOpsNotedCallback callback); - - /** - * @param clientId - * @param code - * @param uid - * @param packageName - * @param attributionTag - * @param startIfModeDefault - * @param message - * @param attributionFlags - * @param attributionChainId - * @return - */ - int startOperation(@NonNull IBinder clientId, int code, int uid, - @Nullable String packageName, @Nullable String attributionTag, - boolean startIfModeDefault, @NonNull String message, - @AppOpsManager.AttributionFlags int attributionFlags, - int attributionChainId); - - - int startOperationUnchecked(IBinder clientId, int code, int uid, @NonNull String packageName, - @Nullable String attributionTag, int proxyUid, String proxyPackageName, - @Nullable String proxyAttributionTag, @AppOpsManager.OpFlags int flags, - boolean startIfModeDefault, @AppOpsManager.AttributionFlags int attributionFlags, - int attributionChainId, boolean dryRun); - - /** - * - * @param clientId - * @param code - * @param uid - * @param packageName - * @param attributionTag - */ - void finishOperation(IBinder clientId, int code, int uid, String packageName, - String attributionTag); - - /** - * - * @param clientId - * @param code - * @param uid - * @param packageName - * @param attributionTag - */ - void finishOperationUnchecked(IBinder clientId, int code, int uid, String packageName, - String attributionTag); - - /** - * - * @param uidPackageNames - * @param visible - */ - void updateAppWidgetVisibility(SparseArray<String> uidPackageNames, boolean visible); - - /** - * - */ - void readState(); - - /** - * - */ - void writeState(); - - /** - * - * @param uid - * @param packageName - */ - void packageRemoved(int uid, String packageName); - - /** - * - * @param uid - */ - void uidRemoved(int uid); - - /** - * - * @param uid - * @param procState - * @param capability - */ - void updateUidProcState(int uid, int procState, - @ActivityManager.ProcessCapability int capability); - - /** - * - * @param ops - * @return - */ - List<AppOpsManager.PackageOps> getPackagesForOps(int[] ops); - - /** - * - * @param uid - * @param packageName - * @param ops - * @return - */ - List<AppOpsManager.PackageOps> getOpsForPackage(int uid, String packageName, - int[] ops); - - /** - * - * @param uid - * @param packageName - * @param attributionTag - * @param opNames - * @param dataType - * @param filter - * @param beginTimeMillis - * @param endTimeMillis - * @param flags - * @param callback - */ - void getHistoricalOps(int uid, String packageName, String attributionTag, - List<String> opNames, int dataType, int filter, long beginTimeMillis, - long endTimeMillis, int flags, RemoteCallback callback); - - /** - * - * @param uid - * @param packageName - * @param attributionTag - * @param opNames - * @param dataType - * @param filter - * @param beginTimeMillis - * @param endTimeMillis - * @param flags - * @param callback - */ - void getHistoricalOpsFromDiskRaw(int uid, String packageName, String attributionTag, - List<String> opNames, int dataType, int filter, long beginTimeMillis, - long endTimeMillis, int flags, RemoteCallback callback); - - /** - * - */ - void reloadNonHistoricalState(); - - /** - * - * @param uid - * @param ops - * @return - */ - List<AppOpsManager.PackageOps> getUidOps(int uid, int[] ops); - - /** - * - * @param owners - */ - void setDeviceAndProfileOwners(SparseIntArray owners); - - // used in audio restriction calls, might just copy the logic to avoid having this call. - /** - * - * @param callingPid - * @param callingUid - * @param targetUid - */ - void enforceManageAppOpsModes(int callingPid, int callingUid, int targetUid); - - /** - * - * @param code - * @param uid - * @param mode - * @param permissionPolicyCallback - */ - void setUidMode(int code, int uid, int mode, - @Nullable IAppOpsCallback permissionPolicyCallback); - - /** - * - * @param code - * @param uid - * @param packageName - * @param mode - * @param permissionPolicyCallback - */ - void setMode(int code, int uid, @NonNull String packageName, int mode, - @Nullable IAppOpsCallback permissionPolicyCallback); - - /** - * - * @param reqUserId - * @param reqPackageName - */ - void resetAllModes(int reqUserId, String reqPackageName); - - /** - * - * @param code - * @param uid - * @param packageName - * @param attributionTag - * @param raw - * @return - */ - int checkOperation(int code, int uid, String packageName, - @Nullable String attributionTag, boolean raw); - - /** - * - * @param uid - * @param packageName - * @return - */ - int checkPackage(int uid, String packageName); - - /** - * - * @param code - * @param uid - * @param packageName - * @param attributionTag - * @param message - * @return - */ - int noteOperation(int code, int uid, @Nullable String packageName, - @Nullable String attributionTag, @Nullable String message); - - /** - * - * @param code - * @param uid - * @param packageName - * @param attributionTag - * @param proxyUid - * @param proxyPackageName - * @param proxyAttributionTag - * @param flags - * @return - */ - @AppOpsManager.Mode - int noteOperationUnchecked(int code, int uid, @NonNull String packageName, - @Nullable String attributionTag, int proxyUid, String proxyPackageName, - @Nullable String proxyAttributionTag, @AppOpsManager.OpFlags int flags); - - boolean isAttributionTagValid(int uid, @NonNull String packageName, - @Nullable String attributionTag, @Nullable String proxyPackageName); - - /** - * - * @param fd - * @param pw - * @param args - */ - @NeverCompile - // Avoid size overhead of debugging code. - void dump(FileDescriptor fd, PrintWriter pw, String[] args); - - /** - * - * @param restrictions - * @param token - * @param userHandle - */ - void setUserRestrictions(Bundle restrictions, IBinder token, int userHandle); - - /** - * - * @param code - * @param restricted - * @param token - * @param userHandle - * @param excludedPackageTags - */ - void setUserRestriction(int code, boolean restricted, IBinder token, int userHandle, - PackageTagsList excludedPackageTags); - - /** - * - * @param code - * @param restricted - * @param token - */ - void setGlobalRestriction(int code, boolean restricted, IBinder token); - - /** - * - * @param code - * @param user - * @param pkg - * @param attributionTag - * @return - */ - int getOpRestrictionCount(int code, UserHandle user, String pkg, - String attributionTag); - - /** - * - * @param code - * @param uid - */ - // added to interface for audio restriction stuff - void notifyWatchersOfChange(int code, int uid); - - /** - * - * @param userHandle - * @throws RemoteException - */ - void removeUser(int userHandle) throws RemoteException; - - /** - * - * @param code - * @param uid - * @param packageName - * @return - */ - boolean isOperationActive(int code, int uid, String packageName); - - /** - * - * @param op - * @param proxyPackageName - * @param proxyAttributionTag - * @param proxiedUid - * @param proxiedPackageName - * @return - */ - // TODO this one might not need to be in the interface - boolean isProxying(int op, @NonNull String proxyPackageName, - @NonNull String proxyAttributionTag, int proxiedUid, - @NonNull String proxiedPackageName); - - /** - * - * @param packageName - */ - void resetPackageOpsNoHistory(@NonNull String packageName); - - /** - * - * @param mode - * @param baseSnapshotInterval - * @param compressionStep - */ - void setHistoryParameters(@AppOpsManager.HistoricalMode int mode, - long baseSnapshotInterval, int compressionStep); - - /** - * - * @param offsetMillis - */ - void offsetHistory(long offsetMillis); - - /** - * - * @param ops - */ - void addHistoricalOps(AppOpsManager.HistoricalOps ops); - - /** - * - */ - void resetHistoryParameters(); - - /** - * - */ - void clearHistory(); - - /** - * - * @param offlineDurationMillis - */ - void rebootHistory(long offlineDurationMillis); -} diff --git a/services/core/java/com/android/server/appop/AppOpsUidStateTrackerImpl.java b/services/core/java/com/android/server/appop/AppOpsUidStateTrackerImpl.java index c1434e4d9f4d..5114bd59f084 100644 --- a/services/core/java/com/android/server/appop/AppOpsUidStateTrackerImpl.java +++ b/services/core/java/com/android/server/appop/AppOpsUidStateTrackerImpl.java @@ -59,7 +59,7 @@ class AppOpsUidStateTrackerImpl implements AppOpsUidStateTracker { private final DelayableExecutor mExecutor; private final Clock mClock; private ActivityManagerInternal mActivityManagerInternal; - private AppOpsServiceImpl.Constants mConstants; + private AppOpsService.Constants mConstants; private SparseIntArray mUidStates = new SparseIntArray(); private SparseIntArray mPendingUidStates = new SparseIntArray(); @@ -85,7 +85,7 @@ class AppOpsUidStateTrackerImpl implements AppOpsUidStateTracker { AppOpsUidStateTrackerImpl(ActivityManagerInternal activityManagerInternal, Handler handler, Executor lockingExecutor, Clock clock, - AppOpsServiceImpl.Constants constants) { + AppOpsService.Constants constants) { this(activityManagerInternal, new DelayableExecutor() { @Override @@ -102,7 +102,7 @@ class AppOpsUidStateTrackerImpl implements AppOpsUidStateTracker { @VisibleForTesting AppOpsUidStateTrackerImpl(ActivityManagerInternal activityManagerInternal, - DelayableExecutor executor, Clock clock, AppOpsServiceImpl.Constants constants, + DelayableExecutor executor, Clock clock, AppOpsService.Constants constants, Thread executorThread) { mActivityManagerInternal = activityManagerInternal; mExecutor = executor; diff --git a/services/core/java/com/android/server/appop/AttributedOp.java b/services/core/java/com/android/server/appop/AttributedOp.java index 797026908619..dcc36bcf6149 100644 --- a/services/core/java/com/android/server/appop/AttributedOp.java +++ b/services/core/java/com/android/server/appop/AttributedOp.java @@ -40,9 +40,9 @@ import java.util.List; import java.util.NoSuchElementException; final class AttributedOp { - private final @NonNull AppOpsServiceImpl mAppOpsService; + private final @NonNull AppOpsService mAppOpsService; public final @Nullable String tag; - public final @NonNull AppOpsServiceImpl.Op parent; + public final @NonNull AppOpsService.Op parent; /** * Last successful accesses (noteOp + finished startOp) for each uidState/opFlag combination @@ -80,8 +80,8 @@ final class AttributedOp { // @GuardedBy("mAppOpsService") @Nullable ArrayMap<IBinder, InProgressStartOpEvent> mPausedInProgressEvents; - AttributedOp(@NonNull AppOpsServiceImpl appOpsService, @Nullable String tag, - @NonNull AppOpsServiceImpl.Op parent) { + AttributedOp(@NonNull AppOpsService appOpsService, @Nullable String tag, + @NonNull AppOpsService.Op parent) { mAppOpsService = appOpsService; this.tag = tag; this.parent = parent; @@ -131,8 +131,8 @@ final class AttributedOp { AppOpsManager.OpEventProxyInfo proxyInfo = null; if (proxyUid != Process.INVALID_UID) { - proxyInfo = mAppOpsService.mOpEventProxyInfoPool.acquire(proxyUid, - proxyPackageName, proxyAttributionTag); + proxyInfo = mAppOpsService.mOpEventProxyInfoPool.acquire(proxyUid, proxyPackageName, + proxyAttributionTag); } AppOpsManager.NoteOpEvent existingEvent = mAccessEvents.get(key); @@ -238,7 +238,7 @@ final class AttributedOp { if (event == null) { event = mAppOpsService.mInProgressStartOpEventPool.acquire(startTime, SystemClock.elapsedRealtime(), clientId, tag, - PooledLambda.obtainRunnable(AppOpsServiceImpl::onClientDeath, this, clientId), + PooledLambda.obtainRunnable(AppOpsService::onClientDeath, this, clientId), proxyUid, proxyPackageName, proxyAttributionTag, uidState, flags, attributionFlags, attributionChainId); events.put(clientId, event); @@ -251,9 +251,9 @@ final class AttributedOp { event.mNumUnfinishedStarts++; if (isStarted) { - mAppOpsService.mHistoricalRegistry.incrementOpAccessedCount(parent.op, - parent.uid, parent.packageName, tag, uidState, flags, startTime, - attributionFlags, attributionChainId); + mAppOpsService.mHistoricalRegistry.incrementOpAccessedCount(parent.op, parent.uid, + parent.packageName, tag, uidState, flags, startTime, attributionFlags, + attributionChainId); } } @@ -309,8 +309,8 @@ final class AttributedOp { mAccessEvents.put(makeKey(event.getUidState(), event.getFlags()), finishedEvent); - mAppOpsService.mHistoricalRegistry.increaseOpAccessDuration(parent.op, - parent.uid, parent.packageName, tag, event.getUidState(), + mAppOpsService.mHistoricalRegistry.increaseOpAccessDuration(parent.op, parent.uid, + parent.packageName, tag, event.getUidState(), event.getFlags(), finishedEvent.getNoteTime(), finishedEvent.getDuration(), event.getAttributionFlags(), event.getAttributionChainId()); @@ -334,13 +334,13 @@ final class AttributedOp { @SuppressWarnings("GuardedBy") // Lock is held on mAppOpsService private void finishPossiblyPaused(@NonNull IBinder clientId, boolean isPausing) { if (!isPaused()) { - Slog.wtf(AppOpsServiceImpl.TAG, "No ops running or paused"); + Slog.wtf(AppOpsService.TAG, "No ops running or paused"); return; } int indexOfToken = mPausedInProgressEvents.indexOfKey(clientId); if (indexOfToken < 0) { - Slog.wtf(AppOpsServiceImpl.TAG, "No op running or paused for the client"); + Slog.wtf(AppOpsService.TAG, "No op running or paused for the client"); return; } else if (isPausing) { // already paused @@ -416,9 +416,9 @@ final class AttributedOp { mInProgressEvents.put(event.getClientId(), event); event.setStartElapsedTime(SystemClock.elapsedRealtime()); event.setStartTime(startTime); - mAppOpsService.mHistoricalRegistry.incrementOpAccessedCount(parent.op, - parent.uid, parent.packageName, tag, event.getUidState(), event.getFlags(), - startTime, event.getAttributionFlags(), event.getAttributionChainId()); + mAppOpsService.mHistoricalRegistry.incrementOpAccessedCount(parent.op, parent.uid, + parent.packageName, tag, event.getUidState(), event.getFlags(), startTime, + event.getAttributionFlags(), event.getAttributionChainId()); if (shouldSendActive) { mAppOpsService.scheduleOpActiveChangedIfNeededLocked(parent.op, parent.uid, parent.packageName, tag, true, event.getAttributionFlags(), @@ -503,8 +503,8 @@ final class AttributedOp { newEvent.mNumUnfinishedStarts += numPreviousUnfinishedStarts - 1; } } catch (RemoteException e) { - if (AppOpsServiceImpl.DEBUG) { - Slog.e(AppOpsServiceImpl.TAG, + if (AppOpsService.DEBUG) { + Slog.e(AppOpsService.TAG, "Cannot switch to new uidState " + newState); } } @@ -555,8 +555,8 @@ final class AttributedOp { ArrayMap<IBinder, InProgressStartOpEvent> ignoredEvents = opToAdd.isRunning() ? opToAdd.mInProgressEvents : opToAdd.mPausedInProgressEvents; - Slog.w(AppOpsServiceImpl.TAG, "Ignoring " + ignoredEvents.size() - + " app-ops, running: " + opToAdd.isRunning()); + Slog.w(AppOpsService.TAG, "Ignoring " + ignoredEvents.size() + " app-ops, running: " + + opToAdd.isRunning()); int numInProgressEvents = ignoredEvents.size(); for (int i = 0; i < numInProgressEvents; i++) { @@ -668,22 +668,16 @@ final class AttributedOp { /** * Create a new {@link InProgressStartOpEvent}. * - * @param startTime The time {@link AppOpCheckingServiceInterface#startOperation} - * was called - * @param startElapsedTime The elapsed time whe - * {@link AppOpCheckingServiceInterface#startOperation} was called - * @param clientId The client id of the caller of - * {@link AppOpCheckingServiceInterface#startOperation} + * @param startTime The time {@link #startOperation} was called + * @param startElapsedTime The elapsed time when {@link #startOperation} was called + * @param clientId The client id of the caller of {@link #startOperation} * @param attributionTag The attribution tag for the operation. * @param onDeath The code to execute on client death - * @param uidState The uidstate of the app - * {@link AppOpCheckingServiceInterface#startOperation} was called - * for + * @param uidState The uidstate of the app {@link #startOperation} was called for * @param attributionFlags the attribution flags for this operation. * @param attributionChainId the unique id of the attribution chain this op is a part of. - * @param proxy The proxy information, if - * {@link AppOpCheckingServiceInterface#startProxyOperation} was - * called + * @param proxy The proxy information, if {@link #startProxyOperation} was + * called * @param flags The trusted/nontrusted/self flags. * @throws RemoteException If the client is dying */ @@ -724,21 +718,15 @@ final class AttributedOp { /** * Reinit existing object with new state. * - * @param startTime The time {@link AppOpCheckingServiceInterface#startOperation} - * was called - * @param startElapsedTime The elapsed time when - * {@link AppOpCheckingServiceInterface#startOperation} was called - * @param clientId The client id of the caller of - * {@link AppOpCheckingServiceInterface#startOperation} + * @param startTime The time {@link #startOperation} was called + * @param startElapsedTime The elapsed time when {@link #startOperation} was called + * @param clientId The client id of the caller of {@link #startOperation} * @param attributionTag The attribution tag for this operation. * @param onDeath The code to execute on client death - * @param uidState The uidstate of the app - * {@link AppOpCheckingServiceInterface#startOperation} was called - * for + * @param uidState The uidstate of the app {@link #startOperation} was called for * @param flags The flags relating to the proxy - * @param proxy The proxy information, if - * {@link AppOpCheckingServiceInterface#startProxyOperation was - * called + * @param proxy The proxy information, if {@link #startProxyOperation} + * was called * @param attributionFlags the attribution flags for this operation. * @param attributionChainId the unique id of the attribution chain this op is a part of. * @param proxyPool The pool to release diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index fb6511c7b321..9b433cf654b0 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -10003,6 +10003,7 @@ public class AudioService extends IAudioService.Stub static final int LOG_NB_EVENTS_VOLUME = 40; static final int LOG_NB_EVENTS_DYN_POLICY = 10; static final int LOG_NB_EVENTS_SPATIAL = 30; + static final int LOG_NB_EVENTS_SOUND_DOSE = 30; static final EventLogger sLifecycleLogger = new EventLogger(LOG_NB_EVENTS_LIFECYCLE, @@ -11419,6 +11420,11 @@ public class AudioService extends IAudioService.Stub public void onStop() { unregisterAudioPolicyAsync(mPolicyCallback); } + + @Override + public void onCapturedContentResize(int width, int height) { + // Ignore resize of the captured content. + } }; UnregisterOnStopCallback mProjectionCallback; diff --git a/services/core/java/com/android/server/audio/AudioServiceEvents.java b/services/core/java/com/android/server/audio/AudioServiceEvents.java index b920517166ec..d30bec70ce9e 100644 --- a/services/core/java/com/android/server/audio/AudioServiceEvents.java +++ b/services/core/java/com/android/server/audio/AudioServiceEvents.java @@ -476,4 +476,53 @@ public class AudioServiceEvents { } } } + + static final class SoundDoseEvent extends EventLogger.Event { + static final int MOMENTARY_EXPOSURE = 0; + static final int DOSE_UPDATE = 1; + static final int DOSE_REPEAT_5X = 2; + static final int DOSE_ACCUMULATION_START = 3; + final int mEventType; + final float mFloatValue; + final long mLongValue; + + private SoundDoseEvent(int event, float f, long l) { + mEventType = event; + mFloatValue = f; + mLongValue = l; + } + + static SoundDoseEvent getMomentaryExposureEvent(float mel) { + return new SoundDoseEvent(MOMENTARY_EXPOSURE, mel, 0 /*ignored*/); + } + + static SoundDoseEvent getDoseUpdateEvent(float csd, long totalDuration) { + return new SoundDoseEvent(DOSE_UPDATE, csd, totalDuration); + } + + static SoundDoseEvent getDoseRepeat5xEvent() { + return new SoundDoseEvent(DOSE_REPEAT_5X, 0 /*ignored*/, 0 /*ignored*/); + } + + static SoundDoseEvent getDoseAccumulationStartEvent() { + return new SoundDoseEvent(DOSE_ACCUMULATION_START, 0 /*ignored*/, 0 /*ignored*/); + } + + @Override + public String eventToString() { + switch (mEventType) { + case MOMENTARY_EXPOSURE: + return String.format("momentary exposure MEL=%.2f", mFloatValue); + case DOSE_UPDATE: + return String.format(java.util.Locale.US, + "dose update CSD=%.1f%% total duration=%d", + mFloatValue * 100.0f, mLongValue); + case DOSE_REPEAT_5X: + return "CSD reached 500%"; + case DOSE_ACCUMULATION_START: + return "CSD accumulating: RS2 entered"; + } + return new StringBuilder("FIXME invalid event type:").append(mEventType).toString(); + } + } } diff --git a/services/core/java/com/android/server/audio/SoundDoseHelper.java b/services/core/java/com/android/server/audio/SoundDoseHelper.java index 0d0de8ad2886..5fe9ada8c9cc 100644 --- a/services/core/java/com/android/server/audio/SoundDoseHelper.java +++ b/services/core/java/com/android/server/audio/SoundDoseHelper.java @@ -43,6 +43,8 @@ import android.util.MathUtils; import com.android.internal.annotations.GuardedBy; import com.android.server.audio.AudioService.AudioHandler; import com.android.server.audio.AudioService.ISafeHearingVolumeController; +import com.android.server.audio.AudioServiceEvents.SoundDoseEvent; +import com.android.server.utils.EventLogger; import java.io.PrintWriter; import java.util.ArrayList; @@ -94,6 +96,9 @@ public class SoundDoseHelper { private static final float CUSTOM_RS2_VALUE = 90; + private final EventLogger mLogger = new EventLogger(AudioService.LOG_NB_EVENTS_SOUND_DOSE, + "CSD updates"); + private int mMcc = 0; final Object mSafeMediaVolumeStateLock = new Object(); @@ -147,17 +152,21 @@ public class SoundDoseHelper { public void onMomentaryExposure(float currentMel, int deviceId) { Log.w(TAG, "DeviceId " + deviceId + " triggered momentary exposure with value: " + currentMel); + mLogger.enqueue(SoundDoseEvent.getMomentaryExposureEvent(currentMel)); } public void onNewCsdValue(float currentCsd, SoundDoseRecord[] records) { Log.i(TAG, "onNewCsdValue: " + currentCsd); mCurrentCsd = currentCsd; mDoseRecords.addAll(Arrays.asList(records)); + long totalDuration = 0; for (SoundDoseRecord record : records) { Log.i(TAG, " new record: csd=" + record.value + " averageMel=" + record.averageMel + " timestamp=" + record.timestamp + " duration=" + record.duration); + totalDuration += record.duration; } + mLogger.enqueue(SoundDoseEvent.getDoseUpdateEvent(currentCsd, totalDuration)); } }; @@ -400,6 +409,9 @@ public class SoundDoseHelper { pw.print(" mMusicActiveMs="); pw.println(mMusicActiveMs); pw.print(" mMcc="); pw.println(mMcc); pw.print(" mPendingVolumeCommand="); pw.println(mPendingVolumeCommand); + pw.println(); + mLogger.dump(pw); + pw.println(); } /*package*/void reset() { diff --git a/services/core/java/com/android/server/display/ColorFade.java b/services/core/java/com/android/server/display/ColorFade.java index 372bc8ad94a1..a4bd6a699ceb 100644 --- a/services/core/java/com/android/server/display/ColorFade.java +++ b/services/core/java/com/android/server/display/ColorFade.java @@ -16,7 +16,7 @@ package com.android.server.display; -import static com.android.server.wm.utils.RotationAnimationUtils.hasProtectedContent; +import static com.android.internal.policy.TransitionAnimation.hasProtectedContent; import android.content.Context; import android.graphics.BLASTBufferQueue; diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index c700ccbdc0a1..329e3ca0d5b9 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -1852,15 +1852,7 @@ public final class DisplayManagerService extends SystemService { Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.USER_PREFERRED_RESOLUTION_WIDTH, resolutionWidth); mDisplayDeviceRepo.forEachLocked((DisplayDevice device) -> { - // If there is a display specific mode, don't override that - final Point deviceUserPreferredResolution = - mPersistentDataStore.getUserPreferredResolution(device); - final float deviceRefreshRate = - mPersistentDataStore.getUserPreferredRefreshRate(device); - if (!isValidResolution(deviceUserPreferredResolution) - && !isValidRefreshRate(deviceRefreshRate)) { - device.setUserPreferredDisplayModeLocked(mode); - } + device.setUserPreferredDisplayModeLocked(mode); }); } @@ -3723,44 +3715,21 @@ public final class DisplayManagerService extends SystemService { @Override public Set<DisplayInfo> getPossibleDisplayInfo(int displayId) { synchronized (mSyncRoot) { - // Retrieve the group associated with this display id. - final int displayGroupId = - mLogicalDisplayMapper.getDisplayGroupIdFromDisplayIdLocked(displayId); - if (displayGroupId == Display.INVALID_DISPLAY_GROUP) { - Slog.w(TAG, - "Can't get possible display info since display group for " + displayId - + " does not exist"); - return new ArraySet<>(); - } - - // Assume any display in this group can be swapped out for the given display id. Set<DisplayInfo> possibleInfo = new ArraySet<>(); - final DisplayGroup group = mLogicalDisplayMapper.getDisplayGroupLocked( - displayGroupId); - for (int i = 0; i < group.getSizeLocked(); i++) { - final int id = group.getIdLocked(i); - final LogicalDisplay logical = mLogicalDisplayMapper.getDisplayLocked(id); - if (logical == null) { - Slog.w(TAG, - "Can't get possible display info since logical display for " - + "display id " + id + " does not exist, as part of group " - + displayGroupId); - } else { - possibleInfo.add(logical.getDisplayInfoLocked()); - } - } - - // For the supported device states, retrieve the DisplayInfos for the logical - // display layout. + // For each of supported device states, retrieve the display layout of that state, + // and return all of the DisplayInfos (one per state) for the given display id. if (mDeviceStateManager == null) { Slog.w(TAG, "Can't get supported states since DeviceStateManager not ready"); - } else { - final int[] supportedStates = - mDeviceStateManager.getSupportedStateIdentifiers(); - for (int state : supportedStates) { - possibleInfo.addAll( - mLogicalDisplayMapper.getDisplayInfoForStateLocked(state, displayId, - displayGroupId)); + return possibleInfo; + } + final int[] supportedStates = + mDeviceStateManager.getSupportedStateIdentifiers(); + DisplayInfo displayInfo; + for (int state : supportedStates) { + displayInfo = mLogicalDisplayMapper.getDisplayInfoForStateLocked(state, + displayId); + if (displayInfo != null) { + possibleInfo.add(displayInfo); } } return possibleInfo; diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java index 9d478927edd5..75415cd9997f 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController.java +++ b/services/core/java/com/android/server/display/DisplayPowerController.java @@ -453,6 +453,8 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call // PowerManager.BRIGHTNESS_INVALID_FLOAT when there's no temporary adjustment set. private float mTemporaryAutoBrightnessAdjustment; + private boolean mUseAutoBrightness; + private boolean mIsRbcActive; // Whether there's a callback to tell listeners the display has changed scheduled to run. When @@ -683,6 +685,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call @Override public void onSwitchUser(@UserIdInt int newUserId) { handleSettingsChange(true /* userSwitch */); + handleBrightnessModeChange(); if (mBrightnessTracker != null) { mBrightnessTracker.onSwitchUser(newUserId); } @@ -930,6 +933,10 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call mContext.getContentResolver().registerContentObserver( Settings.System.getUriFor(Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ), false /*notifyForDescendants*/, mSettingsObserver, UserHandle.USER_ALL); + mContext.getContentResolver().registerContentObserver( + Settings.System.getUriFor(Settings.System.SCREEN_BRIGHTNESS_MODE), + false /*notifyForDescendants*/, mSettingsObserver, UserHandle.USER_ALL); + handleBrightnessModeChange(); } private void setUpAutoBrightness(Resources resources, Handler handler) { @@ -1335,11 +1342,11 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call final boolean autoBrightnessEnabledInDoze = mAllowAutoBrightnessWhileDozingConfig && Display.isDozeState(state); - final boolean autoBrightnessEnabled = mPowerRequest.useAutoBrightness + final boolean autoBrightnessEnabled = mUseAutoBrightness && (state == Display.STATE_ON || autoBrightnessEnabledInDoze) && Float.isNaN(brightnessState) && mAutomaticBrightnessController != null; - final boolean autoBrightnessDisabledDueToDisplayOff = mPowerRequest.useAutoBrightness + final boolean autoBrightnessDisabledDueToDisplayOff = mUseAutoBrightness && !(state == Display.STATE_ON || autoBrightnessEnabledInDoze); final int autoBrightnessState = autoBrightnessEnabled ? AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED @@ -1691,7 +1698,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call || brightnessAdjustmentFlags != 0) { float lastBrightness = mLastBrightnessEvent.getBrightness(); mTempBrightnessEvent.setInitialBrightness(lastBrightness); - mTempBrightnessEvent.setAutomaticBrightnessEnabled(mPowerRequest.useAutoBrightness); + mTempBrightnessEvent.setAutomaticBrightnessEnabled(mUseAutoBrightness); mLastBrightnessEvent.copyFrom(mTempBrightnessEvent); BrightnessEvent newEvent = new BrightnessEvent(mTempBrightnessEvent); // Adjustment flags (and user-set flag) only get added after the equality checks since @@ -2341,6 +2348,18 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call sendUpdatePowerState(); } + private void handleBrightnessModeChange() { + final int screenBrightnessModeSetting = Settings.System.getIntForUser( + mContext.getContentResolver(), + Settings.System.SCREEN_BRIGHTNESS_MODE, + Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL, UserHandle.USER_CURRENT); + mHandler.post(() -> { + mUseAutoBrightness = screenBrightnessModeSetting + == Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC; + updatePowerState(); + }); + } + private float getAutoBrightnessAdjustmentSetting() { final float adj = Settings.System.getFloatForUser(mContext.getContentResolver(), Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, 0.0f, UserHandle.USER_CURRENT); @@ -2425,7 +2444,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call private void notifyBrightnessTrackerChanged(float brightness, boolean userInitiated, boolean hadUserDataPoint) { final float brightnessInNits = convertToNits(brightness); - if (mPowerRequest.useAutoBrightness && brightnessInNits >= 0.0f + if (mUseAutoBrightness && brightnessInNits >= 0.0f && mAutomaticBrightnessController != null && mBrightnessTracker != null) { // We only want to track changes on devices that can actually map the display backlight // values into a physical brightness unit since the value provided by the API is in @@ -2897,7 +2916,11 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call @Override public void onChange(boolean selfChange, Uri uri) { - handleSettingsChange(false /* userSwitch */); + if (uri.equals(Settings.System.getUriFor(Settings.System.SCREEN_BRIGHTNESS_MODE))) { + handleBrightnessModeChange(); + } else { + handleSettingsChange(false /* userSwitch */); + } } } diff --git a/services/core/java/com/android/server/display/DisplayPowerController2.java b/services/core/java/com/android/server/display/DisplayPowerController2.java index 346b340edcd1..111caefa34da 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController2.java +++ b/services/core/java/com/android/server/display/DisplayPowerController2.java @@ -71,6 +71,7 @@ import com.android.server.display.brightness.BrightnessReason; import com.android.server.display.brightness.DisplayBrightnessController; import com.android.server.display.color.ColorDisplayService.ColorDisplayServiceInternal; import com.android.server.display.color.ColorDisplayService.ReduceBrightColorsListener; +import com.android.server.display.state.DisplayStateController; import com.android.server.display.utils.SensorUtils; import com.android.server.display.whitebalance.DisplayWhiteBalanceController; import com.android.server.display.whitebalance.DisplayWhiteBalanceFactory; @@ -346,6 +347,9 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal // Tracks and manages the proximity state of the associated display. private final DisplayPowerProximityStateController mDisplayPowerProximityStateController; + // Tracks and manages the display state of the associated display. + private final DisplayStateController mDisplayStateController; + // A record of state for skipping brightness ramps. private int mSkipRampState = RAMP_STATE_SKIP_NONE; @@ -396,6 +400,8 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal // PowerManager.BRIGHTNESS_INVALID_FLOAT when there's no temporary adjustment set. private float mTemporaryAutoBrightnessAdjustment; + private boolean mUseAutoBrightness; + private boolean mIsRbcActive; // Animators. @@ -434,6 +440,7 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal mDisplayPowerProximityStateController = mInjector.getDisplayPowerProximityStateController( mWakelockController, mDisplayDeviceConfig, mHandler.getLooper(), () -> updatePowerState(), mDisplayId, mSensorManager); + mDisplayStateController = new DisplayStateController(mDisplayPowerProximityStateController); mTag = "DisplayPowerController2[" + mDisplayId + "]"; mDisplayDevice = mLogicalDisplay.getPrimaryDisplayDeviceLocked(); @@ -600,6 +607,7 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal @Override public void onSwitchUser(@UserIdInt int newUserId) { handleSettingsChange(true /* userSwitch */); + handleBrightnessModeChange(); if (mBrightnessTracker != null) { mBrightnessTracker.onSwitchUser(newUserId); } @@ -842,6 +850,10 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal mContext.getContentResolver().registerContentObserver( Settings.System.getUriFor(Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ), false /*notifyForDescendants*/, mSettingsObserver, UserHandle.USER_ALL); + mContext.getContentResolver().registerContentObserver( + Settings.System.getUriFor(Settings.System.SCREEN_BRIGHTNESS_MODE), + false /*notifyForDescendants*/, mSettingsObserver, UserHandle.USER_ALL); + handleBrightnessModeChange(); } private void setUpAutoBrightness(Resources resources, Handler handler) { @@ -1121,39 +1133,8 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal mustNotify = !mDisplayReadyLocked; } - // Compute the basic display state using the policy. - // We might override this below based on other factors. - // Initialise brightness as invalid. - int state; - boolean performScreenOffTransition = false; - switch (mPowerRequest.policy) { - case DisplayPowerRequest.POLICY_OFF: - state = Display.STATE_OFF; - performScreenOffTransition = true; - break; - case DisplayPowerRequest.POLICY_DOZE: - if (mPowerRequest.dozeScreenState != Display.STATE_UNKNOWN) { - state = mPowerRequest.dozeScreenState; - } else { - state = Display.STATE_DOZE; - } - break; - case DisplayPowerRequest.POLICY_DIM: - case DisplayPowerRequest.POLICY_BRIGHT: - default: - state = Display.STATE_ON; - break; - } - assert (state != Display.STATE_UNKNOWN); - - mDisplayPowerProximityStateController.updateProximityState(mPowerRequest, state); - - if (!mIsEnabled - || mIsInTransition - || mDisplayPowerProximityStateController.isScreenOffBecauseOfProximity()) { - state = Display.STATE_OFF; - } - + int state = mDisplayStateController + .updateDisplayState(mPowerRequest, mIsEnabled, mIsInTransition); // Initialize things the first time the power state is changed. if (mustInitialize) { initialize(state); @@ -1163,7 +1144,7 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal // The transition may be deferred, so after this point we will use the // actual state instead of the desired one. final int oldState = mPowerState.getScreenState(); - animateScreenStateChange(state, performScreenOffTransition); + animateScreenStateChange(state, mDisplayStateController.shouldPerformScreenOffTransition()); state = mPowerState.getScreenState(); DisplayBrightnessState displayBrightnessState = mDisplayBrightnessController @@ -1174,11 +1155,11 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal final boolean autoBrightnessEnabledInDoze = mDisplayBrightnessController.isAllowAutoBrightnessWhileDozingConfig() && Display.isDozeState(state); - final boolean autoBrightnessEnabled = mPowerRequest.useAutoBrightness + final boolean autoBrightnessEnabled = mUseAutoBrightness && (state == Display.STATE_ON || autoBrightnessEnabledInDoze) && Float.isNaN(brightnessState) && mAutomaticBrightnessController != null; - final boolean autoBrightnessDisabledDueToDisplayOff = mPowerRequest.useAutoBrightness + final boolean autoBrightnessDisabledDueToDisplayOff = mUseAutoBrightness && !(state == Display.STATE_ON || autoBrightnessEnabledInDoze); final int autoBrightnessState = autoBrightnessEnabled ? AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED @@ -1510,7 +1491,7 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal || brightnessAdjustmentFlags != 0) { float lastBrightness = mLastBrightnessEvent.getBrightness(); mTempBrightnessEvent.setInitialBrightness(lastBrightness); - mTempBrightnessEvent.setAutomaticBrightnessEnabled(mPowerRequest.useAutoBrightness); + mTempBrightnessEvent.setAutomaticBrightnessEnabled(mUseAutoBrightness); mLastBrightnessEvent.copyFrom(mTempBrightnessEvent); BrightnessEvent newEvent = new BrightnessEvent(mTempBrightnessEvent); // Adjustment flags (and user-set flag) only get added after the equality checks since @@ -2045,6 +2026,18 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal sendUpdatePowerState(); } + private void handleBrightnessModeChange() { + final int screenBrightnessModeSetting = Settings.System.getIntForUser( + mContext.getContentResolver(), + Settings.System.SCREEN_BRIGHTNESS_MODE, + Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL, UserHandle.USER_CURRENT); + mHandler.post(() -> { + mUseAutoBrightness = screenBrightnessModeSetting + == Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC; + updatePowerState(); + }); + } + private float getAutoBrightnessAdjustmentSetting() { final float adj = Settings.System.getFloatForUser(mContext.getContentResolver(), Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, 0.0f, UserHandle.USER_CURRENT); @@ -2131,7 +2124,7 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal private void notifyBrightnessTrackerChanged(float brightness, boolean userInitiated, boolean hadUserDataPoint) { final float brightnessInNits = convertToNits(brightness); - if (mPowerRequest.useAutoBrightness && brightnessInNits >= 0.0f + if (mUseAutoBrightness && brightnessInNits >= 0.0f && mAutomaticBrightnessController != null && mBrightnessTracker != null) { // We only want to track changes on devices that can actually map the display backlight // values into a physical brightness unit since the value provided by the API is in @@ -2274,10 +2267,6 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal if (mDisplayBrightnessController != null) { mDisplayBrightnessController.dump(pw); } - - if (mDisplayPowerProximityStateController != null) { - mDisplayPowerProximityStateController.dumpLocal(pw); - } } @@ -2499,7 +2488,11 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal @Override public void onChange(boolean selfChange, Uri uri) { - handleSettingsChange(false /* userSwitch */); + if (uri.equals(Settings.System.getUriFor(Settings.System.SCREEN_BRIGHTNESS_MODE))) { + handleBrightnessModeChange(); + } else { + handleSettingsChange(false /* userSwitch */); + } } } diff --git a/services/core/java/com/android/server/display/LogicalDisplay.java b/services/core/java/com/android/server/display/LogicalDisplay.java index c7b27deb420d..ad426b5e00a2 100644 --- a/services/core/java/com/android/server/display/LogicalDisplay.java +++ b/services/core/java/com/android/server/display/LogicalDisplay.java @@ -66,7 +66,6 @@ import java.util.Objects; */ final class LogicalDisplay { private static final String TAG = "LogicalDisplay"; - // The layer stack we use when the display has been blanked to prevent any // of its content from appearing. private static final int BLANK_LAYER_STACK = -1; diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java index 80f47a138d08..d7983aecd37b 100644 --- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java +++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java @@ -19,6 +19,7 @@ package com.android.server.display; import static android.view.Display.DEFAULT_DISPLAY; import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.Context; import android.hardware.devicestate.DeviceStateManager; import android.os.Handler; @@ -29,7 +30,6 @@ import android.os.SystemClock; import android.os.SystemProperties; import android.text.TextUtils; import android.util.ArrayMap; -import android.util.ArraySet; import android.util.IndentingPrintWriter; import android.util.Slog; import android.util.SparseArray; @@ -45,7 +45,6 @@ import com.android.server.display.layout.Layout; import java.io.PrintWriter; import java.util.Arrays; -import java.util.Set; import java.util.function.Consumer; /** @@ -324,58 +323,44 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { } /** - * Returns the set of {@link DisplayInfo} for this device state, only fetching the info that is - * part of the same display group as the provided display id. The DisplayInfo represent the - * logical display layouts possible for the given device state. + * Returns the {@link DisplayInfo} for this device state, indicated by the given display id. The + * DisplayInfo represents the attributes of the indicated display in the layout associated with + * this state. This is used to get display information for various displays in various states; + * e.g. to help apps preload resources for the possible display states. * * @param deviceState the state to query possible layouts for - * @param displayId the display id to apply to all displays within the group - * @param groupId the display group to filter display info for. Must be the same group as - * the display with the provided display id. + * @param displayId the display id to retrieve + * @return {@code null} if no corresponding {@link DisplayInfo} could be found, or the + * {@link DisplayInfo} with a matching display id. */ - public Set<DisplayInfo> getDisplayInfoForStateLocked(int deviceState, int displayId, - int groupId) { - Set<DisplayInfo> displayInfos = new ArraySet<>(); + @Nullable + public DisplayInfo getDisplayInfoForStateLocked(int deviceState, int displayId) { + // Retrieve the layout for this particular state. final Layout layout = mDeviceStateToLayoutMap.get(deviceState); - final int layoutSize = layout.size(); - for (int i = 0; i < layoutSize; i++) { - Layout.Display displayLayout = layout.getAt(i); - if (displayLayout == null) { - continue; - } - - // If the underlying display-device we want to use for this display - // doesn't exist, then skip it. This can happen at startup as display-devices - // trickle in one at a time. When the new display finally shows up, the layout is - // recalculated so that the display is properly added to the current layout. - final DisplayAddress address = displayLayout.getAddress(); - final DisplayDevice device = mDisplayDeviceRepo.getByAddressLocked(address); - if (device == null) { - Slog.w(TAG, "The display device (" + address + "), is not available" - + " for the display state " + deviceState); - continue; - } - - // Find or create the LogicalDisplay to map the DisplayDevice to. - final int logicalDisplayId = displayLayout.getLogicalDisplayId(); - final LogicalDisplay logicalDisplay = getDisplayLocked(logicalDisplayId); - if (logicalDisplay == null) { - Slog.w(TAG, "The logical display (" + address + "), is not available" - + " for the display state " + deviceState); - continue; - } - final DisplayInfo temp = logicalDisplay.getDisplayInfoLocked(); - DisplayInfo displayInfo = new DisplayInfo(temp); - if (displayInfo.displayGroupId != groupId) { - // Ignore any displays not in the provided group. - continue; - } - // A display in the same group can be swapped out at any point, so set the display id - // for all results to the provided display id. - displayInfo.displayId = displayId; - displayInfos.add(displayInfo); + if (layout == null) { + return null; + } + // Retrieve the details of the given display within this layout. + Layout.Display display = layout.getById(displayId); + if (display == null) { + return null; + } + // Retrieve the display info for the display that matches the display id. + final DisplayDevice device = mDisplayDeviceRepo.getByAddressLocked(display.getAddress()); + if (device == null) { + Slog.w(TAG, "The display device (" + display.getAddress() + "), is not available" + + " for the display state " + mDeviceState); + return null; + } + LogicalDisplay logicalDisplay = getDisplayLocked(device, /* includeDisabled= */ true); + if (logicalDisplay == null) { + Slog.w(TAG, "The logical display associated with address (" + display.getAddress() + + "), is not available for the display state " + mDeviceState); + return null; } - return displayInfos; + DisplayInfo displayInfo = new DisplayInfo(logicalDisplay.getDisplayInfoLocked()); + displayInfo.displayId = displayId; + return displayInfo; } public void dumpLocked(PrintWriter pw) { diff --git a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java index a118b2fa37c3..7c647cf6f4aa 100644 --- a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java +++ b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java @@ -599,6 +599,15 @@ public class VirtualDisplayAdapter extends DisplayAdapter { handleMediaProjectionStoppedLocked(mAppToken); } } + + @Override + public void onCapturedContentResize(int width, int height) { + // Do nothing when we tell the client that the content is resized - it is up to them + // to decide to update the VirtualDisplay and Surface. + // We could only update the VirtualDisplay size, anyway (which the client wouldn't + // expect), and there will still be letterboxing on the output content since the + // Surface and VirtualDisplay would then have different aspect ratios. + } } @VisibleForTesting diff --git a/services/core/java/com/android/server/display/state/DisplayStateController.java b/services/core/java/com/android/server/display/state/DisplayStateController.java new file mode 100644 index 000000000000..546478e480e0 --- /dev/null +++ b/services/core/java/com/android/server/display/state/DisplayStateController.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.display.state; + +import android.hardware.display.DisplayManagerInternal; +import android.util.IndentingPrintWriter; +import android.view.Display; + +import com.android.server.display.DisplayPowerProximityStateController; + +import java.io.PrintWriter; + +/** + * Maintains the DisplayState of the system. + * Internally, this accounts for the proximity changes, and notifying the system + * clients about the changes + */ +public class DisplayStateController { + private DisplayPowerProximityStateController mDisplayPowerProximityStateController; + private boolean mPerformScreenOffTransition = false; + + public DisplayStateController(DisplayPowerProximityStateController + displayPowerProximityStateController) { + this.mDisplayPowerProximityStateController = displayPowerProximityStateController; + } + + /** + * Updates the DisplayState and notifies the system. Also accounts for the + * events being emitted by the proximity sensors + * + * @param displayPowerRequest The request to update the display state + * @param isDisplayEnabled A boolean flag representing if the display is enabled + * @param isDisplayInTransition A boolean flag representing if the display is undergoing the + * transition phase + */ + public int updateDisplayState(DisplayManagerInternal.DisplayPowerRequest displayPowerRequest, + boolean isDisplayEnabled, boolean isDisplayInTransition) { + mPerformScreenOffTransition = false; + // Compute the basic display state using the policy. + // We might override this below based on other factors. + // Initialise brightness as invalid. + int state; + switch (displayPowerRequest.policy) { + case DisplayManagerInternal.DisplayPowerRequest.POLICY_OFF: + state = Display.STATE_OFF; + mPerformScreenOffTransition = true; + break; + case DisplayManagerInternal.DisplayPowerRequest.POLICY_DOZE: + if (displayPowerRequest.dozeScreenState != Display.STATE_UNKNOWN) { + state = displayPowerRequest.dozeScreenState; + } else { + state = Display.STATE_DOZE; + } + break; + case DisplayManagerInternal.DisplayPowerRequest.POLICY_DIM: + case DisplayManagerInternal.DisplayPowerRequest.POLICY_BRIGHT: + default: + state = Display.STATE_ON; + break; + } + assert (state != Display.STATE_UNKNOWN); + + mDisplayPowerProximityStateController.updateProximityState(displayPowerRequest, state); + + if (!isDisplayEnabled || isDisplayInTransition + || mDisplayPowerProximityStateController.isScreenOffBecauseOfProximity()) { + state = Display.STATE_OFF; + } + + return state; + } + + /** + * Checks if the screen off transition is to be performed or not. + */ + public boolean shouldPerformScreenOffTransition() { + return mPerformScreenOffTransition; + } + + /** + * Used to dump the state. + * + * @param pw The PrintWriter used to dump the state. + */ + public void dumpsys(PrintWriter pw) { + pw.println(); + pw.println("DisplayPowerProximityStateController:"); + pw.println(" mPerformScreenOffTransition:" + mPerformScreenOffTransition); + IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " "); + if (mDisplayPowerProximityStateController != null) { + mDisplayPowerProximityStateController.dumpLocal(ipw); + } + } +} diff --git a/services/core/java/com/android/server/input/InputManagerInternal.java b/services/core/java/com/android/server/input/InputManagerInternal.java index 298098a572b2..01a564d6816f 100644 --- a/services/core/java/com/android/server/input/InputManagerInternal.java +++ b/services/core/java/com/android/server/input/InputManagerInternal.java @@ -171,4 +171,19 @@ public abstract class InputManagerInternal { * {@see Light.LIGHT_TYPE_KEYBOARD_BACKLIGHT} */ public abstract void decrementKeyboardBacklight(int deviceId); + + /** + * Add a runtime association between the input port and device type. Input ports are expected to + * be unique. + * @param inputPort The port of the input device. + * @param type The type of the device. E.g. "touchNavigation". + */ + public abstract void setTypeAssociation(@NonNull String inputPort, @NonNull String type); + + /** + * Removes a runtime association between the input device and type. + * + * @param inputPort The port of the input device. + */ + public abstract void unsetTypeAssociation(@NonNull String inputPort); } diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index c62abf004daa..1809b1821b5e 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -248,6 +248,13 @@ public class InputManagerService extends IInputManager.Stub @GuardedBy("mAssociationsLock") private final Map<String, String> mUniqueIdAssociations = new ArrayMap<>(); + // Stores input ports associated with device types. For example, adding an association + // {"123", "touchNavigation"} here would mean that a touch device appearing at port "123" would + // enumerate as a "touch navigation" device rather than the default "touchpad as a mouse + // pointer" device. + @GuardedBy("mAssociationsLock") + private final Map<String, String> mDeviceTypeAssociations = new ArrayMap<>(); + // Guards per-display input properties and properties relating to the mouse pointer. // Threads can wait on this lock to be notified the next time the display on which the mouse // pointer is shown has changed. @@ -1905,6 +1912,23 @@ public class InputManagerService extends IInputManager.Stub mNative.changeUniqueIdAssociation(); } + void setTypeAssociationInternal(@NonNull String inputPort, @NonNull String type) { + Objects.requireNonNull(inputPort); + Objects.requireNonNull(type); + synchronized (mAssociationsLock) { + mDeviceTypeAssociations.put(inputPort, type); + } + mNative.changeTypeAssociation(); + } + + void unsetTypeAssociationInternal(@NonNull String inputPort) { + Objects.requireNonNull(inputPort); + synchronized (mAssociationsLock) { + mDeviceTypeAssociations.remove(inputPort); + } + mNative.changeTypeAssociation(); + } + @Override // Binder call public InputSensorInfo[] getSensorList(int deviceId) { return mNative.getSensorList(deviceId); @@ -2221,6 +2245,13 @@ public class InputManagerService extends IInputManager.Stub pw.println(" uniqueId: " + v); }); } + if (!mDeviceTypeAssociations.isEmpty()) { + pw.println("Type Associations:"); + mDeviceTypeAssociations.forEach((k, v) -> { + pw.print(" port: " + k); + pw.println(" type: " + v); + }); + } } } @@ -2630,6 +2661,18 @@ public class InputManagerService extends IInputManager.Stub return flatten(associations); } + // Native callback + @SuppressWarnings("unused") + @VisibleForTesting + String[] getDeviceTypeAssociations() { + final Map<String, String> associations; + synchronized (mAssociationsLock) { + associations = new HashMap<>(mDeviceTypeAssociations); + } + + return flatten(associations); + } + /** * Gets if an input device could dispatch to the given display". * @param deviceId The input device id. @@ -3263,6 +3306,16 @@ public class InputManagerService extends IInputManager.Stub public void decrementKeyboardBacklight(int deviceId) { mKeyboardBacklightController.decrementKeyboardBacklight(deviceId); } + + @Override + public void setTypeAssociation(@NonNull String inputPort, @NonNull String type) { + setTypeAssociationInternal(inputPort, type); + } + + @Override + public void unsetTypeAssociation(@NonNull String inputPort) { + unsetTypeAssociationInternal(inputPort); + } } @Override diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java index 8781c6e2b934..184bc0e3519d 100644 --- a/services/core/java/com/android/server/input/NativeInputManagerService.java +++ b/services/core/java/com/android/server/input/NativeInputManagerService.java @@ -186,6 +186,8 @@ interface NativeInputManagerService { void changeUniqueIdAssociation(); + void changeTypeAssociation(); + void notifyPointerDisplayIdChanged(); void setDisplayEligibilityForPointerCapture(int displayId, boolean enabled); @@ -400,6 +402,9 @@ interface NativeInputManagerService { public native void changeUniqueIdAssociation(); @Override + public native void changeTypeAssociation(); + + @Override public native void notifyPointerDisplayIdChanged(); @Override diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java index ab69de961dda..5f39a523b3ac 100644 --- a/services/core/java/com/android/server/locksettings/LockSettingsService.java +++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java @@ -240,8 +240,7 @@ public class LockSettingsService extends ILockSettings.Stub { protected final UserManager mUserManager; private final IStorageManager mStorageManager; private final IActivityManager mActivityManager; - @VisibleForTesting - protected final SyntheticPasswordManager mSpManager; + private final SyntheticPasswordManager mSpManager; private final java.security.KeyStore mJavaKeyStore; private final RecoverableKeyStoreManager mRecoverableKeyStoreManager; @@ -946,7 +945,7 @@ public class LockSettingsService extends ILockSettings.Stub { if (!isSyntheticPasswordBasedCredentialLocked(userId)) { Slogf.i(TAG, "Creating locksettings state for user %d now that boot " + "is complete", userId); - initializeSyntheticPasswordLocked(userId); + initializeSyntheticPassword(userId); } } } @@ -985,7 +984,7 @@ public class LockSettingsService extends ILockSettings.Stub { long protectorId = getCurrentLskfBasedProtectorId(userId); if (protectorId == SyntheticPasswordManager.NULL_PROTECTOR_ID) { Slogf.i(TAG, "Migrating unsecured user %d to SP-based credential", userId); - initializeSyntheticPasswordLocked(userId); + initializeSyntheticPassword(userId); } else { Slogf.i(TAG, "Existing unsecured user %d has a synthetic password; re-encrypting CE " + "key with it", userId); @@ -1652,13 +1651,7 @@ public class LockSettingsService extends ILockSettings.Stub { Objects.requireNonNull(savedCredential); if (DEBUG) Slog.d(TAG, "setLockCredentialInternal: user=" + userId); synchronized (mSpManager) { - if (!isSyntheticPasswordBasedCredentialLocked(userId)) { - if (!savedCredential.isNone()) { - throw new IllegalStateException("Saved credential given, but user has no SP"); - } - // TODO(b/232452368): this case is only needed by unit tests now; remove it. - initializeSyntheticPasswordLocked(userId); - } else if (savedCredential.isNone() && isProfileWithUnifiedLock(userId)) { + if (savedCredential.isNone() && isProfileWithUnifiedLock(userId)) { // get credential from keystore when profile has unified lock try { //TODO: remove as part of b/80170828 @@ -2322,9 +2315,7 @@ public class LockSettingsService extends ILockSettings.Stub { return; } removeStateForReusedUserIdIfNecessary(userId, userSerialNumber); - synchronized (mSpManager) { - initializeSyntheticPasswordLocked(userId); - } + initializeSyntheticPassword(userId); } } @@ -2650,21 +2641,22 @@ public class LockSettingsService extends ILockSettings.Stub { * until the time when Weaver is guaranteed to be available), or when upgrading from Android 13 * or earlier where users with no LSKF didn't necessarily have an SP. */ - @GuardedBy("mSpManager") @VisibleForTesting - SyntheticPassword initializeSyntheticPasswordLocked(int userId) { - Slog.i(TAG, "Initialize SyntheticPassword for user: " + userId); - Preconditions.checkState(getCurrentLskfBasedProtectorId(userId) == - SyntheticPasswordManager.NULL_PROTECTOR_ID, - "Cannot reinitialize SP"); - - final SyntheticPassword sp = mSpManager.newSyntheticPassword(userId); - final long protectorId = mSpManager.createLskfBasedProtector(getGateKeeperService(), - LockscreenCredential.createNone(), sp, userId); - setCurrentLskfBasedProtectorId(protectorId, userId); - setUserKeyProtection(userId, sp.deriveFileBasedEncryptionKey()); - onSyntheticPasswordKnown(userId, sp); - return sp; + SyntheticPassword initializeSyntheticPassword(int userId) { + synchronized (mSpManager) { + Slog.i(TAG, "Initialize SyntheticPassword for user: " + userId); + Preconditions.checkState(getCurrentLskfBasedProtectorId(userId) == + SyntheticPasswordManager.NULL_PROTECTOR_ID, + "Cannot reinitialize SP"); + + final SyntheticPassword sp = mSpManager.newSyntheticPassword(userId); + final long protectorId = mSpManager.createLskfBasedProtector(getGateKeeperService(), + LockscreenCredential.createNone(), sp, userId); + setCurrentLskfBasedProtectorId(protectorId, userId); + setUserKeyProtection(userId, sp.deriveFileBasedEncryptionKey()); + onSyntheticPasswordKnown(userId, sp); + return sp; + } } @VisibleForTesting @@ -2680,13 +2672,6 @@ public class LockSettingsService extends ILockSettings.Stub { setLong(LSKF_LAST_CHANGED_TIME_KEY, System.currentTimeMillis(), userId); } - @VisibleForTesting - boolean isSyntheticPasswordBasedCredential(int userId) { - synchronized (mSpManager) { - return isSyntheticPasswordBasedCredentialLocked(userId); - } - } - private boolean isSyntheticPasswordBasedCredentialLocked(int userId) { if (userId == USER_FRP) { final int type = mStorage.readPersistentDataBlock().type; @@ -2925,19 +2910,14 @@ public class LockSettingsService extends ILockSettings.Stub { @NonNull EscrowTokenStateChangeCallback callback) { if (DEBUG) Slog.d(TAG, "addEscrowToken: user=" + userId + ", type=" + type); synchronized (mSpManager) { - // If the user has no LSKF, then the token can be activated immediately, after creating - // the user's SP if it doesn't already exist. Otherwise, the token can't be activated - // until the SP is unlocked by another protector (normally the LSKF-based one). + // If the user has no LSKF, then the token can be activated immediately. Otherwise, the + // token can't be activated until the SP is unlocked by another protector (normally the + // LSKF-based one). SyntheticPassword sp = null; if (!isUserSecure(userId)) { long protectorId = getCurrentLskfBasedProtectorId(userId); - if (protectorId == SyntheticPasswordManager.NULL_PROTECTOR_ID) { - // TODO(b/232452368): this case is only needed by unit tests now; remove it. - sp = initializeSyntheticPasswordLocked(userId); - } else { - sp = mSpManager.unlockLskfBasedProtector(getGateKeeperService(), protectorId, - LockscreenCredential.createNone(), userId, null).syntheticPassword; - } + sp = mSpManager.unlockLskfBasedProtector(getGateKeeperService(), protectorId, + LockscreenCredential.createNone(), userId, null).syntheticPassword; } disableEscrowTokenOnNonManagedDevicesIfNeeded(userId); if (!mSpManager.hasEscrowData(userId)) { 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 ed8d852ad2da..50e1fca13877 100644 --- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java +++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java @@ -315,7 +315,7 @@ public final class MediaProjectionManagerService extends SystemService @Override // Binder call public boolean isValidMediaProjection(IMediaProjection projection) { return MediaProjectionManagerService.this.isValidMediaProjection( - projection.asBinder()); + projection == null ? null : projection.asBinder()); } @Override // Binder call @@ -348,7 +348,26 @@ public final class MediaProjectionManagerService extends SystemService } finally { Binder.restoreCallingIdentity(token); } + } + @Override // Binder call + public void notifyActiveProjectionCapturedContentResized(int width, int height) { + if (mContext.checkCallingOrSelfPermission(Manifest.permission.MANAGE_MEDIA_PROJECTION) + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to notify " + + "on captured content resize"); + } + if (!isValidMediaProjection(mProjectionGrant)) { + return; + } + final long token = Binder.clearCallingIdentity(); + try { + if (mProjectionGrant != null && mCallbackDelegate != null) { + mCallbackDelegate.dispatchResize(mProjectionGrant, width, height); + } + } finally { + Binder.restoreCallingIdentity(token); + } } @Override //Binder call @@ -659,9 +678,11 @@ public final class MediaProjectionManagerService extends SystemService private static class CallbackDelegate { private Map<IBinder, IMediaProjectionCallback> mClientCallbacks; + // Map from the IBinder token representing the callback, to the callback instance. + // Represents the callbacks registered on the client's MediaProjectionManager. private Map<IBinder, IMediaProjectionWatcherCallback> mWatcherCallbacks; private Handler mHandler; - private Object mLock = new Object(); + private final Object mLock = new Object(); public CallbackDelegate() { mHandler = new Handler(Looper.getMainLooper(), null, true /*async*/); @@ -715,6 +736,8 @@ public final class MediaProjectionManagerService extends SystemService } synchronized (mLock) { for (IMediaProjectionCallback callback : mClientCallbacks.values()) { + // Notify every callback the client has registered for a particular + // MediaProjection instance. mHandler.post(new ClientStopCallback(callback)); } @@ -724,6 +747,33 @@ public final class MediaProjectionManagerService extends SystemService } } } + + public void dispatchResize(MediaProjection projection, int width, int height) { + if (projection == null) { + Slog.e(TAG, "Tried to dispatch stop notification for a null media projection." + + " Ignoring!"); + return; + } + synchronized (mLock) { + // TODO(b/249827847) Currently the service assumes there is only one projection + // at once - need to find the callback for the given projection, when there are + // multiple sessions. + for (IMediaProjectionCallback callback : mClientCallbacks.values()) { + mHandler.post(() -> { + try { + // Notify every callback the client has registered for a particular + // MediaProjection instance. + callback.onCapturedContentResize(width, height); + } catch (RemoteException e) { + Slog.w(TAG, "Failed to notify media projection has resized to " + width + + " x " + height, e); + } + }); + } + // Do not need to notify watcher callback about resize, since watcher callback + // is for passing along if recording is still ongoing or not. + } + } } private static final class WatcherStartCallback implements Runnable { diff --git a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java index df95f86d1e07..d4c4c694a6c5 100644 --- a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java +++ b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java @@ -17,19 +17,46 @@ package com.android.server.pm; import android.annotation.NonNull; +import android.app.usage.UsageEvents; +import android.app.usage.UsageStatsManagerInternal; import android.content.Context; +import android.content.pm.ApplicationInfo; import android.content.pm.IBackgroundInstallControlService; import android.content.pm.IPackageManager; +import android.content.pm.InstallSourceInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; +import android.content.pm.PackageManagerInternal; import android.content.pm.ParceledListSlice; -import android.os.IBinder; +import android.os.Environment; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; import android.os.RemoteException; import android.os.ServiceManager; +import android.os.SystemClock; +import android.os.UserHandle; +import android.util.ArraySet; +import android.util.AtomicFile; +import android.util.Slog; import android.util.SparseArrayMap; +import android.util.SparseSetArray; +import android.util.proto.ProtoInputStream; +import android.util.proto.ProtoOutputStream; import com.android.internal.annotations.VisibleForTesting; +import com.android.server.LocalServices; +import com.android.server.ServiceThread; import com.android.server.SystemService; +import com.android.server.pm.permission.PermissionManagerServiceInternal; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ListIterator; +import java.util.Set; +import java.util.TreeSet; /** * @hide @@ -37,14 +64,30 @@ import com.android.server.SystemService; public class BackgroundInstallControlService extends SystemService { private static final String TAG = "BackgroundInstallControlService"; + private static final String DISK_FILE_NAME = "states"; + private static final String DISK_DIR_NAME = "bic"; + + private static final int MAX_FOREGROUND_TIME_FRAMES_SIZE = 10; + + private static final int MSG_USAGE_EVENT_RECEIVED = 0; + private static final int MSG_PACKAGE_ADDED = 1; + private static final int MSG_PACKAGE_REMOVED = 2; + private final Context mContext; private final BinderService mBinderService; private final IPackageManager mIPackageManager; + private final PackageManagerInternal mPackageManagerInternal; + private final UsageStatsManagerInternal mUsageStatsManagerInternal; + private final PermissionManagerServiceInternal mPermissionManager; + private final Handler mHandler; + private final File mDiskFile; + - // User ID -> package name -> time diff - // The time diff between the last foreground activity installer and - // the "onPackageAdded" function call. - private final SparseArrayMap<String, Long> mBackgroundInstalledPackages = + private SparseSetArray<String> mBackgroundInstalledPackages = null; + + // User ID -> package name -> set of foreground time frame + private final SparseArrayMap<String, + TreeSet<ForegroundTimeFrame>> mInstallerForegroundTimeFrames = new SparseArrayMap<>(); public BackgroundInstallControlService(@NonNull Context context) { @@ -56,49 +99,385 @@ public class BackgroundInstallControlService extends SystemService { super(injector.getContext()); mContext = injector.getContext(); mIPackageManager = injector.getIPackageManager(); + mPackageManagerInternal = injector.getPackageManagerInternal(); + mPermissionManager = injector.getPermissionManager(); + mHandler = new EventHandler(injector.getLooper(), this); + mDiskFile = injector.getDiskFile(); + mUsageStatsManagerInternal = injector.getUsageStatsManagerInternal(); + mUsageStatsManagerInternal.registerListener( + (userId, event) -> + mHandler.obtainMessage(MSG_USAGE_EVENT_RECEIVED, + userId, + 0, + event).sendToTarget() + ); mBinderService = new BinderService(this); } private static final class BinderService extends IBackgroundInstallControlService.Stub { final BackgroundInstallControlService mService; - BinderService(BackgroundInstallControlService service) { + BinderService(BackgroundInstallControlService service) { mService = service; } @Override public ParceledListSlice<PackageInfo> getBackgroundInstalledPackages( @PackageManager.PackageInfoFlagsBits long flags, int userId) { - ParceledListSlice<PackageInfo> packages; - try { - packages = mService.mIPackageManager.getInstalledPackages(flags, userId); - } catch (RemoteException e) { - throw new IllegalStateException("Package manager not available", e); + return mService.getBackgroundInstalledPackages(flags, userId); + } + } + + @VisibleForTesting + ParceledListSlice<PackageInfo> getBackgroundInstalledPackages( + @PackageManager.PackageInfoFlagsBits long flags, int userId) { + ParceledListSlice<PackageInfo> packages; + try { + packages = mIPackageManager.getInstalledPackages(flags, userId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + + initBackgroundInstalledPackages(); + + ListIterator<PackageInfo> iter = packages.getList().listIterator(); + while (iter.hasNext()) { + String packageName = iter.next().packageName; + if (!mBackgroundInstalledPackages.contains(userId, packageName)) { + iter.remove(); } + } + + return packages; + } + + private static class EventHandler extends Handler { + private final BackgroundInstallControlService mService; - // TODO(b/244216300): to enable the test the usage by BinaryTransparencyService, - // we currently comment out the actual implementation. - // The fake implementation is just to filter out the first app of the list. - // for (int i = 0, size = packages.getList().size(); i < size; i++) { - // String packageName = packages.getList().get(i).packageName; - // if (!mBackgroundInstalledPackages.contains(userId, packageName) { - // packages.getList().remove(i); - // } - // } - if (packages.getList().size() > 0) { - packages.getList().remove(0); + EventHandler(Looper looper, BackgroundInstallControlService service) { + super(looper); + mService = service; + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_USAGE_EVENT_RECEIVED: { + mService.handleUsageEvent((UsageEvents.Event) msg.obj, msg.arg1 /* userId */); + break; + } + case MSG_PACKAGE_ADDED: { + mService.handlePackageAdd((String) msg.obj, msg.arg1 /* userId */); + break; + } + case MSG_PACKAGE_REMOVED: { + mService.handlePackageRemove((String) msg.obj, msg.arg1 /* userId */); + break; + } + default: + Slog.w(TAG, "Unknown message: " + msg.what); } - return packages; } } - /** - * Called when the system service should publish a binder service using - * {@link #publishBinderService(String, IBinder).} - */ + void handlePackageAdd(String packageName, int userId) { + InstallSourceInfo installSourceInfo = null; + try { + installSourceInfo = mIPackageManager.getInstallSourceInfo(packageName); + } catch (RemoteException e) { + // Failed to talk to PackageManagerService Should never happen! + throw e.rethrowFromSystemServer(); + } + String installerPackageName = + installSourceInfo == null ? null : installSourceInfo.getInstallingPackageName(); + if (installerPackageName == null) { + Slog.w(TAG, "fails to get installerPackageName for " + packageName); + return; + } + + ApplicationInfo appInfo = null; + try { + appInfo = mIPackageManager.getApplicationInfo(packageName, + 0, userId); + } catch (RemoteException e) { + // Failed to talk to PackageManagerService Should never happen! + throw e.rethrowFromSystemServer(); + } + + if (appInfo == null) { + Slog.w(TAG, "fails to get appInfo for " + packageName); + return; + } + + // convert up-time to current time. + final long installTimestamp = System.currentTimeMillis() + - (SystemClock.uptimeMillis() - appInfo.createTimestamp); + + if (wasForegroundInstallation(installerPackageName, userId, installTimestamp)) { + return; + } + + initBackgroundInstalledPackages(); + mBackgroundInstalledPackages.add(userId, packageName); + writeBackgroundInstalledPackagesToDisk(); + } + + private boolean wasForegroundInstallation(String installerPackageName, + int userId, long installTimestamp) { + TreeSet<BackgroundInstallControlService.ForegroundTimeFrame> foregroundTimeFrames = + mInstallerForegroundTimeFrames.get(userId, installerPackageName); + + // The installer never run in foreground. + if (foregroundTimeFrames == null) { + return false; + } + + for (var foregroundTimeFrame : foregroundTimeFrames) { + // the foreground time frame starts later than the installation. + // so the installation is outside the foreground time frame. + if (foregroundTimeFrame.startTimeStampMillis > installTimestamp) { + continue; + } + + // the foreground time frame is not over yet. + // the installation is inside the foreground time frame. + if (!foregroundTimeFrame.isDone()) { + return true; + } + + // the foreground time frame ends later than the installation. + // the installation is inside the foreground time frame. + if (installTimestamp <= foregroundTimeFrame.endTimeStampMillis) { + return true; + } + } + + // the installation is not inside any of foreground time frames. + // so it is not a foreground installation. + return false; + } + + void handlePackageRemove(String packageName, int userId) { + initBackgroundInstalledPackages(); + mBackgroundInstalledPackages.remove(userId, packageName); + writeBackgroundInstalledPackagesToDisk(); + } + + void handleUsageEvent(UsageEvents.Event event, int userId) { + if (event.mEventType != UsageEvents.Event.ACTIVITY_RESUMED + && event.mEventType != UsageEvents.Event.ACTIVITY_PAUSED + && event.mEventType != UsageEvents.Event.ACTIVITY_STOPPED) { + return; + } + + if (!isInstaller(event.mPackage, userId)) { + return; + } + + if (!mInstallerForegroundTimeFrames.contains(userId, event.mPackage)) { + mInstallerForegroundTimeFrames.add(userId, event.mPackage, new TreeSet<>()); + } + + TreeSet<BackgroundInstallControlService.ForegroundTimeFrame> foregroundTimeFrames = + mInstallerForegroundTimeFrames.get(userId, event.mPackage); + + if ((foregroundTimeFrames.size() == 0) || foregroundTimeFrames.last().isDone()) { + // ignore the other events if there is no open ForegroundTimeFrame. + if (event.mEventType != UsageEvents.Event.ACTIVITY_RESUMED) { + return; + } + foregroundTimeFrames.add(new ForegroundTimeFrame(event.mTimeStamp)); + } + + foregroundTimeFrames.last().addEvent(event); + + if (foregroundTimeFrames.size() > MAX_FOREGROUND_TIME_FRAMES_SIZE) { + foregroundTimeFrames.pollFirst(); + } + } + + @VisibleForTesting + void writeBackgroundInstalledPackagesToDisk() { + AtomicFile atomicFile = new AtomicFile(mDiskFile); + FileOutputStream fileOutputStream; + try { + fileOutputStream = atomicFile.startWrite(); + } catch (IOException e) { + Slog.e(TAG, "Failed to start write to states protobuf.", e); + return; + } + + try { + ProtoOutputStream protoOutputStream = new ProtoOutputStream(fileOutputStream); + for (int i = 0; i < mBackgroundInstalledPackages.size(); i++) { + int userId = mBackgroundInstalledPackages.keyAt(i); + for (String packageName : mBackgroundInstalledPackages.get(userId)) { + long token = protoOutputStream.start( + BackgroundInstalledPackagesProto.BG_INSTALLED_PKG); + protoOutputStream.write( + BackgroundInstalledPackageProto.PACKAGE_NAME, packageName); + protoOutputStream.write( + BackgroundInstalledPackageProto.USER_ID, userId + 1); + protoOutputStream.end(token); + } + } + protoOutputStream.flush(); + atomicFile.finishWrite(fileOutputStream); + } catch (Exception e) { + Slog.e(TAG, "Failed to finish write to states protobuf.", e); + atomicFile.failWrite(fileOutputStream); + } + } + + @VisibleForTesting + void initBackgroundInstalledPackages() { + if (mBackgroundInstalledPackages != null) { + return; + } + + mBackgroundInstalledPackages = new SparseSetArray<>(); + + if (!mDiskFile.exists()) { + return; + } + + AtomicFile atomicFile = new AtomicFile(mDiskFile); + try (FileInputStream fileInputStream = atomicFile.openRead()) { + ProtoInputStream protoInputStream = new ProtoInputStream(fileInputStream); + + while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + if (protoInputStream.getFieldNumber() + != (int) BackgroundInstalledPackagesProto.BG_INSTALLED_PKG) { + continue; + } + long token = protoInputStream.start( + BackgroundInstalledPackagesProto.BG_INSTALLED_PKG); + String packageName = null; + int userId = UserHandle.USER_NULL; + while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (protoInputStream.getFieldNumber()) { + case (int) BackgroundInstalledPackageProto.PACKAGE_NAME: + packageName = protoInputStream.readString( + BackgroundInstalledPackageProto.PACKAGE_NAME); + break; + case (int) BackgroundInstalledPackageProto.USER_ID: + userId = protoInputStream.readInt( + BackgroundInstalledPackageProto.USER_ID) - 1; + break; + default: + Slog.w(TAG, "Undefined field in proto: " + + protoInputStream.getFieldNumber()); + } + } + protoInputStream.end(token); + if (packageName != null && userId != UserHandle.USER_NULL) { + mBackgroundInstalledPackages.add(userId, packageName); + } else { + Slog.w(TAG, "Fails to get packageName or UserId from proto file"); + } + } + } catch (IOException e) { + Slog.w(TAG, "Error reading state from the disk", e); + } + } + + @VisibleForTesting + SparseSetArray<String> getBackgroundInstalledPackages() { + return mBackgroundInstalledPackages; + } + + @VisibleForTesting + SparseArrayMap<String, TreeSet<ForegroundTimeFrame>> getInstallerForegroundTimeFrames() { + return mInstallerForegroundTimeFrames; + } + + private boolean isInstaller(String pkgName, int userId) { + if (mInstallerForegroundTimeFrames.contains(userId, pkgName)) { + return true; + } + return mPermissionManager.checkPermission(pkgName, + android.Manifest.permission.INSTALL_PACKAGES, + userId) == PackageManager.PERMISSION_GRANTED; + } + @Override public void onStart() { - publishBinderService(Context.BACKGROUND_INSTALL_CONTROL_SERVICE, mBinderService); + onStart(/* isForTesting= */ false); + } + + @VisibleForTesting + void onStart(boolean isForTesting) { + if (!isForTesting) { + publishBinderService(Context.BACKGROUND_INSTALL_CONTROL_SERVICE, mBinderService); + } + + mPackageManagerInternal.getPackageList(new PackageManagerInternal.PackageListObserver() { + @Override + public void onPackageAdded(String packageName, int uid) { + final int userId = UserHandle.getUserId(uid); + mHandler.obtainMessage(MSG_PACKAGE_ADDED, + userId, 0, packageName).sendToTarget(); + } + + @Override + public void onPackageRemoved(String packageName, int uid) { + final int userId = UserHandle.getUserId(uid); + mHandler.obtainMessage(MSG_PACKAGE_REMOVED, + userId, 0, packageName).sendToTarget(); + } + }); + } + + // The foreground time frame (ForegroundTimeFrame) represents the period + // when a package's activities continuously occupy the foreground. + // Each ForegroundTimeFrame starts with an ACTIVITY_RESUMED event, + // and then ends with an ACTIVITY_PAUSED or ACTIVITY_STOPPED event. + // The startTimeStampMillis stores the timestamp of the ACTIVITY_RESUMED event. + // The endTimeStampMillis stores the timestamp of the ACTIVITY_PAUSED or ACTIVITY_STOPPED event + // that wraps up the ForegroundTimeFrame. + // The activities are designed to handle the edge case in which a package's one activity + // seamlessly replace another activity of the same package. Thus, we count these activities + // together as a ForegroundTimeFrame. For this scenario, only when all the activities terminate + // shall consider the completion of the ForegroundTimeFrame. + static final class ForegroundTimeFrame implements Comparable<ForegroundTimeFrame> { + public final long startTimeStampMillis; + public long endTimeStampMillis; + public final Set<Integer> activities; + + public int compareTo(ForegroundTimeFrame o) { + int comp = Long.compare(startTimeStampMillis, o.startTimeStampMillis); + if (comp != 0) return comp; + + return Integer.compare(hashCode(), o.hashCode()); + } + + ForegroundTimeFrame(long startTimeStampMillis) { + this.startTimeStampMillis = startTimeStampMillis; + endTimeStampMillis = 0; + activities = new ArraySet<>(); + } + + public boolean isDone() { + return endTimeStampMillis != 0; + } + + public void addEvent(UsageEvents.Event event) { + switch (event.mEventType) { + case UsageEvents.Event.ACTIVITY_RESUMED: + activities.add(event.mInstanceId); + break; + case UsageEvents.Event.ACTIVITY_PAUSED: + case UsageEvents.Event.ACTIVITY_STOPPED: + if (activities.contains(event.mInstanceId)) { + activities.remove(event.mInstanceId); + if (activities.size() == 0) { + endTimeStampMillis = event.mTimeStamp; + } + } + break; + default: + } + } } /** @@ -108,6 +487,16 @@ public class BackgroundInstallControlService extends SystemService { Context getContext(); IPackageManager getIPackageManager(); + + PackageManagerInternal getPackageManagerInternal(); + + UsageStatsManagerInternal getUsageStatsManagerInternal(); + + PermissionManagerServiceInternal getPermissionManager(); + + Looper getLooper(); + + File getDiskFile(); } private static final class InjectorImpl implements Injector { @@ -126,5 +515,36 @@ public class BackgroundInstallControlService extends SystemService { public IPackageManager getIPackageManager() { return IPackageManager.Stub.asInterface(ServiceManager.getService("package")); } + + @Override + public PackageManagerInternal getPackageManagerInternal() { + return LocalServices.getService(PackageManagerInternal.class); + } + + @Override + public UsageStatsManagerInternal getUsageStatsManagerInternal() { + return LocalServices.getService(UsageStatsManagerInternal.class); + } + + @Override + public PermissionManagerServiceInternal getPermissionManager() { + return LocalServices.getService(PermissionManagerServiceInternal.class); + } + + @Override + public Looper getLooper() { + ServiceThread serviceThread = new ServiceThread(TAG, + android.os.Process.THREAD_PRIORITY_FOREGROUND, true /* allowIo */); + serviceThread.start(); + return serviceThread.getLooper(); + + } + + @Override + public File getDiskFile() { + File dir = new File(Environment.getDataSystemDirectory(), DISK_DIR_NAME); + File file = new File(dir, DISK_FILE_NAME); + return file; + } } } diff --git a/services/core/java/com/android/server/pm/Computer.java b/services/core/java/com/android/server/pm/Computer.java index 60621a0eaaef..ea8428351a81 100644 --- a/services/core/java/com/android/server/pm/Computer.java +++ b/services/core/java/com/android/server/pm/Computer.java @@ -570,8 +570,9 @@ public interface Computer extends PackageDataSnapshot { @PackageManager.InstallReason int getInstallReason(@NonNull String packageName, @UserIdInt int userId); - boolean canPackageQuery(@NonNull String sourcePackageName, @NonNull String targetPackageName, - @UserIdInt int userId); + @NonNull + boolean[] canPackageQuery(@NonNull String sourcePackageName, + @NonNull String[] targetPackageNames, @UserIdInt int userId); boolean canForwardTo(@NonNull Intent intent, @Nullable String resolvedType, @UserIdInt int sourceUserId, @UserIdInt int targetUserId); diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java index b8fba51c4d60..06aadd92dd51 100644 --- a/services/core/java/com/android/server/pm/ComputerEngine.java +++ b/services/core/java/com/android/server/pm/ComputerEngine.java @@ -159,6 +159,7 @@ import java.io.IOException; import java.io.PrintWriter; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; @@ -5337,29 +5338,42 @@ public class ComputerEngine implements Computer { } @Override - public boolean canPackageQuery(@NonNull String sourcePackageName, - @NonNull String targetPackageName, @UserIdInt int userId) { - if (!mUserManager.exists(userId)) return false; + @NonNull + public boolean[] canPackageQuery(@NonNull String sourcePackageName, + @NonNull String[] targetPackageNames, @UserIdInt int userId) { + final int targetSize = targetPackageNames.length; + final boolean[] results = new boolean[targetSize]; + if (!mUserManager.exists(userId)) { + return results; + } final int callingUid = Binder.getCallingUid(); enforceCrossUserPermission(callingUid, userId, false /*requireFullPermission*/, - false /*checkShell*/, "may package query"); + false /*checkShell*/, "can package query"); + final PackageStateInternal sourceSetting = getPackageStateInternal(sourcePackageName); - final PackageStateInternal targetSetting = getPackageStateInternal(targetPackageName); - boolean throwException = sourceSetting == null || targetSetting == null; - if (!throwException) { - final boolean filterSource = - shouldFilterApplicationIncludingUninstalled(sourceSetting, callingUid, userId); - final boolean filterTarget = - shouldFilterApplicationIncludingUninstalled(targetSetting, callingUid, userId); - // The caller must have visibility of the both packages - throwException = filterSource || filterTarget; + final PackageStateInternal[] targetSettings = new PackageStateInternal[targetSize]; + // Throw exception if the caller without the visibility of source package + boolean throwException = + (sourceSetting == null || shouldFilterApplicationIncludingUninstalled( + sourceSetting, callingUid, userId)); + for (int i = 0; !throwException && i < targetSize; i++) { + targetSettings[i] = getPackageStateInternal(targetPackageNames[i]); + // Throw exception if the caller without the visibility of target package + throwException = + (targetSettings[i] == null || shouldFilterApplicationIncludingUninstalled( + targetSettings[i], callingUid, userId)); } if (throwException) { throw new ParcelableException(new PackageManager.NameNotFoundException("Package(s) " - + sourcePackageName + " and/or " + targetPackageName + " not found.")); + + sourcePackageName + " and/or " + Arrays.toString(targetPackageNames) + + " not found.")); } + final int sourcePackageUid = UserHandle.getUid(userId, sourceSetting.getAppId()); - return !shouldFilterApplication(targetSetting, sourcePackageUid, userId); + for (int i = 0; i < targetSize; i++) { + results[i] = !shouldFilterApplication(targetSettings[i], sourcePackageUid, userId); + } + return results; } /* diff --git a/services/core/java/com/android/server/pm/DexOptHelper.java b/services/core/java/com/android/server/pm/DexOptHelper.java index 0066592de8c4..5661399c2ed7 100644 --- a/services/core/java/com/android/server/pm/DexOptHelper.java +++ b/services/core/java/com/android/server/pm/DexOptHelper.java @@ -62,6 +62,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.logging.MetricsLogger; import com.android.server.LocalManagerRegistry; import com.android.server.art.ArtManagerLocal; +import com.android.server.art.DexUseManagerLocal; import com.android.server.art.model.ArtFlags; import com.android.server.art.model.OptimizeParams; import com.android.server.art.model.OptimizeResult; @@ -932,9 +933,23 @@ public final class DexOptHelper { } /** - * Returns {@link ArtManagerLocal} if one is found and should be used for package optimization. + * Returns {@link DexUseManagerLocal} if ART Service should be used for package optimization. */ - private @Nullable ArtManagerLocal getArtManagerLocal() { + public static @Nullable DexUseManagerLocal getDexUseManagerLocal() { + if (!"true".equals(SystemProperties.get("dalvik.vm.useartservice", ""))) { + return null; + } + try { + return LocalManagerRegistry.getManagerOrThrow(DexUseManagerLocal.class); + } catch (ManagerNotFoundException e) { + throw new RuntimeException(e); + } + } + + /** + * Returns {@link ArtManagerLocal} if ART Service should be used for package optimization. + */ + private static @Nullable ArtManagerLocal getArtManagerLocal() { if (!"true".equals(SystemProperties.get("dalvik.vm.useartservice", ""))) { return null; } diff --git a/services/core/java/com/android/server/pm/IPackageManagerBase.java b/services/core/java/com/android/server/pm/IPackageManagerBase.java index 5c3890c192dd..38efc104bdf9 100644 --- a/services/core/java/com/android/server/pm/IPackageManagerBase.java +++ b/services/core/java/com/android/server/pm/IPackageManagerBase.java @@ -1153,9 +1153,10 @@ public abstract class IPackageManagerBase extends IPackageManager.Stub { @Override @Deprecated - public final boolean canPackageQuery(@NonNull String sourcePackageName, - @NonNull String targetPackageName, @UserIdInt int userId) { - return snapshot().canPackageQuery(sourcePackageName, targetPackageName, userId); + @NonNull + public final boolean[] canPackageQuery(@NonNull String sourcePackageName, + @NonNull String[] targetPackageNames, @UserIdInt int userId) { + return snapshot().canPackageQuery(sourcePackageName, targetPackageNames, userId); } @Override diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index e6e2f7988493..759ec67cfba8 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -193,6 +193,7 @@ import com.android.server.ServiceThread; import com.android.server.SystemConfig; import com.android.server.Watchdog; import com.android.server.apphibernation.AppHibernationManagerInternal; +import com.android.server.art.DexUseManagerLocal; import com.android.server.compat.CompatChange; import com.android.server.compat.PlatformCompat; import com.android.server.pm.Installer.InstallerException; @@ -212,6 +213,7 @@ import com.android.server.pm.permission.LegacyPermissionManagerService; import com.android.server.pm.permission.PermissionManagerService; import com.android.server.pm.permission.PermissionManagerServiceInternal; import com.android.server.pm.pkg.AndroidPackage; +import com.android.server.pm.pkg.PackageState; import com.android.server.pm.pkg.PackageStateInternal; import com.android.server.pm.pkg.PackageUserState; import com.android.server.pm.pkg.PackageUserStateInternal; @@ -3266,6 +3268,7 @@ public class PackageManagerService implements PackageSender, TestUtilityService return isPackageDeviceAdmin(packageName, UserHandle.USER_ALL); } + // TODO(b/261957226): centralise this logic in DPM boolean isPackageDeviceAdmin(String packageName, int userId) { final IDevicePolicyManager dpm = getDevicePolicyManager(); try { @@ -3292,6 +3295,9 @@ public class PackageManagerService implements PackageSender, TestUtilityService if (dpm.packageHasActiveAdmins(packageName, users[i])) { return true; } + if (isDeviceManagementRoleHolder(packageName, users[i])) { + return true; + } } } } catch (RemoteException e) { @@ -3299,6 +3305,24 @@ public class PackageManagerService implements PackageSender, TestUtilityService return false; } + private boolean isDeviceManagementRoleHolder(String packageName, int userId) { + return Objects.equals(packageName, getDevicePolicyManagementRoleHolderPackageName(userId)); + } + + @Nullable + private String getDevicePolicyManagementRoleHolderPackageName(int userId) { + return Binder.withCleanCallingIdentity(() -> { + RoleManager roleManager = mContext.getSystemService(RoleManager.class); + List<String> roleHolders = + roleManager.getRoleHoldersAsUser( + RoleManager.ROLE_DEVICE_POLICY_MANAGEMENT, UserHandle.of(userId)); + if (roleHolders.isEmpty()) { + return null; + } + return roleHolders.get(0); + }); + } + /** Returns the device policy manager interface. */ private IDevicePolicyManager getDevicePolicyManager() { if (mDevicePolicyManager == null) { @@ -5308,18 +5332,70 @@ public class PackageManagerService implements PackageSender, TestUtilityService return; } - // TODO(b/254043366): Call `ArtManagerLocal.notifyDexLoad`. + UserHandle user = Binder.getCallingUserHandle(); + int userId = user.getIdentifier(); + + // Proxy the call to either ART Service or the legacy implementation. If the + // implementation is switched with the system property, the dex usage info will be + // incomplete, with these effects: + // + // - Shared dex files may temporarily get compiled for private use. + // - Secondary dex files may not get compiled at all. + // - Stale compiled artifacts for secondary dex files may not get cleaned up. + // + // This recovers in the first background dexopt after the depending apps have been + // loaded for the first time. + + DexUseManagerLocal dexUseManager = DexOptHelper.getDexUseManagerLocal(); + if (dexUseManager != null) { + // TODO(chiuwinson): Retrieve filtered snapshot from Computer instance instead. + try (PackageManagerLocal.FilteredSnapshot filteredSnapshot = + LocalManagerRegistry.getManager(PackageManagerLocal.class) + .withFilteredSnapshot(callingUid, user)) { + if (loaderIsa != null) { + // Check that loaderIsa agrees with the ISA that dexUseManager will + // determine. + PackageState loadingPkgState = + filteredSnapshot.getPackageState(loadingPackageName); + // If we don't find the loading package just pass it through and let + // dexUseManager throw on it. + if (loadingPkgState != null) { + String loadingPkgAbi = loadingPkgState.getPrimaryCpuAbi(); + if (loadingPkgAbi == null) { + loadingPkgAbi = Build.SUPPORTED_ABIS[0]; + } + String loadingPkgDexCodeIsa = InstructionSets.getDexCodeInstructionSet( + VMRuntime.getInstructionSet(loadingPkgAbi)); + if (!loaderIsa.equals(loadingPkgDexCodeIsa)) { + // TODO(b/251903639): Make this crash to surface this problem + // better. + Slog.w(PackageManagerService.TAG, + "Invalid loaderIsa in notifyDexLoad call from " + + loadingPackageName + ", uid " + callingUid + + ": expected " + loadingPkgDexCodeIsa + ", got " + + loaderIsa); + return; + } + } + } - int userId = UserHandle.getCallingUserId(); - ApplicationInfo ai = - snapshot.getApplicationInfo(loadingPackageName, /*flags*/ 0, userId); - if (ai == null) { - Slog.w(PackageManagerService.TAG, "Loading a package that does not exist for the calling user. package=" - + loadingPackageName + ", user=" + userId); - return; + // This is called from binder, so exceptions thrown here are caught and handled + // by it. + dexUseManager.notifyDexContainersLoaded( + filteredSnapshot, loadingPackageName, classLoaderContextMap); + } + } else { + ApplicationInfo ai = + snapshot.getApplicationInfo(loadingPackageName, /*flags*/ 0, userId); + if (ai == null) { + Slog.w(PackageManagerService.TAG, + "Loading a package that does not exist for the calling user. package=" + + loadingPackageName + ", user=" + userId); + return; + } + mDexManager.notifyDexLoad(ai, classLoaderContextMap, loaderIsa, userId, + Process.isIsolated(callingUid)); } - mDexManager.notifyDexLoad(ai, classLoaderContextMap, loaderIsa, userId, - Process.isIsolated(callingUid)); } @Override diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java index 01dee132a324..7fec0eb00fe4 100644 --- a/services/core/java/com/android/server/pm/Settings.java +++ b/services/core/java/com/android/server/pm/Settings.java @@ -369,8 +369,6 @@ public final class Settings implements Watchable, Snappable { // Current settings file. private final File mSettingsFilename; - // Compressed current settings file. - private final File mCompressedSettingsFilename; // Previous settings file. // Removed when the current settings file successfully stored. private final File mPreviousSettingsFilename; @@ -641,7 +639,6 @@ public final class Settings implements Watchable, Snappable { mRuntimePermissionsPersistence = null; mPermissionDataProvider = null; mSettingsFilename = null; - mCompressedSettingsFilename = null; mPreviousSettingsFilename = null; mPackageListFilename = null; mStoppedPackagesFilename = null; @@ -713,7 +710,6 @@ public final class Settings implements Watchable, Snappable { |FileUtils.S_IROTH|FileUtils.S_IXOTH, -1, -1); mSettingsFilename = new File(mSystemDir, "packages.xml"); - mCompressedSettingsFilename = new File(mSystemDir, "packages.compressed"); mPreviousSettingsFilename = new File(mSystemDir, "packages-backup.xml"); mPackageListFilename = new File(mSystemDir, "packages.list"); FileUtils.setPermissions(mPackageListFilename, 0640, SYSTEM_UID, PACKAGE_INFO_GID); @@ -755,7 +751,6 @@ public final class Settings implements Watchable, Snappable { mLock = null; mRuntimePermissionsPersistence = r.mRuntimePermissionsPersistence; mSettingsFilename = null; - mCompressedSettingsFilename = null; mPreviousSettingsFilename = null; mPackageListFilename = null; mStoppedPackagesFilename = null; @@ -2597,8 +2592,6 @@ public final class Settings implements Watchable, Snappable { Slog.w(PackageManagerService.TAG, "Preserving older settings backup"); } } - // Compressed settings are not valid anymore. - mCompressedSettingsFilename.delete(); mPastSignatures.clear(); @@ -2688,30 +2681,10 @@ public final class Settings implements Watchable, Snappable { mPreviousSettingsFilename.delete(); FileUtils.setPermissions(mSettingsFilename.toString(), - FileUtils.S_IRUSR | FileUtils.S_IWUSR | FileUtils.S_IRGRP | FileUtils.S_IWGRP, + FileUtils.S_IRUSR|FileUtils.S_IWUSR + |FileUtils.S_IRGRP|FileUtils.S_IWGRP, -1, -1); - final FileInputStream fis = new FileInputStream(mSettingsFilename); - final AtomicFile compressed = new AtomicFile(mCompressedSettingsFilename); - final FileOutputStream fos = compressed.startWrite(); - - BackgroundThread.getHandler().post(() -> { - try { - if (!nativeCompressLz4(fis.getFD().getInt$(), fos.getFD().getInt$())) { - throw new IOException("Failed to compress"); - } - compressed.finishWrite(fos); - FileUtils.setPermissions(mCompressedSettingsFilename.toString(), - FileUtils.S_IRUSR | FileUtils.S_IWUSR | FileUtils.S_IRGRP - | FileUtils.S_IWGRP, -1, -1); - } catch (IOException e) { - Slog.e(PackageManagerService.TAG, "Failed to write compressed settings file: " - + mCompressedSettingsFilename, e); - compressed.delete(); - } - IoUtils.closeQuietly(fis); - }); - writeKernelMappingLPr(); writePackageListLPr(); writeAllUsersPackageRestrictionsLPr(sync); @@ -2734,8 +2707,6 @@ public final class Settings implements Watchable, Snappable { //Debug.stopMethodTracing(); } - private native boolean nativeCompressLz4(int inputFd, int outputFd); - private void writeKernelRemoveUserLPr(int userId) { if (mKernelMappingFilename == null) return; diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index 0a650c912b3e..251da8441e0b 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -6986,10 +6986,8 @@ public class UserManagerService extends IUserManager.Stub { @UserAssignmentResult public int assignUserToDisplayOnStart(@UserIdInt int userId, @UserIdInt int profileGroupId, @UserStartMode int userStartMode, int displayId) { - // TODO(245939659): change UserVisibilityMediator to take @UserStartMode - boolean foreground = userStartMode == UserManagerInternal.USER_START_MODE_FOREGROUND; return mUserVisibilityMediator.assignUserToDisplayOnStart(userId, profileGroupId, - foreground, displayId); + userStartMode, displayId); } @Override diff --git a/services/core/java/com/android/server/pm/UserVisibilityMediator.java b/services/core/java/com/android/server/pm/UserVisibilityMediator.java index 92e8f55287d4..40d87bc33ebd 100644 --- a/services/core/java/com/android/server/pm/UserVisibilityMediator.java +++ b/services/core/java/com/android/server/pm/UserVisibilityMediator.java @@ -23,7 +23,11 @@ import static android.view.Display.DEFAULT_DISPLAY; import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_FAILURE; import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE; import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE; +import static com.android.server.pm.UserManagerInternal.USER_START_MODE_BACKGROUND; +import static com.android.server.pm.UserManagerInternal.USER_START_MODE_BACKGROUND_VISIBLE; +import static com.android.server.pm.UserManagerInternal.USER_START_MODE_FOREGROUND; import static com.android.server.pm.UserManagerInternal.userAssignmentResultToString; +import static com.android.server.pm.UserManagerInternal.userStartModeToString; import android.annotation.IntDef; import android.annotation.Nullable; @@ -43,6 +47,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.Preconditions; import com.android.server.am.EventLogTags; import com.android.server.pm.UserManagerInternal.UserAssignmentResult; +import com.android.server.pm.UserManagerInternal.UserStartMode; import com.android.server.pm.UserManagerInternal.UserVisibilityListener; import com.android.server.utils.Slogf; @@ -142,7 +147,8 @@ public final class UserVisibilityMediator implements Dumpable { * See {@link UserManagerInternal#assignUserToDisplayOnStart(int, int, int, int)}. */ public @UserAssignmentResult int assignUserToDisplayOnStart(@UserIdInt int userId, - @UserIdInt int unResolvedProfileGroupId, boolean foreground, int displayId) { + @UserIdInt int unResolvedProfileGroupId, @UserStartMode int userStartMode, + int displayId) { Preconditions.checkArgument(!isSpecialUserId(userId), "user id cannot be generic: %d", userId); // This method needs to perform 4 actions: @@ -161,14 +167,16 @@ public final class UserVisibilityMediator implements Dumpable { ? userId : unResolvedProfileGroupId; if (DBG) { - Slogf.d(TAG, "assignUserToDisplayOnStart(%d, %d, %b, %d): actualProfileGroupId=%d", - userId, unResolvedProfileGroupId, foreground, displayId, profileGroupId); + Slogf.d(TAG, "assignUserToDisplayOnStart(%d, %d, %s, %d): actualProfileGroupId=%d", + userId, unResolvedProfileGroupId, userStartModeToString(userStartMode), + displayId, profileGroupId); } int result; IntArray visibleUsersBefore, visibleUsersAfter; synchronized (mLock) { - result = getUserVisibilityOnStartLocked(userId, profileGroupId, foreground, displayId); + result = getUserVisibilityOnStartLocked(userId, profileGroupId, userStartMode, + displayId); if (DBG) { Slogf.d(TAG, "result of getUserVisibilityOnStartLocked(%s)", userAssignmentResultToString(result)); @@ -185,7 +193,7 @@ public final class UserVisibilityMediator implements Dumpable { visibleUsersBefore = getVisibleUsers(); // Set current user / profiles state - if (foreground) { + if (userStartMode == USER_START_MODE_FOREGROUND) { mCurrentUserId = userId; } if (DBG) { @@ -228,8 +236,23 @@ public final class UserVisibilityMediator implements Dumpable { @GuardedBy("mLock") @UserAssignmentResult - private int getUserVisibilityOnStartLocked(@UserIdInt int userId, - @UserIdInt int profileGroupId, boolean foreground, int displayId) { + private int getUserVisibilityOnStartLocked(@UserIdInt int userId, @UserIdInt int profileGroupId, + @UserStartMode int userStartMode, int displayId) { + + // Check for invalid combinations first + if (userStartMode == USER_START_MODE_BACKGROUND && displayId != DEFAULT_DISPLAY) { + Slogf.wtf(TAG, "cannot start user (%d) as BACKGROUND_USER on secondary display (%d) " + + "(it should be BACKGROUND_USER_VISIBLE", userId, displayId); + return USER_ASSIGNMENT_RESULT_FAILURE; + } + if (userStartMode == USER_START_MODE_BACKGROUND_VISIBLE + && displayId == DEFAULT_DISPLAY && !isProfile(userId, profileGroupId)) { + Slogf.wtf(TAG, "cannot start full user (%d) visible on default display", userId); + return USER_ASSIGNMENT_RESULT_FAILURE; + } + + boolean foreground = userStartMode == USER_START_MODE_FOREGROUND; + if (displayId != DEFAULT_DISPLAY) { if (foreground) { Slogf.w(TAG, "getUserVisibilityOnStartLocked(%d, %d, %b, %d) failed: cannot start " diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index d5fbe46c5582..b5d4afe2cdf2 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -2901,7 +2901,9 @@ public class PhoneWindowManager implements WindowManagerPolicy { } break; case KeyEvent.KEYCODE_RECENT_APPS: - // TODO(b/261621522): Handle recents key presses + if (down && repeatCount == 0) { + showRecentApps(false /* triggeredFromAltTab */); + } return key_consumed; case KeyEvent.KEYCODE_APP_SWITCH: if (!keyguardOn) { diff --git a/services/core/java/com/android/server/power/PowerGroup.java b/services/core/java/com/android/server/power/PowerGroup.java index 1c4e143b27e6..522c6c8deb6b 100644 --- a/services/core/java/com/android/server/power/PowerGroup.java +++ b/services/core/java/com/android/server/power/PowerGroup.java @@ -407,16 +407,14 @@ public class PowerGroup { return mDisplayPowerRequest.policy; } - boolean updateLocked(float screenBrightnessOverride, boolean autoBrightness, - boolean useProximitySensor, boolean boostScreenBrightness, int dozeScreenState, - float dozeScreenBrightness, boolean overrideDrawWakeLock, - PowerSaveState powerSaverState, boolean quiescent, boolean dozeAfterScreenOff, - boolean bootCompleted, boolean screenBrightnessBoostInProgress, - boolean waitForNegativeProximity) { + boolean updateLocked(float screenBrightnessOverride, boolean useProximitySensor, + boolean boostScreenBrightness, int dozeScreenState, float dozeScreenBrightness, + boolean overrideDrawWakeLock, PowerSaveState powerSaverState, boolean quiescent, + boolean dozeAfterScreenOff, boolean bootCompleted, + boolean screenBrightnessBoostInProgress, boolean waitForNegativeProximity) { mDisplayPowerRequest.policy = getDesiredScreenPolicyLocked(quiescent, dozeAfterScreenOff, bootCompleted, screenBrightnessBoostInProgress); mDisplayPowerRequest.screenBrightnessOverride = screenBrightnessOverride; - mDisplayPowerRequest.useAutoBrightness = autoBrightness; mDisplayPowerRequest.useProximitySensor = useProximitySensor; mDisplayPowerRequest.boostScreenBrightness = boostScreenBrightness; diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java index 6e3c827e46f0..b5ddc0656282 100644 --- a/services/core/java/com/android/server/power/PowerManagerService.java +++ b/services/core/java/com/android/server/power/PowerManagerService.java @@ -581,10 +581,6 @@ public final class PowerManagerService extends SystemService private boolean mIsFaceDown = false; private long mLastFlipTime = 0L; - // The screen brightness mode. - // One of the Settings.System.SCREEN_BRIGHTNESS_MODE_* constants. - private int mScreenBrightnessModeSetting; - // The screen brightness setting override from the window manager // to allow the current foreground activity to override the brightness. private float mScreenBrightnessOverrideFromWindowManager = @@ -1457,10 +1453,6 @@ public final class PowerManagerService extends SystemService mSystemProperties.set(SYSTEM_PROPERTY_RETAIL_DEMO_ENABLED, retailDemoValue); } - mScreenBrightnessModeSetting = Settings.System.getIntForUser(resolver, - Settings.System.SCREEN_BRIGHTNESS_MODE, - Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL, UserHandle.USER_CURRENT); - mDirty |= DIRTY_SETTINGS; } @@ -3432,23 +3424,18 @@ public final class PowerManagerService extends SystemService final PowerGroup powerGroup = mPowerGroups.valueAt(idx); final int groupId = powerGroup.getGroupId(); - // Determine appropriate screen brightness and auto-brightness adjustments. - final boolean autoBrightness; + // Determine appropriate screen brightness. final float screenBrightnessOverride; if (!mBootCompleted) { // Keep the brightness steady during boot. This requires the // bootloader brightness and the default brightness to be identical. - autoBrightness = false; screenBrightnessOverride = mScreenBrightnessDefault; } else if (isValidBrightness(mScreenBrightnessOverrideFromWindowManager)) { - autoBrightness = false; screenBrightnessOverride = mScreenBrightnessOverrideFromWindowManager; } else { - autoBrightness = (mScreenBrightnessModeSetting - == Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC); screenBrightnessOverride = PowerManager.BRIGHTNESS_INVALID_FLOAT; } - boolean ready = powerGroup.updateLocked(screenBrightnessOverride, autoBrightness, + boolean ready = powerGroup.updateLocked(screenBrightnessOverride, shouldUseProximitySensorLocked(), shouldBoostScreenBrightness(), mDozeScreenStateOverrideFromDreamManager, mDozeScreenBrightnessOverrideFromDreamManagerFloat, @@ -3469,7 +3456,6 @@ public final class PowerManagerService extends SystemService powerGroup.getUserActivitySummaryLocked()) + ", mBootCompleted=" + mBootCompleted + ", screenBrightnessOverride=" + screenBrightnessOverride - + ", useAutoBrightness=" + autoBrightness + ", mScreenBrightnessBoostInProgress=" + mScreenBrightnessBoostInProgress + ", sQuiescent=" + sQuiescent); @@ -4488,7 +4474,6 @@ public final class PowerManagerService extends SystemService + mMaximumScreenOffTimeoutFromDeviceAdmin + " (enforced=" + isMaximumScreenOffTimeoutFromDeviceAdminEnforcedLocked() + ")"); pw.println(" mStayOnWhilePluggedInSetting=" + mStayOnWhilePluggedInSetting); - pw.println(" mScreenBrightnessModeSetting=" + mScreenBrightnessModeSetting); pw.println(" mScreenBrightnessOverrideFromWindowManager=" + mScreenBrightnessOverrideFromWindowManager); pw.println(" mUserActivityTimeoutOverrideFromWindowManager=" @@ -4866,9 +4851,6 @@ public final class PowerManagerService extends SystemService proto.end(stayOnWhilePluggedInToken); proto.write( - PowerServiceSettingsAndConfigurationDumpProto.SCREEN_BRIGHTNESS_MODE_SETTING, - mScreenBrightnessModeSetting); - proto.write( PowerServiceSettingsAndConfigurationDumpProto .SCREEN_BRIGHTNESS_OVERRIDE_FROM_WINDOW_MANAGER, mScreenBrightnessOverrideFromWindowManager); diff --git a/services/core/java/com/android/server/trust/TrustManagerService.java b/services/core/java/com/android/server/trust/TrustManagerService.java index c1920576ad36..721872b73f6a 100644 --- a/services/core/java/com/android/server/trust/TrustManagerService.java +++ b/services/core/java/com/android/server/trust/TrustManagerService.java @@ -690,7 +690,7 @@ public class TrustManagerService extends SystemService { */ public void lockUser(int userId) { mLockPatternUtils.requireStrongAuth( - StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST, userId); + StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED, userId); try { WindowManagerGlobal.getWindowManagerService().lockNow(null); } catch (RemoteException e) { @@ -2088,7 +2088,7 @@ public class TrustManagerService extends SystemService { if (mStrongAuthTracker.isTrustAllowedForUser(mUserId)) { if (DEBUG) Slog.d(TAG, "Revoking all trust because of trust timeout"); mLockPatternUtils.requireStrongAuth( - mStrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST, mUserId); + mStrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED, mUserId); } maybeLockScreen(mUserId); } diff --git a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java index 05df22f124ed..3be16a1fec44 100644 --- a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java +++ b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java @@ -26,6 +26,7 @@ import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED; import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; import static android.net.NetworkCapabilities.TRANSPORT_WIFI; import static android.net.ipsec.ike.exceptions.IkeProtocolException.ERROR_TYPE_AUTHENTICATION_FAILED; +import static android.net.vcn.VcnGatewayConnectionConfig.VCN_GATEWAY_OPTION_ENABLE_DATA_STALL_RECOVERY_WITH_MOBILITY; import static android.net.vcn.VcnManager.VCN_ERROR_CODE_CONFIG_ERROR; import static android.net.vcn.VcnManager.VCN_ERROR_CODE_INTERNAL_ERROR; import static android.net.vcn.VcnManager.VCN_ERROR_CODE_NETWORK_ERROR; @@ -36,6 +37,8 @@ import static com.android.server.VcnManagementService.VDBG; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; +import android.net.ConnectivityDiagnosticsManager; +import android.net.ConnectivityDiagnosticsManager.ConnectivityDiagnosticsCallback; import android.net.ConnectivityManager; import android.net.InetAddresses; import android.net.IpPrefix; @@ -50,6 +53,7 @@ import android.net.NetworkAgent; import android.net.NetworkAgentConfig; import android.net.NetworkCapabilities; import android.net.NetworkProvider; +import android.net.NetworkRequest; import android.net.NetworkScore; import android.net.RouteInfo; import android.net.TelephonyNetworkSpecifier; @@ -546,6 +550,39 @@ public class VcnGatewayConnection extends StateMachine { } } + /** + * Sent when there is a suspected data stall on a network + * + * <p>Only relevant in the Connected state. + * + * @param arg1 The "all" token; this signal is always honored. + * @param obj @NonNull An EventDataStallSuspectedInfo instance with relevant data. + */ + private static final int EVENT_DATA_STALL_SUSPECTED = 13; + + private static class EventDataStallSuspectedInfo implements EventInfo { + @NonNull public final Network network; + + EventDataStallSuspectedInfo(@NonNull Network network) { + this.network = network; + } + + @Override + public int hashCode() { + return Objects.hash(network); + } + + @Override + public boolean equals(@Nullable Object other) { + if (!(other instanceof EventDataStallSuspectedInfo)) { + return false; + } + + final EventDataStallSuspectedInfo rhs = (EventDataStallSuspectedInfo) other; + return Objects.equals(network, rhs.network); + } + } + @VisibleForTesting(visibility = Visibility.PRIVATE) @NonNull final DisconnectedState mDisconnectedState = new DisconnectedState(); @@ -578,10 +615,13 @@ public class VcnGatewayConnection extends StateMachine { @NonNull private final VcnUnderlyingNetworkControllerCallback mUnderlyingNetworkControllerCallback; + @NonNull private final VcnConnectivityDiagnosticsCallback mConnectivityDiagnosticsCallback; + private final boolean mIsMobileDataEnabled; @NonNull private final IpSecManager mIpSecManager; @NonNull private final ConnectivityManager mConnectivityManager; + @NonNull private final ConnectivityDiagnosticsManager mConnectivityDiagnosticsManager; @Nullable private IpSecTunnelInterface mTunnelIface = null; @@ -748,6 +788,20 @@ public class VcnGatewayConnection extends StateMachine { mUnderlyingNetworkControllerCallback); mIpSecManager = mVcnContext.getContext().getSystemService(IpSecManager.class); mConnectivityManager = mVcnContext.getContext().getSystemService(ConnectivityManager.class); + mConnectivityDiagnosticsManager = + mVcnContext.getContext().getSystemService(ConnectivityDiagnosticsManager.class); + + mConnectivityDiagnosticsCallback = new VcnConnectivityDiagnosticsCallback(); + + if (mConnectionConfig.hasGatewayOption( + VCN_GATEWAY_OPTION_ENABLE_DATA_STALL_RECOVERY_WITH_MOBILITY)) { + final NetworkRequest diagRequest = + new NetworkRequest.Builder().addTransportType(TRANSPORT_CELLULAR).build(); + mConnectivityDiagnosticsManager.registerConnectivityDiagnosticsCallback( + diagRequest, + new HandlerExecutor(new Handler(vcnContext.getLooper())), + mConnectivityDiagnosticsCallback); + } addState(mDisconnectedState); addState(mDisconnectingState); @@ -810,6 +864,9 @@ public class VcnGatewayConnection extends StateMachine { mUnderlyingNetworkController.teardown(); mGatewayStatusCallback.onQuit(); + + mConnectivityDiagnosticsManager.unregisterConnectivityDiagnosticsCallback( + mConnectivityDiagnosticsCallback); } /** @@ -828,6 +885,20 @@ public class VcnGatewayConnection extends StateMachine { sendMessageAndAcquireWakeLock(EVENT_SUBSCRIPTIONS_CHANGED, TOKEN_ALL); } + private class VcnConnectivityDiagnosticsCallback extends ConnectivityDiagnosticsCallback { + @Override + public void onDataStallSuspected(ConnectivityDiagnosticsManager.DataStallReport report) { + mVcnContext.ensureRunningOnLooperThread(); + + final Network network = report.getNetwork(); + logInfo("Data stall suspected on " + network); + sendMessageAndAcquireWakeLock( + EVENT_DATA_STALL_SUSPECTED, + TOKEN_ALL, + new EventDataStallSuspectedInfo(network)); + } + } + private class VcnUnderlyingNetworkControllerCallback implements UnderlyingNetworkControllerCallback { @Override @@ -1367,7 +1438,8 @@ public class VcnGatewayConnection extends StateMachine { case EVENT_SUBSCRIPTIONS_CHANGED: // Fallthrough case EVENT_SAFE_MODE_TIMEOUT_EXCEEDED: // Fallthrough case EVENT_MIGRATION_COMPLETED: // Fallthrough - case EVENT_IKE_CONNECTION_INFO_CHANGED: + case EVENT_IKE_CONNECTION_INFO_CHANGED: // Fallthrough + case EVENT_DATA_STALL_SUSPECTED: logUnexpectedEvent(msg.what); break; default: @@ -1925,6 +1997,11 @@ public class VcnGatewayConnection extends StateMachine { mIkeConnectionInfo = ((EventIkeConnectionInfoChangedInfo) msg.obj).ikeConnectionInfo; break; + case EVENT_DATA_STALL_SUSPECTED: + final Network networkWithDataStall = + ((EventDataStallSuspectedInfo) msg.obj).network; + handleDataStallSuspected(networkWithDataStall); + break; default: logUnhandledMessage(msg); break; @@ -1985,6 +2062,15 @@ public class VcnGatewayConnection extends StateMachine { } } + private void handleDataStallSuspected(Network networkWithDataStall) { + if (mUnderlying != null + && mNetworkAgent != null + && mNetworkAgent.getNetwork().equals(networkWithDataStall)) { + logInfo("Perform Mobility update to recover from suspected data stall"); + mIkeSession.setNetwork(mUnderlying.network); + } + } + protected void setupInterfaceAndNetworkAgent( int token, @NonNull IpSecTunnelInterface tunnelIface, @@ -2424,6 +2510,11 @@ public class VcnGatewayConnection extends StateMachine { } @VisibleForTesting(visibility = Visibility.PRIVATE) + ConnectivityDiagnosticsCallback getConnectivityDiagnosticsCallback() { + return mConnectivityDiagnosticsCallback; + } + + @VisibleForTesting(visibility = Visibility.PRIVATE) UnderlyingNetworkRecord getUnderlyingNetwork() { return mUnderlying; } diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java index 4480d521323e..37450acc6f3d 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java @@ -603,6 +603,13 @@ public class WallpaperManagerService extends IWallpaperManager.Stub * for display. */ void generateCrop(WallpaperData wallpaper) { + TimingsTraceAndSlog t = new TimingsTraceAndSlog(TAG); + t.traceBegin("WPMS.generateCrop"); + generateCropInternal(wallpaper); + t.traceEnd(); + } + + private void generateCropInternal(WallpaperData wallpaper) { boolean success = false; // Only generate crop for default display. diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java index b70c8b0d58f4..2b49a81b34bc 100644 --- a/services/core/java/com/android/server/wm/ActivityClientController.java +++ b/services/core/java/com/android/server/wm/ActivityClientController.java @@ -687,29 +687,32 @@ class ActivityClientController extends IActivityClientController.Stub { @Override public int getLaunchedFromUid(IBinder token) { - if (!canGetLaunchedFrom()) { - return INVALID_UID; - } + final int uid = Binder.getCallingUid(); + final boolean isInternalCaller = isInternalCallerGetLaunchedFrom(uid); synchronized (mGlobalLock) { final ActivityRecord r = ActivityRecord.forTokenLocked(token); - return r != null ? r.launchedFromUid : INVALID_UID; + if (r != null && (isInternalCaller || r.mShareIdentity || r.launchedFromUid == uid)) { + return r.launchedFromUid; + } } + return INVALID_UID; } @Override public String getLaunchedFromPackage(IBinder token) { - if (!canGetLaunchedFrom()) { - return null; - } + final int uid = Binder.getCallingUid(); + final boolean isInternalCaller = isInternalCallerGetLaunchedFrom(uid); synchronized (mGlobalLock) { final ActivityRecord r = ActivityRecord.forTokenLocked(token); - return r != null ? r.launchedFromPackage : null; + if (r != null && (isInternalCaller || r.mShareIdentity || r.launchedFromUid == uid)) { + return r.launchedFromPackage; + } } + return null; } - /** Whether the caller can get the package or uid that launched its activity. */ - private boolean canGetLaunchedFrom() { - final int uid = Binder.getCallingUid(); + /** Whether the call to one of the getLaunchedFrom APIs is performed by an internal caller. */ + private boolean isInternalCallerGetLaunchedFrom(int uid) { if (UserHandle.getAppId(uid) == SYSTEM_UID) { return true; } diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index f725d1a11243..efd0ffab1f60 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -670,7 +670,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A private boolean mCurrentLaunchCanTurnScreenOn = true; /** Whether our surface was set to be showing in the last call to {@link #prepareSurfaces} */ - private boolean mLastSurfaceShowing = true; + private boolean mLastSurfaceShowing; /** * The activity is opaque and fills the entire space of this task. @@ -873,6 +873,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A boolean mEnteringAnimation; boolean mOverrideTaskTransition; boolean mDismissKeyguard; + boolean mShareIdentity; /** True if the activity has reported stopped; False if the activity becomes visible. */ boolean mAppStopped; @@ -1233,8 +1234,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A pw.println(prefix + "supportsEnterPipOnTaskSwitch: " + supportsEnterPipOnTaskSwitch); } - if (info.getMaxAspectRatio() != 0) { - pw.println(prefix + "maxAspectRatio=" + info.getMaxAspectRatio()); + if (getMaxAspectRatio() != 0) { + pw.println(prefix + "maxAspectRatio=" + getMaxAspectRatio()); } final float minAspectRatio = getMinAspectRatio(); if (minAspectRatio != 0) { @@ -1574,6 +1575,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A newParent.setResumedActivity(this, "onParentChanged"); mImeInsetsFrozenUntilStartInput = false; } + mLetterboxUiController.onActivityParentChanged(newParent); } if (rootTask != null && rootTask.topRunningActivity() == this) { @@ -1997,6 +1999,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A mOverrideTaskTransition = options.getOverrideTaskTransition(); mDismissKeyguard = options.getDismissKeyguard(); + mShareIdentity = options.getShareIdentity(); } ColorDisplayService.ColorDisplayServiceInternal cds = LocalServices.getService( @@ -5470,7 +5473,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // no animation but there will still be a transition set. // We still need to delay hiding the surface such that it // can be synchronized with showing the next surface in the transition. - if (!isVisible() && !delayed && !displayContent.mAppTransition.isTransitionSet()) { + if (!usingShellTransitions && !isVisible() && !delayed + && !displayContent.mAppTransition.isTransitionSet()) { SurfaceControl.openTransaction(); try { forAllWindows(win -> { @@ -7400,6 +7404,11 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } @Override + boolean showSurfaceOnCreation() { + return false; + } + + @Override void prepareSurfaces() { final boolean show = isVisible() || isAnimating(PARENTS, ANIMATION_TYPE_APP_TRANSITION | ANIMATION_TYPE_RECENTS); @@ -7638,6 +7647,15 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A @Configuration.Orientation @Override int getRequestedConfigurationOrientation(boolean forDisplay) { + if (mLetterboxUiController.hasInheritedOrientation()) { + final RootDisplayArea root = getRootDisplayArea(); + if (forDisplay && root != null && root.isOrientationDifferentFromDisplay()) { + return ActivityInfo.reverseOrientation( + mLetterboxUiController.getInheritedOrientation()); + } else { + return mLetterboxUiController.getInheritedOrientation(); + } + } if (mOrientation == SCREEN_ORIENTATION_BEHIND && task != null) { // We use Task here because we want to be consistent with what happens in // multi-window mode where other tasks orientations are ignored. @@ -7765,6 +7783,9 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A @Nullable CompatDisplayInsets getCompatDisplayInsets() { + if (mLetterboxUiController.hasInheritedLetterboxBehavior()) { + return mLetterboxUiController.getInheritedCompatDisplayInsets(); + } return mCompatDisplayInsets; } @@ -7847,6 +7868,10 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // TODO(b/36505427): Consider moving this method and similar ones to ConfigurationContainer. private void updateCompatDisplayInsets() { + if (mLetterboxUiController.hasInheritedLetterboxBehavior()) { + mCompatDisplayInsets = mLetterboxUiController.getInheritedCompatDisplayInsets(); + return; + } if (mCompatDisplayInsets != null || !shouldCreateCompatDisplayInsets()) { // The override configuration is set only once in size compatibility mode. return; @@ -7908,6 +7933,9 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A @Override float getCompatScale() { + if (mLetterboxUiController.hasInheritedLetterboxBehavior()) { + return mLetterboxUiController.getInheritedSizeCompatScale(); + } return hasSizeCompatBounds() ? mSizeCompatScale : super.getCompatScale(); } @@ -8017,6 +8045,16 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } /** + * @return The orientation to use to understand if reachability is enabled. + */ + @ActivityInfo.ScreenOrientation + int getOrientationForReachability() { + return mLetterboxUiController.hasInheritedLetterboxBehavior() + ? mLetterboxUiController.getInheritedOrientation() + : getRequestedConfigurationOrientation(); + } + + /** * Returns whether activity bounds are letterboxed. * * <p>Note that letterbox UI may not be shown even when this returns {@code true}. See {@link @@ -8056,6 +8094,10 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A if (!ignoreVisibility && !mVisibleRequested) { return APP_COMPAT_STATE_CHANGED__STATE__NOT_VISIBLE; } + // TODO(b/256564921): Investigate if we need new metrics for translucent activities + if (mLetterboxUiController.hasInheritedLetterboxBehavior()) { + return mLetterboxUiController.getInheritedAppCompatState(); + } if (mInSizeCompatModeForBounds) { return APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_SIZE_COMPAT_MODE; } @@ -8526,6 +8568,11 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } private boolean isInSizeCompatModeForBounds(final Rect appBounds, final Rect containerBounds) { + if (mLetterboxUiController.hasInheritedLetterboxBehavior()) { + // To avoid wrong app behaviour, we decided to disable SCM when a translucent activity + // is letterboxed. + return false; + } final int appWidth = appBounds.width(); final int appHeight = appBounds.height(); final int containerAppWidth = containerBounds.width(); @@ -8546,10 +8593,11 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // The rest of the condition is that only one side is smaller than the container, but it // still needs to exclude the cases where the size is limited by the fixed aspect ratio. - if (info.getMaxAspectRatio() > 0) { + final float maxAspectRatio = getMaxAspectRatio(); + if (maxAspectRatio > 0) { final float aspectRatio = (0.5f + Math.max(appWidth, appHeight)) / Math.min(appWidth, appHeight); - if (aspectRatio >= info.getMaxAspectRatio()) { + if (aspectRatio >= maxAspectRatio) { // The current size has reached the max aspect ratio. return false; } @@ -8771,7 +8819,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // TODO(b/36505427): Consider moving this method and similar ones to ConfigurationContainer. private boolean applyAspectRatio(Rect outBounds, Rect containingAppBounds, Rect containingBounds, float desiredAspectRatio) { - final float maxAspectRatio = info.getMaxAspectRatio(); + final float maxAspectRatio = getMaxAspectRatio(); final Task rootTask = getRootTask(); final float minAspectRatio = getMinAspectRatio(); final TaskFragment organizedTf = getOrganizedTaskFragment(); @@ -8878,6 +8926,9 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A * Returns the min aspect ratio of this activity. */ float getMinAspectRatio() { + if (mLetterboxUiController.hasInheritedLetterboxBehavior()) { + return mLetterboxUiController.getInheritedMinAspectRatio(); + } if (info.applicationInfo == null) { return info.getMinAspectRatio(); } @@ -8922,11 +8973,18 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A && parent.getWindowConfiguration().getWindowingMode() == WINDOWING_MODE_FULLSCREEN; } + float getMaxAspectRatio() { + if (mLetterboxUiController.hasInheritedLetterboxBehavior()) { + return mLetterboxUiController.getInheritedMaxAspectRatio(); + } + return info.getMaxAspectRatio(); + } + /** * Returns true if the activity has maximum or minimum aspect ratio. */ private boolean hasFixedAspectRatio() { - return info.getMaxAspectRatio() != 0 || getMinAspectRatio() != 0; + return getMaxAspectRatio() != 0 || getMinAspectRatio() != 0; } /** diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index 866fef76f3ca..eb04687d7d0c 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -3616,6 +3616,19 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { } @Override + public void setSplitScreenResizing(boolean resizing) { + enforceTaskPermission("setSplitScreenResizing()"); + final long ident = Binder.clearCallingIdentity(); + try { + synchronized (mGlobalLock) { + mTaskSupervisor.setSplitScreenResizing(resizing); + } + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + @Override public IWindowOrganizerController getWindowOrganizerController() { return mWindowOrganizerController; } diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java index 23c2ec71336f..33c90a00234b 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java +++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java @@ -207,6 +207,9 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { // Used to indicate that a task is removed it should also be removed from recents. static final boolean REMOVE_FROM_RECENTS = true; + /** True if the docked root task is currently being resized. */ + private boolean mDockedRootTaskResizing; + // Activity actions an app cannot start if it uses a permission which is not granted. private static final ArrayMap<String, String> ACTION_TO_RUNTIME_PERMISSION = new ArrayMap<>(); @@ -1523,6 +1526,15 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { return mLaunchParamsController; } + void setSplitScreenResizing(boolean resizing) { + if (resizing == mDockedRootTaskResizing) { + return; + } + + mDockedRootTaskResizing = resizing; + mWindowManager.setDockedRootTaskResizing(resizing); + } + private void removePinnedRootTaskInSurfaceTransaction(Task rootTask) { /** * Workaround: Force-stop all the activities in the root pinned task before we reparent them diff --git a/services/core/java/com/android/server/wm/ContentRecorder.java b/services/core/java/com/android/server/wm/ContentRecorder.java index 6e23ed966ddc..8d5d0d5c1ce2 100644 --- a/services/core/java/com/android/server/wm/ContentRecorder.java +++ b/services/core/java/com/android/server/wm/ContentRecorder.java @@ -16,6 +16,7 @@ package com.android.server.wm; +import static android.content.Context.MEDIA_PROJECTION_SERVICE; import static android.content.res.Configuration.ORIENTATION_UNDEFINED; import static android.view.ContentRecordingSession.RECORD_CONTENT_DISPLAY; import static android.view.ContentRecordingSession.RECORD_CONTENT_TASK; @@ -27,8 +28,10 @@ import android.annotation.Nullable; import android.content.res.Configuration; import android.graphics.Point; import android.graphics.Rect; -import android.media.projection.MediaProjectionManager; +import android.media.projection.IMediaProjectionManager; import android.os.IBinder; +import android.os.RemoteException; +import android.os.ServiceManager; import android.provider.DeviceConfig; import android.view.ContentRecordingSession; import android.view.Display; @@ -83,13 +86,7 @@ final class ContentRecorder implements WindowContainerListener { private int mLastOrientation = ORIENTATION_UNDEFINED; ContentRecorder(@NonNull DisplayContent displayContent) { - this(displayContent, () -> { - MediaProjectionManager mpm = displayContent.mWmService.mContext.getSystemService( - MediaProjectionManager.class); - if (mpm != null) { - mpm.stopActiveProjection(); - } - }); + this(displayContent, new RemoteMediaProjectionManagerWrapper()); } @VisibleForTesting @@ -445,6 +442,9 @@ final class ContentRecorder implements WindowContainerListener { .setPosition(mRecordedSurface, shiftedX /* x */, shiftedY /* y */) .apply(); mLastRecordedBounds = new Rect(recordedContentBounds); + // Request to notify the client about the resize. + mMediaProjectionManager.notifyActiveProjectionCapturedContentResized( + mLastRecordedBounds.width(), mLastRecordedBounds.height()); } /** @@ -503,6 +503,56 @@ final class ContentRecorder implements WindowContainerListener { @VisibleForTesting interface MediaProjectionManagerWrapper { void stopActiveProjection(); + void notifyActiveProjectionCapturedContentResized(int width, int height); + } + + private static final class RemoteMediaProjectionManagerWrapper implements + MediaProjectionManagerWrapper { + @Nullable private IMediaProjectionManager mIMediaProjectionManager = null; + + @Override + public void stopActiveProjection() { + fetchMediaProjectionManager(); + if (mIMediaProjectionManager == null) { + return; + } + try { + mIMediaProjectionManager.stopActiveProjection(); + } catch (RemoteException e) { + ProtoLog.e(WM_DEBUG_CONTENT_RECORDING, + "Unable to tell MediaProjectionManagerService to stop the active " + + "projection: %s", + e); + } + } + + @Override + public void notifyActiveProjectionCapturedContentResized(int width, int height) { + fetchMediaProjectionManager(); + if (mIMediaProjectionManager == null) { + return; + } + try { + mIMediaProjectionManager.notifyActiveProjectionCapturedContentResized(width, + height); + } catch (RemoteException e) { + ProtoLog.e(WM_DEBUG_CONTENT_RECORDING, + "Unable to tell MediaProjectionManagerService about resizing the active " + + "projection: %s", + e); + } + } + + private void fetchMediaProjectionManager() { + if (mIMediaProjectionManager != null) { + return; + } + IBinder b = ServiceManager.getService(MEDIA_PROJECTION_SERVICE); + if (b == null) { + return; + } + mIMediaProjectionManager = IMediaProjectionManager.Stub.asInterface(b); + } } private boolean isRecordingContentTask() { diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 169e7703b1ee..c6dc24f32837 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -525,6 +525,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp /** Remove this display when animation on it has completed. */ private boolean mDeferredRemoval; + final DockedTaskDividerController mDividerControllerLocked; final PinnedTaskController mPinnedTaskController; final ArrayList<WindowState> mTapExcludedWindows = new ArrayList<>(); @@ -1162,6 +1163,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp mDisplayPolicy.systemReady(); } mWindowCornerRadius = mDisplayPolicy.getWindowCornerRadius(); + mDividerControllerLocked = new DockedTaskDividerController(this); mPinnedTaskController = new PinnedTaskController(mWmService, this); final Transaction pendingTransaction = getPendingTransaction(); @@ -2592,6 +2594,10 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp } } + DockedTaskDividerController getDockedDividerController() { + return mDividerControllerLocked; + } + PinnedTaskController getPinnedTaskController() { return mPinnedTaskController; } diff --git a/services/core/java/com/android/server/wm/DockedTaskDividerController.java b/services/core/java/com/android/server/wm/DockedTaskDividerController.java new file mode 100644 index 000000000000..925a6d858a3d --- /dev/null +++ b/services/core/java/com/android/server/wm/DockedTaskDividerController.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm; + +import android.graphics.Rect; + +/** + * Keeps information about the docked task divider. + */ +public class DockedTaskDividerController { + + private final DisplayContent mDisplayContent; + private boolean mResizing; + + private final Rect mTouchRegion = new Rect(); + + DockedTaskDividerController(DisplayContent displayContent) { + mDisplayContent = displayContent; + } + + boolean isResizing() { + return mResizing; + } + + void setResizing(boolean resizing) { + if (mResizing != resizing) { + mResizing = resizing; + resetDragResizingChangeReported(); + } + } + + void setTouchRegion(Rect touchRegion) { + mTouchRegion.set(touchRegion); + // We need to report touchable region changes to accessibility. + if (mDisplayContent.mWmService.mAccessibilityController.hasCallbacks()) { + mDisplayContent.mWmService.mAccessibilityController.onSomeWindowResizedOrMoved( + mDisplayContent.getDisplayId()); + } + } + + void getTouchRegion(Rect outRegion) { + outRegion.set(mTouchRegion); + } + + private void resetDragResizingChangeReported() { + mDisplayContent.forAllWindows(WindowState::resetDragResizingChangeReported, + true /* traverseTopToBottom */); + } +} diff --git a/services/core/java/com/android/server/wm/DragResizeMode.java b/services/core/java/com/android/server/wm/DragResizeMode.java new file mode 100644 index 000000000000..684cf06e08b8 --- /dev/null +++ b/services/core/java/com/android/server/wm/DragResizeMode.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.server.wm; + +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; + +/** + * Describes the mode in which a window is drag resizing. + */ +class DragResizeMode { + + /** + * Freeform mode: Client surface is fullscreen, and client is responsible to draw window at + * the correct position. + */ + static final int DRAG_RESIZE_MODE_FREEFORM = 0; + + /** + * Mode for resizing the docked (and adjacent) root task: Client surface is fullscreen, but + * window is drawn at (0, 0), window manager is responsible for positioning the surface when + * dragging. + */ + static final int DRAG_RESIZE_MODE_DOCKED_DIVIDER = 1; + + static boolean isModeAllowedForRootTask(Task rootTask, int mode) { + switch (mode) { + case DRAG_RESIZE_MODE_FREEFORM: + return rootTask.getWindowingMode() == WINDOWING_MODE_FREEFORM; + default: + return false; + } + } +} diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/LetterboxConfiguration.java index c19353cb2676..127a7bf1c9a5 100644 --- a/services/core/java/com/android/server/wm/LetterboxConfiguration.java +++ b/services/core/java/com/android/server/wm/LetterboxConfiguration.java @@ -21,6 +21,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.graphics.Color; +import android.provider.DeviceConfig; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; @@ -103,6 +104,10 @@ final class LetterboxConfiguration { final Context mContext; + // Responsible for the persistence of letterbox[Horizontal|Vertical]PositionMultiplier + @NonNull + private final LetterboxConfigurationPersister mLetterboxConfigurationPersister; + // Aspect ratio of letterbox for fixed orientation, values <= // MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO will be ignored. private float mFixedOrientationLetterboxAspectRatio; @@ -165,9 +170,12 @@ final class LetterboxConfiguration { // Whether using split screen aspect ratio as a default aspect ratio for unresizable apps. private boolean mIsSplitScreenAspectRatioForUnresizableAppsEnabled; - // Responsible for the persistence of letterbox[Horizontal|Vertical]PositionMultiplier - @NonNull - private final LetterboxConfigurationPersister mLetterboxConfigurationPersister; + // Whether letterboxing strategy is enabled for translucent activities. If {@value false} + // all the feature is disabled + private boolean mTranslucentLetterboxingEnabled; + + // Allows to enable letterboxing strategy for translucent activities ignoring flags. + private boolean mTranslucentLetterboxingOverrideEnabled; LetterboxConfiguration(Context systemUiContext) { this(systemUiContext, new LetterboxConfigurationPersister(systemUiContext, @@ -206,6 +214,8 @@ final class LetterboxConfiguration { R.dimen.config_letterboxDefaultMinAspectRatioForUnresizableApps)); mIsSplitScreenAspectRatioForUnresizableAppsEnabled = mContext.getResources().getBoolean( R.bool.config_letterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled); + mTranslucentLetterboxingEnabled = mContext.getResources().getBoolean( + R.bool.config_letterboxIsEnabledForTranslucentActivities); mLetterboxConfigurationPersister = letterboxConfigurationPersister; mLetterboxConfigurationPersister.start(); } @@ -817,6 +827,32 @@ final class LetterboxConfiguration { R.bool.config_letterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled); } + boolean isTranslucentLetterboxingEnabled() { + return mTranslucentLetterboxingOverrideEnabled || (mTranslucentLetterboxingEnabled + && isTranslucentLetterboxingAllowed()); + } + + void setTranslucentLetterboxingEnabled(boolean translucentLetterboxingEnabled) { + mTranslucentLetterboxingEnabled = translucentLetterboxingEnabled; + } + + void setTranslucentLetterboxingOverrideEnabled( + boolean translucentLetterboxingOverrideEnabled) { + mTranslucentLetterboxingOverrideEnabled = translucentLetterboxingOverrideEnabled; + setTranslucentLetterboxingEnabled(translucentLetterboxingOverrideEnabled); + } + + /** + * Resets whether we use the constraints override strategy for letterboxing when dealing + * with translucent activities {@link R.bool.config_letterboxIsEnabledForTranslucentActivities}. + */ + void resetTranslucentLetterboxingEnabled() { + final boolean newValue = mContext.getResources().getBoolean( + R.bool.config_letterboxIsEnabledForTranslucentActivities); + setTranslucentLetterboxingEnabled(newValue); + setTranslucentLetterboxingOverrideEnabled(false); + } + /** Calculates a new letterboxPositionForHorizontalReachability value and updates the store */ private void updatePositionForHorizontalReachability( Function<Integer, Integer> newHorizonalPositionFun) { @@ -839,4 +875,9 @@ final class LetterboxConfiguration { nextVerticalPosition); } + // TODO(b/262378106): Cache runtime flag and implement DeviceConfig.OnPropertiesChangedListener + static boolean isTranslucentLetterboxingAllowed() { + return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_WINDOW_MANAGER, + "enable_translucent_activity_letterbox", false); + } } diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java index bcea6f4db1dc..a53a5fc00b0c 100644 --- a/services/core/java/com/android/server/wm/LetterboxUiController.java +++ b/services/core/java/com/android/server/wm/LetterboxUiController.java @@ -17,6 +17,7 @@ package com.android.server.wm; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; @@ -27,6 +28,7 @@ import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANG import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__RIGHT; import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__TOP; import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__LETTERBOX_POSITION__UNKNOWN_POSITION; +import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__UNKNOWN; import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__BOTTOM_TO_CENTER; import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_BOTTOM; import static com.android.internal.util.FrameworkStatsLog.LETTERBOX_POSITION_CHANGED__POSITION_CHANGE__CENTER_TO_LEFT; @@ -82,13 +84,44 @@ final class LetterboxUiController { private static final String TAG = TAG_WITH_CLASS_NAME ? "LetterboxUiController" : TAG_ATM; + private static final float UNDEFINED_ASPECT_RATIO = 0f; + private final Point mTmpPoint = new Point(); private final LetterboxConfiguration mLetterboxConfiguration; + private final ActivityRecord mActivityRecord; + /* + * WindowContainerListener responsible to make translucent activities inherit + * constraints from the first opaque activity beneath them. It's null for not + * translucent activities. + */ + @Nullable + private WindowContainerListener mLetterboxConfigListener; + private boolean mShowWallpaperForLetterboxBackground; + // In case of transparent activities we might need to access the aspectRatio of the + // first opaque activity beneath. + private float mInheritedMinAspectRatio = UNDEFINED_ASPECT_RATIO; + private float mInheritedMaxAspectRatio = UNDEFINED_ASPECT_RATIO; + + @Configuration.Orientation + private int mInheritedOrientation = Configuration.ORIENTATION_UNDEFINED; + + // The app compat state for the opaque activity if any + private int mInheritedAppCompatState = APP_COMPAT_STATE_CHANGED__STATE__UNKNOWN; + + // If true it means that the opaque activity beneath a translucent one is in SizeCompatMode. + private boolean mIsInheritedInSizeCompatMode; + + // This is the SizeCompatScale of the opaque activity beneath a translucent one + private float mInheritedSizeCompatScale; + + // The CompatDisplayInsets of the opaque activity beneath the translucent one. + private ActivityRecord.CompatDisplayInsets mInheritedCompatDisplayInsets; + @Nullable private Letterbox mLetterbox; @@ -220,7 +253,9 @@ final class LetterboxUiController { : mActivityRecord.inMultiWindowMode() ? mActivityRecord.getTask().getBounds() : mActivityRecord.getRootTask().getParent().getBounds(); - mLetterbox.layout(spaceToFill, w.getFrame(), mTmpPoint); + final Rect innerFrame = hasInheritedLetterboxBehavior() + ? mActivityRecord.getWindowConfiguration().getBounds() : w.getFrame(); + mLetterbox.layout(spaceToFill, innerFrame, mTmpPoint); } else if (mLetterbox != null) { mLetterbox.hide(); } @@ -305,7 +340,9 @@ final class LetterboxUiController { } private void handleHorizontalDoubleTap(int x) { - if (!isHorizontalReachabilityEnabled() || mActivityRecord.isInTransition()) { + // TODO(b/260857308): Investigate if enabling reachability for translucent activity + if (hasInheritedLetterboxBehavior() || !isHorizontalReachabilityEnabled() + || mActivityRecord.isInTransition()) { return; } @@ -341,7 +378,9 @@ final class LetterboxUiController { } private void handleVerticalDoubleTap(int y) { - if (!isVerticalReachabilityEnabled() || mActivityRecord.isInTransition()) { + // TODO(b/260857308): Investigate if enabling reachability for translucent activity + if (hasInheritedLetterboxBehavior() || !isVerticalReachabilityEnabled() + || mActivityRecord.isInTransition()) { return; } @@ -390,7 +429,7 @@ final class LetterboxUiController { && parentConfiguration.windowConfiguration.getWindowingMode() == WINDOWING_MODE_FULLSCREEN && (parentConfiguration.orientation == ORIENTATION_LANDSCAPE - && mActivityRecord.getRequestedConfigurationOrientation() == ORIENTATION_PORTRAIT); + && mActivityRecord.getOrientationForReachability() == ORIENTATION_PORTRAIT); } private boolean isHorizontalReachabilityEnabled() { @@ -412,7 +451,7 @@ final class LetterboxUiController { && parentConfiguration.windowConfiguration.getWindowingMode() == WINDOWING_MODE_FULLSCREEN && (parentConfiguration.orientation == ORIENTATION_PORTRAIT - && mActivityRecord.getRequestedConfigurationOrientation() == ORIENTATION_LANDSCAPE); + && mActivityRecord.getOrientationForReachability() == ORIENTATION_LANDSCAPE); } private boolean isVerticalReachabilityEnabled() { @@ -576,9 +615,7 @@ final class LetterboxUiController { // Rounded corners should be displayed above the taskbar. bounds.bottom = Math.min(bounds.bottom, getTaskbarInsetsSource(mainWindow).getFrame().top); - if (mActivityRecord.inSizeCompatMode() && mActivityRecord.getCompatScale() < 1.0f) { - bounds.scale(1.0f / mActivityRecord.getCompatScale()); - } + scaleIfNeeded(bounds); } private int getInsetsStateCornerRadius( @@ -788,4 +825,144 @@ final class LetterboxUiController { w.mAttrs.insetsFlags.appearance ); } + + /** + * Handles translucent activities letterboxing inheriting constraints from the + * first opaque activity beneath. + * @param parent The parent container. + */ + void onActivityParentChanged(WindowContainer<?> parent) { + if (!mLetterboxConfiguration.isTranslucentLetterboxingEnabled()) { + return; + } + if (mLetterboxConfigListener != null) { + mLetterboxConfigListener.onRemoved(); + clearInheritedConfig(); + } + // In case mActivityRecord.getCompatDisplayInsets() is not null we don't apply the + // opaque activity constraints because we're expecting the activity is already letterboxed. + if (mActivityRecord.getTask() == null || mActivityRecord.getCompatDisplayInsets() != null + || mActivityRecord.fillsParent()) { + return; + } + final ActivityRecord firstOpaqueActivityBeneath = mActivityRecord.getTask().getActivity( + ActivityRecord::fillsParent, mActivityRecord, false /* includeBoundary */, + true /* traverseTopToBottom */); + if (firstOpaqueActivityBeneath == null + || mActivityRecord.launchedFromUid != firstOpaqueActivityBeneath.getUid()) { + // We skip letterboxing if the translucent activity doesn't have any opaque + // activities beneath of if it's launched from a different user (e.g. notification) + return; + } + inheritConfiguration(firstOpaqueActivityBeneath); + mLetterboxConfigListener = WindowContainer.overrideConfigurationPropagation( + mActivityRecord, firstOpaqueActivityBeneath, + (opaqueConfig, transparentConfig) -> { + final Configuration mutatedConfiguration = new Configuration(); + final Rect parentBounds = parent.getWindowConfiguration().getBounds(); + final Rect bounds = mutatedConfiguration.windowConfiguration.getBounds(); + final Rect letterboxBounds = opaqueConfig.windowConfiguration.getBounds(); + // We cannot use letterboxBounds directly here because the position relies on + // letterboxing. Using letterboxBounds directly, would produce a double offset. + bounds.set(parentBounds.left, parentBounds.top, + parentBounds.left + letterboxBounds.width(), + parentBounds.top + letterboxBounds.height()); + // We need to initialize appBounds to avoid NPE. The actual value will + // be set ahead when resolving the Configuration for the activity. + mutatedConfiguration.windowConfiguration.setAppBounds(new Rect()); + return mutatedConfiguration; + }); + } + + /** + * @return {@code true} if the current activity is translucent with an opaque activity + * beneath. In this case it will inherit bounds, orientation and aspect ratios from + * the first opaque activity beneath. + */ + boolean hasInheritedLetterboxBehavior() { + return mLetterboxConfigListener != null && !mActivityRecord.matchParentBounds(); + } + + /** + * @return {@code true} if the current activity is translucent with an opaque activity + * beneath and needs to inherit its orientation. + */ + boolean hasInheritedOrientation() { + // To force a different orientation, the transparent one needs to have an explicit one + // otherwise the existing one is fine and the actual orientation will depend on the + // bounds. + // To avoid wrong behaviour, we're not forcing orientation for activities with not + // fixed orientation (e.g. permission dialogs). + return hasInheritedLetterboxBehavior() + && mActivityRecord.mOrientation != SCREEN_ORIENTATION_UNSPECIFIED; + } + + float getInheritedMinAspectRatio() { + return mInheritedMinAspectRatio; + } + + float getInheritedMaxAspectRatio() { + return mInheritedMaxAspectRatio; + } + + int getInheritedAppCompatState() { + return mInheritedAppCompatState; + } + + float getInheritedSizeCompatScale() { + return mInheritedSizeCompatScale; + } + + @Configuration.Orientation + int getInheritedOrientation() { + return mInheritedOrientation; + } + + public ActivityRecord.CompatDisplayInsets getInheritedCompatDisplayInsets() { + return mInheritedCompatDisplayInsets; + } + + private void inheritConfiguration(ActivityRecord firstOpaque) { + // To avoid wrong behaviour, we're not forcing a specific aspet ratio to activities + // which are not already providing one (e.g. permission dialogs) and presumably also + // not resizable. + if (mActivityRecord.getMinAspectRatio() != UNDEFINED_ASPECT_RATIO) { + mInheritedMinAspectRatio = firstOpaque.getMinAspectRatio(); + } + if (mActivityRecord.getMaxAspectRatio() != UNDEFINED_ASPECT_RATIO) { + mInheritedMaxAspectRatio = firstOpaque.getMaxAspectRatio(); + } + mInheritedOrientation = firstOpaque.getRequestedConfigurationOrientation(); + mInheritedAppCompatState = firstOpaque.getAppCompatState(); + mIsInheritedInSizeCompatMode = firstOpaque.inSizeCompatMode(); + mInheritedSizeCompatScale = firstOpaque.getCompatScale(); + mInheritedCompatDisplayInsets = firstOpaque.getCompatDisplayInsets(); + } + + private void clearInheritedConfig() { + mLetterboxConfigListener = null; + mInheritedMinAspectRatio = UNDEFINED_ASPECT_RATIO; + mInheritedMaxAspectRatio = UNDEFINED_ASPECT_RATIO; + mInheritedOrientation = Configuration.ORIENTATION_UNDEFINED; + mInheritedAppCompatState = APP_COMPAT_STATE_CHANGED__STATE__UNKNOWN; + mIsInheritedInSizeCompatMode = false; + mInheritedSizeCompatScale = 1f; + mInheritedCompatDisplayInsets = null; + } + + private void scaleIfNeeded(Rect bounds) { + if (boundsNeedToScale()) { + bounds.scale(1.0f / mActivityRecord.getCompatScale()); + } + } + + private boolean boundsNeedToScale() { + if (hasInheritedLetterboxBehavior()) { + return mIsInheritedInSizeCompatMode + && mInheritedSizeCompatScale < 1.0f; + } else { + return mActivityRecord.inSizeCompatMode() + && mActivityRecord.getCompatScale() < 1.0f; + } + } } diff --git a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java index 449e77fca399..d395f12f8a01 100644 --- a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java +++ b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java @@ -31,6 +31,7 @@ import static com.android.server.wm.ScreenRotationAnimationProto.STARTED; import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_SCREEN_ROTATION; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; +import static com.android.server.wm.utils.CoordinateTransforms.computeRotationMatrix; import android.animation.ArgbEvaluator; import android.content.Context; @@ -60,7 +61,6 @@ import com.android.internal.protolog.common.ProtoLog; import com.android.server.display.DisplayControl; import com.android.server.wm.SurfaceAnimator.AnimationType; import com.android.server.wm.SurfaceAnimator.OnAnimationFinishedCallback; -import com.android.server.wm.utils.RotationAnimationUtils; import java.io.PrintWriter; @@ -378,8 +378,7 @@ class ScreenRotationAnimation { // to the snapshot to make it stay in the same original position // with the current screen rotation. int delta = deltaRotation(rotation, mOriginalRotation); - RotationAnimationUtils.createRotationMatrix(delta, mOriginalWidth, mOriginalHeight, - mSnapshotInitialMatrix); + computeRotationMatrix(delta, mOriginalWidth, mOriginalHeight, mSnapshotInitialMatrix); setRotationTransform(t, mSnapshotInitialMatrix); } diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 7291e27840a9..07e3b836bd1d 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -469,6 +469,7 @@ class Task extends TaskFragment { // Whether the task is currently being drag-resized private boolean mDragResizing; + private int mDragResizeMode; // This represents the last resolved activity values for this task // NOTE: This value needs to be persisted with each task @@ -2809,6 +2810,11 @@ class Task extends TaskFragment { } final Task rootTask = getRootTask(); + final DisplayContent displayContent = rootTask.getDisplayContent(); + // It doesn't matter if we in particular are part of the resize, since we couldn't have + // a DimLayer anyway if we weren't visible. + final boolean dockedResizing = displayContent != null + && displayContent.mDividerControllerLocked.isResizing(); if (inFreeformWindowingMode()) { boolean[] foundTop = { false }; forAllActivities(a -> { getMaxVisibleBounds(a, out, foundTop); }); @@ -2819,10 +2825,18 @@ class Task extends TaskFragment { if (!matchParentBounds()) { // When minimizing the root docked task when going home, we don't adjust the task bounds - // so we need to intersect the task bounds with the root task bounds here.. - rootTask.getBounds(mTmpRect); - mTmpRect.intersect(getBounds()); - out.set(mTmpRect); + // so we need to intersect the task bounds with the root task bounds here. + // + // If we are Docked Resizing with snap points, the task bounds could be smaller than the + // root task bounds and so we don't even want to use them. Even if the app should not be + // resized the Dim should keep up with the divider. + if (dockedResizing) { + rootTask.getBounds(out); + } else { + rootTask.getBounds(mTmpRect); + mTmpRect.intersect(getBounds()); + out.set(mTmpRect); + } } else { out.set(getBounds()); } @@ -2853,15 +2867,16 @@ class Task extends TaskFragment { } } - void setDragResizing(boolean dragResizing) { + void setDragResizing(boolean dragResizing, int dragResizeMode) { if (mDragResizing != dragResizing) { - // No need to check if allowed if it's leaving dragResize + // No need to check if the mode is allowed if it's leaving dragResize if (dragResizing - && !(getRootTask().getWindowingMode() == WINDOWING_MODE_FREEFORM)) { - throw new IllegalArgumentException("Drag resize not allow for root task id=" - + getRootTaskId()); + && !DragResizeMode.isModeAllowedForRootTask(getRootTask(), dragResizeMode)) { + throw new IllegalArgumentException("Drag resize mode not allow for root task id=" + + getRootTaskId() + " dragResizeMode=" + dragResizeMode); } mDragResizing = dragResizing; + mDragResizeMode = dragResizeMode; resetDragResizingChangeReported(); } } @@ -2870,6 +2885,10 @@ class Task extends TaskFragment { return mDragResizing; } + int getDragResizeMode() { + return mDragResizeMode; + } + void adjustBoundsForDisplayChangeIfNeeded(final DisplayContent displayContent) { if (displayContent == null) { return; diff --git a/services/core/java/com/android/server/wm/TaskPositioner.java b/services/core/java/com/android/server/wm/TaskPositioner.java index 9b3fb6b881c4..5b32149b818d 100644 --- a/services/core/java/com/android/server/wm/TaskPositioner.java +++ b/services/core/java/com/android/server/wm/TaskPositioner.java @@ -27,6 +27,7 @@ import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_NONE; import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_RIGHT; import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_TOP; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ORIENTATION; +import static com.android.server.wm.DragResizeMode.DRAG_RESIZE_MODE_FREEFORM; import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_TASK_POSITIONING; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; @@ -367,7 +368,7 @@ class TaskPositioner implements IBinder.DeathRecipient { private void endDragLocked() { mResizing = false; - mTask.setDragResizing(false); + mTask.setDragResizing(false, DRAG_RESIZE_MODE_FREEFORM); } /** Returns true if the move operation should be ended. */ @@ -379,7 +380,7 @@ class TaskPositioner implements IBinder.DeathRecipient { if (mCtrlType != CTRL_NONE) { resizeDrag(x, y); - mTask.setDragResizing(true); + mTask.setDragResizing(true, DRAG_RESIZE_MODE_FREEFORM); return false; } diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index 1d17cd43384a..64574a7e215b 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -642,7 +642,6 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< if (showSurfaceOnCreation()) { getSyncTransaction().show(mSurfaceControl); } - onSurfaceShown(getSyncTransaction()); updateSurfacePositionNonOrganized(); if (mLastMagnificationSpec != null) { applyMagnificationSpec(getSyncTransaction(), mLastMagnificationSpec); @@ -697,13 +696,6 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< scheduleAnimation(); } - /** - * Called when the surface is shown for the first time. - */ - void onSurfaceShown(Transaction t) { - // do nothing - } - // Temp. holders for a chain of containers we are currently processing. private final LinkedList<WindowContainer> mTmpChain1 = new LinkedList<>(); private final LinkedList<WindowContainer> mTmpChain2 = new LinkedList<>(); @@ -3989,27 +3981,54 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< unregisterConfigurationChangeListener(listener); } + static void overrideConfigurationPropagation(WindowContainer<?> receiver, + WindowContainer<?> supplier) { + overrideConfigurationPropagation(receiver, supplier, null /* configurationMerger */); + } + /** * Forces the receiver container to always use the configuration of the supplier container as * its requested override configuration. It allows to propagate configuration without changing * the relationship between child and parent. + * + * @param receiver The {@link WindowContainer<?>} which will receive the {@link + * Configuration} result of the merging operation. + * @param supplier The {@link WindowContainer<?>} which provides the initial {@link + * Configuration}. + * @param configurationMerger A {@link ConfigurationMerger} which combines the {@link + * Configuration} of the receiver and the supplier. */ - static void overrideConfigurationPropagation(WindowContainer<?> receiver, - WindowContainer<?> supplier) { + static WindowContainerListener overrideConfigurationPropagation(WindowContainer<?> receiver, + WindowContainer<?> supplier, @Nullable ConfigurationMerger configurationMerger) { final ConfigurationContainerListener listener = new ConfigurationContainerListener() { @Override public void onMergedOverrideConfigurationChanged(Configuration mergedOverrideConfig) { - receiver.onRequestedOverrideConfigurationChanged(supplier.getConfiguration()); + final Configuration mergedConfiguration = + configurationMerger != null + ? configurationMerger.merge(mergedOverrideConfig, + receiver.getConfiguration()) + : supplier.getConfiguration(); + receiver.onRequestedOverrideConfigurationChanged(mergedConfiguration); } }; supplier.registerConfigurationChangeListener(listener); - receiver.registerWindowContainerListener(new WindowContainerListener() { + final WindowContainerListener wcListener = new WindowContainerListener() { @Override public void onRemoved() { receiver.unregisterWindowContainerListener(this); supplier.unregisterConfigurationChangeListener(listener); } - }); + }; + receiver.registerWindowContainerListener(wcListener); + return wcListener; + } + + /** + * Abstraction for functions merging two {@link Configuration} objects into one. + */ + @FunctionalInterface + interface ConfigurationMerger { + Configuration merge(Configuration first, Configuration second); } /** diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 23bce36fc5d4..be1e7e6977fa 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -7128,6 +7128,20 @@ public class WindowManagerService extends IWindowManager.Stub return 0; } + void setDockedRootTaskResizing(boolean resizing) { + getDefaultDisplayContentLocked().getDockedDividerController().setResizing(resizing); + requestTraversal(); + } + + @Override + public void setDockedTaskDividerTouchRegion(Rect touchRegion) { + synchronized (mGlobalLock) { + final DisplayContent dc = getDefaultDisplayContentLocked(); + dc.getDockedDividerController().setTouchRegion(touchRegion); + dc.updateTouchExcludeRegion(); + } + } + void setForceDesktopModeOnExternalDisplays(boolean forceDesktopModeOnExternalDisplays) { synchronized (mGlobalLock) { mForceDesktopModeOnExternalDisplays = forceDesktopModeOnExternalDisplays; diff --git a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java index 46a30fb725de..060784d777f0 100644 --- a/services/core/java/com/android/server/wm/WindowManagerShellCommand.java +++ b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java @@ -978,6 +978,29 @@ public class WindowManagerShellCommand extends ShellCommand { return 0; } + private int runSetTranslucentLetterboxingEnabled(PrintWriter pw) { + String arg = getNextArg(); + final boolean enabled; + switch (arg) { + case "true": + case "1": + enabled = true; + break; + case "false": + case "0": + enabled = false; + break; + default: + getErrPrintWriter().println("Error: expected true, 1, false, 0, but got " + arg); + return -1; + } + + synchronized (mInternal.mGlobalLock) { + mLetterboxConfiguration.setTranslucentLetterboxingOverrideEnabled(enabled); + } + return 0; + } + private int runSetLetterboxStyle(PrintWriter pw) throws RemoteException { if (peekNextArg() == null) { getErrPrintWriter().println("Error: No arguments provided."); @@ -1033,6 +1056,9 @@ public class WindowManagerShellCommand extends ShellCommand { case "--isSplitScreenAspectRatioForUnresizableAppsEnabled": runSetLetterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled(pw); break; + case "--isTranslucentLetterboxingEnabled": + runSetTranslucentLetterboxingEnabled(pw); + break; default: getErrPrintWriter().println( "Error: Unrecognized letterbox style option: " + arg); @@ -1096,6 +1122,9 @@ public class WindowManagerShellCommand extends ShellCommand { mLetterboxConfiguration .getIsSplitScreenAspectRatioForUnresizableAppsEnabled(); break; + case "isTranslucentLetterboxingEnabled": + mLetterboxConfiguration.resetTranslucentLetterboxingEnabled(); + break; default: getErrPrintWriter().println( "Error: Unrecognized letterbox style option: " + arg); @@ -1196,6 +1225,7 @@ public class WindowManagerShellCommand extends ShellCommand { mLetterboxConfiguration.resetDefaultPositionForVerticalReachability(); mLetterboxConfiguration.resetIsEducationEnabled(); mLetterboxConfiguration.resetIsSplitScreenAspectRatioForUnresizableAppsEnabled(); + mLetterboxConfiguration.resetTranslucentLetterboxingEnabled(); } } @@ -1232,7 +1262,6 @@ public class WindowManagerShellCommand extends ShellCommand { pw.println("Is using split screen aspect ratio as aspect ratio for unresizable apps: " + mLetterboxConfiguration .getIsSplitScreenAspectRatioForUnresizableAppsEnabled()); - pw.println("Background type: " + LetterboxConfiguration.letterboxBackgroundTypeToString( mLetterboxConfiguration.getLetterboxBackgroundType())); @@ -1242,6 +1271,12 @@ public class WindowManagerShellCommand extends ShellCommand { + mLetterboxConfiguration.getLetterboxBackgroundWallpaperBlurRadius()); pw.println(" Wallpaper dark scrim alpha: " + mLetterboxConfiguration.getLetterboxBackgroundWallpaperDarkScrimAlpha()); + + if (mLetterboxConfiguration.isTranslucentLetterboxingEnabled()) { + pw.println("Letterboxing for translucent activities: enabled"); + } else { + pw.println("Letterboxing for translucent activities: disabled"); + } } return 0; } @@ -1434,12 +1469,16 @@ public class WindowManagerShellCommand extends ShellCommand { pw.println(" --isSplitScreenAspectRatioForUnresizableAppsEnabled [true|1|false|0]"); pw.println(" Whether using split screen aspect ratio as a default aspect ratio for"); pw.println(" unresizable apps."); + pw.println(" --isTranslucentLetterboxingEnabled [true|1|false|0]"); + pw.println(" Whether letterboxing for translucent activities is enabled."); + pw.println(" reset-letterbox-style [aspectRatio|cornerRadius|backgroundType"); pw.println(" |backgroundColor|wallpaperBlurRadius|wallpaperDarkScrimAlpha"); pw.println(" |horizontalPositionMultiplier|verticalPositionMultiplier"); pw.println(" |isHorizontalReachabilityEnabled|isVerticalReachabilityEnabled"); - pw.println(" isEducationEnabled||defaultPositionMultiplierForHorizontalReachability"); - pw.println(" ||defaultPositionMultiplierForVerticalReachability]"); + pw.println(" |isEducationEnabled||defaultPositionMultiplierForHorizontalReachability"); + pw.println(" |isTranslucentLetterboxingEnabled"); + pw.println(" |defaultPositionMultiplierForVerticalReachability]"); pw.println(" Resets overrides to default values for specified properties separated"); pw.println(" by space, e.g. 'reset-letterbox-style aspectRatio cornerRadius'."); pw.println(" If no arguments provided, all values will be reset."); diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index 9b69369d8195..72411727361a 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -51,6 +51,7 @@ import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WINDOW_ORGANI import static com.android.server.wm.ActivityTaskManagerService.LAYOUT_REASON_CONFIG_CHANGED; import static com.android.server.wm.ActivityTaskManagerService.enforceTaskPermission; import static com.android.server.wm.ActivityTaskSupervisor.PRESERVE_WINDOWS; +import static com.android.server.wm.DragResizeMode.DRAG_RESIZE_MODE_FREEFORM; import static com.android.server.wm.Task.FLAG_FORCE_HIDDEN_FOR_PINNED_TASK; import static com.android.server.wm.Task.FLAG_FORCE_HIDDEN_FOR_TASK_ORG; import static com.android.server.wm.TaskFragment.EMBEDDING_ALLOWED; @@ -721,7 +722,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub } if ((c.getChangeMask() & WindowContainerTransaction.Change.CHANGE_DRAG_RESIZING) != 0) { - tr.setDragResizing(c.getDragResizing()); + tr.setDragResizing(c.getDragResizing(), DRAG_RESIZE_MODE_FREEFORM); } final int childWindowingMode = c.getActivityWindowingMode(); diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 4d6f7bc5011e..1b7bd9e1f36f 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -36,6 +36,9 @@ import static android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_ import static android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_FRAME; import static android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION; import static android.view.ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_VISIBLE; +import static android.view.WindowCallbacks.RESIZE_MODE_DOCKED_DIVIDER; +import static android.view.WindowCallbacks.RESIZE_MODE_FREEFORM; +import static android.view.WindowCallbacks.RESIZE_MODE_INVALID; import static android.view.WindowInsets.Type.navigationBars; import static android.view.WindowInsets.Type.systemBars; import static android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE; @@ -120,6 +123,8 @@ import static com.android.server.policy.WindowManagerPolicy.TRANSIT_PREVIEW_DONE import static com.android.server.wm.AnimationSpecProto.MOVE; import static com.android.server.wm.DisplayContent.IME_TARGET_LAYERING; import static com.android.server.wm.DisplayContent.logsGestureExclusionRestrictions; +import static com.android.server.wm.DragResizeMode.DRAG_RESIZE_MODE_DOCKED_DIVIDER; +import static com.android.server.wm.DragResizeMode.DRAG_RESIZE_MODE_FREEFORM; import static com.android.server.wm.IdentifierProto.HASH_CODE; import static com.android.server.wm.IdentifierProto.TITLE; import static com.android.server.wm.IdentifierProto.USER_ID; @@ -365,6 +370,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP boolean mHidden = true; // Used to determine if to show child windows. private boolean mDragResizing; private boolean mDragResizingChangeReported = true; + private int mResizeMode; private boolean mRedrawForSyncReported; /** @@ -3938,14 +3944,24 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP if (isDragResizeChanged) { setDragResizing(); } - final boolean isDragResizing = isDragResizing(); + int resizeMode = RESIZE_MODE_INVALID; + if (isDragResizing()) { + switch (getResizeMode()) { + case DRAG_RESIZE_MODE_FREEFORM: + resizeMode = RESIZE_MODE_FREEFORM; + break; + case DRAG_RESIZE_MODE_DOCKED_DIVIDER: + resizeMode = RESIZE_MODE_DOCKED_DIVIDER; + break; + } + } markRedrawForSyncReported(); try { mClient.resized(mClientWindowFrames, reportDraw, mLastReportedConfiguration, getCompatInsetsState(), forceRelayout, alwaysConsumeSystemBars, displayId, - syncWithBuffers ? mSyncSeqId : -1, isDragResizing); + syncWithBuffers ? mSyncSeqId : -1, resizeMode); if (drawPending && prevRotation >= 0 && prevRotation != mLastReportedConfiguration .getMergedConfiguration().windowConfiguration.getRotation()) { mOrientationChangeRedrawRequestTime = SystemClock.elapsedRealtime(); @@ -4188,6 +4204,10 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP super.resetDragResizingChangeReported(); } + int getResizeMode() { + return mResizeMode; + } + private boolean computeDragResizing() { final Task task = getTask(); if (task == null) { @@ -4210,7 +4230,8 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP return true; } - return false; + return getDisplayContent().mDividerControllerLocked.isResizing() + && !task.inFreeformWindowingMode() && !isGoneForLayout(); } void setDragResizing() { @@ -4219,12 +4240,25 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP return; } mDragResizing = resizing; + final Task task = getTask(); + if (task != null && task.isDragResizing()) { + mResizeMode = task.getDragResizeMode(); + } else { + mResizeMode = mDragResizing && getDisplayContent().mDividerControllerLocked.isResizing() + ? DRAG_RESIZE_MODE_DOCKED_DIVIDER + : DRAG_RESIZE_MODE_FREEFORM; + } } boolean isDragResizing() { return mDragResizing; } + boolean isDockedResizing() { + return (mDragResizing && getResizeMode() == DRAG_RESIZE_MODE_DOCKED_DIVIDER) + || (isChildWindow() && getParentWindow().isDockedResizing()); + } + @CallSuper @Override public void dumpDebug(ProtoOutputStream proto, long fieldId, @@ -5900,6 +5934,10 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP // level. Because the animation runs before display is rotated, task bounds should // represent the frames in display space coordinates. outFrame.set(getTask().getBounds()); + } else if (isDockedResizing()) { + // If we are animating while docked resizing, then use the root task bounds as the + // animation target (which will be different than the task bounds) + outFrame.set(getTask().getParent().getBounds()); } else { outFrame.set(getParentFrame()); } diff --git a/services/core/java/com/android/server/wm/utils/CoordinateTransforms.java b/services/core/java/com/android/server/wm/utils/CoordinateTransforms.java index a2f37a56598d..7dc8a31b6310 100644 --- a/services/core/java/com/android/server/wm/utils/CoordinateTransforms.java +++ b/services/core/java/com/android/server/wm/utils/CoordinateTransforms.java @@ -22,11 +22,9 @@ import static android.view.Surface.ROTATION_270; import static android.view.Surface.ROTATION_90; import android.annotation.Dimension; -import android.annotation.Nullable; import android.graphics.Matrix; -import android.graphics.Rect; -import android.graphics.RectF; import android.view.DisplayInfo; +import android.view.Surface; import android.view.Surface.Rotation; public class CoordinateTransforms { @@ -137,19 +135,24 @@ public class CoordinateTransforms { out.postConcat(tmp); } - /** - * Transforms a rect using a transformation matrix - * - * @param transform the transformation to apply to the rect - * @param inOutRect the rect to transform - * @param tmp a temporary value, if null the function will allocate its own. - */ - public static void transformRect(Matrix transform, Rect inOutRect, @Nullable RectF tmp) { - if (tmp == null) { - tmp = new RectF(); + /** Computes the matrix that rotates the original w x h by the rotation delta. */ + public static void computeRotationMatrix(int rotationDelta, int w, int h, Matrix outMatrix) { + switch (rotationDelta) { + case Surface.ROTATION_0: + outMatrix.reset(); + break; + case Surface.ROTATION_90: + outMatrix.setRotate(90); + outMatrix.postTranslate(h, 0); + break; + case Surface.ROTATION_180: + outMatrix.setRotate(180); + outMatrix.postTranslate(w, h); + break; + case Surface.ROTATION_270: + outMatrix.setRotate(270); + outMatrix.postTranslate(0, w); + break; } - tmp.set(inOutRect); - transform.mapRect(tmp); - inOutRect.set((int) tmp.left, (int) tmp.top, (int) tmp.right, (int) tmp.bottom); } } diff --git a/services/core/java/com/android/server/wm/utils/RotationAnimationUtils.java b/services/core/java/com/android/server/wm/utils/RotationAnimationUtils.java deleted file mode 100644 index c11a6d02eb18..000000000000 --- a/services/core/java/com/android/server/wm/utils/RotationAnimationUtils.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server.wm.utils; - -import static android.hardware.HardwareBuffer.USAGE_PROTECTED_CONTENT; - -import android.graphics.Matrix; -import android.hardware.HardwareBuffer; -import android.view.Surface; - - -/** Helper functions for the {@link com.android.server.wm.ScreenRotationAnimation} class*/ -public class RotationAnimationUtils { - - /** - * @return whether the hardwareBuffer passed in is marked as protected. - */ - public static boolean hasProtectedContent(HardwareBuffer hardwareBuffer) { - return (hardwareBuffer.getUsage() & USAGE_PROTECTED_CONTENT) == USAGE_PROTECTED_CONTENT; - } - - public static void createRotationMatrix(int rotation, int width, int height, Matrix outMatrix) { - switch (rotation) { - case Surface.ROTATION_0: - outMatrix.reset(); - break; - case Surface.ROTATION_90: - outMatrix.setRotate(90, 0, 0); - outMatrix.postTranslate(height, 0); - break; - case Surface.ROTATION_180: - outMatrix.setRotate(180, 0, 0); - outMatrix.postTranslate(width, height); - break; - case Surface.ROTATION_270: - outMatrix.setRotate(270, 0, 0); - outMatrix.postTranslate(0, width); - break; - } - } -} diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp index 07819b9a18fc..e66168815cd0 100644 --- a/services/core/jni/Android.bp +++ b/services/core/jni/Android.bp @@ -71,7 +71,6 @@ cc_library_static { "com_android_server_PersistentDataBlockService.cpp", "com_android_server_am_LowMemDetector.cpp", "com_android_server_pm_PackageManagerShellCommandDataLoader.cpp", - "com_android_server_pm_Settings.cpp", "com_android_server_sensor_SensorService.cpp", "com_android_server_wm_TaskFpsCallbackController.cpp", "onload.cpp", @@ -153,7 +152,6 @@ cc_defaults { "libpsi", "libdataloader", "libincfs", - "liblz4", "android.hardware.audio.common@2.0", "android.media.audio.common.types-V1-ndk", "android.hardware.broadcastradio@1.0", @@ -234,26 +232,3 @@ filegroup { "com_android_server_app_GameManagerService.cpp", ], } - -// Settings JNI library for unit tests. -cc_library_shared { - name: "libservices.core.settings.testonly", - defaults: ["libservices.core-libs"], - - cpp_std: "c++2a", - cflags: [ - "-Wall", - "-Werror", - "-Wno-unused-parameter", - "-Wthread-safety", - ], - - srcs: [ - "com_android_server_pm_Settings.cpp", - "onload_settings.cpp", - ], - - header_libs: [ - "bionic_libc_platform_headers", - ], -} diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp index 145e0885b105..c36c57159279 100644 --- a/services/core/jni/com_android_server_input_InputManagerService.cpp +++ b/services/core/jni/com_android_server_input_InputManagerService.cpp @@ -120,6 +120,7 @@ static struct { jmethodID getExcludedDeviceNames; jmethodID getInputPortAssociations; jmethodID getInputUniqueIdAssociations; + jmethodID getDeviceTypeAssociations; jmethodID getKeyRepeatTimeout; jmethodID getKeyRepeatDelay; jmethodID getHoverTapTimeout; @@ -411,6 +412,8 @@ private: void ensureSpriteControllerLocked(); sp<SurfaceControl> getParentSurfaceForPointers(int displayId); static bool checkAndClearExceptionFromCallback(JNIEnv* env, const char* methodName); + std::unordered_map<std::string, std::string> readMapFromInterleavedJavaArray( + jmethodID method, const char* methodName); static inline JNIEnv* jniEnv() { return AndroidRuntime::getJNIEnv(); } }; @@ -583,21 +586,14 @@ void NativeInputManager::getReaderConfiguration(InputReaderConfiguration* outCon } env->DeleteLocalRef(portAssociations); } - outConfig->uniqueIdAssociations.clear(); - jobjectArray uniqueIdAssociations = jobjectArray( - env->CallObjectMethod(mServiceObj, gServiceClassInfo.getInputUniqueIdAssociations)); - if (!checkAndClearExceptionFromCallback(env, "getInputUniqueIdAssociations") && - uniqueIdAssociations) { - jsize length = env->GetArrayLength(uniqueIdAssociations); - for (jsize i = 0; i < length / 2; i++) { - std::string inputDeviceUniqueId = - getStringElementFromJavaArray(env, uniqueIdAssociations, 2 * i); - std::string displayUniqueId = - getStringElementFromJavaArray(env, uniqueIdAssociations, 2 * i + 1); - outConfig->uniqueIdAssociations.insert({inputDeviceUniqueId, displayUniqueId}); - } - env->DeleteLocalRef(uniqueIdAssociations); - } + + outConfig->uniqueIdAssociations = + readMapFromInterleavedJavaArray(gServiceClassInfo.getInputUniqueIdAssociations, + "getInputUniqueIdAssociations"); + + outConfig->deviceTypeAssociations = + readMapFromInterleavedJavaArray(gServiceClassInfo.getDeviceTypeAssociations, + "getDeviceTypeAssociations"); jint hoverTapTimeout = env->CallIntMethod(mServiceObj, gServiceClassInfo.getHoverTapTimeout); @@ -647,6 +643,23 @@ void NativeInputManager::getReaderConfiguration(InputReaderConfiguration* outCon } // release lock } +std::unordered_map<std::string, std::string> NativeInputManager::readMapFromInterleavedJavaArray( + jmethodID method, const char* methodName) { + JNIEnv* env = jniEnv(); + jobjectArray javaArray = jobjectArray(env->CallObjectMethod(mServiceObj, method)); + std::unordered_map<std::string, std::string> map; + if (!checkAndClearExceptionFromCallback(env, methodName) && javaArray) { + jsize length = env->GetArrayLength(javaArray); + for (jsize i = 0; i < length / 2; i++) { + std::string key = getStringElementFromJavaArray(env, javaArray, 2 * i); + std::string value = getStringElementFromJavaArray(env, javaArray, 2 * i + 1); + map.insert({key, value}); + } + } + env->DeleteLocalRef(javaArray); + return map; +} + std::shared_ptr<PointerControllerInterface> NativeInputManager::obtainPointerController( int32_t /* deviceId */) { ATRACE_CALL(); @@ -2237,6 +2250,12 @@ static void nativeChangeUniqueIdAssociation(JNIEnv* env, jobject nativeImplObj) InputReaderConfiguration::CHANGE_DISPLAY_INFO); } +static void nativeChangeTypeAssociation(JNIEnv* env, jobject nativeImplObj) { + NativeInputManager* im = getNativeInputManager(env, nativeImplObj); + im->getInputManager()->getReader().requestRefreshConfiguration( + InputReaderConfiguration::CHANGE_DEVICE_TYPE); +} + static void nativeSetMotionClassifierEnabled(JNIEnv* env, jobject nativeImplObj, jboolean enabled) { NativeInputManager* im = getNativeInputManager(env, nativeImplObj); @@ -2425,6 +2444,7 @@ static const JNINativeMethod gInputManagerMethods[] = { {"canDispatchToDisplay", "(II)Z", (void*)nativeCanDispatchToDisplay}, {"notifyPortAssociationsChanged", "()V", (void*)nativeNotifyPortAssociationsChanged}, {"changeUniqueIdAssociation", "()V", (void*)nativeChangeUniqueIdAssociation}, + {"changeTypeAssociation", "()V", (void*)nativeChangeTypeAssociation}, {"setDisplayEligibilityForPointerCapture", "(IZ)V", (void*)nativeSetDisplayEligibilityForPointerCapture}, {"setMotionClassifierEnabled", "(Z)V", (void*)nativeSetMotionClassifierEnabled}, @@ -2546,6 +2566,9 @@ int register_android_server_InputManager(JNIEnv* env) { GET_METHOD_ID(gServiceClassInfo.getInputUniqueIdAssociations, clazz, "getInputUniqueIdAssociations", "()[Ljava/lang/String;"); + GET_METHOD_ID(gServiceClassInfo.getDeviceTypeAssociations, clazz, "getDeviceTypeAssociations", + "()[Ljava/lang/String;"); + GET_METHOD_ID(gServiceClassInfo.getKeyRepeatTimeout, clazz, "getKeyRepeatTimeout", "()I"); diff --git a/services/core/jni/com_android_server_pm_Settings.cpp b/services/core/jni/com_android_server_pm_Settings.cpp deleted file mode 100644 index 9633a115d718..000000000000 --- a/services/core/jni/com_android_server_pm_Settings.cpp +++ /dev/null @@ -1,161 +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. - */ - -#define ATRACE_TAG ATRACE_TAG_ADB -#define LOG_TAG "Settings-jni" -#include <android-base/file.h> -#include <android-base/logging.h> -#include <android-base/no_destructor.h> -#include <core_jni_helpers.h> -#include <lz4frame.h> -#include <nativehelper/JNIHelp.h> - -#include <vector> - -namespace android { - -namespace { - -struct LZ4FCContextDeleter { - void operator()(LZ4F_cctx* cctx) { LZ4F_freeCompressionContext(cctx); } -}; - -static constexpr int LZ4_BUFFER_SIZE = 64 * 1024; - -static bool writeToFile(std::vector<char>& outBuffer, int fdOut) { - if (!android::base::WriteFully(fdOut, outBuffer.data(), outBuffer.size())) { - PLOG(ERROR) << "Error to write to output file"; - return false; - } - outBuffer.clear(); - return true; -} - -static bool compressAndWriteLz4(LZ4F_cctx* context, std::vector<char>& inBuffer, - std::vector<char>& outBuffer, int fdOut) { - auto inSize = inBuffer.size(); - if (inSize > 0) { - auto prvSize = outBuffer.size(); - auto outSize = LZ4F_compressBound(inSize, nullptr); - outBuffer.resize(prvSize + outSize); - auto rc = LZ4F_compressUpdate(context, outBuffer.data() + prvSize, outSize, inBuffer.data(), - inSize, nullptr); - if (LZ4F_isError(rc)) { - LOG(ERROR) << "LZ4F_compressUpdate failed: " << LZ4F_getErrorName(rc); - return false; - } - outBuffer.resize(prvSize + rc); - } - - if (outBuffer.size() > LZ4_BUFFER_SIZE) { - return writeToFile(outBuffer, fdOut); - } - - return true; -} - -static jboolean nativeCompressLz4(JNIEnv* env, jclass klass, jint fdIn, jint fdOut) { - LZ4F_cctx* cctx; - if (LZ4F_createCompressionContext(&cctx, LZ4F_VERSION) != 0) { - LOG(ERROR) << "Failed to initialize LZ4 compression context."; - return false; - } - std::unique_ptr<LZ4F_cctx, LZ4FCContextDeleter> context(cctx); - - std::vector<char> inBuffer, outBuffer; - inBuffer.reserve(LZ4_BUFFER_SIZE); - outBuffer.reserve(2 * LZ4_BUFFER_SIZE); - - LZ4F_preferences_t prefs; - - memset(&prefs, 0, sizeof(prefs)); - - // Set compression parameters. - prefs.autoFlush = 0; - prefs.compressionLevel = 0; - prefs.frameInfo.blockMode = LZ4F_blockLinked; - prefs.frameInfo.blockSizeID = LZ4F_default; - prefs.frameInfo.blockChecksumFlag = LZ4F_noBlockChecksum; - prefs.frameInfo.contentChecksumFlag = LZ4F_contentChecksumEnabled; - prefs.favorDecSpeed = 0; - - struct stat sb; - if (fstat(fdIn, &sb) == -1) { - PLOG(ERROR) << "Failed to obtain input file size."; - return false; - } - prefs.frameInfo.contentSize = sb.st_size; - - // Write header first. - outBuffer.resize(LZ4F_HEADER_SIZE_MAX); - auto rc = LZ4F_compressBegin(context.get(), outBuffer.data(), outBuffer.size(), &prefs); - if (LZ4F_isError(rc)) { - LOG(ERROR) << "LZ4F_compressBegin failed: " << LZ4F_getErrorName(rc); - return false; - } - outBuffer.resize(rc); - - bool eof = false; - while (!eof) { - constexpr auto capacity = LZ4_BUFFER_SIZE; - inBuffer.resize(capacity); - auto read = TEMP_FAILURE_RETRY(::read(fdIn, inBuffer.data(), inBuffer.size())); - if (read < 0) { - PLOG(ERROR) << "Failed to read from input file."; - return false; - } - - inBuffer.resize(read); - - if (read == 0) { - eof = true; - } - - if (!compressAndWriteLz4(context.get(), inBuffer, outBuffer, fdOut)) { - return false; - } - } - - // Footer. - auto prvSize = outBuffer.size(); - outBuffer.resize(outBuffer.capacity()); - rc = LZ4F_compressEnd(context.get(), outBuffer.data() + prvSize, outBuffer.size() - prvSize, - nullptr); - if (LZ4F_isError(rc)) { - LOG(ERROR) << "LZ4F_compressEnd failed: " << LZ4F_getErrorName(rc); - return false; - } - outBuffer.resize(prvSize + rc); - - if (!writeToFile(outBuffer, fdOut)) { - return false; - } - - return true; -} - -static const JNINativeMethod method_table[] = { - {"nativeCompressLz4", "(II)Z", (void*)nativeCompressLz4}, -}; - -} // namespace - -int register_android_server_com_android_server_pm_Settings(JNIEnv* env) { - return jniRegisterNativeMethods(env, "com/android/server/pm/Settings", method_table, - NELEM(method_table)); -} - -} // namespace android diff --git a/services/core/jni/onload.cpp b/services/core/jni/onload.cpp index 184505713420..00f851f9f4ff 100644 --- a/services/core/jni/onload.cpp +++ b/services/core/jni/onload.cpp @@ -56,7 +56,6 @@ int register_android_server_am_LowMemDetector(JNIEnv* env); int register_com_android_server_soundtrigger_middleware_AudioSessionProviderImpl(JNIEnv* env); int register_com_android_server_soundtrigger_middleware_ExternalCaptureStateTracker(JNIEnv* env); int register_android_server_com_android_server_pm_PackageManagerShellCommandDataLoader(JNIEnv* env); -int register_android_server_com_android_server_pm_Settings(JNIEnv* env); int register_android_server_AdbDebuggingManager(JNIEnv* env); int register_android_server_FaceService(JNIEnv* env); int register_android_server_GpuService(JNIEnv* env); @@ -115,7 +114,6 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) register_com_android_server_soundtrigger_middleware_AudioSessionProviderImpl(env); register_com_android_server_soundtrigger_middleware_ExternalCaptureStateTracker(env); register_android_server_com_android_server_pm_PackageManagerShellCommandDataLoader(env); - register_android_server_com_android_server_pm_Settings(env); register_android_server_AdbDebuggingManager(env); register_android_server_FaceService(env); register_android_server_GpuService(env); diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 509d75bf9b76..c68eea8f35b9 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -109,6 +109,7 @@ import com.android.server.ambientcontext.AmbientContextManagerService; import com.android.server.appbinding.AppBindingService; import com.android.server.art.ArtManagerLocal; import com.android.server.art.ArtModuleServiceInitializer; +import com.android.server.art.DexUseManagerLocal; import com.android.server.attention.AttentionManagerService; import com.android.server.audio.AudioService; import com.android.server.biometrics.AuthService; @@ -1226,6 +1227,13 @@ public final class SystemServer implements Dumpable { Watchdog.getInstance().resumeWatchingCurrentThread("packagemanagermain"); } + // DexUseManagerLocal needs to be loaded after PackageManagerLocal has been registered, but + // before PackageManagerService starts processing binder calls to notifyDexLoad. + // DexUseManagerLocal may also call artd, so ensure ArtModuleServiceManager is instantiated. + ArtModuleServiceInitializer.setArtModuleServiceManager(new ArtModuleServiceManager()); + LocalManagerRegistry.addManager( + DexUseManagerLocal.class, DexUseManagerLocal.createInstance()); + mFirstBoot = mPackageManagerService.isFirstBoot(); mPackageManager = mSystemContext.getPackageManager(); t.traceEnd(); @@ -2729,7 +2737,6 @@ public final class SystemServer implements Dumpable { t.traceEnd(); t.traceBegin("ArtManagerLocal"); - ArtModuleServiceInitializer.setArtModuleServiceManager(new ArtModuleServiceManager()); LocalManagerRegistry.addManager(ArtManagerLocal.class, new ArtManagerLocal(context)); t.traceEnd(); diff --git a/services/permission/java/com/android/server/permission/access/AccessCheckingService.kt b/services/permission/java/com/android/server/permission/access/AccessCheckingService.kt index f648be9626b6..f493b8985c51 100644 --- a/services/permission/java/com/android/server/permission/access/AccessCheckingService.kt +++ b/services/permission/java/com/android/server/permission/access/AccessCheckingService.kt @@ -24,7 +24,7 @@ import com.android.server.SystemConfig import com.android.server.SystemService import com.android.server.appop.AppOpsCheckingServiceInterface import com.android.server.permission.access.appop.AppOpService -import com.android.server.permission.access.collection.* // ktlint-disable no-wildcard-imports +import com.android.server.permission.access.collection.IntSet import com.android.server.permission.access.permission.PermissionService import com.android.server.pm.PackageManagerLocal import com.android.server.pm.UserManagerService diff --git a/services/permission/java/com/android/server/permission/access/permission/PermissionFlags.kt b/services/permission/java/com/android/server/permission/access/permission/PermissionFlags.kt index 1b055202af7c..6b2b1856f7fe 100644 --- a/services/permission/java/com/android/server/permission/access/permission/PermissionFlags.kt +++ b/services/permission/java/com/android/server/permission/access/permission/PermissionFlags.kt @@ -16,18 +16,468 @@ package com.android.server.permission.access.permission +import android.app.AppOpsManager +import android.app.admin.DevicePolicyManager +import android.content.pm.PackageManager +import android.os.Build +import android.permission.PermissionManager +import com.android.server.permission.access.util.andInv +import com.android.server.permission.access.util.hasAnyBit +import com.android.server.permission.access.util.hasBits + +/** + * A set of internal permission flags that's better than the set of `FLAG_PERMISSION_*` constants on + * [PackageManager]. + * + * The old binary permission state is now tracked by multiple `*_GRANTED` and `*_REVOKED` flags, so + * that: + * + * - With [INSTALL_GRANTED] and [INSTALL_REVOKED], we can now get rid of the old per-package + * `areInstallPermissionsFixed` attribute and correctly track it per-permission, finally fixing + * edge cases during module rollbacks. + * + * - With [LEGACY_GRANTED] and [IMPLICIT_GRANTED], we can now ensure that legacy permissions and + * implicit permissions split from non-runtime permissions are never revoked, without checking + * split permissions and package state everywhere slowly and in slightly different ways. + * + * - With [RESTRICTION_REVOKED], we can now get rid of the error-prone logic about revoking and + * potentially re-granting permissions upon restriction state changes. + * + * Permission grants due to protection level are now tracked by [PROTECTION_GRANTED], and permission + * grants due to [PackageManager.grantRuntimePermission] are now tracked by [RUNTIME_GRANTED]. + * + * The [PackageManager.FLAG_PERMISSION_REVIEW_REQUIRED] and + * [PackageManager.FLAG_PERMISSION_REVOKE_WHEN_REQUESTED] flags are now unified into [IMPLICIT], and + * they can be differentiated by the presence of [LEGACY_GRANTED]. + * + * The rest of the permission flags have a 1:1 mapping to the old `FLAG_PERMISSION_*` constants, and + * don't have any effect on the binary permission state. + */ object PermissionFlags { + /** + * Permission flag for a normal permission that is granted at package installation. + */ const val INSTALL_GRANTED = 1 shl 0 + + /** + * Permission flag for a normal permission that is revoked at package installation. + * + * Normally packages that have already been installed cannot be granted new normal permissions + * until its next installation (update), so this flag helps track that the normal permission was + * revoked upon its most recent installation. + */ const val INSTALL_REVOKED = 1 shl 1 + + /** + * Permission flag for a signature or internal permission that is granted based on the + * permission's protection level, including its protection and protection flags. + * + * For example, this flag may be set when the permission is a signature permission and the + * package is having a compatible signing certificate with the package defining the permission, + * or when the permission is a privileged permission and the package is a privileged app with + * its permission in the + * [privileged permission allowlist](https://source.android.com/docs/core/permissions/perms-allowlist). + */ const val PROTECTION_GRANTED = 1 shl 2 - const val ROLE_GRANTED = 1 shl 3 - // For permissions that are granted in other ways, - // ex: via an API or implicit permissions that inherit from granted install permissions - const val OTHER_GRANTED = 1 shl 4 - // For the permissions that are implicit for the package - const val IMPLICIT = 1 shl 5 + /** + * Permission flag for a role or runtime permission that is or was granted by a role. + * + * @see PackageManager.FLAG_PERMISSION_GRANTED_BY_ROLE + */ + const val ROLE = 1 shl 3 + + /** + * Permission flag for a development, role or runtime permission that is granted via + * [PackageManager.grantRuntimePermission]. + */ + const val RUNTIME_GRANTED = 1 shl 4 + + /** + * Permission flag for a runtime permission whose state is set by the user. + * + * For example, this flag may be set when the permission is allowed by the user in the + * request permission dialog, or managed in the permission settings. + * + * @see PackageManager.FLAG_PERMISSION_USER_SET + */ + const val USER_SET = 1 shl 5 + + /** + * Permission flag for a runtime permission whose state is (revoked and) fixed by the user. + * + * For example, this flag may be set when the permission is denied twice by the user in the + * request permission dialog. + * + * @see PackageManager.FLAG_PERMISSION_USER_FIXED + */ + const val USER_FIXED = 1 shl 6 + + /** + * Permission flag for a runtime permission whose state is set and fixed by the device policy + * via [DevicePolicyManager.setPermissionGrantState]. + * + * @see PackageManager.FLAG_PERMISSION_POLICY_FIXED + */ + const val POLICY_FIXED = 1 shl 7 + + /** + * Permission flag for a runtime permission that is (pregranted and) fixed by the system. + * + * For example, this flag may be set in + * [com.android.server.pm.permission.DefaultPermissionGrantPolicy]. + * + * @see PackageManager.FLAG_PERMISSION_SYSTEM_FIXED + */ + const val SYSTEM_FIXED = 1 shl 8 + + /** + * Permission flag for a runtime permission that is or was pregranted by the system. + * + * For example, this flag may be set in + * [com.android.server.pm.permission.DefaultPermissionGrantPolicy]. + * + * @see PackageManager.FLAG_PERMISSION_SYSTEM_FIXED + */ + const val PREGRANT = 1 shl 9 + + /** + * Permission flag for a runtime permission that is granted because the package targets a legacy + * SDK version before [Build.VERSION_CODES.M] and doesn't support runtime permissions. + * + * As long as this flag is set, the permission should always be considered granted, although + * [APP_OP_REVOKED] may cause the app op for the runtime permission to be revoked. Once the + * package targets a higher SDK version so that it started supporting runtime permissions, this + * flag should be removed and the remaining flags should take effect. + * + * @see PackageManager.FLAG_PERMISSION_REVIEW_REQUIRED + * @see PackageManager.FLAG_PERMISSION_REVOKED_COMPAT + */ + const val LEGACY_GRANTED = 1 shl 10 + + /** + * Permission flag for a runtime permission that is granted because the package targets a lower + * SDK version and the permission is implicit to it as a + * [split permission][PermissionManager.SplitPermissionInfo] from other non-runtime permissions. + * + * As long as this flag is set, the permission should always be considered granted, although + * [APP_OP_REVOKED] may cause the app op for the runtime permission to be revoked. Once the + * package targets a higher SDK version so that the permission is no longer implicit to it, this + * flag should be removed and the remaining flags should take effect. + * + * @see PackageManager.FLAG_PERMISSION_REVOKE_WHEN_REQUESTED + * @see PackageManager.FLAG_PERMISSION_REVOKED_COMPAT + */ + const val IMPLICIT_GRANTED = 1 shl 11 + + /** + * Permission flag for a runtime permission that is granted because the package targets a legacy + * SDK version before [Build.VERSION_CODES.M] and doesn't support runtime permissions, so that + * it needs to be reviewed by the user; or granted because the package targets a lower SDK + * version and the permission is implicit to it as a + * [split permission][PermissionManager.SplitPermissionInfo] from other non-runtime permissions, + * so that it needs to be revoked when it's no longer implicit. + * + * @see PackageManager.FLAG_PERMISSION_REVIEW_REQUIRED + * @see PackageManager.FLAG_PERMISSION_REVOKE_WHEN_REQUESTED + */ + const val IMPLICIT = 1 shl 12 + + /** + * Permission flag for a runtime permission that is user-sensitive when it's granted. + * + * This flag is informational and managed by PermissionController. + * + * @see PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_GRANTED + */ + const val USER_SENSITIVE_WHEN_GRANTED = 1 shl 13 + + /** + * Permission flag for a runtime permission that is user-sensitive when it's revoked. + * + * This flag is informational and managed by PermissionController. + * + * @see PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_DENIED + */ + const val USER_SENSITIVE_WHEN_REVOKED = 1 shl 14 + + /** + * Permission flag for a restricted runtime permission that is exempt by the package's + * installer. + * + * For example, this flag may be set when the installer applied the exemption as part of the + * [session parameters](https://developer.android.com/reference/android/content/pm/PackageInstaller.SessionParams#setWhitelistedRestrictedPermissions(java.util.Set%3Cjava.lang.String%3E)). + * + * The permission will be restricted when none of the exempt flags is set. + * + * @see PackageManager.FLAG_PERMISSION_RESTRICTION_INSTALLER_EXEMPT + */ + const val INSTALLER_EXEMPT = 1 shl 15 + + /** + * Permission flag for a restricted runtime permission that is exempt by the system. + * + * For example, this flag may be set when the package is a system app. + * + * The permission will be restricted when none of the exempt flags is set. + * + * @see PackageManager.FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT + */ + const val SYSTEM_EXEMPT = 1 shl 16 + + /** + * Permission flag for a restricted runtime permission that is exempt due to system upgrade. + * + * For example, this flag may be set when the package was installed before the system was + * upgraded to [Build.VERSION_CODES.Q], when restricted permissions were introduced. + * + * The permission will be restricted when none of the exempt flags is set. + * + * @see PackageManager.FLAG_PERMISSION_RESTRICTION_UPGRADE_EXEMPT + */ + const val UPGRADE_EXEMPT = 1 shl 17 + + /** + * Permission flag for a restricted runtime permission that is revoked due to being hard + * restricted. + * + * @see PackageManager.FLAG_PERMISSION_APPLY_RESTRICTION + */ + const val RESTRICTION_REVOKED = 1 shl 18 + + /** + * Permission flag for a restricted runtime permission that is soft restricted. + * + * @see PackageManager.FLAG_PERMISSION_APPLY_RESTRICTION + */ + const val SOFT_RESTRICTED = 1 shl 19 + + /** + * Permission flag for a runtime permission whose app op is revoked. + * + * For example, this flag may be set when the runtime permission is legacy or implicit but still + * "revoked" by the user in permission settings, or when the app op mode for the runtime + * permission is set to revoked via [AppOpsManager.setUidMode]. + * + * @see PackageManager.FLAG_PERMISSION_REVOKED_COMPAT + */ + const val APP_OP_REVOKED = 1 shl 20 + + /** + * Permission flag for a runtime permission that is granted as one-time. + * + * For example, this flag may be set when the user selected "Only this time" in the request + * permission dialog. + * + * This flag, along with other user decisions when it is set, should never be persisted, and + * should be removed once the permission is revoked. + * + * @see PackageManager.FLAG_PERMISSION_ONE_TIME + */ + const val ONE_TIME = 1 shl 21 + + /** + * Permission flag for a runtime permission that was revoked due to app hibernation. + * + * This flag is informational and added by PermissionController, and should be removed once the + * permission is granted again. + * + * @see PackageManager.FLAG_PERMISSION_AUTO_REVOKED + */ + const val HIBERNATION = 1 shl 22 + + /** + * Permission flag for a runtime permission that is selected by the user. + * + * For example, this flag may be set when one of the coarse/fine location accuracies is + * selected by the user. + * + * This flag is informational and managed by PermissionController. + * + * @see PackageManager.FLAG_PERMISSION_SELECTED_LOCATION_ACCURACY + */ + const val USER_SELECTED = 1 shl 23 + + /** + * Mask for all permission flags. + */ const val MASK_ALL = 0.inv() - const val MASK_GRANTED = INSTALL_GRANTED or PROTECTION_GRANTED or OTHER_GRANTED or ROLE_GRANTED - const val MASK_RUNTIME = OTHER_GRANTED or IMPLICIT + + /** + * Mask for all permission flags that may be applied to a runtime permission. + */ + const val MASK_RUNTIME = ROLE or RUNTIME_GRANTED or USER_SET or USER_FIXED or POLICY_FIXED or + SYSTEM_FIXED or PREGRANT or LEGACY_GRANTED or IMPLICIT_GRANTED or IMPLICIT or + USER_SENSITIVE_WHEN_GRANTED or USER_SENSITIVE_WHEN_REVOKED or INSTALLER_EXEMPT or + SYSTEM_EXEMPT or UPGRADE_EXEMPT or RESTRICTION_REVOKED or SOFT_RESTRICTED or + APP_OP_REVOKED or ONE_TIME or HIBERNATION or USER_SELECTED + + /** + * Mask for all API permission flags about permission restriction. + */ + private const val API_MASK_RESTRICTION = + PackageManager.FLAG_PERMISSION_RESTRICTION_INSTALLER_EXEMPT or + PackageManager.FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT or + PackageManager.FLAG_PERMISSION_RESTRICTION_UPGRADE_EXEMPT or + PackageManager.FLAG_PERMISSION_APPLY_RESTRICTION + + /** + * Mask for all permission flags about permission restriction. + */ + private const val MASK_RESTRICTION = INSTALLER_EXEMPT or SYSTEM_EXEMPT or + UPGRADE_EXEMPT or RESTRICTION_REVOKED or SOFT_RESTRICTED + + fun isPermissionGranted(policyFlags: Int): Boolean { + if (policyFlags.hasBits(INSTALL_GRANTED)) { + return true + } + if (policyFlags.hasBits(INSTALL_REVOKED)) { + return false + } + if (policyFlags.hasBits(PROTECTION_GRANTED)) { + return true + } + if (policyFlags.hasBits(LEGACY_GRANTED) || policyFlags.hasBits(IMPLICIT_GRANTED)) { + return true + } + if (policyFlags.hasBits(RESTRICTION_REVOKED)) { + return false + } + return policyFlags.hasBits(RUNTIME_GRANTED) + } + + fun isAppOpGranted(policyFlags: Int): Boolean = + isPermissionGranted(policyFlags) && !policyFlags.hasBits(APP_OP_REVOKED) + + fun isReviewRequired(policyFlags: Int): Boolean = + policyFlags.hasBits(LEGACY_GRANTED) && policyFlags.hasBits(IMPLICIT) + + fun toApiFlags(policyFlags: Int): Int { + var apiFlags = 0 + if (policyFlags.hasBits(USER_SET)) { + apiFlags = apiFlags or PackageManager.FLAG_PERMISSION_USER_SET + } + if (policyFlags.hasBits(USER_FIXED)) { + apiFlags = apiFlags or PackageManager.FLAG_PERMISSION_USER_FIXED + } + if (policyFlags.hasBits(POLICY_FIXED)) { + apiFlags = apiFlags or PackageManager.FLAG_PERMISSION_POLICY_FIXED + } + if (policyFlags.hasBits(SYSTEM_FIXED)) { + apiFlags = apiFlags or PackageManager.FLAG_PERMISSION_SYSTEM_FIXED + } + if (policyFlags.hasBits(PREGRANT)) { + apiFlags = apiFlags or PackageManager.FLAG_PERMISSION_GRANTED_BY_DEFAULT + } + if (policyFlags.hasBits(IMPLICIT)) { + apiFlags = apiFlags or if (policyFlags.hasBits(LEGACY_GRANTED)) { + PackageManager.FLAG_PERMISSION_REVIEW_REQUIRED + } else { + PackageManager.FLAG_PERMISSION_REVOKE_WHEN_REQUESTED + } + } + if (policyFlags.hasBits(USER_SENSITIVE_WHEN_GRANTED)) { + apiFlags = apiFlags or PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_GRANTED + } + if (policyFlags.hasBits(USER_SENSITIVE_WHEN_REVOKED)) { + apiFlags = apiFlags or PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_DENIED + } + if (policyFlags.hasBits(INSTALLER_EXEMPT)) { + apiFlags = apiFlags or PackageManager.FLAG_PERMISSION_RESTRICTION_INSTALLER_EXEMPT + } + if (policyFlags.hasBits(SYSTEM_EXEMPT)) { + apiFlags = apiFlags or PackageManager.FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT + } + if (policyFlags.hasBits(UPGRADE_EXEMPT)) { + apiFlags = apiFlags or PackageManager.FLAG_PERMISSION_RESTRICTION_UPGRADE_EXEMPT + } + if (policyFlags.hasBits(RESTRICTION_REVOKED) || policyFlags.hasBits(SOFT_RESTRICTED)) { + apiFlags = apiFlags or PackageManager.FLAG_PERMISSION_APPLY_RESTRICTION + } + if (policyFlags.hasBits(ROLE)) { + apiFlags = apiFlags or PackageManager.FLAG_PERMISSION_GRANTED_BY_ROLE + } + if (policyFlags.hasBits(APP_OP_REVOKED)) { + apiFlags = apiFlags or PackageManager.FLAG_PERMISSION_REVOKED_COMPAT + } + if (policyFlags.hasBits(ONE_TIME)) { + apiFlags = apiFlags or PackageManager.FLAG_PERMISSION_ONE_TIME + } + if (policyFlags.hasBits(HIBERNATION)) { + apiFlags = apiFlags or PackageManager.FLAG_PERMISSION_AUTO_REVOKED + } + if (policyFlags.hasBits(USER_SELECTED)) { + apiFlags = apiFlags or PackageManager.FLAG_PERMISSION_SELECTED_LOCATION_ACCURACY + } + return apiFlags + } + + fun setRuntimePermissionGranted(policyFlags: Int, isGranted: Boolean): Int = + if (isGranted) policyFlags or RUNTIME_GRANTED else policyFlags andInv RUNTIME_GRANTED + + fun updatePolicyFlags(policyFlags: Int, apiFlagMask: Int, apiFlagValues: Int): Int { + check(!apiFlagMask.hasAnyBit(API_MASK_RESTRICTION)) { + "Permission flags about permission restriction can only be directly mutated by the" + + " policy" + } + val oldApiFlags = toApiFlags(policyFlags) + val newApiFlags = (oldApiFlags andInv apiFlagMask) or (apiFlagValues and apiFlagMask) + return toPolicyFlags(policyFlags, newApiFlags) + } + + private fun toPolicyFlags(oldPolicyFlags: Int, apiFlags: Int): Int { + var policyFlags = 0 + policyFlags = policyFlags or (oldPolicyFlags and INSTALL_GRANTED) + policyFlags = policyFlags or (oldPolicyFlags and INSTALL_REVOKED) + policyFlags = policyFlags or (oldPolicyFlags and PROTECTION_GRANTED) + if (apiFlags.hasBits(PackageManager.FLAG_PERMISSION_GRANTED_BY_ROLE)) { + policyFlags = policyFlags or ROLE + } + policyFlags = policyFlags or (oldPolicyFlags and RUNTIME_GRANTED) + if (apiFlags.hasBits(PackageManager.FLAG_PERMISSION_USER_SET)) { + policyFlags = policyFlags or USER_SET + } + if (apiFlags.hasBits(PackageManager.FLAG_PERMISSION_USER_FIXED)) { + policyFlags = policyFlags or USER_FIXED + } + if (apiFlags.hasBits(PackageManager.FLAG_PERMISSION_POLICY_FIXED)) { + policyFlags = policyFlags or POLICY_FIXED + } + if (apiFlags.hasBits(PackageManager.FLAG_PERMISSION_SYSTEM_FIXED)) { + policyFlags = policyFlags or SYSTEM_FIXED + } + if (apiFlags.hasBits(PackageManager.FLAG_PERMISSION_GRANTED_BY_DEFAULT)) { + policyFlags = policyFlags or PREGRANT + } + policyFlags = policyFlags or (oldPolicyFlags and LEGACY_GRANTED) + policyFlags = policyFlags or (oldPolicyFlags and IMPLICIT_GRANTED) + if (apiFlags.hasBits(PackageManager.FLAG_PERMISSION_REVIEW_REQUIRED) || + apiFlags.hasBits(PackageManager.FLAG_PERMISSION_REVOKE_WHEN_REQUESTED)) { + policyFlags = policyFlags or IMPLICIT + } + if (apiFlags.hasBits(PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_GRANTED)) { + policyFlags = policyFlags or USER_SENSITIVE_WHEN_GRANTED + } + if (apiFlags.hasBits(PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_DENIED)) { + policyFlags = policyFlags or USER_SENSITIVE_WHEN_REVOKED + } + // FLAG_PERMISSION_APPLY_RESTRICTION can be either REVOKED_BY_RESTRICTION when the + // permission is hard restricted, or SOFT_RESTRICTED when the permission is soft restricted. + // However since we should never allow indirect mutation of restriction state, we can just + // get the flags about restriction from the old policy flags. + policyFlags = policyFlags or (oldPolicyFlags and MASK_RESTRICTION) + if (apiFlags.hasBits(PackageManager.FLAG_PERMISSION_REVOKED_COMPAT)) { + policyFlags = policyFlags or APP_OP_REVOKED + } + if (apiFlags.hasBits(PackageManager.FLAG_PERMISSION_ONE_TIME)) { + policyFlags = policyFlags or ONE_TIME + } + if (apiFlags.hasBits(PackageManager.FLAG_PERMISSION_AUTO_REVOKED)) { + policyFlags = policyFlags or HIBERNATION + } + if (apiFlags.hasBits(PackageManager.FLAG_PERMISSION_SELECTED_LOCATION_ACCURACY)) { + policyFlags = policyFlags or USER_SELECTED + } + return policyFlags + } } diff --git a/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt b/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt index f04734caedba..82017362da29 100644 --- a/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt +++ b/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt @@ -232,7 +232,12 @@ class PermissionService( } override fun getPermissionFlags(packageName: String, permissionName: String, userId: Int): Int { - TODO("Not yet implemented") + // TODO: Implement permission checks. + val appId = 0 + val flags = service.getState { + with(policy) { getPermissionFlags(appId, userId, permissionName) } + } + return PermissionFlags.toApiFlags(flags) } override fun isPermissionRevokedByPolicy( @@ -244,7 +249,12 @@ class PermissionService( } override fun isPermissionsReviewRequired(packageName: String, userId: Int): Boolean { - TODO("Not yet implemented") + val packageState = packageManagerLocal.withUnfilteredSnapshot() + .use { it.packageStates[packageName] } ?: return false + val permissionFlags = service.getState { + with(policy) { getUidPermissionFlags(packageState.appId, userId) } + } ?: return false + return permissionFlags.anyIndexed { _, _, flags -> PermissionFlags.isReviewRequired(flags) } } override fun shouldShowRequestPermissionRationale( diff --git a/services/permission/java/com/android/server/permission/access/permission/UidPermissionPolicy.kt b/services/permission/java/com/android/server/permission/access/permission/UidPermissionPolicy.kt index a17a31797270..b2f52cc814cb 100644 --- a/services/permission/java/com/android/server/permission/access/permission/UidPermissionPolicy.kt +++ b/services/permission/java/com/android/server/permission/access/permission/UidPermissionPolicy.kt @@ -37,7 +37,6 @@ import com.android.server.permission.access.SystemState import com.android.server.permission.access.UidUri import com.android.server.permission.access.collection.* // ktlint-disable no-wildcard-imports import com.android.server.permission.access.util.andInv -import com.android.server.permission.access.util.hasAnyBit import com.android.server.permission.access.util.hasBits import com.android.server.pm.KnownPackages import com.android.server.pm.parsing.PackageInfoUtils @@ -474,10 +473,10 @@ class UidPermissionPolicy : SchemePolicy() { // should only affect the other static flags, but not dynamic flags like development or // role. This may be useful in the case of an updated system app. if (permission.isDevelopment) { - newFlags = newFlags or (oldFlags and PermissionFlags.OTHER_GRANTED) + newFlags = newFlags or (oldFlags and PermissionFlags.RUNTIME_GRANTED) } if (permission.isRole) { - newFlags = newFlags or (oldFlags and PermissionFlags.ROLE_GRANTED) + newFlags = newFlags or (oldFlags and PermissionFlags.ROLE) } setPermissionFlags(appId, userId, permissionName, newFlags) } else if (permission.isRuntime) { @@ -519,8 +518,8 @@ class UidPermissionPolicy : SchemePolicy() { "Unknown source permission $sourcePermissionName in split permissions" } val sourceFlags = getPermissionFlags(appId, userId, sourcePermissionName) - val isSourceGranted = sourceFlags.hasAnyBit(PermissionFlags.MASK_GRANTED) - val isNewGranted = newFlags.hasAnyBit(PermissionFlags.MASK_GRANTED) + val isSourceGranted = PermissionFlags.isPermissionGranted(sourceFlags) + val isNewGranted = PermissionFlags.isPermissionGranted(newFlags) val isGrantingNewFromRevoke = isSourceGranted && !isNewGranted if (isSourceGranted == isNewGranted || isGrantingNewFromRevoke) { if (isGrantingNewFromRevoke) { @@ -528,7 +527,7 @@ class UidPermissionPolicy : SchemePolicy() { } newFlags = newFlags or (sourceFlags and PermissionFlags.MASK_RUNTIME) if (!sourcePermission.isRuntime && isSourceGranted) { - newFlags = newFlags or PermissionFlags.OTHER_GRANTED + newFlags = newFlags or PermissionFlags.IMPLICIT_GRANTED } } } @@ -836,6 +835,9 @@ class UidPermissionPolicy : SchemePolicy() { fun GetStateScope.getPermission(permissionName: String): Permission? = state.systemState.permissions[permissionName] + fun GetStateScope.getUidPermissionFlags(appId: Int, userId: Int): IndexedMap<String, Int>? = + state.userStates[userId]?.uidPermissionFlags?.get(appId) + fun GetStateScope.getPermissionFlags( appId: Int, userId: Int, @@ -854,7 +856,8 @@ class UidPermissionPolicy : SchemePolicy() { appId: Int, userId: Int, permissionName: String - ): Int = state.userStates[userId].uidPermissionFlags[appId].getWithDefault(permissionName, 0) + ): Int = + state.userStates[userId]?.uidPermissionFlags?.get(appId).getWithDefault(permissionName, 0) fun MutateStateScope.setPermissionFlags( appId: Int, @@ -875,7 +878,7 @@ class UidPermissionPolicy : SchemePolicy() { val uidPermissionFlags = userState.uidPermissionFlags var permissionFlags = uidPermissionFlags[appId] val oldFlags = permissionFlags.getWithDefault(permissionName, 0) - val newFlags = (oldFlags andInv flagMask) or flagValues + val newFlags = (oldFlags andInv flagMask) or (flagValues and flagMask) if (oldFlags == newFlags) { return false } diff --git a/services/proguard.flags b/services/proguard.flags index 6cdf11c3c685..ba4560f1e8dc 100644 --- a/services/proguard.flags +++ b/services/proguard.flags @@ -78,6 +78,13 @@ -keep,allowoptimization,allowaccessmodification class com.android.server.wm.** implements com.android.server.wm.DisplayAreaPolicy$Provider # JNI keep rules +# The global keep rule for native methods allows stripping of such methods if they're unreferenced +# in Java. However, because system_server explicitly registers these methods from native code, +# stripping them in Java can cause runtime issues. As such, conservatively keep all such methods in +# system_server subpackages as long as the containing class is also kept or referenced. +-keepclassmembers class com.android.server.** { + native <methods>; +} # TODO(b/210510433): Revisit and fix with @Keep, or consider auto-generating from # frameworks/base/services/core/jni/onload.cpp. -keep,allowoptimization,allowaccessmodification class com.android.server.broadcastradio.hal1.BroadcastRadioService { *; } diff --git a/services/tests/PackageManagerServiceTests/server/Android.bp b/services/tests/PackageManagerServiceTests/server/Android.bp index cc2659308a95..ebd6b649c4a8 100644 --- a/services/tests/PackageManagerServiceTests/server/Android.bp +++ b/services/tests/PackageManagerServiceTests/server/Android.bp @@ -94,7 +94,6 @@ android_test { "libunwindstack", "libutils", "netd_aidl_interface-V5-cpp", - "libservices.core.settings.testonly", ], dxflags: ["--multi-dex"], diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java index b3f64b6db54c..3727d660f9a7 100644 --- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java +++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java @@ -111,10 +111,6 @@ public class PackageManagerSettingsTests { private static final String PACKAGE_NAME_3 = "com.android.app3"; private static final int TEST_RESOURCE_ID = 2131231283; - static { - System.loadLibrary("services.core.settings.testonly"); - } - @Mock RuntimePermissionsPersistence mRuntimePermissionsPersistence; @Mock diff --git a/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsLegacyRestrictionsTest.java b/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsLegacyRestrictionsTest.java index be13bad70e16..021d01cca381 100644 --- a/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsLegacyRestrictionsTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsLegacyRestrictionsTest.java @@ -48,7 +48,7 @@ public class AppOpsLegacyRestrictionsTest { StaticMockitoSession mSession; @Mock - AppOpsServiceImpl.Constants mConstants; + AppOpsService.Constants mConstants; @Mock Context mContext; diff --git a/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsServiceTest.java index 7d4bc6f47ad4..c0688d131610 100644 --- a/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsServiceTest.java @@ -22,8 +22,6 @@ import static android.app.AppOpsManager.OP_FLAGS_ALL; import static android.app.AppOpsManager.OP_READ_SMS; import static android.app.AppOpsManager.OP_WIFI_SCAN; import static android.app.AppOpsManager.OP_WRITE_SMS; -import static android.app.AppOpsManager.resolvePackageName; -import static android.os.Process.INVALID_UID; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; @@ -41,7 +39,6 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.nullable; -import android.app.AppOpsManager; import android.app.AppOpsManager.OpEntry; import android.app.AppOpsManager.PackageOps; import android.content.ContentResolver; @@ -89,13 +86,13 @@ public class AppOpsServiceTest { private File mAppOpsFile; private Handler mHandler; - private AppOpsServiceImpl mAppOpsService; + private AppOpsService mAppOpsService; private int mMyUid; private long mTestStartMillis; private StaticMockitoSession mMockingSession; private void setupAppOpsService() { - mAppOpsService = new AppOpsServiceImpl(mAppOpsFile, mHandler, spy(sContext)); + mAppOpsService = new AppOpsService(mAppOpsFile, mHandler, spy(sContext)); mAppOpsService.mHistoricalRegistry.systemReady(sContext.getContentResolver()); // Always approve all permission checks @@ -164,20 +161,17 @@ public class AppOpsServiceTest { @Test public void testNoteOperationAndGetOpsForPackage() { - mAppOpsService.setMode(OP_READ_SMS, mMyUid, sMyPackageName, MODE_ALLOWED, null); - mAppOpsService.setMode(OP_WRITE_SMS, mMyUid, sMyPackageName, MODE_ERRORED, null); + mAppOpsService.setMode(OP_READ_SMS, mMyUid, sMyPackageName, MODE_ALLOWED); + mAppOpsService.setMode(OP_WRITE_SMS, mMyUid, sMyPackageName, MODE_ERRORED); // Note an op that's allowed. - mAppOpsService.noteOperationUnchecked(OP_READ_SMS, mMyUid, - resolvePackageName(mMyUid, sMyPackageName), null, - INVALID_UID, null, null, AppOpsManager.OP_FLAG_SELF); + mAppOpsService.noteOperation(OP_READ_SMS, mMyUid, sMyPackageName, null, false, null, false); List<PackageOps> loggedOps = getLoggedOps(); assertContainsOp(loggedOps, OP_READ_SMS, mTestStartMillis, -1, MODE_ALLOWED); // Note another op that's not allowed. - mAppOpsService.noteOperationUnchecked(OP_WRITE_SMS, mMyUid, - resolvePackageName(mMyUid, sMyPackageName), null, - INVALID_UID, null, null, AppOpsManager.OP_FLAG_SELF); + mAppOpsService.noteOperation(OP_WRITE_SMS, mMyUid, sMyPackageName, null, false, null, + false); loggedOps = getLoggedOps(); assertContainsOp(loggedOps, OP_READ_SMS, mTestStartMillis, -1, MODE_ALLOWED); assertContainsOp(loggedOps, OP_WRITE_SMS, -1, mTestStartMillis, MODE_ERRORED); @@ -191,20 +185,18 @@ public class AppOpsServiceTest { @Test public void testNoteOperationAndGetOpsForPackage_controlledByDifferentOp() { // This op controls WIFI_SCAN - mAppOpsService.setMode(OP_COARSE_LOCATION, mMyUid, sMyPackageName, MODE_ALLOWED, null); + mAppOpsService.setMode(OP_COARSE_LOCATION, mMyUid, sMyPackageName, MODE_ALLOWED); - assertThat(mAppOpsService.noteOperationUnchecked(OP_WIFI_SCAN, mMyUid, - resolvePackageName(mMyUid, sMyPackageName), null, - INVALID_UID, null, null, AppOpsManager.OP_FLAG_SELF)).isEqualTo(MODE_ALLOWED); + assertThat(mAppOpsService.noteOperation(OP_WIFI_SCAN, mMyUid, sMyPackageName, null, false, + null, false).getOpMode()).isEqualTo(MODE_ALLOWED); assertContainsOp(getLoggedOps(), OP_WIFI_SCAN, mTestStartMillis, -1, MODE_ALLOWED /* default for WIFI_SCAN; this is not changed or used in this test */); // Now set COARSE_LOCATION to ERRORED -> this will make WIFI_SCAN disabled as well. - mAppOpsService.setMode(OP_COARSE_LOCATION, mMyUid, sMyPackageName, MODE_ERRORED, null); - assertThat(mAppOpsService.noteOperationUnchecked(OP_WIFI_SCAN, mMyUid, - resolvePackageName(mMyUid, sMyPackageName), null, - INVALID_UID, null, null, AppOpsManager.OP_FLAG_SELF)).isEqualTo(MODE_ERRORED); + mAppOpsService.setMode(OP_COARSE_LOCATION, mMyUid, sMyPackageName, MODE_ERRORED); + assertThat(mAppOpsService.noteOperation(OP_WIFI_SCAN, mMyUid, sMyPackageName, null, false, + null, false).getOpMode()).isEqualTo(MODE_ERRORED); assertContainsOp(getLoggedOps(), OP_WIFI_SCAN, mTestStartMillis, mTestStartMillis, MODE_ALLOWED /* default for WIFI_SCAN; this is not changed or used in this test */); @@ -213,14 +205,11 @@ public class AppOpsServiceTest { // Tests the dumping and restoring of the in-memory state to/from XML. @Test public void testStatePersistence() { - mAppOpsService.setMode(OP_READ_SMS, mMyUid, sMyPackageName, MODE_ALLOWED, null); - mAppOpsService.setMode(OP_WRITE_SMS, mMyUid, sMyPackageName, MODE_ERRORED, null); - mAppOpsService.noteOperationUnchecked(OP_READ_SMS, mMyUid, - resolvePackageName(mMyUid, sMyPackageName), null, - INVALID_UID, null, null, AppOpsManager.OP_FLAG_SELF); - mAppOpsService.noteOperationUnchecked(OP_WRITE_SMS, mMyUid, - resolvePackageName(mMyUid, sMyPackageName), null, - INVALID_UID, null, null, AppOpsManager.OP_FLAG_SELF); + mAppOpsService.setMode(OP_READ_SMS, mMyUid, sMyPackageName, MODE_ALLOWED); + mAppOpsService.setMode(OP_WRITE_SMS, mMyUid, sMyPackageName, MODE_ERRORED); + mAppOpsService.noteOperation(OP_READ_SMS, mMyUid, sMyPackageName, null, false, null, false); + mAppOpsService.noteOperation(OP_WRITE_SMS, mMyUid, sMyPackageName, null, false, null, + false); mAppOpsService.writeState(); // Create a new app ops service which will initialize its state from XML. @@ -235,10 +224,8 @@ public class AppOpsServiceTest { // Tests that ops are persisted during shutdown. @Test public void testShutdown() { - mAppOpsService.setMode(OP_READ_SMS, mMyUid, sMyPackageName, MODE_ALLOWED, null); - mAppOpsService.noteOperationUnchecked(OP_READ_SMS, mMyUid, - resolvePackageName(mMyUid, sMyPackageName), null, - INVALID_UID, null, null, AppOpsManager.OP_FLAG_SELF); + mAppOpsService.setMode(OP_READ_SMS, mMyUid, sMyPackageName, MODE_ALLOWED); + mAppOpsService.noteOperation(OP_READ_SMS, mMyUid, sMyPackageName, null, false, null, false); mAppOpsService.shutdown(); // Create a new app ops service which will initialize its state from XML. @@ -251,10 +238,8 @@ public class AppOpsServiceTest { @Test public void testGetOpsForPackage() { - mAppOpsService.setMode(OP_READ_SMS, mMyUid, sMyPackageName, MODE_ALLOWED, null); - mAppOpsService.noteOperationUnchecked(OP_READ_SMS, mMyUid, - resolvePackageName(mMyUid, sMyPackageName), null, - INVALID_UID, null, null, AppOpsManager.OP_FLAG_SELF); + mAppOpsService.setMode(OP_READ_SMS, mMyUid, sMyPackageName, MODE_ALLOWED); + mAppOpsService.noteOperation(OP_READ_SMS, mMyUid, sMyPackageName, null, false, null, false); // Query all ops List<PackageOps> loggedOps = mAppOpsService.getOpsForPackage( @@ -282,10 +267,8 @@ public class AppOpsServiceTest { @Test public void testPackageRemoved() { - mAppOpsService.setMode(OP_READ_SMS, mMyUid, sMyPackageName, MODE_ALLOWED, null); - mAppOpsService.noteOperationUnchecked(OP_READ_SMS, mMyUid, - resolvePackageName(mMyUid, sMyPackageName), null, - INVALID_UID, null, null, AppOpsManager.OP_FLAG_SELF); + mAppOpsService.setMode(OP_READ_SMS, mMyUid, sMyPackageName, MODE_ALLOWED); + mAppOpsService.noteOperation(OP_READ_SMS, mMyUid, sMyPackageName, null, false, null, false); List<PackageOps> loggedOps = getLoggedOps(); assertContainsOp(loggedOps, OP_READ_SMS, mTestStartMillis, -1, MODE_ALLOWED); @@ -339,10 +322,8 @@ public class AppOpsServiceTest { @Test public void testUidRemoved() { - mAppOpsService.setMode(OP_READ_SMS, mMyUid, sMyPackageName, MODE_ALLOWED, null); - mAppOpsService.noteOperationUnchecked(OP_READ_SMS, mMyUid, - resolvePackageName(mMyUid, sMyPackageName), null, - INVALID_UID, null, null, AppOpsManager.OP_FLAG_SELF); + mAppOpsService.setMode(OP_READ_SMS, mMyUid, sMyPackageName, MODE_ALLOWED); + mAppOpsService.noteOperation(OP_READ_SMS, mMyUid, sMyPackageName, null, false, null, false); List<PackageOps> loggedOps = getLoggedOps(); assertContainsOp(loggedOps, OP_READ_SMS, mTestStartMillis, -1, MODE_ALLOWED); diff --git a/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUidStateTrackerTest.java b/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUidStateTrackerTest.java index 3efd5e701013..98e895a86f9e 100644 --- a/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUidStateTrackerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUidStateTrackerTest.java @@ -76,7 +76,7 @@ public class AppOpsUidStateTrackerTest { ActivityManagerInternal mAmi; @Mock - AppOpsServiceImpl.Constants mConstants; + AppOpsService.Constants mConstants; AppOpsUidStateTrackerTestExecutor mExecutor = new AppOpsUidStateTrackerTestExecutor(); diff --git a/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUpgradeTest.java b/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUpgradeTest.java index 302fa0f0c528..9eed6ada3a37 100644 --- a/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUpgradeTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/appop/AppOpsUpgradeTest.java @@ -121,13 +121,13 @@ public class AppOpsUpgradeTest { } } - private void assertSameModes(SparseArray<AppOpsServiceImpl.UidState> uidStates, + private void assertSameModes(SparseArray<AppOpsService.UidState> uidStates, int op1, int op2) { int numberOfNonDefaultOps = 0; final int defaultModeOp1 = AppOpsManager.opToDefaultMode(op1); final int defaultModeOp2 = AppOpsManager.opToDefaultMode(op2); for (int i = 0; i < uidStates.size(); i++) { - final AppOpsServiceImpl.UidState uidState = uidStates.valueAt(i); + final AppOpsService.UidState uidState = uidStates.valueAt(i); SparseIntArray opModes = uidState.getNonDefaultUidModes(); if (opModes != null) { final int uidMode1 = opModes.get(op1, defaultModeOp1); @@ -141,12 +141,12 @@ public class AppOpsUpgradeTest { continue; } for (int j = 0; j < uidState.pkgOps.size(); j++) { - final AppOpsServiceImpl.Ops ops = uidState.pkgOps.valueAt(j); + final AppOpsService.Ops ops = uidState.pkgOps.valueAt(j); if (ops == null) { continue; } - final AppOpsServiceImpl.Op _op1 = ops.get(op1); - final AppOpsServiceImpl.Op _op2 = ops.get(op2); + final AppOpsService.Op _op1 = ops.get(op1); + final AppOpsService.Op _op2 = ops.get(op2); final int mode1 = (_op1 == null) ? defaultModeOp1 : _op1.getMode(); final int mode2 = (_op2 == null) ? defaultModeOp2 : _op2.getMode(); assertEquals(mode1, mode2); @@ -199,7 +199,7 @@ public class AppOpsUpgradeTest { public void upgradeRunAnyInBackground() { extractAppOpsFile(APP_OPS_UNVERSIONED_ASSET_PATH); - AppOpsServiceImpl testService = new AppOpsServiceImpl(sAppOpsFile, mHandler, mTestContext); + AppOpsService testService = new AppOpsService(sAppOpsFile, mHandler, mTestContext); testService.upgradeRunAnyInBackgroundLocked(); assertSameModes(testService.mUidStates, AppOpsManager.OP_RUN_IN_BACKGROUND, @@ -244,7 +244,7 @@ public class AppOpsUpgradeTest { return UserHandle.getUid(userId, appIds[index]); }).when(mPackageManagerInternal).getPackageUid(anyString(), anyLong(), anyInt()); - AppOpsServiceImpl testService = new AppOpsServiceImpl(sAppOpsFile, mHandler, mTestContext); + AppOpsService testService = new AppOpsService(sAppOpsFile, mHandler, mTestContext); testService.upgradeScheduleExactAlarmLocked(); @@ -259,7 +259,7 @@ public class AppOpsUpgradeTest { } else { expectedMode = previousMode; } - final AppOpsServiceImpl.UidState uidState = testService.mUidStates.get(uid); + final AppOpsService.UidState uidState = testService.mUidStates.get(uid); assertEquals(expectedMode, uidState.getUidMode(OP_SCHEDULE_EXACT_ALARM)); } } @@ -268,7 +268,7 @@ public class AppOpsUpgradeTest { int[] unrelatedUidsInFile = {10225, 10178}; for (int uid : unrelatedUidsInFile) { - final AppOpsServiceImpl.UidState uidState = testService.mUidStates.get(uid); + final AppOpsService.UidState uidState = testService.mUidStates.get(uid); assertEquals(AppOpsManager.opToDefaultMode(OP_SCHEDULE_EXACT_ALARM), uidState.getUidMode(OP_SCHEDULE_EXACT_ALARM)); } @@ -278,8 +278,8 @@ public class AppOpsUpgradeTest { public void upgradeFromNoFile() { assertFalse(sAppOpsFile.exists()); - AppOpsServiceImpl testService = spy( - new AppOpsServiceImpl(sAppOpsFile, mHandler, mTestContext)); + AppOpsService testService = spy( + new AppOpsService(sAppOpsFile, mHandler, mTestContext)); doNothing().when(testService).upgradeRunAnyInBackgroundLocked(); doNothing().when(testService).upgradeScheduleExactAlarmLocked(); @@ -296,7 +296,7 @@ public class AppOpsUpgradeTest { AppOpsDataParser parser = new AppOpsDataParser(sAppOpsFile); assertTrue(parser.parse()); - assertEquals(AppOpsServiceImpl.CURRENT_VERSION, parser.mVersion); + assertEquals(AppOpsService.CURRENT_VERSION, parser.mVersion); } @Test @@ -306,8 +306,8 @@ public class AppOpsUpgradeTest { assertTrue(parser.parse()); assertEquals(AppOpsDataParser.NO_VERSION, parser.mVersion); - AppOpsServiceImpl testService = spy( - new AppOpsServiceImpl(sAppOpsFile, mHandler, mTestContext)); + AppOpsService testService = spy( + new AppOpsService(sAppOpsFile, mHandler, mTestContext)); doNothing().when(testService).upgradeRunAnyInBackgroundLocked(); doNothing().when(testService).upgradeScheduleExactAlarmLocked(); @@ -320,7 +320,7 @@ public class AppOpsUpgradeTest { testService.writeState(); assertTrue(parser.parse()); - assertEquals(AppOpsServiceImpl.CURRENT_VERSION, parser.mVersion); + assertEquals(AppOpsService.CURRENT_VERSION, parser.mVersion); } @Test @@ -330,8 +330,8 @@ public class AppOpsUpgradeTest { assertTrue(parser.parse()); assertEquals(1, parser.mVersion); - AppOpsServiceImpl testService = spy( - new AppOpsServiceImpl(sAppOpsFile, mHandler, mTestContext)); + AppOpsService testService = spy( + new AppOpsService(sAppOpsFile, mHandler, mTestContext)); doNothing().when(testService).upgradeRunAnyInBackgroundLocked(); doNothing().when(testService).upgradeScheduleExactAlarmLocked(); @@ -344,7 +344,7 @@ public class AppOpsUpgradeTest { testService.writeState(); assertTrue(parser.parse()); - assertEquals(AppOpsServiceImpl.CURRENT_VERSION, parser.mVersion); + assertEquals(AppOpsService.CURRENT_VERSION, parser.mVersion); } /** diff --git a/services/tests/mockingservicestests/src/com/android/server/display/state/DisplayStateControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/display/state/DisplayStateControllerTest.java new file mode 100644 index 000000000000..880501f39ac2 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/display/state/DisplayStateControllerTest.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.display.state; + + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.hardware.display.DisplayManagerInternal; +import android.view.Display; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.server.display.DisplayPowerProximityStateController; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public final class DisplayStateControllerTest { + private static final boolean DISPLAY_ENABLED = true; + private static final boolean DISPLAY_IN_TRANSITION = true; + + private DisplayStateController mDisplayStateController; + + @Mock + private DisplayPowerProximityStateController mDisplayPowerProximityStateController; + + @Before + public void before() { + MockitoAnnotations.initMocks(this); + mDisplayStateController = new DisplayStateController(mDisplayPowerProximityStateController); + } + + @Test + public void updateProximityStateEvaluatesStateOffPolicyAsExpected() { + when(mDisplayPowerProximityStateController.isScreenOffBecauseOfProximity()).thenReturn( + false); + DisplayManagerInternal.DisplayPowerRequest displayPowerRequest = mock( + DisplayManagerInternal.DisplayPowerRequest.class); + + displayPowerRequest.policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_OFF; + int state = mDisplayStateController.updateDisplayState(displayPowerRequest, DISPLAY_ENABLED, + !DISPLAY_IN_TRANSITION); + assertEquals(Display.STATE_OFF, state); + verify(mDisplayPowerProximityStateController).updateProximityState(displayPowerRequest, + Display.STATE_OFF); + assertEquals(true, mDisplayStateController.shouldPerformScreenOffTransition()); + } + + @Test + public void updateProximityStateEvaluatesDozePolicyAsExpected() { + when(mDisplayPowerProximityStateController.isScreenOffBecauseOfProximity()).thenReturn( + false); + validDisplayState(DisplayManagerInternal.DisplayPowerRequest.POLICY_DOZE, + Display.STATE_DOZE, DISPLAY_ENABLED, !DISPLAY_IN_TRANSITION); + } + + @Test + public void updateProximityStateEvaluatesDimPolicyAsExpected() { + when(mDisplayPowerProximityStateController.isScreenOffBecauseOfProximity()).thenReturn( + false); + validDisplayState(DisplayManagerInternal.DisplayPowerRequest.POLICY_DIM, + Display.STATE_ON, DISPLAY_ENABLED, !DISPLAY_IN_TRANSITION); + } + + @Test + public void updateProximityStateEvaluatesDimBrightAsExpected() { + when(mDisplayPowerProximityStateController.isScreenOffBecauseOfProximity()).thenReturn( + false); + validDisplayState(DisplayManagerInternal.DisplayPowerRequest.POLICY_BRIGHT, + Display.STATE_ON, DISPLAY_ENABLED, !DISPLAY_IN_TRANSITION); + } + + @Test + public void updateProximityStateWorksAsExpectedWhenDisplayDisabled() { + when(mDisplayPowerProximityStateController.isScreenOffBecauseOfProximity()).thenReturn( + false); + DisplayManagerInternal.DisplayPowerRequest displayPowerRequest = mock( + DisplayManagerInternal.DisplayPowerRequest.class); + + displayPowerRequest.policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_BRIGHT; + int state = mDisplayStateController.updateDisplayState(displayPowerRequest, + !DISPLAY_ENABLED, !DISPLAY_IN_TRANSITION); + assertEquals(Display.STATE_OFF, state); + verify(mDisplayPowerProximityStateController).updateProximityState(displayPowerRequest, + Display.STATE_ON); + assertEquals(false, mDisplayStateController.shouldPerformScreenOffTransition()); + } + + @Test + public void updateProximityStateWorksAsExpectedWhenTransitionPhase() { + when(mDisplayPowerProximityStateController.isScreenOffBecauseOfProximity()).thenReturn( + false); + DisplayManagerInternal.DisplayPowerRequest displayPowerRequest = mock( + DisplayManagerInternal.DisplayPowerRequest.class); + + displayPowerRequest.policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_BRIGHT; + int state = mDisplayStateController.updateDisplayState(displayPowerRequest, DISPLAY_ENABLED, + DISPLAY_IN_TRANSITION); + assertEquals(Display.STATE_OFF, state); + verify(mDisplayPowerProximityStateController).updateProximityState(displayPowerRequest, + Display.STATE_ON); + assertEquals(false, mDisplayStateController.shouldPerformScreenOffTransition()); + } + + @Test + public void updateProximityStateWorksAsExpectedWhenScreenOffBecauseOfProximity() { + when(mDisplayPowerProximityStateController.isScreenOffBecauseOfProximity()).thenReturn( + true); + DisplayManagerInternal.DisplayPowerRequest displayPowerRequest = mock( + DisplayManagerInternal.DisplayPowerRequest.class); + + displayPowerRequest.policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_BRIGHT; + int state = mDisplayStateController.updateDisplayState(displayPowerRequest, DISPLAY_ENABLED, + !DISPLAY_IN_TRANSITION); + assertEquals(Display.STATE_OFF, state); + verify(mDisplayPowerProximityStateController).updateProximityState(displayPowerRequest, + Display.STATE_ON); + assertEquals(false, mDisplayStateController.shouldPerformScreenOffTransition()); + } + + private void validDisplayState(int policy, int displayState, boolean isEnabled, + boolean isInTransition) { + DisplayManagerInternal.DisplayPowerRequest displayPowerRequest = mock( + DisplayManagerInternal.DisplayPowerRequest.class); + displayPowerRequest.policy = policy; + int state = mDisplayStateController.updateDisplayState(displayPowerRequest, isEnabled, + isInTransition); + assertEquals(displayState, state); + verify(mDisplayPowerProximityStateController).updateProximityState(displayPowerRequest, + displayState); + assertEquals(false, mDisplayStateController.shouldPerformScreenOffTransition()); + } +} diff --git a/services/tests/mockingservicestests/src/com/android/server/job/JobConcurrencyManagerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/JobConcurrencyManagerTest.java index a9dc4af07bab..480a4f358bc0 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/JobConcurrencyManagerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/JobConcurrencyManagerTest.java @@ -217,7 +217,7 @@ public final class JobConcurrencyManagerTest { assertEquals(0, preferredUidOnly.size()); assertEquals(0, stoppable.size()); assertEquals(0, assignmentInfo.minPreferredUidOnlyWaitingTimeMs); - assertEquals(0, assignmentInfo.numRunningTopEj); + assertEquals(0, assignmentInfo.numRunningImmediacyPrivileged); } @Test @@ -239,7 +239,7 @@ public final class JobConcurrencyManagerTest { assertEquals(0, preferredUidOnly.size()); assertEquals(0, stoppable.size()); assertEquals(0, assignmentInfo.minPreferredUidOnlyWaitingTimeMs); - assertEquals(0, assignmentInfo.numRunningTopEj); + assertEquals(0, assignmentInfo.numRunningImmediacyPrivileged); } @Test @@ -265,15 +265,14 @@ public final class JobConcurrencyManagerTest { assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, preferredUidOnly.size()); assertEquals(0, stoppable.size()); assertEquals(0, assignmentInfo.minPreferredUidOnlyWaitingTimeMs); - assertEquals(0, assignmentInfo.numRunningTopEj); + assertEquals(0, assignmentInfo.numRunningImmediacyPrivileged); } @Test - public void testPrepareForAssignmentDetermination_onlyRunningTopEjs() { + public void testPrepareForAssignmentDetermination_onlyStartedWithImmediacyPrivilege() { for (int i = 0; i < JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT; ++i) { JobStatus job = createJob(mDefaultUserId * UserHandle.PER_USER_RANGE + i); - job.startedAsExpeditedJob = true; - job.lastEvaluatedBias = JobInfo.BIAS_TOP_APP; + job.startedWithImmediacyPrivilege = true; mJobConcurrencyManager.addRunningJobForTesting(job); } @@ -294,7 +293,7 @@ public final class JobConcurrencyManagerTest { assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT / 2, stoppable.size()); assertEquals(0, assignmentInfo.minPreferredUidOnlyWaitingTimeMs); assertEquals(JobConcurrencyManager.STANDARD_CONCURRENCY_LIMIT, - assignmentInfo.numRunningTopEj); + assignmentInfo.numRunningImmediacyPrivileged); } @Test @@ -500,6 +499,38 @@ public final class JobConcurrencyManagerTest { } @Test + public void testHasImmediacyPrivilege() { + JobStatus job = createJob(mDefaultUserId * UserHandle.PER_USER_RANGE, 0); + spyOn(job); + assertFalse(mJobConcurrencyManager.hasImmediacyPrivilegeLocked(job)); + + doReturn(false).when(job).shouldTreatAsExpeditedJob(); + doReturn(false).when(job).shouldTreatAsUserInitiated(); + job.lastEvaluatedBias = JobInfo.BIAS_TOP_APP; + assertFalse(mJobConcurrencyManager.hasImmediacyPrivilegeLocked(job)); + + doReturn(true).when(job).shouldTreatAsExpeditedJob(); + doReturn(false).when(job).shouldTreatAsUserInitiated(); + job.lastEvaluatedBias = JobInfo.BIAS_DEFAULT; + assertFalse(mJobConcurrencyManager.hasImmediacyPrivilegeLocked(job)); + + doReturn(false).when(job).shouldTreatAsExpeditedJob(); + doReturn(true).when(job).shouldTreatAsUserInitiated(); + job.lastEvaluatedBias = JobInfo.BIAS_DEFAULT; + assertFalse(mJobConcurrencyManager.hasImmediacyPrivilegeLocked(job)); + + doReturn(false).when(job).shouldTreatAsExpeditedJob(); + doReturn(true).when(job).shouldTreatAsUserInitiated(); + job.lastEvaluatedBias = JobInfo.BIAS_TOP_APP; + assertTrue(mJobConcurrencyManager.hasImmediacyPrivilegeLocked(job)); + + doReturn(true).when(job).shouldTreatAsExpeditedJob(); + doReturn(false).when(job).shouldTreatAsUserInitiated(); + job.lastEvaluatedBias = JobInfo.BIAS_TOP_APP; + assertTrue(mJobConcurrencyManager.hasImmediacyPrivilegeLocked(job)); + } + + @Test public void testIsPkgConcurrencyLimited_top() { final JobStatus topJob = createJob(mDefaultUserId * UserHandle.PER_USER_RANGE, 0); topJob.lastEvaluatedBias = JobInfo.BIAS_TOP_APP; diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorMUMDTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorMUMDTest.java index 6d8910eb8cdd..58cff944868c 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorMUMDTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorMUMDTest.java @@ -107,8 +107,8 @@ public final class UserVisibilityMediatorMUMDTest extends UserVisibilityMediator onVisible(PROFILE_USER_ID)); startForegroundUser(PARENT_USER_ID); - int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID, BG, - DEFAULT_DISPLAY); + int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID, + BG_VISIBLE, DEFAULT_DISPLAY); assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE); expectUserIsVisible(PROFILE_USER_ID); @@ -138,7 +138,8 @@ public final class UserVisibilityMediatorMUMDTest extends UserVisibilityMediator public void testStartBgUser_onInvalidDisplay() throws Exception { AsyncUserVisibilityListener listener = addListenerForNoEvents(); - int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG, INVALID_DISPLAY); + int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG_VISIBLE, + INVALID_DISPLAY); assertStartUserResult(result, USER_ASSIGNMENT_RESULT_FAILURE); @@ -151,7 +152,7 @@ public final class UserVisibilityMediatorMUMDTest extends UserVisibilityMediator public void testStartBgUser_onSecondaryDisplay_displayAvailable() throws Exception { AsyncUserVisibilityListener listener = addListenerForEvents(onVisible(USER_ID)); - int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG, + int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG_VISIBLE, SECONDARY_DISPLAY_ID); assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE); @@ -176,7 +177,7 @@ public final class UserVisibilityMediatorMUMDTest extends UserVisibilityMediator expectUserIsNotVisibleOnDisplay("before", PARENT_USER_ID, SECONDARY_DISPLAY_ID); expectUserIsNotVisibleOnDisplay("before", PROFILE_USER_ID, SECONDARY_DISPLAY_ID); - int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG, + int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG_VISIBLE, SECONDARY_DISPLAY_ID); assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE); @@ -189,7 +190,7 @@ public final class UserVisibilityMediatorMUMDTest extends UserVisibilityMediator AsyncUserVisibilityListener listener = addListenerForEvents(onVisible(OTHER_USER_ID)); startUserInSecondaryDisplay(OTHER_USER_ID, SECONDARY_DISPLAY_ID); - int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG, + int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG_VISIBLE, SECONDARY_DISPLAY_ID); assertStartUserResult(result, USER_ASSIGNMENT_RESULT_FAILURE); @@ -205,7 +206,7 @@ public final class UserVisibilityMediatorMUMDTest extends UserVisibilityMediator AsyncUserVisibilityListener listener = addListenerForEvents(onVisible(USER_ID)); startUserInSecondaryDisplay(USER_ID, OTHER_SECONDARY_DISPLAY_ID); - int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG, + int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG_VISIBLE, SECONDARY_DISPLAY_ID); assertStartUserResult(result, USER_ASSIGNMENT_RESULT_FAILURE); @@ -228,8 +229,8 @@ public final class UserVisibilityMediatorMUMDTest extends UserVisibilityMediator AsyncUserVisibilityListener listener = addListenerForEvents(onVisible(PARENT_USER_ID)); startUserInSecondaryDisplay(PARENT_USER_ID, OTHER_SECONDARY_DISPLAY_ID); - int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID, BG, - DEFAULT_DISPLAY); + int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID, + BG_VISIBLE, DEFAULT_DISPLAY); assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE); expectUserIsNotVisibleAtAll(PROFILE_USER_ID); diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorSUSDTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorSUSDTest.java index 1065392be8ce..3d64c29e463c 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorSUSDTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorSUSDTest.java @@ -106,8 +106,8 @@ public final class UserVisibilityMediatorSUSDTest extends UserVisibilityMediator onVisible(PROFILE_USER_ID)); startForegroundUser(PARENT_USER_ID); - int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID, BG, - DEFAULT_DISPLAY); + int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID, + BG_VISIBLE, DEFAULT_DISPLAY); assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE); expectUserIsVisible(PROFILE_USER_ID); @@ -126,7 +126,7 @@ public final class UserVisibilityMediatorSUSDTest extends UserVisibilityMediator public void testStartBgUser_onSecondaryDisplay() throws Exception { AsyncUserVisibilityListener listener = addListenerForNoEvents(); - int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG, + int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG_VISIBLE, SECONDARY_DISPLAY_ID); assertStartUserResult(result, USER_ASSIGNMENT_RESULT_FAILURE); diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorTestCase.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorTestCase.java index 4487d136b708..74fd9ff3163c 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorTestCase.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserVisibilityMediatorTestCase.java @@ -26,6 +26,9 @@ import static android.view.Display.INVALID_DISPLAY; import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_FAILURE; import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE; import static com.android.server.pm.UserManagerInternal.USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE; +import static com.android.server.pm.UserManagerInternal.USER_START_MODE_BACKGROUND; +import static com.android.server.pm.UserManagerInternal.USER_START_MODE_BACKGROUND_VISIBLE; +import static com.android.server.pm.UserManagerInternal.USER_START_MODE_FOREGROUND; import static com.android.server.pm.UserManagerInternal.userAssignmentResultToString; import static com.android.server.pm.UserVisibilityChangedEvent.onInvisible; import static com.android.server.pm.UserVisibilityChangedEvent.onVisible; @@ -99,8 +102,9 @@ abstract class UserVisibilityMediatorTestCase extends ExtendedMockitoTestCase { */ protected static final int OTHER_SECONDARY_DISPLAY_ID = 108; - protected static final boolean FG = true; - protected static final boolean BG = false; + protected static final int FG = USER_START_MODE_FOREGROUND; + protected static final int BG = USER_START_MODE_BACKGROUND; + protected static final int BG_VISIBLE = USER_START_MODE_BACKGROUND_VISIBLE; private Handler mHandler; protected AsyncUserVisibilityListener.Factory mListenerFactory; @@ -154,8 +158,7 @@ abstract class UserVisibilityMediatorTestCase extends ExtendedMockitoTestCase { public final void testStartBgUser_onDefaultDisplay() throws Exception { AsyncUserVisibilityListener listener = addListenerForNoEvents(); - int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG, - DEFAULT_DISPLAY); + int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG, DEFAULT_DISPLAY); assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE); expectUserIsNotVisibleAtAll(USER_ID); @@ -166,6 +169,34 @@ abstract class UserVisibilityMediatorTestCase extends ExtendedMockitoTestCase { } @Test + public final void testStartBgUser_onDefaultDisplay_visible() throws Exception { + AsyncUserVisibilityListener listener = addListenerForNoEvents(); + + int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG_VISIBLE, + DEFAULT_DISPLAY); + assertStartUserResult(result, USER_ASSIGNMENT_RESULT_FAILURE); + + expectUserIsNotVisibleAtAll(PROFILE_USER_ID); + expectNoDisplayAssignedToUser(PROFILE_USER_ID); + + listener.verify(); + } + + @Test + public final void testStartBgUser_onSecondaryDisplay_invisible() throws Exception { + AsyncUserVisibilityListener listener = addListenerForNoEvents(); + + int result = mMediator.assignUserToDisplayOnStart(USER_ID, USER_ID, BG, + SECONDARY_DISPLAY_ID); + assertStartUserResult(result, USER_ASSIGNMENT_RESULT_FAILURE); + + expectUserIsNotVisibleAtAll(PROFILE_USER_ID); + expectNoDisplayAssignedToUser(PROFILE_USER_ID); + + listener.verify(); + } + + @Test public final void testStartBgSystemUser_onSecondaryDisplay() throws Exception { AsyncUserVisibilityListener listener = addListenerForEvents( onInvisible(INITIAL_CURRENT_USER_ID), @@ -228,8 +259,8 @@ abstract class UserVisibilityMediatorTestCase extends ExtendedMockitoTestCase { throws Exception { AsyncUserVisibilityListener listener = addListenerForNoEvents(); - int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID, BG, - DEFAULT_DISPLAY); + int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID, + BG_VISIBLE, DEFAULT_DISPLAY); assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE); expectUserIsNotVisibleAtAll(PROFILE_USER_ID); @@ -244,8 +275,8 @@ abstract class UserVisibilityMediatorTestCase extends ExtendedMockitoTestCase { AsyncUserVisibilityListener listener = addListenerForNoEvents(); startBackgroundUser(PARENT_USER_ID); - int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID, BG, - DEFAULT_DISPLAY); + int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID, + BG_VISIBLE, DEFAULT_DISPLAY); assertStartUserResult(result, USER_ASSIGNMENT_RESULT_SUCCESS_INVISIBLE); expectUserIsNotVisibleAtAll(PROFILE_USER_ID); @@ -261,6 +292,21 @@ abstract class UserVisibilityMediatorTestCase extends ExtendedMockitoTestCase { public final void testStartBgProfile_onSecondaryDisplay() throws Exception { AsyncUserVisibilityListener listener = addListenerForNoEvents(); + int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID, + BG_VISIBLE, SECONDARY_DISPLAY_ID); + assertStartUserResult(result, USER_ASSIGNMENT_RESULT_FAILURE); + + expectUserIsNotVisibleAtAll(PROFILE_USER_ID); + expectNoDisplayAssignedToUser(PROFILE_USER_ID); + expectNoUserAssignedToDisplay(SECONDARY_DISPLAY_ID); + + listener.verify(); + } + + @Test + public final void testStartBgProfile_onSecondaryDisplay_invisible() throws Exception { + AsyncUserVisibilityListener listener = addListenerForNoEvents(); + int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID, BG, SECONDARY_DISPLAY_ID); assertStartUserResult(result, USER_ASSIGNMENT_RESULT_FAILURE); @@ -384,8 +430,8 @@ abstract class UserVisibilityMediatorTestCase extends ExtendedMockitoTestCase { Log.d(TAG, "starting default profile (" + PROFILE_USER_ID + ") in background after starting" + " its parent (" + PARENT_USER_ID + ") on foreground"); - int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID, BG, - DEFAULT_DISPLAY); + int result = mMediator.assignUserToDisplayOnStart(PROFILE_USER_ID, PARENT_USER_ID, + BG_VISIBLE, DEFAULT_DISPLAY); if (result != USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE) { throw new IllegalStateException("Failed to start profile user " + PROFILE_USER_ID + ": mediator returned " + userAssignmentResultToString(result)); @@ -403,7 +449,7 @@ abstract class UserVisibilityMediatorTestCase extends ExtendedMockitoTestCase { Preconditions.checkArgument(displayId != INVALID_DISPLAY && displayId != DEFAULT_DISPLAY, "must pass a secondary display, not %d", displayId); Log.d(TAG, "startUserInSecondaryDisplay(" + userId + ", " + displayId + ")"); - int result = mMediator.assignUserToDisplayOnStart(userId, userId, BG, displayId); + int result = mMediator.assignUserToDisplayOnStart(userId, userId, BG_VISIBLE, displayId); if (result != USER_ASSIGNMENT_RESULT_SUCCESS_VISIBLE) { throw new IllegalStateException("Failed to startuser " + userId + " on background: mediator returned " + userAssignmentResultToString(result)); diff --git a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java index 98037d792bf4..0dfe664432c3 100644 --- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java @@ -243,14 +243,6 @@ public class UserControllerTest { } @Test - public void testStartUserOnSecondaryDisplay_defaultDisplay() { - assertThrows(IllegalArgumentException.class, () -> mUserController - .startUserOnSecondaryDisplay(TEST_USER_ID, Display.DEFAULT_DISPLAY)); - - verifyUserNeverAssignedToDisplay(); - } - - @Test public void testStartUserOnSecondaryDisplay() { boolean started = mUserController.startUserOnSecondaryDisplay(TEST_USER_ID, 42); diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java index d2f2af1b91b6..9c7c574f8e31 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java @@ -21,6 +21,7 @@ import static com.google.common.truth.Truth.assertWithMessage; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.startsWith; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -143,4 +144,38 @@ public class InputControllerTest { verify(mInputManagerInternalMock).setVirtualMousePointerDisplayId(eq(1)); } + @Test + public void createNavigationTouchpad_hasDeviceId() { + final IBinder deviceToken = new Binder(); + mInputController.createNavigationTouchpad("name", /*vendorId= */ 1, /*productId= */ 1, + deviceToken, /* displayId= */ 1, /* touchpadHeight= */ 50, /* touchpadWidth= */ 50); + + int deviceId = mInputController.getInputDeviceId(deviceToken); + int[] deviceIds = InputManager.getInstance().getInputDeviceIds(); + + assertWithMessage("InputManager's deviceIds list should contain id of the device").that( + deviceIds).asList().contains(deviceId); + } + + @Test + public void createNavigationTouchpad_setsTypeAssociation() { + final IBinder deviceToken = new Binder(); + mInputController.createNavigationTouchpad("name", /*vendorId= */ 1, /*productId= */ 1, + deviceToken, /* displayId= */ 1, /* touchpadHeight= */ 50, /* touchpadWidth= */ 50); + + verify(mInputManagerInternalMock).setTypeAssociation( + startsWith("virtualNavigationTouchpad:"), eq("touchNavigation")); + } + + @Test + public void createAndUnregisterNavigationTouchpad_unsetsTypeAssociation() { + final IBinder deviceToken = new Binder(); + mInputController.createNavigationTouchpad("name", /*vendorId= */ 1, /*productId= */ 1, + deviceToken, /* displayId= */ 1, /* touchpadHeight= */ 50, /* touchpadWidth= */ 50); + + mInputController.unregisterInputDevice(deviceToken); + + verify(mInputManagerInternalMock).unsetTypeAssociation( + startsWith("virtualNavigationTouchpad:")); + } } diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java index 5a3d7b580748..31e53d56f520 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java @@ -69,6 +69,7 @@ import android.hardware.input.VirtualMouseButtonEvent; import android.hardware.input.VirtualMouseConfig; import android.hardware.input.VirtualMouseRelativeEvent; import android.hardware.input.VirtualMouseScrollEvent; +import android.hardware.input.VirtualNavigationTouchpadConfig; import android.hardware.input.VirtualTouchEvent; import android.hardware.input.VirtualTouchscreenConfig; import android.net.MacAddress; @@ -134,8 +135,6 @@ public class VirtualDeviceManagerServiceTest { private static final int UID_2 = 10; private static final int UID_3 = 10000; private static final int UID_4 = 10001; - private static final int ASSOCIATION_ID_1 = 1; - private static final int ASSOCIATION_ID_2 = 2; private static final int PRODUCT_ID = 10; private static final int VENDOR_ID = 5; private static final String UNIQUE_ID = "uniqueid"; @@ -177,6 +176,14 @@ public class VirtualDeviceManagerServiceTest { .setWidthInPixels(WIDTH) .setHeightInPixels(HEIGHT) .build(); + private static final VirtualNavigationTouchpadConfig NAVIGATION_TOUCHPAD_CONFIG = + new VirtualNavigationTouchpadConfig.Builder( + /* touchpadHeight= */ HEIGHT, /* touchpadWidth= */ WIDTH) + .setVendorId(VENDOR_ID) + .setProductId(PRODUCT_ID) + .setInputDeviceName(DEVICE_NAME) + .setAssociatedDisplayId(DISPLAY_ID) + .build(); private Context mContext; private InputManagerMockHelper mInputManagerMockHelper; @@ -307,13 +314,13 @@ public class VirtualDeviceManagerServiceTest { mContext.getSystemService(WindowManager.class), threadVerifier); mSensorController = new SensorController(new Object(), VIRTUAL_DEVICE_ID); - mAssociationInfo = new AssociationInfo(ASSOCIATION_ID_1, 0, null, + mAssociationInfo = new AssociationInfo(/* associationId= */ 1, 0, null, MacAddress.BROADCAST_ADDRESS, "", null, null, true, false, false, 0, 0); mVdms = new VirtualDeviceManagerService(mContext); mLocalService = mVdms.getLocalServiceInstance(); mVdm = mVdms.new VirtualDeviceManagerImpl(); - mDeviceImpl = createVirtualDevice(VIRTUAL_DEVICE_ID, DEVICE_OWNER_UID_1, mAssociationInfo); + mDeviceImpl = createVirtualDevice(VIRTUAL_DEVICE_ID, DEVICE_OWNER_UID_1); } @Test @@ -377,7 +384,8 @@ public class VirtualDeviceManagerServiceTest { .build(); mDeviceImpl = new VirtualDeviceImpl(mContext, mAssociationInfo, new Binder(), /* ownerUid */ 0, VIRTUAL_DEVICE_ID, - mInputController, mSensorController, (int associationId) -> {}, + mInputController, mSensorController, + /* onDeviceCloseListener= */ (int deviceId) -> {}, mPendingTrampolineCallback, mActivityListener, mRunningAppsChangedCallback, params); mVdms.addVirtualDevice(mDeviceImpl); @@ -396,9 +404,7 @@ public class VirtualDeviceManagerServiceTest { int firstDeviceId = mDeviceImpl.getDeviceId(); int secondDeviceId = VIRTUAL_DEVICE_ID + 1; - createVirtualDevice(secondDeviceId, DEVICE_OWNER_UID_2, - new AssociationInfo(ASSOCIATION_ID_2, 0, null, - MacAddress.BROADCAST_ADDRESS, "", null, null, true, false, false, 0, 0)); + createVirtualDevice(secondDeviceId, DEVICE_OWNER_UID_2); int secondDeviceOwner = mLocalService.getDeviceOwnerUid(secondDeviceId); assertThat(secondDeviceOwner).isEqualTo(DEVICE_OWNER_UID_2); @@ -457,9 +463,7 @@ public class VirtualDeviceManagerServiceTest { public void getDeviceIdsForUid_twoDevicesUidOnOne_returnsCorrectId() { int secondDeviceId = VIRTUAL_DEVICE_ID + 1; - VirtualDeviceImpl secondDevice = createVirtualDevice(secondDeviceId, DEVICE_OWNER_UID_2, - new AssociationInfo(ASSOCIATION_ID_2, 0, null, - MacAddress.BROADCAST_ADDRESS, "", null, null, true, false, false, 0, 0)); + VirtualDeviceImpl secondDevice = createVirtualDevice(secondDeviceId, DEVICE_OWNER_UID_2); GenericWindowPolicyController gwpc = secondDevice.createWindowPolicyController(new ArrayList<>()); @@ -474,9 +478,7 @@ public class VirtualDeviceManagerServiceTest { public void getDeviceIdsForUid_twoDevicesUidOnBoth_returnsCorrectId() { int secondDeviceId = VIRTUAL_DEVICE_ID + 1; - VirtualDeviceImpl secondDevice = createVirtualDevice(secondDeviceId, DEVICE_OWNER_UID_2, - new AssociationInfo(ASSOCIATION_ID_2, 0, null, - MacAddress.BROADCAST_ADDRESS, "", null, null, true, false, false, 0, 0)); + VirtualDeviceImpl secondDevice = createVirtualDevice(secondDeviceId, DEVICE_OWNER_UID_2); GenericWindowPolicyController gwpc1 = mDeviceImpl.createWindowPolicyController(new ArrayList<>()); GenericWindowPolicyController gwpc2 = @@ -526,7 +528,7 @@ public class VirtualDeviceManagerServiceTest { ArraySet<Integer> uids = new ArraySet<>(Arrays.asList(UID_1, UID_2)); mLocalService.registerAppsOnVirtualDeviceListener(mAppsOnVirtualDeviceListener); - mVdms.notifyRunningAppsChanged(ASSOCIATION_ID_1, uids); + mVdms.notifyRunningAppsChanged(mDeviceImpl.getDeviceId(), uids); TestableLooper.get(this).processAllMessages(); verify(mAppsOnVirtualDeviceListener).onAppsOnAnyVirtualDeviceChanged(uids); @@ -539,13 +541,13 @@ public class VirtualDeviceManagerServiceTest { mLocalService.registerAppsOnVirtualDeviceListener(mAppsOnVirtualDeviceListener); // Notifies that the running apps on the first virtual device has changed. - mVdms.notifyRunningAppsChanged(ASSOCIATION_ID_1, uidsOnDevice1); + mVdms.notifyRunningAppsChanged(mDeviceImpl.getDeviceId(), uidsOnDevice1); TestableLooper.get(this).processAllMessages(); verify(mAppsOnVirtualDeviceListener).onAppsOnAnyVirtualDeviceChanged( new ArraySet<>(Arrays.asList(UID_1, UID_2))); // Notifies that the running apps on the second virtual device has changed. - mVdms.notifyRunningAppsChanged(ASSOCIATION_ID_2, uidsOnDevice2); + mVdms.notifyRunningAppsChanged(mDeviceImpl.getDeviceId() + 1, uidsOnDevice2); TestableLooper.get(this).processAllMessages(); // The union of the apps running on both virtual devices are sent to the listeners. verify(mAppsOnVirtualDeviceListener).onAppsOnAnyVirtualDeviceChanged( @@ -553,7 +555,7 @@ public class VirtualDeviceManagerServiceTest { // Notifies that the running apps on the first virtual device has changed again. uidsOnDevice1.remove(UID_2); - mVdms.notifyRunningAppsChanged(ASSOCIATION_ID_1, uidsOnDevice1); + mVdms.notifyRunningAppsChanged(mDeviceImpl.getDeviceId(), uidsOnDevice1); mLocalService.onAppsOnVirtualDeviceChanged(); TestableLooper.get(this).processAllMessages(); // The union of the apps running on both virtual devices are sent to the listeners. @@ -562,7 +564,7 @@ public class VirtualDeviceManagerServiceTest { // Notifies that the running apps on the first virtual device has changed but with the same // set of UIDs. - mVdms.notifyRunningAppsChanged(ASSOCIATION_ID_1, uidsOnDevice1); + mVdms.notifyRunningAppsChanged(mDeviceImpl.getDeviceId(), uidsOnDevice1); mLocalService.onAppsOnVirtualDeviceChanged(); TestableLooper.get(this).processAllMessages(); // Listeners should not be notified. @@ -707,8 +709,67 @@ public class VirtualDeviceManagerServiceTest { .build(); mDeviceImpl.createVirtualTouchscreen(positiveConfig, BINDER); assertWithMessage( - "Virtual touchscreen should create input device descriptor on successful creation" - + ".").that(mInputController.getInputDeviceDescriptors()).isNotEmpty(); + "Virtual touchscreen should create input device descriptor on successful creation" + + ".").that(mInputController.getInputDeviceDescriptors()).isNotEmpty(); + } + + @Test + public void createVirtualNavigationTouchpad_noDisplay_failsSecurityException() { + assertThrows(SecurityException.class, + () -> mDeviceImpl.createVirtualNavigationTouchpad(NAVIGATION_TOUCHPAD_CONFIG, + BINDER)); + } + + @Test + public void createVirtualNavigationTouchpad_zeroDisplayDimension_failsWithException() { + mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID); + assertThrows(IllegalArgumentException.class, + () -> { + final VirtualNavigationTouchpadConfig zeroConfig = + new VirtualNavigationTouchpadConfig.Builder( + /* touchpadHeight= */ 0, /* touchpadWidth= */ 0) + .setVendorId(VENDOR_ID) + .setProductId(PRODUCT_ID) + .setInputDeviceName(DEVICE_NAME) + .setAssociatedDisplayId(DISPLAY_ID) + .build(); + mDeviceImpl.createVirtualNavigationTouchpad(zeroConfig, BINDER); + }); + } + + @Test + public void createVirtualNavigationTouchpad_negativeDisplayDimension_failsWithException() { + mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID); + assertThrows(IllegalArgumentException.class, + () -> { + final VirtualNavigationTouchpadConfig zeroConfig = + new VirtualNavigationTouchpadConfig.Builder( + /* touchpadHeight= */ -50, /* touchpadWidth= */ 50) + .setVendorId(VENDOR_ID) + .setProductId(PRODUCT_ID) + .setInputDeviceName(DEVICE_NAME) + .setAssociatedDisplayId(DISPLAY_ID) + .build(); + mDeviceImpl.createVirtualNavigationTouchpad(zeroConfig, BINDER); + }); + } + + @Test + public void createVirtualNavigationTouchpad_positiveDisplayDimension_successful() { + mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID); + VirtualNavigationTouchpadConfig positiveConfig = + new VirtualNavigationTouchpadConfig.Builder( + /* touchpadHeight= */ 50, /* touchpadWidth= */ 50) + .setVendorId(VENDOR_ID) + .setProductId(PRODUCT_ID) + .setInputDeviceName(DEVICE_NAME) + .setAssociatedDisplayId(DISPLAY_ID) + .build(); + mDeviceImpl.createVirtualNavigationTouchpad(positiveConfig, BINDER); + assertWithMessage( + "Virtual navigation touchpad should create input device descriptor on successful " + + "creation" + + ".").that(mInputController.getInputDeviceDescriptors()).isNotEmpty(); } @Test @@ -755,6 +816,16 @@ public class VirtualDeviceManagerServiceTest { } @Test + public void createVirtualNavigationTouchpad_noPermission_failsSecurityException() { + mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID); + doCallRealMethod().when(mContext).enforceCallingOrSelfPermission( + eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString()); + assertThrows(SecurityException.class, + () -> mDeviceImpl.createVirtualNavigationTouchpad(NAVIGATION_TOUCHPAD_CONFIG, + BINDER)); + } + + @Test public void createVirtualSensor_noPermission_failsSecurityException() { doCallRealMethod().when(mContext).enforceCallingOrSelfPermission( eq(Manifest.permission.CREATE_VIRTUAL_DEVICE), anyString()); @@ -818,7 +889,18 @@ public class VirtualDeviceManagerServiceTest { mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID); mDeviceImpl.createVirtualTouchscreen(TOUCHSCREEN_CONFIG, BINDER); assertWithMessage("Virtual touchscreen should register fd when the display matches").that( - mInputController.getInputDeviceDescriptors()).isNotEmpty(); + mInputController.getInputDeviceDescriptors()).isNotEmpty(); + verify(mNativeWrapperMock).openUinputTouchscreen(eq(DEVICE_NAME), eq(VENDOR_ID), + eq(PRODUCT_ID), anyString(), eq(HEIGHT), eq(WIDTH)); + } + + @Test + public void createVirtualNavigationTouchpad_hasDisplay_obtainFileDescriptor() { + mDeviceImpl.mVirtualDisplayIds.add(DISPLAY_ID); + mDeviceImpl.createVirtualNavigationTouchpad(NAVIGATION_TOUCHPAD_CONFIG, BINDER); + assertWithMessage("Virtual navigation touchpad should register fd when the display matches") + .that( + mInputController.getInputDeviceDescriptors()).isNotEmpty(); verify(mNativeWrapperMock).openUinputTouchscreen(eq(DEVICE_NAME), eq(VENDOR_ID), eq(PRODUCT_ID), anyString(), eq(HEIGHT), eq(WIDTH)); } @@ -1288,18 +1370,17 @@ public class VirtualDeviceManagerServiceTest { intent.filterEquals(blockedAppIntent)), any(), any()); } - private VirtualDeviceImpl createVirtualDevice(int virtualDeviceId, int ownerUid, - AssociationInfo associationInfo) { + private VirtualDeviceImpl createVirtualDevice(int virtualDeviceId, int ownerUid) { VirtualDeviceParams params = new VirtualDeviceParams .Builder() .setBlockedActivities(getBlockedActivities()) .build(); VirtualDeviceImpl virtualDeviceImpl = new VirtualDeviceImpl(mContext, - associationInfo, new Binder(), ownerUid, virtualDeviceId, - mInputController, mSensorController, (int associationId) -> {}, + mAssociationInfo, new Binder(), ownerUid, virtualDeviceId, + mInputController, mSensorController, + /* onDeviceCloseListener= */ (int deviceId) -> {}, mPendingTrampolineCallback, mActivityListener, mRunningAppsChangedCallback, params); mVdms.addVirtualDevice(virtualDeviceImpl); return virtualDeviceImpl; } - } diff --git a/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java b/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java index c7caa4372b0e..c6a0b0f936a5 100644 --- a/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java +++ b/services/tests/servicestests/src/com/android/server/display/LogicalDisplayMapperTest.java @@ -18,10 +18,14 @@ package com.android.server.display; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.DEFAULT_DISPLAY_GROUP; +import static android.view.Display.TYPE_INTERNAL; +import static android.view.Display.TYPE_VIRTUAL; +import static com.android.server.display.DeviceStateToLayoutMap.STATE_DEFAULT; import static com.android.server.display.DisplayAdapter.DISPLAY_DEVICE_EVENT_ADDED; import static com.android.server.display.DisplayAdapter.DISPLAY_DEVICE_EVENT_CHANGED; import static com.android.server.display.DisplayAdapter.DISPLAY_DEVICE_EVENT_REMOVED; +import static com.android.server.display.DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY; import static com.android.server.display.LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_ADDED; import static com.android.server.display.LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_REMOVED; @@ -69,7 +73,6 @@ import org.mockito.Spy; import java.io.InputStream; import java.io.OutputStream; import java.util.Arrays; -import java.util.Set; @SmallTest @RunWith(AndroidJUnit4.class) @@ -155,8 +158,8 @@ public class LogicalDisplayMapperTest { @Test public void testDisplayDeviceAddAndRemove_Internal() { - DisplayDevice device = createDisplayDevice(Display.TYPE_INTERNAL, 600, 800, - DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY); + DisplayDevice device = createDisplayDevice(TYPE_INTERNAL, 600, 800, + FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY); // add LogicalDisplay displayAdded = add(device); @@ -177,7 +180,7 @@ public class LogicalDisplayMapperTest { testDisplayDeviceAddAndRemove_NonInternal(Display.TYPE_EXTERNAL); testDisplayDeviceAddAndRemove_NonInternal(Display.TYPE_WIFI); testDisplayDeviceAddAndRemove_NonInternal(Display.TYPE_OVERLAY); - testDisplayDeviceAddAndRemove_NonInternal(Display.TYPE_VIRTUAL); + testDisplayDeviceAddAndRemove_NonInternal(TYPE_VIRTUAL); testDisplayDeviceAddAndRemove_NonInternal(Display.TYPE_UNKNOWN); // Call the internal test again, just to verify that adding non-internal displays @@ -187,9 +190,9 @@ public class LogicalDisplayMapperTest { @Test public void testDisplayDeviceAdd_TwoInternalOneDefault() { - DisplayDevice device1 = createDisplayDevice(Display.TYPE_INTERNAL, 600, 800, 0); - DisplayDevice device2 = createDisplayDevice(Display.TYPE_INTERNAL, 600, 800, - DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY); + DisplayDevice device1 = createDisplayDevice(TYPE_INTERNAL, 600, 800, 0); + DisplayDevice device2 = createDisplayDevice(TYPE_INTERNAL, 600, 800, + FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY); LogicalDisplay display1 = add(device1); assertEquals(info(display1).address, info(device1).address); @@ -202,10 +205,10 @@ public class LogicalDisplayMapperTest { @Test public void testDisplayDeviceAdd_TwoInternalBothDefault() { - DisplayDevice device1 = createDisplayDevice(Display.TYPE_INTERNAL, 600, 800, - DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY); - DisplayDevice device2 = createDisplayDevice(Display.TYPE_INTERNAL, 600, 800, - DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY); + DisplayDevice device1 = createDisplayDevice(TYPE_INTERNAL, 600, 800, + FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY); + DisplayDevice device2 = createDisplayDevice(TYPE_INTERNAL, 600, 800, + FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY); LogicalDisplay display1 = add(device1); assertEquals(info(display1).address, info(device1).address); @@ -220,7 +223,7 @@ public class LogicalDisplayMapperTest { @Test public void testDisplayDeviceAddAndRemove_OneExternalDefault() { DisplayDevice device = createDisplayDevice(Display.TYPE_EXTERNAL, 600, 800, - DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY); + FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY); // add LogicalDisplay displayAdded = add(device); @@ -238,10 +241,10 @@ public class LogicalDisplayMapperTest { @Test public void testDisplayDeviceAddAndRemove_SwitchDefault() { - DisplayDevice device1 = createDisplayDevice(Display.TYPE_INTERNAL, 600, 800, - DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY); - DisplayDevice device2 = createDisplayDevice(Display.TYPE_INTERNAL, 600, 800, - DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY); + DisplayDevice device1 = createDisplayDevice(TYPE_INTERNAL, 600, 800, + FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY); + DisplayDevice device2 = createDisplayDevice(TYPE_INTERNAL, 600, 800, + FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY); LogicalDisplay display1 = add(device1); assertEquals(info(display1).address, info(device1).address); @@ -267,10 +270,10 @@ public class LogicalDisplayMapperTest { @Test public void testGetDisplayIdsLocked() { - add(createDisplayDevice(Display.TYPE_INTERNAL, 600, 800, - DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY)); + add(createDisplayDevice(TYPE_INTERNAL, 600, 800, + FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY)); add(createDisplayDevice(Display.TYPE_EXTERNAL, 600, 800, 0)); - add(createDisplayDevice(Display.TYPE_VIRTUAL, 600, 800, 0)); + add(createDisplayDevice(TYPE_VIRTUAL, 600, 800, 0)); int [] ids = mLogicalDisplayMapper.getDisplayIdsLocked(Process.SYSTEM_UID, /* includeDisabled= */ true); @@ -280,71 +283,98 @@ public class LogicalDisplayMapperTest { } @Test - public void testGetDisplayInfoForStateLocked_oneDisplayGroup_internalType() { - add(createDisplayDevice(Display.TYPE_INTERNAL, 600, 800, - DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY)); - add(createDisplayDevice(Display.TYPE_INTERNAL, 200, 800, - DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY)); - add(createDisplayDevice(Display.TYPE_INTERNAL, 700, 800, - DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY)); - - Set<DisplayInfo> displayInfos = mLogicalDisplayMapper.getDisplayInfoForStateLocked( - DeviceStateToLayoutMap.STATE_DEFAULT, DEFAULT_DISPLAY, DEFAULT_DISPLAY_GROUP); - assertThat(displayInfos.size()).isEqualTo(1); - for (DisplayInfo displayInfo : displayInfos) { - assertThat(displayInfo.displayId).isEqualTo(DEFAULT_DISPLAY); - assertThat(displayInfo.displayGroupId).isEqualTo(DEFAULT_DISPLAY_GROUP); - assertThat(displayInfo.logicalWidth).isEqualTo(600); - assertThat(displayInfo.logicalHeight).isEqualTo(800); - } + public void testGetDisplayInfoForStateLocked_defaultLayout() { + final DisplayDevice device1 = createDisplayDevice(TYPE_INTERNAL, 600, 800, + FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY); + final DisplayDevice device2 = createDisplayDevice(TYPE_INTERNAL, 200, 800, + FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY); + + add(device1); + add(device2); + + Layout layout1 = new Layout(); + layout1.createDisplayLocked(info(device1).address, /* isDefault= */ true, + /* isEnabled= */ true, mIdProducer); + layout1.createDisplayLocked(info(device2).address, /* isDefault= */ false, + /* isEnabled= */ true, mIdProducer); + when(mDeviceStateToLayoutMapSpy.get(STATE_DEFAULT)).thenReturn(layout1); + assertThat(layout1.size()).isEqualTo(2); + final int logicalId2 = layout1.getByAddress(info(device2).address).getLogicalDisplayId(); + + final DisplayInfo displayInfoDefault = mLogicalDisplayMapper.getDisplayInfoForStateLocked( + STATE_DEFAULT, DEFAULT_DISPLAY); + assertThat(displayInfoDefault.displayId).isEqualTo(DEFAULT_DISPLAY); + assertThat(displayInfoDefault.logicalWidth).isEqualTo(width(device1)); + assertThat(displayInfoDefault.logicalHeight).isEqualTo(height(device1)); + + final DisplayInfo displayInfoOther = mLogicalDisplayMapper.getDisplayInfoForStateLocked( + STATE_DEFAULT, logicalId2); + assertThat(displayInfoOther).isNotNull(); + assertThat(displayInfoOther.displayId).isEqualTo(logicalId2); + assertThat(displayInfoOther.logicalWidth).isEqualTo(width(device2)); + assertThat(displayInfoOther.logicalHeight).isEqualTo(height(device2)); } @Test - public void testGetDisplayInfoForStateLocked_oneDisplayGroup_differentTypes() { - add(createDisplayDevice(Display.TYPE_INTERNAL, 600, 800, - DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY)); - add(createDisplayDevice(Display.TYPE_INTERNAL, 200, 800, - DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY)); - add(createDisplayDevice(Display.TYPE_EXTERNAL, 700, 800, - DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY)); - - Set<DisplayInfo> displayInfos = mLogicalDisplayMapper.getDisplayInfoForStateLocked( - DeviceStateToLayoutMap.STATE_DEFAULT, DEFAULT_DISPLAY, DEFAULT_DISPLAY_GROUP); - assertThat(displayInfos.size()).isEqualTo(1); - for (DisplayInfo displayInfo : displayInfos) { - assertThat(displayInfo.displayId).isEqualTo(DEFAULT_DISPLAY); - assertThat(displayInfo.displayGroupId).isEqualTo(DEFAULT_DISPLAY_GROUP); - assertThat(displayInfo.logicalWidth).isEqualTo(600); - assertThat(displayInfo.logicalHeight).isEqualTo(800); - } - } + public void testGetDisplayInfoForStateLocked_multipleLayouts() { + final DisplayDevice device1 = createDisplayDevice(TYPE_INTERNAL, 600, 800, + FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY); + final DisplayDevice device2 = createDisplayDevice(TYPE_INTERNAL, 200, 800, + FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY); + final DisplayDevice device3 = createDisplayDevice(TYPE_VIRTUAL, 700, 800, + DisplayDeviceInfo.FLAG_OWN_DISPLAY_GROUP); - @Test - public void testGetDisplayInfoForStateLocked_multipleDisplayGroups_defaultGroup() { - add(createDisplayDevice(Display.TYPE_INTERNAL, 600, 800, - DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY)); - add(createDisplayDevice(Display.TYPE_INTERNAL, 200, 800, - DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY)); - add(createDisplayDevice(Display.TYPE_VIRTUAL, 700, 800, - DisplayDeviceInfo.FLAG_OWN_DISPLAY_GROUP)); - - Set<DisplayInfo> displayInfos = mLogicalDisplayMapper.getDisplayInfoForStateLocked( - DeviceStateToLayoutMap.STATE_DEFAULT, DEFAULT_DISPLAY, DEFAULT_DISPLAY_GROUP); - assertThat(displayInfos.size()).isEqualTo(1); - for (DisplayInfo displayInfo : displayInfos) { - assertThat(displayInfo.displayId).isEqualTo(DEFAULT_DISPLAY); - assertThat(displayInfo.displayGroupId).isEqualTo(DEFAULT_DISPLAY_GROUP); - assertThat(displayInfo.logicalWidth).isEqualTo(600); - assertThat(displayInfo.logicalHeight).isEqualTo(800); - } + add(device1); + add(device2); + add(device3); + + Layout layout1 = new Layout(); + layout1.createDisplayLocked(info(device1).address, + /* isDefault= */ true, /* isEnabled= */ true, mIdProducer); + when(mDeviceStateToLayoutMapSpy.get(STATE_DEFAULT)).thenReturn(layout1); + + final int layoutState2 = 2; + Layout layout2 = new Layout(); + layout2.createDisplayLocked(info(device2).address, + /* isDefault= */ false, /* isEnabled= */ true, mIdProducer); + // Device3 is the default display. + layout2.createDisplayLocked(info(device3).address, + /* isDefault= */ true, /* isEnabled= */ true, mIdProducer); + when(mDeviceStateToLayoutMapSpy.get(layoutState2)).thenReturn(layout2); + assertThat(layout2.size()).isEqualTo(2); + final int logicalId2 = layout2.getByAddress(info(device2).address).getLogicalDisplayId(); + + // Default layout. + final DisplayInfo displayInfoLayout1Default = + mLogicalDisplayMapper.getDisplayInfoForStateLocked( + STATE_DEFAULT, DEFAULT_DISPLAY); + assertThat(displayInfoLayout1Default.displayId).isEqualTo(DEFAULT_DISPLAY); + assertThat(displayInfoLayout1Default.logicalWidth).isEqualTo(width(device1)); + assertThat(displayInfoLayout1Default.logicalHeight).isEqualTo(height(device1)); + + // Second layout, where device3 is the default display. + final DisplayInfo displayInfoLayout2Default = + mLogicalDisplayMapper.getDisplayInfoForStateLocked( + layoutState2, DEFAULT_DISPLAY); + assertThat(displayInfoLayout2Default.displayId).isEqualTo(DEFAULT_DISPLAY); + assertThat(displayInfoLayout2Default.logicalWidth).isEqualTo(width(device3)); + assertThat(displayInfoLayout2Default.logicalHeight).isEqualTo(height(device3)); + + final DisplayInfo displayInfoLayout2Other = + mLogicalDisplayMapper.getDisplayInfoForStateLocked( + layoutState2, logicalId2); + assertThat(displayInfoLayout2Other).isNotNull(); + assertThat(displayInfoLayout2Other.displayId).isEqualTo(logicalId2); + assertThat(displayInfoLayout2Other.logicalWidth).isEqualTo(width(device2)); + assertThat(displayInfoLayout2Other.logicalHeight).isEqualTo(height(device2)); } @Test public void testSingleDisplayGroup() { - LogicalDisplay display1 = add(createDisplayDevice(Display.TYPE_INTERNAL, 600, 800, - DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY)); - LogicalDisplay display2 = add(createDisplayDevice(Display.TYPE_INTERNAL, 600, 800, 0)); - LogicalDisplay display3 = add(createDisplayDevice(Display.TYPE_VIRTUAL, 600, 800, 0)); + LogicalDisplay display1 = add(createDisplayDevice(TYPE_INTERNAL, 600, 800, + FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY)); + LogicalDisplay display2 = add(createDisplayDevice(TYPE_INTERNAL, 600, 800, 0)); + LogicalDisplay display3 = add(createDisplayDevice(TYPE_VIRTUAL, 600, 800, 0)); assertEquals(DEFAULT_DISPLAY_GROUP, mLogicalDisplayMapper.getDisplayGroupIdFromDisplayIdLocked(id(display1))); @@ -356,12 +386,12 @@ public class LogicalDisplayMapperTest { @Test public void testMultipleDisplayGroups() { - LogicalDisplay display1 = add(createDisplayDevice(Display.TYPE_INTERNAL, 600, 800, - DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY)); - LogicalDisplay display2 = add(createDisplayDevice(Display.TYPE_INTERNAL, 600, 800, 0)); + LogicalDisplay display1 = add(createDisplayDevice(TYPE_INTERNAL, 600, 800, + FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY)); + LogicalDisplay display2 = add(createDisplayDevice(TYPE_INTERNAL, 600, 800, 0)); - TestDisplayDevice device3 = createDisplayDevice(Display.TYPE_VIRTUAL, 600, 800, + TestDisplayDevice device3 = createDisplayDevice(TYPE_VIRTUAL, 600, 800, DisplayDeviceInfo.FLAG_OWN_DISPLAY_GROUP); LogicalDisplay display3 = add(device3); @@ -519,10 +549,10 @@ public class LogicalDisplayMapperTest { @Test public void testDeviceStateLocked() { - DisplayDevice device1 = createDisplayDevice(Display.TYPE_INTERNAL, 600, 800, - DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY); - DisplayDevice device2 = createDisplayDevice(Display.TYPE_INTERNAL, 600, 800, - DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY); + DisplayDevice device1 = createDisplayDevice(TYPE_INTERNAL, 600, 800, + FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY); + DisplayDevice device2 = createDisplayDevice(TYPE_INTERNAL, 600, 800, + FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY); Layout layout = new Layout(); layout.createDisplayLocked(device1.getDisplayDeviceInfoLocked().address, @@ -577,14 +607,11 @@ public class LogicalDisplayMapperTest { DisplayAddress displayAddressThree = new TestUtils.TestDisplayAddress(); TestDisplayDevice device1 = createDisplayDevice(displayAddressOne, "one", - Display.TYPE_INTERNAL, 600, 800, - DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY); + TYPE_INTERNAL, 600, 800, DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DEFAULT_DISPLAY); TestDisplayDevice device2 = createDisplayDevice(displayAddressTwo, "two", - Display.TYPE_INTERNAL, 200, 800, - DisplayDeviceInfo.FLAG_OWN_DISPLAY_GROUP); + TYPE_INTERNAL, 200, 800, DisplayDeviceInfo.FLAG_OWN_DISPLAY_GROUP); TestDisplayDevice device3 = createDisplayDevice(displayAddressThree, "three", - Display.TYPE_INTERNAL, 600, 900, - DisplayDeviceInfo.FLAG_OWN_DISPLAY_GROUP); + TYPE_INTERNAL, 600, 900, DisplayDeviceInfo.FLAG_OWN_DISPLAY_GROUP); Layout threeDevicesEnabledLayout = new Layout(); threeDevicesEnabledLayout.createDisplayLocked( @@ -603,7 +630,7 @@ public class LogicalDisplayMapperTest { /* isEnabled= */ true, mIdProducer); - when(mDeviceStateToLayoutMapSpy.get(DeviceStateToLayoutMap.STATE_DEFAULT)) + when(mDeviceStateToLayoutMapSpy.get(STATE_DEFAULT)) .thenReturn(threeDevicesEnabledLayout); LogicalDisplay display1 = add(device1); @@ -735,6 +762,14 @@ public class LogicalDisplayMapperTest { return device.getDisplayDeviceInfoLocked(); } + private int width(DisplayDevice device) { + return info(device).width; + } + + private int height(DisplayDevice device) { + return info(device).height; + } + private DisplayInfo info(LogicalDisplay display) { return display.getDisplayInfoLocked(); } diff --git a/services/tests/servicestests/src/com/android/server/input/InputManagerServiceTests.kt b/services/tests/servicestests/src/com/android/server/input/InputManagerServiceTests.kt index e390bccf41d8..3326f80f5f91 100644 --- a/services/tests/servicestests/src/com/android/server/input/InputManagerServiceTests.kt +++ b/services/tests/servicestests/src/com/android/server/input/InputManagerServiceTests.kt @@ -16,6 +16,7 @@ package com.android.server.input + import android.content.Context import android.content.ContextWrapper import android.hardware.display.DisplayViewport @@ -25,6 +26,7 @@ import android.platform.test.annotations.Presubmit import android.view.Display import android.view.PointerIcon import androidx.test.InstrumentationRegistry +import com.google.common.truth.Truth.assertThat import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import org.junit.Assert.assertFalse @@ -287,6 +289,28 @@ class InputManagerServiceTests { verify(native).setPointerAcceleration(eq(5f)) } + @Test + fun setDeviceTypeAssociation_setsDeviceTypeAssociation() { + val inputPort = "inputPort" + val type = "type" + + localService.setTypeAssociation(inputPort, type) + + assertThat(service.getDeviceTypeAssociations()).asList().containsExactly(inputPort, type) + .inOrder() + } + + @Test + fun setAndUnsetDeviceTypeAssociation_deviceTypeAssociationIsMissing() { + val inputPort = "inputPort" + val type = "type" + + localService.setTypeAssociation(inputPort, type) + localService.unsetTypeAssociation(inputPort) + + assertTrue(service.getDeviceTypeAssociations().isEmpty()) + } + private fun setVirtualMousePointerDisplayIdAndVerify(overrideDisplayId: Int) { val thread = Thread { localService.setVirtualMousePointerDisplayId(overrideDisplayId) } thread.start() diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java index 196226a220a7..41e3a08be6c5 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java @@ -59,65 +59,65 @@ import org.junit.runner.RunWith; @Presubmit @RunWith(AndroidJUnit4.class) public class LockSettingsServiceTests extends BaseLockSettingsServiceTests { + @Before - public void disableProcessCaches() { + public void setUp() { PropertyInvalidatedCache.disableForTestMode(); + mService.initializeSyntheticPassword(PRIMARY_USER_ID); + mService.initializeSyntheticPassword(MANAGED_PROFILE_USER_ID); } @Test - public void testCreatePasswordPrimaryUser() throws RemoteException { - testCreateCredential(PRIMARY_USER_ID, newPassword("password")); + public void testSetPasswordPrimaryUser() throws RemoteException { + setAndVerifyCredential(PRIMARY_USER_ID, newPassword("password")); } @Test - public void testCreatePasswordFailsWithoutLockScreen() throws RemoteException { - testCreateCredentialFailsWithoutLockScreen(PRIMARY_USER_ID, newPassword("password")); + public void testSetPasswordFailsWithoutLockScreen() throws RemoteException { + testSetCredentialFailsWithoutLockScreen(PRIMARY_USER_ID, newPassword("password")); } @Test - public void testCreatePatternPrimaryUser() throws RemoteException { - testCreateCredential(PRIMARY_USER_ID, newPattern("123456789")); + public void testSetPatternPrimaryUser() throws RemoteException { + setAndVerifyCredential(PRIMARY_USER_ID, newPattern("123456789")); } @Test - public void testCreatePatternFailsWithoutLockScreen() throws RemoteException { - testCreateCredentialFailsWithoutLockScreen(PRIMARY_USER_ID, newPattern("123456789")); + public void testSetPatternFailsWithoutLockScreen() throws RemoteException { + testSetCredentialFailsWithoutLockScreen(PRIMARY_USER_ID, newPattern("123456789")); } @Test public void testChangePasswordPrimaryUser() throws RemoteException { - testChangeCredentials(PRIMARY_USER_ID, newPattern("78963214"), newPassword("asdfghjk")); + testChangeCredential(PRIMARY_USER_ID, newPattern("78963214"), newPassword("asdfghjk")); } @Test public void testChangePatternPrimaryUser() throws RemoteException { - testChangeCredentials(PRIMARY_USER_ID, newPassword("!£$%^&*(())"), newPattern("1596321")); + testChangeCredential(PRIMARY_USER_ID, newPassword("!£$%^&*(())"), newPattern("1596321")); } @Test public void testChangePasswordFailPrimaryUser() throws RemoteException { - initializeStorageWithCredential(PRIMARY_USER_ID, newPassword("password")); - + setCredential(PRIMARY_USER_ID, newPassword("password")); assertFalse(mService.setLockCredential(newPassword("newpwd"), newPassword("badpwd"), PRIMARY_USER_ID)); - assertVerifyCredentials(PRIMARY_USER_ID, newPassword("password")); + assertVerifyCredential(PRIMARY_USER_ID, newPassword("password")); } @Test public void testClearPasswordPrimaryUser() throws RemoteException { - initializeStorageWithCredential(PRIMARY_USER_ID, newPassword("password")); - assertTrue(mService.setLockCredential(nonePassword(), newPassword("password"), - PRIMARY_USER_ID)); - assertEquals(CREDENTIAL_TYPE_NONE, mService.getCredentialType(PRIMARY_USER_ID)); - assertEquals(0, mGateKeeperService.getSecureUserId(PRIMARY_USER_ID)); + setCredential(PRIMARY_USER_ID, newPassword("password")); + clearCredential(PRIMARY_USER_ID, newPassword("password")); } @Test public void testManagedProfileUnifiedChallenge() throws RemoteException { + mService.initializeSyntheticPassword(TURNED_OFF_PROFILE_USER_ID); + final LockscreenCredential firstUnifiedPassword = newPassword("pwd-1"); final LockscreenCredential secondUnifiedPassword = newPassword("pwd-2"); - assertTrue(mService.setLockCredential(firstUnifiedPassword, - nonePassword(), PRIMARY_USER_ID)); + setCredential(PRIMARY_USER_ID, firstUnifiedPassword); mService.setSeparateProfileChallengeEnabled(MANAGED_PROFILE_USER_ID, false, null); final long primarySid = mGateKeeperService.getSecureUserId(PRIMARY_USER_ID); final long profileSid = mGateKeeperService.getSecureUserId(MANAGED_PROFILE_USER_ID); @@ -146,14 +146,12 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests { assertNull(mGateKeeperService.getAuthToken(TURNED_OFF_PROFILE_USER_ID)); // Change primary password and verify that profile SID remains - assertTrue(mService.setLockCredential( - secondUnifiedPassword, firstUnifiedPassword, PRIMARY_USER_ID)); + setCredential(PRIMARY_USER_ID, secondUnifiedPassword, firstUnifiedPassword); assertEquals(profileSid, mGateKeeperService.getSecureUserId(MANAGED_PROFILE_USER_ID)); assertNull(mGateKeeperService.getAuthToken(TURNED_OFF_PROFILE_USER_ID)); // Clear unified challenge - assertTrue(mService.setLockCredential(nonePassword(), - secondUnifiedPassword, PRIMARY_USER_ID)); + clearCredential(PRIMARY_USER_ID, secondUnifiedPassword); assertEquals(0, mGateKeeperService.getSecureUserId(PRIMARY_USER_ID)); assertEquals(0, mGateKeeperService.getSecureUserId(MANAGED_PROFILE_USER_ID)); assertEquals(0, mGateKeeperService.getSecureUserId(TURNED_OFF_PROFILE_USER_ID)); @@ -163,12 +161,8 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests { public void testManagedProfileSeparateChallenge() throws RemoteException { final LockscreenCredential primaryPassword = newPassword("primary"); final LockscreenCredential profilePassword = newPassword("profile"); - assertTrue(mService.setLockCredential(primaryPassword, - nonePassword(), - PRIMARY_USER_ID)); - assertTrue(mService.setLockCredential(profilePassword, - nonePassword(), - MANAGED_PROFILE_USER_ID)); + setCredential(PRIMARY_USER_ID, primaryPassword); + setCredential(MANAGED_PROFILE_USER_ID, profilePassword); final long primarySid = mGateKeeperService.getSecureUserId(PRIMARY_USER_ID); final long profileSid = mGateKeeperService.getSecureUserId(MANAGED_PROFILE_USER_ID); @@ -191,8 +185,7 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests { assertNotNull(mGateKeeperService.getAuthToken(MANAGED_PROFILE_USER_ID)); assertEquals(profileSid, mGateKeeperService.getSecureUserId(MANAGED_PROFILE_USER_ID)); - assertTrue(mService.setLockCredential( - newPassword("pwd"), primaryPassword, PRIMARY_USER_ID)); + setCredential(PRIMARY_USER_ID, newPassword("pwd"), primaryPassword); assertEquals(VerifyCredentialResponse.RESPONSE_OK, mService.verifyCredential( profilePassword, MANAGED_PROFILE_USER_ID, 0 /* flags */) .getResponseCode()); @@ -207,8 +200,7 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests { assertEquals(CREDENTIAL_TYPE_NONE, mService.getCredentialType(MANAGED_PROFILE_USER_ID)); // Set a separate challenge on the profile - assertTrue(mService.setLockCredential( - newPassword("12345678"), nonePassword(), MANAGED_PROFILE_USER_ID)); + setCredential(MANAGED_PROFILE_USER_ID, newPassword("12345678")); assertNotEquals(0, mGateKeeperService.getSecureUserId(MANAGED_PROFILE_USER_ID)); assertEquals(CREDENTIAL_TYPE_PASSWORD, mService.getCredentialType(MANAGED_PROFILE_USER_ID)); @@ -221,11 +213,7 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests { @Test public void testSetLockCredential_forPrimaryUser_sendsCredentials() throws Exception { - assertTrue(mService.setLockCredential( - newPassword("password"), - nonePassword(), - PRIMARY_USER_ID)); - + setCredential(PRIMARY_USER_ID, newPassword("password")); verify(mRecoverableKeyStoreManager) .lockScreenSecretChanged(CREDENTIAL_TYPE_PASSWORD, "password".getBytes(), PRIMARY_USER_ID); @@ -234,11 +222,7 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests { @Test public void testSetLockCredential_forProfileWithSeparateChallenge_sendsCredentials() throws Exception { - assertTrue(mService.setLockCredential( - newPattern("12345"), - nonePassword(), - MANAGED_PROFILE_USER_ID)); - + setCredential(MANAGED_PROFILE_USER_ID, newPattern("12345")); verify(mRecoverableKeyStoreManager) .lockScreenSecretChanged(CREDENTIAL_TYPE_PATTERN, "12345".getBytes(), MANAGED_PROFILE_USER_ID); @@ -248,13 +232,8 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests { public void testSetLockCredential_forProfileWithSeparateChallenge_updatesCredentials() throws Exception { mService.setSeparateProfileChallengeEnabled(MANAGED_PROFILE_USER_ID, true, null); - initializeStorageWithCredential(MANAGED_PROFILE_USER_ID, newPattern("12345")); - - assertTrue(mService.setLockCredential( - newPassword("newPassword"), - newPattern("12345"), - MANAGED_PROFILE_USER_ID)); - + setCredential(MANAGED_PROFILE_USER_ID, newPattern("12345")); + setCredential(MANAGED_PROFILE_USER_ID, newPassword("newPassword"), newPattern("12345")); verify(mRecoverableKeyStoreManager) .lockScreenSecretChanged(CREDENTIAL_TYPE_PASSWORD, "newPassword".getBytes(), MANAGED_PROFILE_USER_ID); @@ -264,12 +243,7 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests { public void testSetLockCredential_forProfileWithUnifiedChallenge_doesNotSendRandomCredential() throws Exception { mService.setSeparateProfileChallengeEnabled(MANAGED_PROFILE_USER_ID, false, null); - - assertTrue(mService.setLockCredential( - newPattern("12345"), - nonePassword(), - PRIMARY_USER_ID)); - + setCredential(PRIMARY_USER_ID, newPattern("12345")); verify(mRecoverableKeyStoreManager, never()) .lockScreenSecretChanged( eq(CREDENTIAL_TYPE_PASSWORD), any(), eq(MANAGED_PROFILE_USER_ID)); @@ -281,13 +255,9 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests { throws Exception { final LockscreenCredential oldCredential = newPassword("oldPassword"); final LockscreenCredential newCredential = newPassword("newPassword"); - initializeStorageWithCredential(PRIMARY_USER_ID, oldCredential); + setCredential(PRIMARY_USER_ID, oldCredential); mService.setSeparateProfileChallengeEnabled(MANAGED_PROFILE_USER_ID, false, null); - - assertTrue(mService.setLockCredential( - newCredential, - oldCredential, - PRIMARY_USER_ID)); + setCredential(PRIMARY_USER_ID, newCredential, oldCredential); verify(mRecoverableKeyStoreManager) .lockScreenSecretChanged(CREDENTIAL_TYPE_PASSWORD, newCredential.getCredential(), @@ -301,13 +271,9 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests { public void testSetLockCredential_forPrimaryUserWithUnifiedChallengeProfile_removesBothCredentials() throws Exception { - initializeStorageWithCredential(PRIMARY_USER_ID, newPassword("oldPassword")); + setCredential(PRIMARY_USER_ID, newPassword("oldPassword")); mService.setSeparateProfileChallengeEnabled(MANAGED_PROFILE_USER_ID, false, null); - - assertTrue(mService.setLockCredential( - nonePassword(), - newPassword("oldPassword"), - PRIMARY_USER_ID)); + clearCredential(PRIMARY_USER_ID, newPassword("oldPassword")); verify(mRecoverableKeyStoreManager) .lockScreenSecretChanged(CREDENTIAL_TYPE_NONE, null, PRIMARY_USER_ID); @@ -316,11 +282,10 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests { } @Test - public void testSetLockCredential_nullCredential_removeBiometrics() throws RemoteException { - initializeStorageWithCredential(PRIMARY_USER_ID, newPattern("123654")); + public void testClearLockCredential_removesBiometrics() throws RemoteException { + setCredential(PRIMARY_USER_ID, newPattern("123654")); mService.setSeparateProfileChallengeEnabled(MANAGED_PROFILE_USER_ID, false, null); - - mService.setLockCredential(nonePassword(), newPattern("123654"), PRIMARY_USER_ID); + clearCredential(PRIMARY_USER_ID, newPattern("123654")); // Verify fingerprint is removed verify(mFingerprintManager).removeAll(eq(PRIMARY_USER_ID), any()); @@ -335,14 +300,9 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests { throws Exception { final LockscreenCredential parentPassword = newPassword("parentPassword"); final LockscreenCredential profilePassword = newPassword("profilePassword"); - initializeStorageWithCredential(PRIMARY_USER_ID, parentPassword); + setCredential(PRIMARY_USER_ID, parentPassword); mService.setSeparateProfileChallengeEnabled(MANAGED_PROFILE_USER_ID, false, null); - - assertTrue(mService.setLockCredential( - profilePassword, - nonePassword(), - MANAGED_PROFILE_USER_ID)); - + setCredential(MANAGED_PROFILE_USER_ID, profilePassword); verify(mRecoverableKeyStoreManager) .lockScreenSecretChanged(CREDENTIAL_TYPE_PASSWORD, profilePassword.getCredential(), MANAGED_PROFILE_USER_ID); @@ -356,9 +316,8 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests { final LockscreenCredential profilePassword = newPattern("12345"); mService.setSeparateProfileChallengeEnabled( MANAGED_PROFILE_USER_ID, true, profilePassword); - initializeStorageWithCredential(PRIMARY_USER_ID, parentPassword); - // Create and verify separate profile credentials. - testCreateCredential(MANAGED_PROFILE_USER_ID, profilePassword); + setCredential(PRIMARY_USER_ID, parentPassword); + setAndVerifyCredential(MANAGED_PROFILE_USER_ID, profilePassword); mService.setSeparateProfileChallengeEnabled( MANAGED_PROFILE_USER_ID, false, profilePassword); @@ -372,7 +331,7 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests { @Test public void testVerifyCredential_forPrimaryUser_sendsCredentials() throws Exception { final LockscreenCredential password = newPassword("password"); - initializeStorageWithCredential(PRIMARY_USER_ID, password); + setCredential(PRIMARY_USER_ID, password); reset(mRecoverableKeyStoreManager); mService.verifyCredential(password, PRIMARY_USER_ID, 0 /* flags */); @@ -386,10 +345,7 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests { public void testVerifyCredential_forProfileWithSeparateChallenge_sendsCredentials() throws Exception { final LockscreenCredential pattern = newPattern("12345"); - assertTrue(mService.setLockCredential( - pattern, - nonePassword(), - MANAGED_PROFILE_USER_ID)); + setCredential(MANAGED_PROFILE_USER_ID, pattern); reset(mRecoverableKeyStoreManager); mService.verifyCredential(pattern, MANAGED_PROFILE_USER_ID, 0 /* flags */); @@ -403,7 +359,7 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests { public void verifyCredential_forPrimaryUserWithUnifiedChallengeProfile_sendsCredentialsForBoth() throws Exception { final LockscreenCredential pattern = newPattern("12345"); - initializeStorageWithCredential(PRIMARY_USER_ID, pattern); + setCredential(PRIMARY_USER_ID, pattern); mService.setSeparateProfileChallengeEnabled(MANAGED_PROFILE_USER_ID, false, null); reset(mRecoverableKeyStoreManager); @@ -423,7 +379,7 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests { } @Test - public void testCredentialChangeNotPossibleInSecureFrpModeDuringSuw() { + public void testSetCredentialNotPossibleInSecureFrpModeDuringSuw() { setUserSetupComplete(false); setSecureFrpMode(true); try { @@ -433,21 +389,17 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests { } @Test - public void testCredentialChangePossibleInSecureFrpModeAfterSuw() { + public void testSetCredentialPossibleInSecureFrpModeAfterSuw() throws RemoteException { setUserSetupComplete(true); setSecureFrpMode(true); - assertTrue(mService.setLockCredential(newPassword("1234"), nonePassword(), - PRIMARY_USER_ID)); + setCredential(PRIMARY_USER_ID, newPassword("1234")); } @Test public void testPasswordHistoryDisabledByDefault() throws Exception { final int userId = PRIMARY_USER_ID; checkPasswordHistoryLength(userId, 0); - initializeStorageWithCredential(userId, nonePassword()); - checkPasswordHistoryLength(userId, 0); - - assertTrue(mService.setLockCredential(newPassword("1234"), nonePassword(), userId)); + setCredential(userId, newPassword("1234")); checkPasswordHistoryLength(userId, 0); } @@ -456,20 +408,18 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests { final int userId = PRIMARY_USER_ID; when(mDevicePolicyManager.getPasswordHistoryLength(any(), eq(userId))).thenReturn(3); checkPasswordHistoryLength(userId, 0); - initializeStorageWithCredential(userId, nonePassword()); - checkPasswordHistoryLength(userId, 0); - assertTrue(mService.setLockCredential(newPassword("pass1"), nonePassword(), userId)); + setCredential(userId, newPassword("pass1")); checkPasswordHistoryLength(userId, 1); - assertTrue(mService.setLockCredential(newPassword("pass2"), newPassword("pass1"), userId)); + setCredential(userId, newPassword("pass2"), newPassword("pass1")); checkPasswordHistoryLength(userId, 2); - assertTrue(mService.setLockCredential(newPassword("pass3"), newPassword("pass2"), userId)); + setCredential(userId, newPassword("pass3"), newPassword("pass2")); checkPasswordHistoryLength(userId, 3); // maximum length should have been reached - assertTrue(mService.setLockCredential(newPassword("pass4"), newPassword("pass3"), userId)); + setCredential(userId, newPassword("pass4"), newPassword("pass3")); checkPasswordHistoryLength(userId, 3); } @@ -479,18 +429,11 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests { assertEquals(expectedLen, hashes.length); } - private void testCreateCredential(int userId, LockscreenCredential credential) - throws RemoteException { - assertTrue(mService.setLockCredential(credential, nonePassword(), userId)); - assertVerifyCredentials(userId, credential); - } - - private void testCreateCredentialFailsWithoutLockScreen( + private void testSetCredentialFailsWithoutLockScreen( int userId, LockscreenCredential credential) throws RemoteException { mService.mHasSecureLockScreen = false; - try { - mService.setLockCredential(credential, null, userId); + mService.setLockCredential(credential, nonePassword(), userId); fail("An exception should have been thrown."); } catch (UnsupportedOperationException e) { // Success - the exception was expected. @@ -499,14 +442,14 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests { assertEquals(CREDENTIAL_TYPE_NONE, mService.getCredentialType(userId)); } - private void testChangeCredentials(int userId, LockscreenCredential newCredential, + private void testChangeCredential(int userId, LockscreenCredential newCredential, LockscreenCredential oldCredential) throws RemoteException { - initializeStorageWithCredential(userId, oldCredential); - assertTrue(mService.setLockCredential(newCredential, oldCredential, userId)); - assertVerifyCredentials(userId, newCredential); + setCredential(userId, oldCredential); + setCredential(userId, newCredential, oldCredential); + assertVerifyCredential(userId, newCredential); } - private void assertVerifyCredentials(int userId, LockscreenCredential credential) + private void assertVerifyCredential(int userId, LockscreenCredential credential) throws RemoteException{ VerifyCredentialResponse response = mService.verifyCredential(credential, userId, 0 /* flags */); @@ -533,16 +476,29 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests { badCredential, userId, 0 /* flags */).getResponseCode()); } - private void initializeStorageWithCredential(int userId, LockscreenCredential credential) + private void setAndVerifyCredential(int userId, LockscreenCredential newCredential) throws RemoteException { - assertEquals(0, mGateKeeperService.getSecureUserId(userId)); - synchronized (mService.mSpManager) { - mService.initializeSyntheticPasswordLocked(userId); - } - if (credential.isNone()) { + setCredential(userId, newCredential); + assertVerifyCredential(userId, newCredential); + } + + private void setCredential(int userId, LockscreenCredential newCredential) + throws RemoteException { + setCredential(userId, newCredential, nonePassword()); + } + + private void clearCredential(int userId, LockscreenCredential oldCredential) + throws RemoteException { + setCredential(userId, nonePassword(), oldCredential); + } + + private void setCredential(int userId, LockscreenCredential newCredential, + LockscreenCredential oldCredential) throws RemoteException { + assertTrue(mService.setLockCredential(newCredential, oldCredential, userId)); + assertEquals(newCredential.getType(), mService.getCredentialType(userId)); + if (newCredential.isNone()) { assertEquals(0, mGateKeeperService.getSecureUserId(userId)); } else { - assertTrue(mService.setLockCredential(credential, nonePassword(), userId)); assertNotEquals(0, mGateKeeperService.getSecureUserId(userId)); } } diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockscreenFrpTest.java b/services/tests/servicestests/src/com/android/server/locksettings/LockscreenFrpTest.java index fc0ca7eda243..10869dab7f22 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/LockscreenFrpTest.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/LockscreenFrpTest.java @@ -42,14 +42,13 @@ import org.junit.runner.RunWith; public class LockscreenFrpTest extends BaseLockSettingsServiceTests { @Before - public void setDeviceNotProvisioned() throws Exception { + public void setUp() throws Exception { + PropertyInvalidatedCache.disableForTestMode(); + // FRP credential can only be verified prior to provisioning setDeviceProvisioned(false); - } - @Before - public void disableProcessCaches() { - PropertyInvalidatedCache.disableForTestMode(); + mService.initializeSyntheticPassword(PRIMARY_USER_ID); } @Test diff --git a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java index b00467cf8637..57593cf660f2 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java @@ -19,7 +19,6 @@ package com.android.server.locksettings; import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_NONE; import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PASSWORD; import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PASSWORD_OR_PIN; -import static com.android.internal.widget.LockPatternUtils.CURRENT_LSKF_BASED_PROTECTOR_ID_KEY; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -59,9 +58,6 @@ import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import java.io.File; -import java.util.ArrayList; -import java.util.Arrays; - /** * atest FrameworksServicesTests:SyntheticPasswordTests @@ -127,21 +123,11 @@ public class SyntheticPasswordTests extends BaseLockSettingsServiceTests { return mGateKeeperService.getSecureUserId(SyntheticPasswordManager.fakeUserId(userId)) != 0; } - private boolean hasSyntheticPassword(int userId) throws RemoteException { - return mService.getLong(CURRENT_LSKF_BASED_PROTECTOR_ID_KEY, 0, userId) != 0; - } - - private void initializeCredential(LockscreenCredential password, int userId) + private void initSpAndSetCredential(int userId, LockscreenCredential credential) throws RemoteException { - assertTrue(mService.setLockCredential(password, nonePassword(), userId)); - assertEquals(CREDENTIAL_TYPE_PASSWORD, mService.getCredentialType(userId)); - assertTrue(mService.isSyntheticPasswordBasedCredential(userId)); - } - - protected void initializeSyntheticPassword(int userId) { - synchronized (mService.mSpManager) { - mService.initializeSyntheticPasswordLocked(userId); - } + mService.initializeSyntheticPassword(userId); + assertTrue(mService.setLockCredential(credential, nonePassword(), userId)); + assertEquals(credential.getType(), mService.getCredentialType(userId)); } // Tests that the FRP credential is updated when an LSKF-based protector is created for the user @@ -149,7 +135,7 @@ public class SyntheticPasswordTests extends BaseLockSettingsServiceTests { @Test public void testFrpCredentialSyncedIfDeviceProvisioned() throws RemoteException { setDeviceProvisioned(true); - initializeSyntheticPassword(PRIMARY_USER_ID); + mService.initializeSyntheticPassword(PRIMARY_USER_ID); verify(mStorage.mPersistentDataBlockManager).setFrpCredentialHandle(any()); } @@ -159,7 +145,7 @@ public class SyntheticPasswordTests extends BaseLockSettingsServiceTests { @Test public void testEmptyFrpCredentialNotSyncedIfDeviceNotProvisioned() throws RemoteException { setDeviceProvisioned(false); - initializeSyntheticPassword(PRIMARY_USER_ID); + mService.initializeSyntheticPassword(PRIMARY_USER_ID); verify(mStorage.mPersistentDataBlockManager, never()).setFrpCredentialHandle(any()); } @@ -169,7 +155,7 @@ public class SyntheticPasswordTests extends BaseLockSettingsServiceTests { @Test public void testNonEmptyFrpCredentialSyncedIfDeviceNotProvisioned() throws RemoteException { setDeviceProvisioned(false); - initializeSyntheticPassword(PRIMARY_USER_ID); + mService.initializeSyntheticPassword(PRIMARY_USER_ID); verify(mStorage.mPersistentDataBlockManager, never()).setFrpCredentialHandle(any()); mService.setLockCredential(newPassword("password"), nonePassword(), PRIMARY_USER_ID); verify(mStorage.mPersistentDataBlockManager).setFrpCredentialHandle(any()); @@ -180,7 +166,7 @@ public class SyntheticPasswordTests extends BaseLockSettingsServiceTests { final LockscreenCredential password = newPassword("password"); final LockscreenCredential newPassword = newPassword("newpassword"); - initializeCredential(password, PRIMARY_USER_ID); + initSpAndSetCredential(PRIMARY_USER_ID, password); long sid = mGateKeeperService.getSecureUserId(PRIMARY_USER_ID); mService.setLockCredential(newPassword, password, PRIMARY_USER_ID); assertEquals(VerifyCredentialResponse.RESPONSE_OK, mService.verifyCredential( @@ -193,7 +179,7 @@ public class SyntheticPasswordTests extends BaseLockSettingsServiceTests { LockscreenCredential password = newPassword("password"); LockscreenCredential badPassword = newPassword("badpassword"); - initializeCredential(password, PRIMARY_USER_ID); + initSpAndSetCredential(PRIMARY_USER_ID, password); assertEquals(VerifyCredentialResponse.RESPONSE_OK, mService.verifyCredential( password, PRIMARY_USER_ID, 0 /* flags */).getResponseCode()); verify(mActivityManager).unlockUser2(eq(PRIMARY_USER_ID), any()); @@ -207,7 +193,7 @@ public class SyntheticPasswordTests extends BaseLockSettingsServiceTests { LockscreenCredential password = newPassword("password"); LockscreenCredential badPassword = newPassword("newpassword"); - initializeCredential(password, PRIMARY_USER_ID); + initSpAndSetCredential(PRIMARY_USER_ID, password); long sid = mGateKeeperService.getSecureUserId(PRIMARY_USER_ID); // clear password mService.setLockCredential(nonePassword(), password, PRIMARY_USER_ID); @@ -225,7 +211,7 @@ public class SyntheticPasswordTests extends BaseLockSettingsServiceTests { LockscreenCredential password = newPassword("password"); LockscreenCredential badPassword = newPassword("new"); - initializeCredential(password, PRIMARY_USER_ID); + initSpAndSetCredential(PRIMARY_USER_ID, password); mService.setLockCredential(badPassword, password, PRIMARY_USER_ID); assertEquals(VerifyCredentialResponse.RESPONSE_OK, mService.verifyCredential( badPassword, PRIMARY_USER_ID, 0 /* flags */).getResponseCode()); @@ -242,7 +228,7 @@ public class SyntheticPasswordTests extends BaseLockSettingsServiceTests { public void testVerifyPassesPrimaryUserAuthSecret() throws RemoteException { LockscreenCredential password = newPassword("password"); - initializeCredential(password, PRIMARY_USER_ID); + initSpAndSetCredential(PRIMARY_USER_ID, password); reset(mAuthSecretService); assertEquals(VerifyCredentialResponse.RESPONSE_OK, mService.verifyCredential( password, PRIMARY_USER_ID, 0 /* flags */).getResponseCode()); @@ -253,7 +239,7 @@ public class SyntheticPasswordTests extends BaseLockSettingsServiceTests { public void testSecondaryUserDoesNotPassAuthSecret() throws RemoteException { LockscreenCredential password = newPassword("password"); - initializeCredential(password, SECONDARY_USER_ID); + initSpAndSetCredential(SECONDARY_USER_ID, password); assertEquals(VerifyCredentialResponse.RESPONSE_OK, mService.verifyCredential( password, SECONDARY_USER_ID, 0 /* flags */).getResponseCode()); verify(mAuthSecretService, never()).setPrimaryUserCredential(any(byte[].class)); @@ -262,7 +248,7 @@ public class SyntheticPasswordTests extends BaseLockSettingsServiceTests { @Test public void testUnlockUserKeyIfUnsecuredPassesPrimaryUserAuthSecret() throws RemoteException { LockscreenCredential password = newPassword("password"); - initializeCredential(password, PRIMARY_USER_ID); + initSpAndSetCredential(PRIMARY_USER_ID, password); mService.setLockCredential(nonePassword(), password, PRIMARY_USER_ID); reset(mAuthSecretService); @@ -275,7 +261,7 @@ public class SyntheticPasswordTests extends BaseLockSettingsServiceTests { LockscreenCredential password = newPassword("password"); LockscreenCredential pattern = newPattern("123654"); byte[] token = "some-high-entropy-secure-token".getBytes(); - initializeCredential(password, PRIMARY_USER_ID); + initSpAndSetCredential(PRIMARY_USER_ID, password); // Disregard any reportPasswordChanged() invocations as part of credential setup. flushHandlerTasks(); reset(mDevicePolicyManager); @@ -310,7 +296,7 @@ public class SyntheticPasswordTests extends BaseLockSettingsServiceTests { LockscreenCredential password = newPassword("password"); LockscreenCredential pattern = newPattern("123654"); byte[] token = "some-high-entropy-secure-token".getBytes(); - initializeCredential(password, PRIMARY_USER_ID); + initSpAndSetCredential(PRIMARY_USER_ID, password); byte[] storageKey = mStorageManager.getUserUnlockToken(PRIMARY_USER_ID); long handle = mLocalService.addEscrowToken(token, PRIMARY_USER_ID, null); @@ -336,7 +322,7 @@ public class SyntheticPasswordTests extends BaseLockSettingsServiceTests { LockscreenCredential pattern = newPattern("123654"); LockscreenCredential newPassword = newPassword("password"); byte[] token = "some-high-entropy-secure-token".getBytes(); - initializeCredential(password, PRIMARY_USER_ID); + initSpAndSetCredential(PRIMARY_USER_ID, password); byte[] storageKey = mStorageManager.getUserUnlockToken(PRIMARY_USER_ID); long handle = mLocalService.addEscrowToken(token, PRIMARY_USER_ID, null); @@ -356,38 +342,20 @@ public class SyntheticPasswordTests extends BaseLockSettingsServiceTests { } @Test - public void testEscrowTokenActivatedImmediatelyIfNoUserPasswordNeedsMigration() - throws RemoteException { - final byte[] token = "some-high-entropy-secure-token".getBytes(); - long handle = mLocalService.addEscrowToken(token, PRIMARY_USER_ID, null); - assertTrue(mLocalService.isEscrowTokenActive(handle, PRIMARY_USER_ID)); - assertEquals(0, mGateKeeperService.getSecureUserId(PRIMARY_USER_ID)); - assertTrue(hasSyntheticPassword(PRIMARY_USER_ID)); - } - - @Test - public void testEscrowTokenActivatedImmediatelyIfNoUserPasswordNoMigration() - throws RemoteException { + public void testEscrowTokenActivatedImmediatelyIfNoUserPassword() throws RemoteException { final byte[] token = "some-high-entropy-secure-token".getBytes(); - // By first setting a password and then clearing it, we enter the state where SP is - // initialized but the user currently has no password - initializeCredential(newPassword("password"), PRIMARY_USER_ID); - assertTrue(mService.setLockCredential(nonePassword(), newPassword("password"), - PRIMARY_USER_ID)); - assertTrue(mService.isSyntheticPasswordBasedCredential(PRIMARY_USER_ID)); - + mService.initializeSyntheticPassword(PRIMARY_USER_ID); long handle = mLocalService.addEscrowToken(token, PRIMARY_USER_ID, null); assertTrue(mLocalService.isEscrowTokenActive(handle, PRIMARY_USER_ID)); assertEquals(0, mGateKeeperService.getSecureUserId(PRIMARY_USER_ID)); - assertTrue(hasSyntheticPassword(PRIMARY_USER_ID)); } @Test public void testEscrowTokenActivatedLaterWithUserPassword() throws RemoteException { byte[] token = "some-high-entropy-secure-token".getBytes(); LockscreenCredential password = newPassword("password"); - mService.setLockCredential(password, nonePassword(), PRIMARY_USER_ID); + initSpAndSetCredential(PRIMARY_USER_ID, password); long handle = mLocalService.addEscrowToken(token, PRIMARY_USER_ID, null); // Token not activated immediately since user password exists assertFalse(mLocalService.isEscrowTokenActive(handle, PRIMARY_USER_ID)); @@ -407,6 +375,7 @@ public class SyntheticPasswordTests extends BaseLockSettingsServiceTests { when(mUserManagerInternal.isUserManaged(PRIMARY_USER_ID)).thenReturn(false); when(mDeviceStateCache.isDeviceProvisioned()).thenReturn(true); + mService.initializeSyntheticPassword(PRIMARY_USER_ID); try { mLocalService.addEscrowToken(token, PRIMARY_USER_ID, null); fail("Escrow token should not be possible on unmanaged device"); @@ -421,7 +390,7 @@ public class SyntheticPasswordTests extends BaseLockSettingsServiceTests { LockscreenCredential password = newPassword("password"); LockscreenCredential pattern = newPattern("123654"); - initializeCredential(password, PRIMARY_USER_ID); + initSpAndSetCredential(PRIMARY_USER_ID, password); long handle0 = mLocalService.addEscrowToken(token0, PRIMARY_USER_ID, null); long handle1 = mLocalService.addEscrowToken(token1, PRIMARY_USER_ID, null); @@ -450,6 +419,7 @@ public class SyntheticPasswordTests extends BaseLockSettingsServiceTests { byte[] token = "some-high-entropy-secure-token".getBytes(); mService.mHasSecureLockScreen = false; + mService.initializeSyntheticPassword(PRIMARY_USER_ID); long handle = mLocalService.addEscrowToken(token, PRIMARY_USER_ID, null); assertTrue(mLocalService.isEscrowTokenActive(handle, PRIMARY_USER_ID)); @@ -473,7 +443,7 @@ public class SyntheticPasswordTests extends BaseLockSettingsServiceTests { @Test public void testGetHashFactorPrimaryUser() throws RemoteException { LockscreenCredential password = newPassword("password"); - mService.setLockCredential(password, nonePassword(), PRIMARY_USER_ID); + initSpAndSetCredential(PRIMARY_USER_ID, password); byte[] hashFactor = mService.getHashFactor(password, PRIMARY_USER_ID); assertNotNull(hashFactor); @@ -486,6 +456,9 @@ public class SyntheticPasswordTests extends BaseLockSettingsServiceTests { @Test public void testGetHashFactorManagedProfileUnifiedChallenge() throws RemoteException { + mService.initializeSyntheticPassword(PRIMARY_USER_ID); + mService.initializeSyntheticPassword(MANAGED_PROFILE_USER_ID); + LockscreenCredential pattern = newPattern("1236"); mService.setLockCredential(pattern, nonePassword(), PRIMARY_USER_ID); mService.setSeparateProfileChallengeEnabled(MANAGED_PROFILE_USER_ID, false, null); @@ -494,6 +467,9 @@ public class SyntheticPasswordTests extends BaseLockSettingsServiceTests { @Test public void testGetHashFactorManagedProfileSeparateChallenge() throws RemoteException { + mService.initializeSyntheticPassword(PRIMARY_USER_ID); + mService.initializeSyntheticPassword(MANAGED_PROFILE_USER_ID); + LockscreenCredential primaryPassword = newPassword("primary"); LockscreenCredential profilePassword = newPassword("profile"); mService.setLockCredential(primaryPassword, nonePassword(), PRIMARY_USER_ID); @@ -594,7 +570,7 @@ public class SyntheticPasswordTests extends BaseLockSettingsServiceTests { LockscreenCredential password = newPassword("testGsiDisablesAuthSecret-password"); - initializeCredential(password, PRIMARY_USER_ID); + initSpAndSetCredential(PRIMARY_USER_ID, password); assertEquals(VerifyCredentialResponse.RESPONSE_OK, mService.verifyCredential( password, PRIMARY_USER_ID, 0 /* flags */).getResponseCode()); verify(mAuthSecretService, never()).setPrimaryUserCredential(any(byte[].class)); @@ -604,7 +580,7 @@ public class SyntheticPasswordTests extends BaseLockSettingsServiceTests { public void testUnlockUserWithToken() throws Exception { LockscreenCredential password = newPassword("password"); byte[] token = "some-high-entropy-secure-token".getBytes(); - initializeCredential(password, PRIMARY_USER_ID); + initSpAndSetCredential(PRIMARY_USER_ID, password); // Disregard any reportPasswordChanged() invocations as part of credential setup. flushHandlerTasks(); reset(mDevicePolicyManager); @@ -625,7 +601,7 @@ public class SyntheticPasswordTests extends BaseLockSettingsServiceTests { @Test public void testPasswordChange_NoOrphanedFilesLeft() throws Exception { LockscreenCredential password = newPassword("password"); - initializeCredential(password, PRIMARY_USER_ID); + initSpAndSetCredential(PRIMARY_USER_ID, password); assertTrue(mService.setLockCredential(password, password, PRIMARY_USER_ID)); assertNoOrphanedFilesLeft(PRIMARY_USER_ID); } @@ -633,6 +609,7 @@ public class SyntheticPasswordTests extends BaseLockSettingsServiceTests { @Test public void testAddingEscrowToken_NoOrphanedFilesLeft() throws Exception { final byte[] token = "some-high-entropy-secure-token".getBytes(); + mService.initializeSyntheticPassword(PRIMARY_USER_ID); for (int i = 0; i < 16; i++) { long handle = mLocalService.addEscrowToken(token, PRIMARY_USER_ID, null); assertTrue(mLocalService.isEscrowTokenActive(handle, PRIMARY_USER_ID)); diff --git a/services/tests/servicestests/src/com/android/server/locksettings/WeakEscrowTokenTests.java b/services/tests/servicestests/src/com/android/server/locksettings/WeakEscrowTokenTests.java index 51ddcef425ce..2c9ba34e850f 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/WeakEscrowTokenTests.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/WeakEscrowTokenTests.java @@ -40,6 +40,7 @@ import com.android.internal.widget.IWeakEscrowTokenRemovedListener; import com.android.internal.widget.LockscreenCredential; import com.android.internal.widget.VerifyCredentialResponse; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -49,6 +50,11 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class WeakEscrowTokenTests extends BaseLockSettingsServiceTests{ + @Before + public void setUp() { + mService.initializeSyntheticPassword(PRIMARY_USER_ID); + } + @Test public void testWeakTokenActivatedImmediatelyIfNoUserPassword() throws RemoteException { diff --git a/services/tests/servicestests/src/com/android/server/locksettings/WeaverBasedSyntheticPasswordTests.java b/services/tests/servicestests/src/com/android/server/locksettings/WeaverBasedSyntheticPasswordTests.java index 6c13a6fe04a0..966c0479d69d 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/WeaverBasedSyntheticPasswordTests.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/WeaverBasedSyntheticPasswordTests.java @@ -36,7 +36,7 @@ public class WeaverBasedSyntheticPasswordTests extends SyntheticPasswordTests { assertEquals(Sets.newHashSet(), mPasswordSlotManager.getUsedSlots()); mStorage.writePersistentDataBlock(PersistentData.TYPE_SP_WEAVER, frpWeaverSlot, 0, new byte[1]); - initializeSyntheticPassword(userId); // This should allocate a Weaver slot. + mService.initializeSyntheticPassword(userId); // This should allocate a Weaver slot. assertEquals(Sets.newHashSet(1), mPasswordSlotManager.getUsedSlots()); } @@ -52,7 +52,7 @@ public class WeaverBasedSyntheticPasswordTests extends SyntheticPasswordTests { assertEquals(Sets.newHashSet(), mPasswordSlotManager.getUsedSlots()); mStorage.writePersistentDataBlock(PersistentData.TYPE_SP_WEAVER, frpWeaverSlot, 0, new byte[1]); - initializeSyntheticPassword(userId); // This should allocate a Weaver slot. + mService.initializeSyntheticPassword(userId); // This should allocate a Weaver slot. assertEquals(Sets.newHashSet(0), mPasswordSlotManager.getUsedSlots()); } } diff --git a/services/tests/servicestests/src/com/android/server/pm/BackgroundInstallControlServiceTest.java b/services/tests/servicestests/src/com/android/server/pm/BackgroundInstallControlServiceTest.java new file mode 100644 index 000000000000..54fa272dd3b6 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/pm/BackgroundInstallControlServiceTest.java @@ -0,0 +1,840 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.pm; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.usage.UsageEvents; +import android.app.usage.UsageEvents.Event; +import android.app.usage.UsageStatsManagerInternal; +import android.app.usage.UsageStatsManagerInternal.UsageEventListener; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.IPackageManager; +import android.content.pm.InstallSourceInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManagerInternal; +import android.content.pm.ParceledListSlice; +import android.os.FileUtils; +import android.os.Looper; +import android.os.RemoteException; +import android.os.SystemClock; +import android.os.UserHandle; +import android.os.test.TestLooper; +import android.platform.test.annotations.Presubmit; +import android.util.AtomicFile; +import android.util.SparseSetArray; +import android.util.proto.ProtoInputStream; +import android.util.proto.ProtoOutputStream; + +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.server.pm.permission.PermissionManagerServiceInternal; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.internal.util.reflection.FieldSetter; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Tests for {@link com.android.server.pm.BackgroundInstallControlService} + */ +@Presubmit +public final class BackgroundInstallControlServiceTest { + private static final String INSTALLER_NAME_1 = "installer1"; + private static final String INSTALLER_NAME_2 = "installer2"; + private static final String PACKAGE_NAME_1 = "package1"; + private static final String PACKAGE_NAME_2 = "package2"; + private static final String PACKAGE_NAME_3 = "package3"; + private static final int USER_ID_1 = 1; + private static final int USER_ID_2 = 2; + private static final long USAGE_EVENT_TIMESTAMP_1 = 1000; + private static final long USAGE_EVENT_TIMESTAMP_2 = 2000; + private static final long USAGE_EVENT_TIMESTAMP_3 = 3000; + private static final long PACKAGE_ADD_TIMESTAMP_1 = 1500; + + private BackgroundInstallControlService mBackgroundInstallControlService; + private PackageManagerInternal.PackageListObserver mPackageListObserver; + private UsageEventListener mUsageEventListener; + private TestLooper mTestLooper; + private Looper mLooper; + private File mFile; + + + @Mock + private Context mContext; + @Mock + private IPackageManager mIPackageManager; + @Mock + private PackageManagerInternal mPackageManagerInternal; + @Mock + private UsageStatsManagerInternal mUsageStatsManagerInternal; + @Mock + private PermissionManagerServiceInternal mPermissionManager; + @Captor + private ArgumentCaptor<PackageManagerInternal.PackageListObserver> mPackageListObserverCaptor; + @Captor + private ArgumentCaptor<UsageEventListener> mUsageEventListenerCaptor; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mTestLooper = new TestLooper(); + mLooper = mTestLooper.getLooper(); + mFile = new File( + InstrumentationRegistry.getInstrumentation().getContext().getCacheDir(), + "test"); + mBackgroundInstallControlService = new BackgroundInstallControlService( + new MockInjector(mContext)); + + verify(mUsageStatsManagerInternal).registerListener(mUsageEventListenerCaptor.capture()); + mUsageEventListener = mUsageEventListenerCaptor.getValue(); + + mBackgroundInstallControlService.onStart(true); + verify(mPackageManagerInternal).getPackageList(mPackageListObserverCaptor.capture()); + mPackageListObserver = mPackageListObserverCaptor.getValue(); + } + + @After + public void tearDown() { + FileUtils.deleteContentsAndDir(mFile); + } + + @Test + public void testInitBackgroundInstalledPackages_empty() { + assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages()); + mBackgroundInstallControlService.initBackgroundInstalledPackages(); + assertNotNull(mBackgroundInstallControlService.getBackgroundInstalledPackages()); + assertEquals(0, + mBackgroundInstallControlService.getBackgroundInstalledPackages().size()); + } + + @Test + public void testInitBackgroundInstalledPackages_one() { + AtomicFile atomicFile = new AtomicFile(mFile); + FileOutputStream fileOutputStream; + try { + fileOutputStream = atomicFile.startWrite(); + } catch (IOException e) { + fail("Failed to start write to states protobuf." + e); + return; + } + + // Write test data to the file on the disk. + try { + ProtoOutputStream protoOutputStream = new ProtoOutputStream(fileOutputStream); + long token = protoOutputStream.start( + BackgroundInstalledPackagesProto.BG_INSTALLED_PKG); + protoOutputStream.write( + BackgroundInstalledPackageProto.PACKAGE_NAME, PACKAGE_NAME_1); + protoOutputStream.write( + BackgroundInstalledPackageProto.USER_ID, USER_ID_1 + 1); + protoOutputStream.end(token); + protoOutputStream.flush(); + atomicFile.finishWrite(fileOutputStream); + } catch (Exception e) { + fail("Failed to finish write to states protobuf. " + e); + atomicFile.failWrite(fileOutputStream); + } + + assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages()); + mBackgroundInstallControlService.initBackgroundInstalledPackages(); + var packages = mBackgroundInstallControlService.getBackgroundInstalledPackages(); + assertNotNull(packages); + assertEquals(1, packages.size()); + assertTrue(packages.contains(USER_ID_1, PACKAGE_NAME_1)); + } + + @Test + public void testInitBackgroundInstalledPackages_two() { + AtomicFile atomicFile = new AtomicFile(mFile); + FileOutputStream fileOutputStream; + try { + fileOutputStream = atomicFile.startWrite(); + } catch (IOException e) { + fail("Failed to start write to states protobuf." + e); + return; + } + + // Write test data to the file on the disk. + try { + ProtoOutputStream protoOutputStream = new ProtoOutputStream(fileOutputStream); + + long token = protoOutputStream.start( + BackgroundInstalledPackagesProto.BG_INSTALLED_PKG); + protoOutputStream.write( + BackgroundInstalledPackageProto.PACKAGE_NAME, PACKAGE_NAME_1); + protoOutputStream.write( + BackgroundInstalledPackageProto.USER_ID, USER_ID_1 + 1); + protoOutputStream.end(token); + + token = protoOutputStream.start( + BackgroundInstalledPackagesProto.BG_INSTALLED_PKG); + protoOutputStream.write( + BackgroundInstalledPackageProto.PACKAGE_NAME, PACKAGE_NAME_2); + protoOutputStream.write( + BackgroundInstalledPackageProto.USER_ID, USER_ID_2 + 1); + protoOutputStream.end(token); + + protoOutputStream.flush(); + atomicFile.finishWrite(fileOutputStream); + } catch (Exception e) { + fail("Failed to finish write to states protobuf. " + e); + atomicFile.failWrite(fileOutputStream); + } + + assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages()); + mBackgroundInstallControlService.initBackgroundInstalledPackages(); + var packages = mBackgroundInstallControlService.getBackgroundInstalledPackages(); + assertNotNull(packages); + assertEquals(2, packages.size()); + assertTrue(packages.contains(USER_ID_1, PACKAGE_NAME_1)); + assertTrue(packages.contains(USER_ID_2, PACKAGE_NAME_2)); + } + + @Test + public void testWriteBackgroundInstalledPackagesToDisk_empty() { + assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages()); + mBackgroundInstallControlService.initBackgroundInstalledPackages(); + var packages = mBackgroundInstallControlService.getBackgroundInstalledPackages(); + assertNotNull(packages); + mBackgroundInstallControlService.writeBackgroundInstalledPackagesToDisk(); + + // Read the file on the disk to verify + var packagesInDisk = new SparseSetArray<>(); + AtomicFile atomicFile = new AtomicFile(mFile); + try (FileInputStream fileInputStream = atomicFile.openRead()) { + ProtoInputStream protoInputStream = new ProtoInputStream(fileInputStream); + + while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + if (protoInputStream.getFieldNumber() + != (int) BackgroundInstalledPackagesProto.BG_INSTALLED_PKG) { + continue; + } + long token = protoInputStream.start( + BackgroundInstalledPackagesProto.BG_INSTALLED_PKG); + String packageName = null; + int userId = UserHandle.USER_NULL; + while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (protoInputStream.getFieldNumber()) { + case (int) BackgroundInstalledPackageProto.PACKAGE_NAME: + packageName = protoInputStream.readString( + BackgroundInstalledPackageProto.PACKAGE_NAME); + break; + case (int) BackgroundInstalledPackageProto.USER_ID: + userId = protoInputStream.readInt( + BackgroundInstalledPackageProto.USER_ID) - 1; + break; + default: + fail("Undefined field in proto: " + + protoInputStream.getFieldNumber()); + } + } + protoInputStream.end(token); + if (packageName != null && userId != UserHandle.USER_NULL) { + packagesInDisk.add(userId, packageName); + } else { + fail("Fails to get packageName or UserId from proto file"); + } + } + } catch (IOException e) { + fail("Error reading state from the disk. " + e); + } + + assertEquals(0, packagesInDisk.size()); + assertEquals(packages.size(), packagesInDisk.size()); + } + + @Test + public void testWriteBackgroundInstalledPackagesToDisk_one() { + assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages()); + mBackgroundInstallControlService.initBackgroundInstalledPackages(); + var packages = mBackgroundInstallControlService.getBackgroundInstalledPackages(); + assertNotNull(packages); + + packages.add(USER_ID_1, PACKAGE_NAME_1); + mBackgroundInstallControlService.writeBackgroundInstalledPackagesToDisk(); + + // Read the file on the disk to verify + var packagesInDisk = new SparseSetArray<>(); + AtomicFile atomicFile = new AtomicFile(mFile); + try (FileInputStream fileInputStream = atomicFile.openRead()) { + ProtoInputStream protoInputStream = new ProtoInputStream(fileInputStream); + + while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + if (protoInputStream.getFieldNumber() + != (int) BackgroundInstalledPackagesProto.BG_INSTALLED_PKG) { + continue; + } + long token = protoInputStream.start( + BackgroundInstalledPackagesProto.BG_INSTALLED_PKG); + String packageName = null; + int userId = UserHandle.USER_NULL; + while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (protoInputStream.getFieldNumber()) { + case (int) BackgroundInstalledPackageProto.PACKAGE_NAME: + packageName = protoInputStream.readString( + BackgroundInstalledPackageProto.PACKAGE_NAME); + break; + case (int) BackgroundInstalledPackageProto.USER_ID: + userId = protoInputStream.readInt( + BackgroundInstalledPackageProto.USER_ID) - 1; + break; + default: + fail("Undefined field in proto: " + + protoInputStream.getFieldNumber()); + } + } + protoInputStream.end(token); + if (packageName != null && userId != UserHandle.USER_NULL) { + packagesInDisk.add(userId, packageName); + } else { + fail("Fails to get packageName or UserId from proto file"); + } + } + } catch (IOException e) { + fail("Error reading state from the disk. " + e); + } + + assertEquals(1, packagesInDisk.size()); + assertEquals(packages.size(), packagesInDisk.size()); + assertTrue(packages.contains(USER_ID_1, PACKAGE_NAME_1)); + } + + @Test + public void testWriteBackgroundInstalledPackagesToDisk_two() { + assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages()); + mBackgroundInstallControlService.initBackgroundInstalledPackages(); + var packages = mBackgroundInstallControlService.getBackgroundInstalledPackages(); + assertNotNull(packages); + + packages.add(USER_ID_1, PACKAGE_NAME_1); + packages.add(USER_ID_2, PACKAGE_NAME_2); + mBackgroundInstallControlService.writeBackgroundInstalledPackagesToDisk(); + + // Read the file on the disk to verify + var packagesInDisk = new SparseSetArray<>(); + AtomicFile atomicFile = new AtomicFile(mFile); + try (FileInputStream fileInputStream = atomicFile.openRead()) { + ProtoInputStream protoInputStream = new ProtoInputStream(fileInputStream); + + while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + if (protoInputStream.getFieldNumber() + != (int) BackgroundInstalledPackagesProto.BG_INSTALLED_PKG) { + continue; + } + long token = protoInputStream.start( + BackgroundInstalledPackagesProto.BG_INSTALLED_PKG); + String packageName = null; + int userId = UserHandle.USER_NULL; + while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (protoInputStream.getFieldNumber()) { + case (int) BackgroundInstalledPackageProto.PACKAGE_NAME: + packageName = protoInputStream.readString( + BackgroundInstalledPackageProto.PACKAGE_NAME); + break; + case (int) BackgroundInstalledPackageProto.USER_ID: + userId = protoInputStream.readInt( + BackgroundInstalledPackageProto.USER_ID) - 1; + break; + default: + fail("Undefined field in proto: " + + protoInputStream.getFieldNumber()); + } + } + protoInputStream.end(token); + if (packageName != null && userId != UserHandle.USER_NULL) { + packagesInDisk.add(userId, packageName); + } else { + fail("Fails to get packageName or UserId from proto file"); + } + } + } catch (IOException e) { + fail("Error reading state from the disk. " + e); + } + + assertEquals(2, packagesInDisk.size()); + assertEquals(packages.size(), packagesInDisk.size()); + assertTrue(packages.contains(USER_ID_1, PACKAGE_NAME_1)); + assertTrue(packages.contains(USER_ID_2, PACKAGE_NAME_2)); + } + + @Test + public void testHandleUsageEvent_permissionDenied() { + assertEquals(0, + mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps()); + doReturn(PackageManager.PERMISSION_DENIED).when(mPermissionManager).checkPermission( + anyString(), anyString(), anyInt()); + generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED, + USER_ID_1, INSTALLER_NAME_1, 0); + mTestLooper.dispatchAll(); + assertEquals(0, + mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps()); + } + + @Test + public void testHandleUsageEvent_permissionGranted() { + assertEquals(0, + mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps()); + doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission( + anyString(), anyString(), anyInt()); + generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED, + USER_ID_1, INSTALLER_NAME_1, 0); + mTestLooper.dispatchAll(); + assertEquals(1, + mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps()); + } + + @Test + public void testHandleUsageEvent_ignoredEvent() { + assertEquals(0, + mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps()); + doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission( + anyString(), anyString(), anyInt()); + generateUsageEvent(UsageEvents.Event.USER_INTERACTION, + USER_ID_1, INSTALLER_NAME_1, 0); + mTestLooper.dispatchAll(); + assertEquals(0, + mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps()); + } + + @Test + public void testHandleUsageEvent_firstActivityResumedHalfTimeFrame() { + assertEquals(0, + mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps()); + doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission( + anyString(), anyString(), anyInt()); + generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED, + USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_1); + mTestLooper.dispatchAll(); + + var installerForegroundTimeFrames = + mBackgroundInstallControlService.getInstallerForegroundTimeFrames(); + assertEquals(1, installerForegroundTimeFrames.numMaps()); + assertEquals(1, installerForegroundTimeFrames.numElementsForKey(USER_ID_1)); + + var foregroundTimeFrames = installerForegroundTimeFrames.get(USER_ID_1, INSTALLER_NAME_1); + assertEquals(1, foregroundTimeFrames.size()); + + var foregroundTimeFrame = foregroundTimeFrames.first(); + assertEquals(USAGE_EVENT_TIMESTAMP_1, foregroundTimeFrame.startTimeStampMillis); + assertFalse(foregroundTimeFrame.isDone()); + } + + @Test + public void testHandleUsageEvent_firstActivityResumedOneTimeFrame() { + assertEquals(0, + mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps()); + doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission( + anyString(), anyString(), anyInt()); + generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED, + USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_1); + generateUsageEvent(Event.ACTIVITY_STOPPED, + USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_2); + mTestLooper.dispatchAll(); + + var installerForegroundTimeFrames = + mBackgroundInstallControlService.getInstallerForegroundTimeFrames(); + assertEquals(1, installerForegroundTimeFrames.numMaps()); + assertEquals(1, installerForegroundTimeFrames.numElementsForKey(USER_ID_1)); + + var foregroundTimeFrames = installerForegroundTimeFrames.get(USER_ID_1, INSTALLER_NAME_1); + assertEquals(1, foregroundTimeFrames.size()); + + var foregroundTimeFrame = foregroundTimeFrames.first(); + assertEquals(USAGE_EVENT_TIMESTAMP_1, foregroundTimeFrame.startTimeStampMillis); + assertTrue(foregroundTimeFrame.isDone()); + } + + @Test + public void testHandleUsageEvent_firstActivityResumedOneAndHalfTimeFrame() { + assertEquals(0, + mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps()); + doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission( + anyString(), anyString(), anyInt()); + generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED, + USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_1); + generateUsageEvent(Event.ACTIVITY_STOPPED, + USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_2); + generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED, + USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_3); + mTestLooper.dispatchAll(); + + var installerForegroundTimeFrames = + mBackgroundInstallControlService.getInstallerForegroundTimeFrames(); + assertEquals(1, installerForegroundTimeFrames.numMaps()); + assertEquals(1, installerForegroundTimeFrames.numElementsForKey(USER_ID_1)); + + var foregroundTimeFrames = installerForegroundTimeFrames.get(USER_ID_1, INSTALLER_NAME_1); + assertEquals(2, foregroundTimeFrames.size()); + + var foregroundTimeFrame1 = foregroundTimeFrames.first(); + assertEquals(USAGE_EVENT_TIMESTAMP_1, foregroundTimeFrame1.startTimeStampMillis); + assertTrue(foregroundTimeFrame1.isDone()); + + var foregroundTimeFrame2 = foregroundTimeFrames.last(); + assertEquals(USAGE_EVENT_TIMESTAMP_3, foregroundTimeFrame2.startTimeStampMillis); + assertFalse(foregroundTimeFrame2.isDone()); + } + + @Test + public void testHandleUsageEvent_firstNoneActivityResumed() { + assertEquals(0, + mBackgroundInstallControlService.getInstallerForegroundTimeFrames().numMaps()); + doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission( + anyString(), anyString(), anyInt()); + generateUsageEvent(Event.ACTIVITY_STOPPED, + USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_1); + mTestLooper.dispatchAll(); + + var installerForegroundTimeFrames = + mBackgroundInstallControlService.getInstallerForegroundTimeFrames(); + assertEquals(1, installerForegroundTimeFrames.numMaps()); + assertEquals(1, installerForegroundTimeFrames.numElementsForKey(USER_ID_1)); + + var foregroundTimeFrames = installerForegroundTimeFrames.get(USER_ID_1, INSTALLER_NAME_1); + assertEquals(0, foregroundTimeFrames.size()); + } + + @Test + public void testHandleUsageEvent_packageAddedNoUsageEvent() throws + RemoteException, NoSuchFieldException { + assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages()); + InstallSourceInfo installSourceInfo = new InstallSourceInfo( + /* initiatingPackageName = */ null, /* initiatingPackageSigningInfo = */ null, + /* originatingPackageName = */ null, + /* installingPackageName = */ INSTALLER_NAME_1); + assertEquals(installSourceInfo.getInstallingPackageName(), INSTALLER_NAME_1); + when(mIPackageManager.getInstallSourceInfo(anyString())).thenReturn(installSourceInfo); + ApplicationInfo appInfo = mock(ApplicationInfo.class); + + when(mIPackageManager.getApplicationInfo( + eq(PACKAGE_NAME_1), + eq(0L), + anyInt()) + ).thenReturn(appInfo); + + long createTimestamp = PACKAGE_ADD_TIMESTAMP_1 + - (System.currentTimeMillis() - SystemClock.uptimeMillis()); + FieldSetter.setField(appInfo, + ApplicationInfo.class.getDeclaredField("createTimestamp"), + createTimestamp); + + int uid = USER_ID_1 * UserHandle.PER_USER_RANGE; + assertEquals(USER_ID_1, UserHandle.getUserId(uid)); + + mPackageListObserver.onPackageAdded(PACKAGE_NAME_1, uid); + mTestLooper.dispatchAll(); + + var packages = mBackgroundInstallControlService.getBackgroundInstalledPackages(); + assertNotNull(packages); + assertEquals(1, packages.size()); + assertTrue(packages.contains(USER_ID_1, PACKAGE_NAME_1)); + } + + @Test + public void testHandleUsageEvent_packageAddedInsideTimeFrame() throws + RemoteException, NoSuchFieldException { + assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages()); + InstallSourceInfo installSourceInfo = new InstallSourceInfo( + /* initiatingPackageName = */ null, /* initiatingPackageSigningInfo = */ null, + /* originatingPackageName = */ null, + /* installingPackageName = */ INSTALLER_NAME_1); + assertEquals(installSourceInfo.getInstallingPackageName(), INSTALLER_NAME_1); + when(mIPackageManager.getInstallSourceInfo(anyString())).thenReturn(installSourceInfo); + ApplicationInfo appInfo = mock(ApplicationInfo.class); + + when(mIPackageManager.getApplicationInfo( + eq(PACKAGE_NAME_1), + eq(0L), + anyInt()) + ).thenReturn(appInfo); + + long createTimestamp = PACKAGE_ADD_TIMESTAMP_1 + - (System.currentTimeMillis() - SystemClock.uptimeMillis()); + FieldSetter.setField(appInfo, + ApplicationInfo.class.getDeclaredField("createTimestamp"), + createTimestamp); + + int uid = USER_ID_1 * UserHandle.PER_USER_RANGE; + assertEquals(USER_ID_1, UserHandle.getUserId(uid)); + + // The following 2 usage events generation is the only difference from the + // testHandleUsageEvent_packageAddedNoUsageEvent test. + // The 2 usage events make the package adding inside a time frame. + // So it's not a background install. Thus, it's null for the return of + // mBackgroundInstallControlService.getBackgroundInstalledPackages() + doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission( + anyString(), anyString(), anyInt()); + generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED, + USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_1); + generateUsageEvent(Event.ACTIVITY_STOPPED, + USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_2); + + mPackageListObserver.onPackageAdded(PACKAGE_NAME_1, uid); + mTestLooper.dispatchAll(); + assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages()); + } + + @Test + public void testHandleUsageEvent_packageAddedOutsideTimeFrame1() throws + RemoteException, NoSuchFieldException { + assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages()); + InstallSourceInfo installSourceInfo = new InstallSourceInfo( + /* initiatingPackageName = */ null, /* initiatingPackageSigningInfo = */ null, + /* originatingPackageName = */ null, + /* installingPackageName = */ INSTALLER_NAME_1); + assertEquals(installSourceInfo.getInstallingPackageName(), INSTALLER_NAME_1); + when(mIPackageManager.getInstallSourceInfo(anyString())).thenReturn(installSourceInfo); + ApplicationInfo appInfo = mock(ApplicationInfo.class); + + when(mIPackageManager.getApplicationInfo( + eq(PACKAGE_NAME_1), + eq(0L), + anyInt()) + ).thenReturn(appInfo); + + long createTimestamp = PACKAGE_ADD_TIMESTAMP_1 + - (System.currentTimeMillis() - SystemClock.uptimeMillis()); + FieldSetter.setField(appInfo, + ApplicationInfo.class.getDeclaredField("createTimestamp"), + createTimestamp); + + int uid = USER_ID_1 * UserHandle.PER_USER_RANGE; + assertEquals(USER_ID_1, UserHandle.getUserId(uid)); + + // The following 2 usage events generation is the only difference from the + // testHandleUsageEvent_packageAddedNoUsageEvent test. + // The 2 usage events make the package adding outside a time frame. + // Compared to testHandleUsageEvent_packageAddedInsideTimeFrame, + // it's a background install. Thus, it's not null for the return of + // mBackgroundInstallControlService.getBackgroundInstalledPackages() + doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission( + anyString(), anyString(), anyInt()); + generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED, + USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_2); + generateUsageEvent(Event.ACTIVITY_STOPPED, + USER_ID_1, INSTALLER_NAME_1, USAGE_EVENT_TIMESTAMP_3); + + mPackageListObserver.onPackageAdded(PACKAGE_NAME_1, uid); + mTestLooper.dispatchAll(); + + var packages = mBackgroundInstallControlService.getBackgroundInstalledPackages(); + assertNotNull(packages); + assertEquals(1, packages.size()); + assertTrue(packages.contains(USER_ID_1, PACKAGE_NAME_1)); + } + @Test + public void testHandleUsageEvent_packageAddedOutsideTimeFrame2() throws + RemoteException, NoSuchFieldException { + assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages()); + InstallSourceInfo installSourceInfo = new InstallSourceInfo( + /* initiatingPackageName = */ null, /* initiatingPackageSigningInfo = */ null, + /* originatingPackageName = */ null, + /* installingPackageName = */ INSTALLER_NAME_1); + assertEquals(installSourceInfo.getInstallingPackageName(), INSTALLER_NAME_1); + when(mIPackageManager.getInstallSourceInfo(anyString())).thenReturn(installSourceInfo); + ApplicationInfo appInfo = mock(ApplicationInfo.class); + + when(mIPackageManager.getApplicationInfo( + eq(PACKAGE_NAME_1), + eq(0L), + anyInt()) + ).thenReturn(appInfo); + + long createTimestamp = PACKAGE_ADD_TIMESTAMP_1 + - (System.currentTimeMillis() - SystemClock.uptimeMillis()); + FieldSetter.setField(appInfo, + ApplicationInfo.class.getDeclaredField("createTimestamp"), + createTimestamp); + + int uid = USER_ID_1 * UserHandle.PER_USER_RANGE; + assertEquals(USER_ID_1, UserHandle.getUserId(uid)); + + // The following 2 usage events generation is the only difference from the + // testHandleUsageEvent_packageAddedNoUsageEvent test. + // These 2 usage events are triggered by INSTALLER_NAME_2. + // The 2 usage events make the package adding outside a time frame. + // Compared to testHandleUsageEvent_packageAddedInsideTimeFrame, + // it's a background install. Thus, it's not null for the return of + // mBackgroundInstallControlService.getBackgroundInstalledPackages() + doReturn(PackageManager.PERMISSION_GRANTED).when(mPermissionManager).checkPermission( + anyString(), anyString(), anyInt()); + generateUsageEvent(UsageEvents.Event.ACTIVITY_RESUMED, + USER_ID_2, INSTALLER_NAME_2, USAGE_EVENT_TIMESTAMP_2); + generateUsageEvent(Event.ACTIVITY_STOPPED, + USER_ID_2, INSTALLER_NAME_2, USAGE_EVENT_TIMESTAMP_3); + + mPackageListObserver.onPackageAdded(PACKAGE_NAME_1, uid); + mTestLooper.dispatchAll(); + + var packages = mBackgroundInstallControlService.getBackgroundInstalledPackages(); + assertNotNull(packages); + assertEquals(1, packages.size()); + assertTrue(packages.contains(USER_ID_1, PACKAGE_NAME_1)); + } + + @Test + public void testPackageRemoved() { + assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages()); + mBackgroundInstallControlService.initBackgroundInstalledPackages(); + var packages = mBackgroundInstallControlService.getBackgroundInstalledPackages(); + assertNotNull(packages); + + packages.add(USER_ID_1, PACKAGE_NAME_1); + packages.add(USER_ID_2, PACKAGE_NAME_2); + + assertEquals(2, packages.size()); + assertTrue(packages.contains(USER_ID_1, PACKAGE_NAME_1)); + assertTrue(packages.contains(USER_ID_2, PACKAGE_NAME_2)); + + int uid = USER_ID_1 * UserHandle.PER_USER_RANGE; + assertEquals(USER_ID_1, UserHandle.getUserId(uid)); + + mPackageListObserver.onPackageRemoved(PACKAGE_NAME_1, uid); + mTestLooper.dispatchAll(); + + assertEquals(1, packages.size()); + assertFalse(packages.contains(USER_ID_1, PACKAGE_NAME_1)); + assertTrue(packages.contains(USER_ID_2, PACKAGE_NAME_2)); + } + + @Test + public void testGetBackgroundInstalledPackages() throws RemoteException { + assertNull(mBackgroundInstallControlService.getBackgroundInstalledPackages()); + mBackgroundInstallControlService.initBackgroundInstalledPackages(); + var bgPackages = mBackgroundInstallControlService.getBackgroundInstalledPackages(); + assertNotNull(bgPackages); + + bgPackages.add(USER_ID_1, PACKAGE_NAME_1); + bgPackages.add(USER_ID_2, PACKAGE_NAME_2); + + assertEquals(2, bgPackages.size()); + assertTrue(bgPackages.contains(USER_ID_1, PACKAGE_NAME_1)); + assertTrue(bgPackages.contains(USER_ID_2, PACKAGE_NAME_2)); + + List<PackageInfo> packages = new ArrayList<>(); + var packageInfo1 = makePackageInfo(PACKAGE_NAME_1); + packages.add(packageInfo1); + var packageInfo2 = makePackageInfo(PACKAGE_NAME_2); + packages.add(packageInfo2); + var packageInfo3 = makePackageInfo(PACKAGE_NAME_3); + packages.add(packageInfo3); + doReturn(new ParceledListSlice<>(packages)).when(mIPackageManager).getInstalledPackages( + anyLong(), anyInt()); + + var resultPackages = + mBackgroundInstallControlService.getBackgroundInstalledPackages(0L, USER_ID_1); + assertEquals(1, resultPackages.getList().size()); + assertTrue(resultPackages.getList().contains(packageInfo1)); + assertFalse(resultPackages.getList().contains(packageInfo2)); + assertFalse(resultPackages.getList().contains(packageInfo3)); + } + + /** + * Mock a usage event occurring. + * + * @param usageEventId id of a usage event + * @param userId user id of a usage event + * @param pkgName package name of a usage event + * @param timestamp timestamp of a usage event + */ + private void generateUsageEvent(int usageEventId, + int userId, + String pkgName, + long timestamp) { + Event event = new Event(usageEventId, timestamp); + event.mPackage = pkgName; + mUsageEventListener.onUsageEvent(userId, event); + } + + private PackageInfo makePackageInfo(String packageName) { + PackageInfo pkg = new PackageInfo(); + pkg.packageName = packageName; + pkg.applicationInfo = new ApplicationInfo(); + return pkg; + } + + private class MockInjector implements BackgroundInstallControlService.Injector { + private final Context mContext; + + MockInjector(Context context) { + mContext = context; + } + + @Override + public Context getContext() { + return mContext; + } + + @Override + public IPackageManager getIPackageManager() { + return mIPackageManager; + } + + @Override + public PackageManagerInternal getPackageManagerInternal() { + return mPackageManagerInternal; + } + + @Override + public UsageStatsManagerInternal getUsageStatsManagerInternal() { + return mUsageStatsManagerInternal; + } + + @Override + public PermissionManagerServiceInternal getPermissionManager() { + return mPermissionManager; + } + + @Override + public Looper getLooper() { + return mLooper; + } + + @Override + public File getDiskFile() { + return mFile; + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/power/PowerGroupTest.java b/services/tests/servicestests/src/com/android/server/power/PowerGroupTest.java index b034b0da387f..fe31b9cbe558 100644 --- a/services/tests/servicestests/src/com/android/server/power/PowerGroupTest.java +++ b/services/tests/servicestests/src/com/android/server/power/PowerGroupTest.java @@ -257,7 +257,6 @@ public class PowerGroupTest { .build(); mPowerGroup.updateLocked(/* screenBrightnessOverride= */ BRIGHTNESS, - /* autoBrightness = */ false, /* useProximitySensor= */ false, /* boostScreenBrightness= */ false, /* dozeScreenStateOverride= */ Display.STATE_ON, @@ -273,7 +272,6 @@ public class PowerGroupTest { mPowerGroup.mDisplayPowerRequest; assertThat(displayPowerRequest.policy).isEqualTo(POLICY_DIM); assertThat(displayPowerRequest.screenBrightnessOverride).isWithin(PRECISION).of(BRIGHTNESS); - assertThat(displayPowerRequest.useAutoBrightness).isEqualTo(false); assertThat(displayPowerRequest.useProximitySensor).isEqualTo(false); assertThat(displayPowerRequest.boostScreenBrightness).isEqualTo(false); assertThat(displayPowerRequest.dozeScreenState).isEqualTo(Display.STATE_UNKNOWN); @@ -297,7 +295,6 @@ public class PowerGroupTest { mPowerGroup.setWakeLockSummaryLocked(WAKE_LOCK_DOZE); mPowerGroup.updateLocked(/* screenBrightnessOverride= */ BRIGHTNESS, - /* autoBrightness = */ true, /* useProximitySensor= */ true, /* boostScreenBrightness= */ true, /* dozeScreenStateOverride= */ Display.STATE_ON, @@ -313,7 +310,6 @@ public class PowerGroupTest { mPowerGroup.mDisplayPowerRequest; assertThat(displayPowerRequest.policy).isEqualTo(POLICY_DOZE); assertThat(displayPowerRequest.screenBrightnessOverride).isWithin(PRECISION).of(BRIGHTNESS); - assertThat(displayPowerRequest.useAutoBrightness).isEqualTo(true); assertThat(displayPowerRequest.useProximitySensor).isEqualTo(true); assertThat(displayPowerRequest.boostScreenBrightness).isEqualTo(true); assertThat(displayPowerRequest.dozeScreenState).isEqualTo(Display.STATE_ON); @@ -336,7 +332,6 @@ public class PowerGroupTest { assertThat(mPowerGroup.getWakefulnessLocked()).isEqualTo(WAKEFULNESS_DOZING); mPowerGroup.updateLocked(/* screenBrightnessOverride= */ BRIGHTNESS, - /* autoBrightness = */ true, /* useProximitySensor= */ true, /* boostScreenBrightness= */ true, /* dozeScreenStateOverride= */ Display.STATE_ON, @@ -352,7 +347,6 @@ public class PowerGroupTest { mPowerGroup.mDisplayPowerRequest; assertThat(displayPowerRequest.policy).isEqualTo(POLICY_OFF); assertThat(displayPowerRequest.screenBrightnessOverride).isWithin(PRECISION).of(BRIGHTNESS); - assertThat(displayPowerRequest.useAutoBrightness).isEqualTo(true); assertThat(displayPowerRequest.useProximitySensor).isEqualTo(true); assertThat(displayPowerRequest.boostScreenBrightness).isEqualTo(true); assertThat(displayPowerRequest.dozeScreenState).isEqualTo(Display.STATE_UNKNOWN); @@ -374,7 +368,6 @@ public class PowerGroupTest { assertThat(mPowerGroup.getWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE); mPowerGroup.updateLocked(/* screenBrightnessOverride= */ BRIGHTNESS, - /* autoBrightness = */ true, /* useProximitySensor= */ true, /* boostScreenBrightness= */ true, /* dozeScreenStateOverride= */ Display.STATE_ON, @@ -390,7 +383,6 @@ public class PowerGroupTest { mPowerGroup.mDisplayPowerRequest; assertThat(displayPowerRequest.policy).isEqualTo(POLICY_OFF); assertThat(displayPowerRequest.screenBrightnessOverride).isWithin(PRECISION).of(BRIGHTNESS); - assertThat(displayPowerRequest.useAutoBrightness).isEqualTo(true); assertThat(displayPowerRequest.useProximitySensor).isEqualTo(true); assertThat(displayPowerRequest.boostScreenBrightness).isEqualTo(true); assertThat(displayPowerRequest.dozeScreenState).isEqualTo(Display.STATE_UNKNOWN); @@ -412,7 +404,6 @@ public class PowerGroupTest { mPowerGroup.sleepLocked(TIMESTAMP1, UID, GO_TO_SLEEP_REASON_TIMEOUT); assertThat(mPowerGroup.getWakefulnessLocked()).isEqualTo(WAKEFULNESS_ASLEEP); mPowerGroup.updateLocked(/* screenBrightnessOverride= */ BRIGHTNESS, - /* autoBrightness = */ true, /* useProximitySensor= */ true, /* boostScreenBrightness= */ true, /* dozeScreenStateOverride= */ Display.STATE_ON, @@ -428,7 +419,6 @@ public class PowerGroupTest { mPowerGroup.mDisplayPowerRequest; assertThat(displayPowerRequest.policy).isEqualTo(POLICY_OFF); assertThat(displayPowerRequest.screenBrightnessOverride).isWithin(PRECISION).of(BRIGHTNESS); - assertThat(displayPowerRequest.useAutoBrightness).isEqualTo(true); assertThat(displayPowerRequest.useProximitySensor).isEqualTo(true); assertThat(displayPowerRequest.boostScreenBrightness).isEqualTo(true); assertThat(displayPowerRequest.dozeScreenState).isEqualTo(Display.STATE_UNKNOWN); @@ -451,7 +441,6 @@ public class PowerGroupTest { assertThat(mPowerGroup.getWakefulnessLocked()).isEqualTo(WAKEFULNESS_DREAMING); mPowerGroup.setWakeLockSummaryLocked(WAKE_LOCK_SCREEN_BRIGHT); mPowerGroup.updateLocked(/* screenBrightnessOverride= */ BRIGHTNESS, - /* autoBrightness = */ true, /* useProximitySensor= */ true, /* boostScreenBrightness= */ true, /* dozeScreenStateOverride= */ Display.STATE_ON, @@ -467,7 +456,6 @@ public class PowerGroupTest { mPowerGroup.mDisplayPowerRequest; assertThat(displayPowerRequest.policy).isEqualTo(POLICY_BRIGHT); assertThat(displayPowerRequest.screenBrightnessOverride).isWithin(PRECISION).of(BRIGHTNESS); - assertThat(displayPowerRequest.useAutoBrightness).isEqualTo(true); assertThat(displayPowerRequest.useProximitySensor).isEqualTo(true); assertThat(displayPowerRequest.boostScreenBrightness).isEqualTo(true); assertThat(displayPowerRequest.dozeScreenState).isEqualTo(Display.STATE_UNKNOWN); @@ -488,7 +476,6 @@ public class PowerGroupTest { .build(); assertThat(mPowerGroup.getWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE); mPowerGroup.updateLocked(/* screenBrightnessOverride= */ BRIGHTNESS, - /* autoBrightness = */ true, /* useProximitySensor= */ true, /* boostScreenBrightness= */ true, /* dozeScreenStateOverride= */ Display.STATE_ON, @@ -504,7 +491,6 @@ public class PowerGroupTest { mPowerGroup.mDisplayPowerRequest; assertThat(displayPowerRequest.policy).isEqualTo(POLICY_BRIGHT); assertThat(displayPowerRequest.screenBrightnessOverride).isWithin(PRECISION).of(BRIGHTNESS); - assertThat(displayPowerRequest.useAutoBrightness).isEqualTo(true); assertThat(displayPowerRequest.useProximitySensor).isEqualTo(true); assertThat(displayPowerRequest.boostScreenBrightness).isEqualTo(true); assertThat(displayPowerRequest.dozeScreenState).isEqualTo(Display.STATE_UNKNOWN); @@ -526,7 +512,6 @@ public class PowerGroupTest { assertThat(mPowerGroup.getWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE); mPowerGroup.setUserActivitySummaryLocked(USER_ACTIVITY_SCREEN_BRIGHT); mPowerGroup.updateLocked(/* screenBrightnessOverride= */ BRIGHTNESS, - /* autoBrightness = */ true, /* useProximitySensor= */ true, /* boostScreenBrightness= */ true, /* dozeScreenStateOverride= */ Display.STATE_ON, @@ -542,7 +527,6 @@ public class PowerGroupTest { mPowerGroup.mDisplayPowerRequest; assertThat(displayPowerRequest.policy).isEqualTo(POLICY_BRIGHT); assertThat(displayPowerRequest.screenBrightnessOverride).isWithin(PRECISION).of(BRIGHTNESS); - assertThat(displayPowerRequest.useAutoBrightness).isEqualTo(true); assertThat(displayPowerRequest.useProximitySensor).isEqualTo(true); assertThat(displayPowerRequest.boostScreenBrightness).isEqualTo(true); assertThat(displayPowerRequest.dozeScreenState).isEqualTo(Display.STATE_UNKNOWN); @@ -563,7 +547,6 @@ public class PowerGroupTest { .build(); assertThat(mPowerGroup.getWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE); mPowerGroup.updateLocked(/* screenBrightnessOverride= */ BRIGHTNESS, - /* autoBrightness = */ true, /* useProximitySensor= */ true, /* boostScreenBrightness= */ true, /* dozeScreenStateOverride= */ Display.STATE_ON, @@ -579,7 +562,6 @@ public class PowerGroupTest { mPowerGroup.mDisplayPowerRequest; assertThat(displayPowerRequest.policy).isEqualTo(POLICY_BRIGHT); assertThat(displayPowerRequest.screenBrightnessOverride).isWithin(PRECISION).of(BRIGHTNESS); - assertThat(displayPowerRequest.useAutoBrightness).isEqualTo(true); assertThat(displayPowerRequest.useProximitySensor).isEqualTo(true); assertThat(displayPowerRequest.boostScreenBrightness).isEqualTo(true); assertThat(displayPowerRequest.dozeScreenState).isEqualTo(Display.STATE_UNKNOWN); diff --git a/services/tests/wmtests/AndroidManifest.xml b/services/tests/wmtests/AndroidManifest.xml index 107bbe1c79e3..593ee4a7fa1a 100644 --- a/services/tests/wmtests/AndroidManifest.xml +++ b/services/tests/wmtests/AndroidManifest.xml @@ -45,6 +45,7 @@ <uses-permission android:name="android.permission.WRITE_DEVICE_CONFIG" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> + <uses-permission android:name="android.permission.MANAGE_MEDIA_PROJECTION"/> <!-- TODO: Remove largeHeap hack when memory leak is fixed (b/123984854) --> <application android:debuggable="true" diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java index 982137b00bfc..f983fa9b49e3 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -3284,7 +3284,7 @@ public class ActivityRecordTests extends WindowTestsBase { assertFalse(app2.mActivityRecord.mImeInsetsFrozenUntilStartInput); verify(app2.mClient, atLeastOnce()).resized(any(), anyBoolean(), any(), insetsStateCaptor.capture(), anyBoolean(), anyBoolean(), anyInt(), anyInt(), - anyBoolean()); + anyInt()); assertFalse(app2.getInsetsState().getSource(ITYPE_IME).isVisible()); } diff --git a/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java b/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java index 49fd1ab60e09..92dd047b5537 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java @@ -52,6 +52,8 @@ import android.view.SurfaceControl; import androidx.annotation.NonNull; import androidx.test.filters.SmallTest; +import com.android.server.wm.ContentRecorder.MediaProjectionManagerWrapper; + import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -79,7 +81,7 @@ public class ContentRecorderTests extends WindowTestsBase { private ContentRecordingSession mTaskSession; private static Point sSurfaceSize; private ContentRecorder mContentRecorder; - @Mock private ContentRecorder.MediaProjectionManagerWrapper mMediaProjectionManagerWrapper; + @Mock private MediaProjectionManagerWrapper mMediaProjectionManagerWrapper; private SurfaceControl mRecordedSurface; // Handle feature flag. private ConfigListener mConfigListener; @@ -241,7 +243,7 @@ public class ContentRecorderTests extends WindowTestsBase { } @Test - public void testOnTaskConfigurationChanged_resizesSurface() { + public void testOnTaskOrientationConfigurationChanged_resizesSurface() { mContentRecorder.setContentRecordingSession(mTaskSession); mContentRecorder.updateRecording(); @@ -256,6 +258,29 @@ public class ContentRecorderTests extends WindowTestsBase { } @Test + public void testOnTaskBoundsConfigurationChanged_notifiesCallback() { + final int recordedWidth = 333; + final int recordedHeight = 999; + // WHEN a recording is ongoing. + mContentRecorder.setContentRecordingSession(mTaskSession); + mContentRecorder.updateRecording(); + assertThat(mContentRecorder.isCurrentlyRecording()).isTrue(); + + // WHEN a configuration change arrives, and the recorded content is a different size. + mTask.setBounds(new Rect(0, 0, recordedWidth, recordedHeight)); + mContentRecorder.onConfigurationChanged(mDefaultDisplay.getLastOrientation()); + assertThat(mContentRecorder.isCurrentlyRecording()).isTrue(); + + // THEN content in the captured DisplayArea is scaled to fit the surface size. + verify(mTransaction, atLeastOnce()).setMatrix(eq(mRecordedSurface), anyFloat(), eq(0f), + eq(0f), + anyFloat()); + // THEN the resize callback is notified. + verify(mMediaProjectionManagerWrapper).notifyActiveProjectionCapturedContentResized( + recordedWidth, recordedHeight); + } + + @Test public void testPauseRecording_pausesRecording() { mContentRecorder.setContentRecordingSession(mDisplaySession); mContentRecorder.updateRecording(); @@ -324,6 +349,9 @@ public class ContentRecorderTests extends WindowTestsBase { int scaledWidth = Math.round((float) displayAreaBounds.width() / xScale); int xInset = (sSurfaceSize.x - scaledWidth) / 2; verify(mTransaction, atLeastOnce()).setPosition(mRecordedSurface, xInset, 0); + // THEN the resize callback is notified. + verify(mMediaProjectionManagerWrapper).notifyActiveProjectionCapturedContentResized( + displayAreaBounds.width(), displayAreaBounds.height()); } private static class RecordingTestToken extends Binder { diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java index 13ea99ae6fec..6877e4f8bfd3 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java @@ -166,6 +166,114 @@ public class SizeCompatTests extends WindowTestsBase { } @Test + public void testApplyStrategyToTranslucentActivities() { + mWm.mLetterboxConfiguration.setTranslucentLetterboxingOverrideEnabled(true); + setUpDisplaySizeWithApp(2000, 1000); + prepareUnresizable(mActivity, 1.5f /* maxAspect */, SCREEN_ORIENTATION_PORTRAIT); + mActivity.info.setMinAspectRatio(1.2f); + mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); + // Translucent Activity + final ActivityRecord translucentActivity = new ActivityBuilder(mAtm) + .setLaunchedFromUid(mActivity.getUid()) + .setScreenOrientation(SCREEN_ORIENTATION_LANDSCAPE) + .setMinAspectRatio(1.1f) + .setMaxAspectRatio(3f) + .build(); + doReturn(false).when(translucentActivity).fillsParent(); + mTask.addChild(translucentActivity); + // We check bounds + final Rect opaqueBounds = mActivity.getConfiguration().windowConfiguration.getBounds(); + final Rect translucentRequestedBounds = translucentActivity.getRequestedOverrideBounds(); + assertEquals(opaqueBounds, translucentRequestedBounds); + // We check orientation + final int translucentOrientation = + translucentActivity.getRequestedConfigurationOrientation(); + assertEquals(ORIENTATION_PORTRAIT, translucentOrientation); + // We check aspect ratios + assertEquals(1.2f, translucentActivity.getMinAspectRatio(), 0.00001f); + assertEquals(1.5f, translucentActivity.getMaxAspectRatio(), 0.00001f); + } + + @Test + public void testNotApplyStrategyToTranslucentActivitiesWithDifferentUid() { + mWm.mLetterboxConfiguration.setTranslucentLetterboxingOverrideEnabled(true); + setUpDisplaySizeWithApp(2000, 1000); + prepareUnresizable(mActivity, 1.5f /* maxAspect */, SCREEN_ORIENTATION_PORTRAIT); + mActivity.info.setMinAspectRatio(1.2f); + mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); + // Translucent Activity + final ActivityRecord translucentActivity = new ActivityBuilder(mAtm) + .setScreenOrientation(SCREEN_ORIENTATION_LANDSCAPE) + .setMinAspectRatio(1.1f) + .setMaxAspectRatio(3f) + .build(); + doReturn(false).when(translucentActivity).fillsParent(); + mTask.addChild(translucentActivity); + // We check bounds + final Rect opaqueBounds = mActivity.getConfiguration().windowConfiguration.getBounds(); + final Rect translucentRequestedBounds = translucentActivity.getRequestedOverrideBounds(); + assertNotEquals(opaqueBounds, translucentRequestedBounds); + } + + @Test + public void testApplyStrategyToMultipleTranslucentActivities() { + mWm.mLetterboxConfiguration.setTranslucentLetterboxingOverrideEnabled(true); + setUpDisplaySizeWithApp(2000, 1000); + prepareUnresizable(mActivity, 1.5f /* maxAspect */, SCREEN_ORIENTATION_PORTRAIT); + mActivity.info.setMinAspectRatio(1.2f); + mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); + // Translucent Activity + final ActivityRecord translucentActivity = new ActivityBuilder(mAtm) + .setLaunchedFromUid(mActivity.getUid()) + .setScreenOrientation(SCREEN_ORIENTATION_LANDSCAPE) + .setMinAspectRatio(1.1f) + .setMaxAspectRatio(3f) + .build(); + doReturn(false).when(translucentActivity).fillsParent(); + mTask.addChild(translucentActivity); + // We check bounds + final Rect opaqueBounds = mActivity.getConfiguration().windowConfiguration.getBounds(); + final Rect translucentRequestedBounds = translucentActivity.getRequestedOverrideBounds(); + assertEquals(opaqueBounds, translucentRequestedBounds); + // Launch another translucent activity + final ActivityRecord translucentActivity2 = new ActivityBuilder(mAtm) + .setLaunchedFromUid(mActivity.getUid()) + .setScreenOrientation(SCREEN_ORIENTATION_LANDSCAPE) + .build(); + doReturn(false).when(translucentActivity2).fillsParent(); + mTask.addChild(translucentActivity2); + // We check bounds + final Rect translucent2RequestedBounds = translucentActivity2.getRequestedOverrideBounds(); + assertEquals(opaqueBounds, translucent2RequestedBounds); + } + + @Test + public void testTranslucentActivitiesDontGoInSizeCompactMode() { + mWm.mLetterboxConfiguration.setTranslucentLetterboxingOverrideEnabled(true); + setUpDisplaySizeWithApp(2800, 1400); + mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); + prepareUnresizable(mActivity, -1f /* maxAspect */, SCREEN_ORIENTATION_PORTRAIT); + // Rotate to put activity in size compat mode. + rotateDisplay(mActivity.mDisplayContent, ROTATION_90); + assertTrue(mActivity.inSizeCompatMode()); + // Rotate back + rotateDisplay(mActivity.mDisplayContent, ROTATION_0); + assertFalse(mActivity.inSizeCompatMode()); + // We launch a transparent activity + final ActivityRecord translucentActivity = new ActivityBuilder(mAtm) + .setLaunchedFromUid(mActivity.getUid()) + .setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT) + .build(); + doReturn(true).when(translucentActivity).fillsParent(); + mTask.addChild(translucentActivity); + // It should not be in SCM + assertFalse(translucentActivity.inSizeCompatMode()); + // We rotate again + rotateDisplay(translucentActivity.mDisplayContent, ROTATION_90); + assertFalse(translucentActivity.inSizeCompatMode()); + } + + @Test public void testRestartProcessIfVisible() { setUpDisplaySizeWithApp(1000, 2500); doNothing().when(mSupervisor).scheduleRestartTimeout(mActivity); diff --git a/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncGroupTest.java b/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncGroupTest.java index 5e1fae095db8..7d9f29c0a63d 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncGroupTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncGroupTest.java @@ -20,6 +20,9 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; import android.platform.test.annotations.Presubmit; import android.view.SurfaceControl; @@ -31,12 +34,15 @@ import org.junit.Before; import org.junit.Test; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; @SmallTest @Presubmit public class SurfaceSyncGroupTest { + private final Executor mExecutor = Runnable::run; + @Before public void setup() { SurfaceSyncGroup.setTransactionFactory(StubTransaction::new); @@ -45,10 +51,11 @@ public class SurfaceSyncGroupTest { @Test public void testSyncOne() throws InterruptedException { final CountDownLatch finishedLatch = new CountDownLatch(1); - SurfaceSyncGroup syncGroup = new SurfaceSyncGroup(transaction -> finishedLatch.countDown()); + SurfaceSyncGroup syncGroup = new SurfaceSyncGroup(); + syncGroup.addSyncCompleteCallback(mExecutor, finishedLatch::countDown); SyncTarget syncTarget = new SyncTarget(); - syncGroup.addToSync(syncTarget); - syncGroup.markSyncReady(); + syncGroup.addToSync(syncTarget, false /* parentSyncGroupMerge */); + syncGroup.onTransactionReady(null); syncTarget.onBufferReady(); @@ -59,15 +66,16 @@ public class SurfaceSyncGroupTest { @Test public void testSyncMultiple() throws InterruptedException { final CountDownLatch finishedLatch = new CountDownLatch(1); - SurfaceSyncGroup syncGroup = new SurfaceSyncGroup(transaction -> finishedLatch.countDown()); + SurfaceSyncGroup syncGroup = new SurfaceSyncGroup(); + syncGroup.addSyncCompleteCallback(mExecutor, finishedLatch::countDown); SyncTarget syncTarget1 = new SyncTarget(); SyncTarget syncTarget2 = new SyncTarget(); SyncTarget syncTarget3 = new SyncTarget(); - syncGroup.addToSync(syncTarget1); - syncGroup.addToSync(syncTarget2); - syncGroup.addToSync(syncTarget3); - syncGroup.markSyncReady(); + syncGroup.addToSync(syncTarget1, false /* parentSyncGroupMerge */); + syncGroup.addToSync(syncTarget2, false /* parentSyncGroupMerge */); + syncGroup.addToSync(syncTarget3, false /* parentSyncGroupMerge */); + syncGroup.onTransactionReady(null); syncTarget1.onBufferReady(); assertNotEquals(0, finishedLatch.getCount()); @@ -83,35 +91,35 @@ public class SurfaceSyncGroupTest { @Test public void testAddSyncWhenSyncComplete() { - final CountDownLatch finishedLatch = new CountDownLatch(1); - SurfaceSyncGroup syncGroup = new SurfaceSyncGroup(transaction -> finishedLatch.countDown()); + SurfaceSyncGroup syncGroup = new SurfaceSyncGroup(); SyncTarget syncTarget1 = new SyncTarget(); SyncTarget syncTarget2 = new SyncTarget(); - assertTrue(syncGroup.addToSync(syncTarget1)); - syncGroup.markSyncReady(); + assertTrue(syncGroup.addToSync(syncTarget1, false /* parentSyncGroupMerge */)); + syncGroup.onTransactionReady(null); // Adding to a sync that has been completed is also invalid since the sync id has been // cleared. - assertFalse(syncGroup.addToSync(syncTarget2)); + assertFalse(syncGroup.addToSync(syncTarget2, false /* parentSyncGroupMerge */)); } @Test - public void testMultiplesyncGroups() throws InterruptedException { + public void testMultipleSyncGroups() throws InterruptedException { final CountDownLatch finishedLatch1 = new CountDownLatch(1); final CountDownLatch finishedLatch2 = new CountDownLatch(1); - SurfaceSyncGroup syncGroup1 = new SurfaceSyncGroup( - transaction -> finishedLatch1.countDown()); - SurfaceSyncGroup syncGroup2 = new SurfaceSyncGroup( - transaction -> finishedLatch2.countDown()); + SurfaceSyncGroup syncGroup1 = new SurfaceSyncGroup(); + SurfaceSyncGroup syncGroup2 = new SurfaceSyncGroup(); + + syncGroup1.addSyncCompleteCallback(mExecutor, finishedLatch1::countDown); + syncGroup2.addSyncCompleteCallback(mExecutor, finishedLatch2::countDown); SyncTarget syncTarget1 = new SyncTarget(); SyncTarget syncTarget2 = new SyncTarget(); - assertTrue(syncGroup1.addToSync(syncTarget1)); - assertTrue(syncGroup2.addToSync(syncTarget2)); - syncGroup1.markSyncReady(); - syncGroup2.markSyncReady(); + assertTrue(syncGroup1.addToSync(syncTarget1, false /* parentSyncGroupMerge */)); + assertTrue(syncGroup2.addToSync(syncTarget2, false /* parentSyncGroupMerge */)); + syncGroup1.onTransactionReady(null); + syncGroup2.onTransactionReady(null); syncTarget1.onBufferReady(); @@ -126,22 +134,23 @@ public class SurfaceSyncGroupTest { } @Test - public void testMergeSync() throws InterruptedException { + public void testAddSyncGroup() throws InterruptedException { final CountDownLatch finishedLatch1 = new CountDownLatch(1); final CountDownLatch finishedLatch2 = new CountDownLatch(1); - SurfaceSyncGroup syncGroup1 = new SurfaceSyncGroup( - transaction -> finishedLatch1.countDown()); - SurfaceSyncGroup syncGroup2 = new SurfaceSyncGroup( - transaction -> finishedLatch2.countDown()); + SurfaceSyncGroup syncGroup1 = new SurfaceSyncGroup(); + SurfaceSyncGroup syncGroup2 = new SurfaceSyncGroup(); + + syncGroup1.addSyncCompleteCallback(mExecutor, finishedLatch1::countDown); + syncGroup2.addSyncCompleteCallback(mExecutor, finishedLatch2::countDown); SyncTarget syncTarget1 = new SyncTarget(); SyncTarget syncTarget2 = new SyncTarget(); - assertTrue(syncGroup1.addToSync(syncTarget1)); - assertTrue(syncGroup2.addToSync(syncTarget2)); - syncGroup1.markSyncReady(); - syncGroup2.merge(syncGroup1); - syncGroup2.markSyncReady(); + assertTrue(syncGroup1.addToSync(syncTarget1, false /* parentSyncGroupMerge */)); + assertTrue(syncGroup2.addToSync(syncTarget2, false /* parentSyncGroupMerge */)); + syncGroup1.onTransactionReady(null); + syncGroup2.addToSync(syncGroup1, false /* parentSyncGroupMerge */); + syncGroup2.onTransactionReady(null); // Finish syncTarget2 first to test that the syncGroup is not complete until the merged sync // is also done. @@ -161,28 +170,29 @@ public class SurfaceSyncGroupTest { } @Test - public void testMergeSyncAlreadyComplete() throws InterruptedException { + public void testAddSyncAlreadyComplete() throws InterruptedException { final CountDownLatch finishedLatch1 = new CountDownLatch(1); final CountDownLatch finishedLatch2 = new CountDownLatch(1); - SurfaceSyncGroup syncGroup1 = new SurfaceSyncGroup( - transaction -> finishedLatch1.countDown()); - SurfaceSyncGroup syncGroup2 = new SurfaceSyncGroup( - transaction -> finishedLatch2.countDown()); + SurfaceSyncGroup syncGroup1 = new SurfaceSyncGroup(); + SurfaceSyncGroup syncGroup2 = new SurfaceSyncGroup(); + + syncGroup1.addSyncCompleteCallback(mExecutor, finishedLatch1::countDown); + syncGroup2.addSyncCompleteCallback(mExecutor, finishedLatch2::countDown); SyncTarget syncTarget1 = new SyncTarget(); SyncTarget syncTarget2 = new SyncTarget(); - assertTrue(syncGroup1.addToSync(syncTarget1)); - assertTrue(syncGroup2.addToSync(syncTarget2)); - syncGroup1.markSyncReady(); + assertTrue(syncGroup1.addToSync(syncTarget1, false /* parentSyncGroupMerge */)); + assertTrue(syncGroup2.addToSync(syncTarget2, false /* parentSyncGroupMerge */)); + syncGroup1.onTransactionReady(null); syncTarget1.onBufferReady(); // The first sync will still get a callback when it's sync requirements are done. finishedLatch1.await(5, TimeUnit.SECONDS); assertEquals(0, finishedLatch1.getCount()); - syncGroup2.merge(syncGroup1); - syncGroup2.markSyncReady(); + syncGroup2.addToSync(syncGroup1, false /* parentSyncGroupMerge */); + syncGroup2.onTransactionReady(null); syncTarget2.onBufferReady(); // Verify that the second sync will receive complete since the merged sync was already @@ -191,18 +201,145 @@ public class SurfaceSyncGroupTest { assertEquals(0, finishedLatch2.getCount()); } - private static class SyncTarget implements SurfaceSyncGroup.SyncTarget { - private SurfaceSyncGroup.TransactionReadyCallback mTransactionReadyCallback; + @Test + public void testAddSyncAlreadyInASync_NewSyncReadyFirst() throws InterruptedException { + final CountDownLatch finishedLatch1 = new CountDownLatch(1); + final CountDownLatch finishedLatch2 = new CountDownLatch(1); + SurfaceSyncGroup syncGroup1 = new SurfaceSyncGroup(); + SurfaceSyncGroup syncGroup2 = new SurfaceSyncGroup(); - @Override - public void onAddedToSyncGroup(SurfaceSyncGroup parentSyncGroup, - SurfaceSyncGroup.TransactionReadyCallback transactionReadyCallback) { - mTransactionReadyCallback = transactionReadyCallback; - } + syncGroup1.addSyncCompleteCallback(mExecutor, finishedLatch1::countDown); + syncGroup2.addSyncCompleteCallback(mExecutor, finishedLatch2::countDown); + + SyncTarget syncTarget1 = new SyncTarget(); + SyncTarget syncTarget2 = new SyncTarget(); + SyncTarget syncTarget3 = new SyncTarget(); + + assertTrue(syncGroup1.addToSync(syncTarget1, false /* parentSyncGroupMerge */)); + assertTrue(syncGroup1.addToSync(syncTarget2, false /* parentSyncGroupMerge */)); + + // Add syncTarget1 to syncGroup2 so it forces syncGroup1 into syncGroup2 + assertTrue(syncGroup2.addToSync(syncTarget1, false /* parentSyncGroupMerge */)); + assertTrue(syncGroup2.addToSync(syncTarget3, false /* parentSyncGroupMerge */)); + + syncGroup1.onTransactionReady(null); + syncGroup2.onTransactionReady(null); + + // Make target1 and target3 ready, but not target2. SyncGroup2 should not be ready since + // SyncGroup2 also waits for all of SyncGroup1 to finish, which includes target2 + syncTarget1.onBufferReady(); + syncTarget3.onBufferReady(); + + // Neither SyncGroup will be ready. + finishedLatch1.await(1, TimeUnit.SECONDS); + finishedLatch2.await(1, TimeUnit.SECONDS); + + assertEquals(1, finishedLatch1.getCount()); + assertEquals(1, finishedLatch2.getCount()); + + syncTarget2.onBufferReady(); + + // Both sync groups should be ready after target2 completed. + finishedLatch1.await(5, TimeUnit.SECONDS); + finishedLatch2.await(5, TimeUnit.SECONDS); + assertEquals(0, finishedLatch1.getCount()); + assertEquals(0, finishedLatch2.getCount()); + } + + @Test + public void testAddSyncAlreadyInASync_OldSyncFinishesFirst() throws InterruptedException { + final CountDownLatch finishedLatch1 = new CountDownLatch(1); + final CountDownLatch finishedLatch2 = new CountDownLatch(1); + SurfaceSyncGroup syncGroup1 = new SurfaceSyncGroup(); + SurfaceSyncGroup syncGroup2 = new SurfaceSyncGroup(); + + syncGroup1.addSyncCompleteCallback(mExecutor, finishedLatch1::countDown); + syncGroup2.addSyncCompleteCallback(mExecutor, finishedLatch2::countDown); + + SyncTarget syncTarget1 = new SyncTarget(); + SyncTarget syncTarget2 = new SyncTarget(); + SyncTarget syncTarget3 = new SyncTarget(); + + assertTrue(syncGroup1.addToSync(syncTarget1, false /* parentSyncGroupMerge */)); + assertTrue(syncGroup1.addToSync(syncTarget2, false /* parentSyncGroupMerge */)); + syncTarget2.onBufferReady(); + + // Add syncTarget1 to syncGroup2 so it forces syncGroup1 into syncGroup2 + assertTrue(syncGroup2.addToSync(syncTarget1, false /* parentSyncGroupMerge */)); + assertTrue(syncGroup2.addToSync(syncTarget3, false /* parentSyncGroupMerge */)); + + syncGroup1.onTransactionReady(null); + syncGroup2.onTransactionReady(null); + + syncTarget1.onBufferReady(); + + // Only SyncGroup1 will be ready, but SyncGroup2 still needs its own targets to be ready. + finishedLatch1.await(1, TimeUnit.SECONDS); + finishedLatch2.await(1, TimeUnit.SECONDS); + + assertEquals(0, finishedLatch1.getCount()); + assertEquals(1, finishedLatch2.getCount()); + + syncTarget3.onBufferReady(); + + // SyncGroup2 is finished after target3 completed. + finishedLatch2.await(1, TimeUnit.SECONDS); + assertEquals(0, finishedLatch2.getCount()); + } + + @Test + public void testParentSyncGroupMerge_true() { + // Temporarily set a new transaction factory so it will return the stub transaction for + // the sync group. + SurfaceControl.Transaction parentTransaction = spy(new StubTransaction()); + SurfaceSyncGroup.setTransactionFactory(() -> parentTransaction); + + final CountDownLatch finishedLatch = new CountDownLatch(1); + SurfaceSyncGroup syncGroup = new SurfaceSyncGroup(); + syncGroup.addSyncCompleteCallback(mExecutor, finishedLatch::countDown); + + SurfaceControl.Transaction targetTransaction = spy(new StubTransaction()); + SurfaceSyncGroup.setTransactionFactory(() -> targetTransaction); + + SyncTarget syncTarget = new SyncTarget(); + assertTrue(syncGroup.addToSync(syncTarget, true /* parentSyncGroupMerge */)); + syncTarget.onTransactionReady(null); + + // When parentSyncGroupMerge is true, the transaction passed in merges the main SyncGroup + // transaction first because it knows the previous parentSyncGroup is older so it should + // be overwritten by anything newer. + verify(targetTransaction).merge(parentTransaction); + verify(parentTransaction).merge(targetTransaction); + } + + @Test + public void testParentSyncGroupMerge_false() { + // Temporarily set a new transaction factory so it will return the stub transaction for + // the sync group. + SurfaceControl.Transaction parentTransaction = spy(new StubTransaction()); + SurfaceSyncGroup.setTransactionFactory(() -> parentTransaction); + + final CountDownLatch finishedLatch = new CountDownLatch(1); + SurfaceSyncGroup syncGroup = new SurfaceSyncGroup(); + syncGroup.addSyncCompleteCallback(mExecutor, finishedLatch::countDown); + + SurfaceControl.Transaction targetTransaction = spy(new StubTransaction()); + SurfaceSyncGroup.setTransactionFactory(() -> targetTransaction); + + SyncTarget syncTarget = new SyncTarget(); + assertTrue(syncGroup.addToSync(syncTarget, false /* parentSyncGroupMerge */)); + syncTarget.onTransactionReady(null); + + // When parentSyncGroupMerge is false, the transaction passed in should not merge + // the main SyncGroup since we don't need to change the transaction order + verify(targetTransaction, never()).merge(parentTransaction); + verify(parentTransaction).merge(targetTransaction); + } + private static class SyncTarget extends SurfaceSyncGroup { void onBufferReady() { SurfaceControl.Transaction t = new StubTransaction(); - mTransactionReadyCallback.onTransactionReady(t); + onTransactionReady(t); } } } diff --git a/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java b/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java index 3f8acc651110..6e72bf360295 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java +++ b/services/tests/wmtests/src/com/android/server/wm/TestIWindow.java @@ -46,7 +46,7 @@ public class TestIWindow extends IWindow.Stub { @Override public void resized(ClientWindowFrames frames, boolean reportDraw, MergedConfiguration mergedConfig, InsetsState insetsState, boolean forceLayout, - boolean alwaysConsumeSystemBars, int displayId, int seqId, boolean dragResizing) + boolean alwaysConsumeSystemBars, int displayId, int seqId, int resizeMode) throws RemoteException { } diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java index 183ccceec4f4..69e3244af1b6 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java @@ -276,9 +276,12 @@ public class WindowStateTests extends WindowTestsBase { assertFalse(imeWindow.canBeImeTarget()); // Simulate the window is in split screen root task. + final DockedTaskDividerController controller = + mDisplayContent.getDockedDividerController(); final Task rootTask = createTask(mDisplayContent, WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD); spyOn(appWindow); + spyOn(controller); spyOn(rootTask); rootTask.setFocusable(false); doReturn(rootTask).when(appWindow).getRootTask(); @@ -772,7 +775,7 @@ public class WindowStateTests extends WindowTestsBase { anyBoolean() /* reportDraw */, any() /* mergedConfig */, any() /* insetsState */, anyBoolean() /* forceLayout */, anyBoolean() /* alwaysConsumeSystemBars */, anyInt() /* displayId */, - anyInt() /* seqId */, anyBoolean() /* dragResizing */); + anyInt() /* seqId */, anyInt() /* resizeMode */); } catch (RemoteException ignored) { } win.reportResized(); diff --git a/services/tests/wmtests/src/com/android/server/wm/utils/CoordinateTransformsTest.java b/services/tests/wmtests/src/com/android/server/wm/utils/CoordinateTransformsTest.java index 99ceb2011db3..3e74626b66d7 100644 --- a/services/tests/wmtests/src/com/android/server/wm/utils/CoordinateTransformsTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/utils/CoordinateTransformsTest.java @@ -21,6 +21,7 @@ import static android.view.Surface.ROTATION_180; import static android.view.Surface.ROTATION_270; import static android.view.Surface.ROTATION_90; +import static com.android.server.wm.utils.CoordinateTransforms.computeRotationMatrix; import static com.android.server.wm.utils.CoordinateTransforms.transformLogicalToPhysicalCoordinates; import static com.android.server.wm.utils.CoordinateTransforms.transformPhysicalToLogicalCoordinates; import static com.android.server.wm.utils.CoordinateTransforms.transformToRotation; @@ -185,6 +186,44 @@ public class CoordinateTransformsTest { assertEquals(mMatrix2, mMatrix); } + @Test + public void rotate_0_bottomRight() { + computeRotationMatrix(ROTATION_0, W, H, mMatrix); + PointF newPoints = checkMappedPoints(W, H); + assertEquals(W, newPoints.x, 0); + assertEquals(H, newPoints.y, 0); + } + + @Test + public void rotate_90_bottomRight() { + computeRotationMatrix(ROTATION_90, W, H, mMatrix); + PointF newPoints = checkMappedPoints(W, H); + assertEquals(0, newPoints.x, 0); + assertEquals(W, newPoints.y, 0); + } + + @Test + public void rotate_180_bottomRight() { + computeRotationMatrix(ROTATION_180, W, H, mMatrix); + PointF newPoints = checkMappedPoints(W, H); + assertEquals(0, newPoints.x, 0); + assertEquals(0, newPoints.y, 0); + } + + @Test + public void rotate_270_bottomRight() { + computeRotationMatrix(ROTATION_270, W, H, mMatrix); + PointF newPoints = checkMappedPoints(W, H); + assertEquals(H, newPoints.x, 0); + assertEquals(0, newPoints.y, 0); + } + + private PointF checkMappedPoints(int x, int y) { + final float[] fs = new float[] {x, y}; + mMatrix.mapPoints(fs); + return new PointF(fs[0], fs[1]); + } + private void assertMatricesAreInverses(Matrix matrix, Matrix matrix2) { final Matrix concat = new Matrix(); concat.setConcat(matrix, matrix2); diff --git a/services/tests/wmtests/src/com/android/server/wm/utils/RotationAnimationUtilsTest.java b/services/tests/wmtests/src/com/android/server/wm/utils/RotationAnimationUtilsTest.java index cd4d65d7dab1..ff43ff718220 100644 --- a/services/tests/wmtests/src/com/android/server/wm/utils/RotationAnimationUtilsTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/utils/RotationAnimationUtilsTest.java @@ -23,15 +23,11 @@ import static org.junit.Assert.assertEquals; import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.ColorSpace; -import android.graphics.Matrix; -import android.graphics.PointF; import android.hardware.HardwareBuffer; import android.platform.test.annotations.Presubmit; -import android.view.Surface; import com.android.internal.policy.TransitionAnimation; -import org.junit.Before; import org.junit.Test; @Presubmit @@ -39,16 +35,8 @@ public class RotationAnimationUtilsTest { private static final int BITMAP_HEIGHT = 100; private static final int BITMAP_WIDTH = 100; - private static final int POINT_WIDTH = 1000; - private static final int POINT_HEIGHT = 2000; private ColorSpace mColorSpace = ColorSpace.get(ColorSpace.Named.DISPLAY_P3); - private Matrix mMatrix; - - @Before - public void setup() { - mMatrix = new Matrix(); - } @Test public void blackLuma() { @@ -93,48 +81,6 @@ public class RotationAnimationUtilsTest { assertEquals(1, borderLuma, 0); } - @Test - public void rotate_0_bottomRight() { - RotationAnimationUtils.createRotationMatrix(Surface.ROTATION_0, - POINT_WIDTH, POINT_HEIGHT, mMatrix); - PointF newPoints = checkMappedPoints(POINT_WIDTH, POINT_HEIGHT); - assertEquals(POINT_WIDTH, newPoints.x, 0); - assertEquals(POINT_HEIGHT, newPoints.y, 0); - } - - @Test - public void rotate_90_bottomRight() { - RotationAnimationUtils.createRotationMatrix(Surface.ROTATION_90, - POINT_WIDTH, POINT_HEIGHT, mMatrix); - PointF newPoints = checkMappedPoints(POINT_WIDTH, POINT_HEIGHT); - assertEquals(0, newPoints.x, 0); - assertEquals(POINT_WIDTH, newPoints.y, 0); - } - - @Test - public void rotate_180_bottomRight() { - RotationAnimationUtils.createRotationMatrix(Surface.ROTATION_180, - POINT_WIDTH, POINT_HEIGHT, mMatrix); - PointF newPoints = checkMappedPoints(POINT_WIDTH, POINT_HEIGHT); - assertEquals(0, newPoints.x, 0); - assertEquals(0, newPoints.y, 0); - } - - @Test - public void rotate_270_bottomRight() { - RotationAnimationUtils.createRotationMatrix(Surface.ROTATION_270, - POINT_WIDTH, POINT_HEIGHT, mMatrix); - PointF newPoints = checkMappedPoints(POINT_WIDTH, POINT_HEIGHT); - assertEquals(POINT_HEIGHT, newPoints.x, 0); - assertEquals(0, newPoints.y, 0); - } - - private PointF checkMappedPoints(int x, int y) { - final float[] fs = new float[] {x, y}; - mMatrix.mapPoints(fs); - return new PointF(fs[0], fs[1]); - } - private Bitmap createBitmap(float luma) { return createBitmap(luma, BITMAP_WIDTH, BITMAP_HEIGHT); } diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/DspTrustedHotwordDetectorSession.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/DspTrustedHotwordDetectorSession.java index 9375b59d9123..84bd71601643 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/DspTrustedHotwordDetectorSession.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/DspTrustedHotwordDetectorSession.java @@ -28,6 +28,7 @@ import android.annotation.NonNull; import android.content.Context; import android.hardware.soundtrigger.SoundTrigger; import android.media.permission.Identity; +import android.os.IBinder; import android.os.PersistableBundle; import android.os.RemoteException; import android.os.SharedMemory; @@ -74,12 +75,13 @@ final class DspTrustedHotwordDetectorSession extends HotwordDetectorSession { DspTrustedHotwordDetectorSession( @NonNull HotwordDetectionConnection.ServiceConnection remoteHotwordDetectionService, - @NonNull Object lock, @NonNull Context context, + @NonNull Object lock, @NonNull Context context, @NonNull IBinder token, @NonNull IHotwordRecognitionStatusCallback callback, int voiceInteractionServiceUid, Identity voiceInteractorIdentity, @NonNull ScheduledExecutorService scheduledExecutorService, boolean logging) { - super(remoteHotwordDetectionService, lock, context, callback, voiceInteractionServiceUid, - voiceInteractorIdentity, scheduledExecutorService, logging); + super(remoteHotwordDetectionService, lock, context, token, callback, + voiceInteractionServiceUid, voiceInteractorIdentity, scheduledExecutorService, + logging); } @SuppressWarnings("GuardedBy") diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java index 411bad601bf4..6a7a2f98d481 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java @@ -49,11 +49,12 @@ import android.os.SharedMemory; import android.provider.DeviceConfig; import android.service.voice.HotwordDetectionService; import android.service.voice.HotwordDetector; -import android.service.voice.IHotwordDetectionService; import android.service.voice.IMicrophoneHotwordDetectionVoiceInteractionCallback; +import android.service.voice.ISandboxedDetectionService; import android.service.voice.VoiceInteractionManagerInternal.HotwordDetectionServiceIdentity; import android.speech.IRecognitionServiceManager; import android.util.Slog; +import android.util.SparseArray; import android.view.contentcapture.IContentCaptureManager; import com.android.internal.annotations.GuardedBy; @@ -68,6 +69,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; import java.util.function.Function; /** @@ -113,18 +115,18 @@ final class HotwordDetectionConnection { @GuardedBy("mLock") private boolean mDebugHotwordLogging = false; + /** + * For multiple detectors feature, we only support one AlwaysOnHotwordDetector and one + * SoftwareHotwordDetector at the same time. We use SparseArray with detector type as the key + * to record the detectors. + */ @GuardedBy("mLock") - private final HotwordDetectorSession mHotwordDetectorSession; + private final SparseArray<HotwordDetectorSession> mHotwordDetectorSessions = + new SparseArray<>(); HotwordDetectionConnection(Object lock, Context context, int voiceInteractionServiceUid, Identity voiceInteractorIdentity, ComponentName serviceName, int userId, - boolean bindInstantServiceAllowed, @Nullable PersistableBundle options, - @Nullable SharedMemory sharedMemory, - @NonNull IHotwordRecognitionStatusCallback callback, int detectorType) { - if (callback == null) { - Slog.w(TAG, "Callback is null while creating connection"); - throw new IllegalArgumentException("Callback is null while creating connection"); - } + boolean bindInstantServiceAllowed, int detectorType) { mLock = lock; mContext = context; mVoiceInteractionServiceUid = voiceInteractionServiceUid; @@ -142,19 +144,6 @@ final class HotwordDetectionConnection { mRemoteHotwordDetectionService = mServiceConnectionFactory.createLocked(); mLastRestartInstant = Instant.now(); - if (detectorType == HotwordDetector.DETECTOR_TYPE_TRUSTED_HOTWORD_DSP) { - mHotwordDetectorSession = new DspTrustedHotwordDetectorSession( - mRemoteHotwordDetectionService, mLock, mContext, callback, - mVoiceInteractionServiceUid, mVoiceInteractorIdentity, - mScheduledExecutorService, mDebugHotwordLogging); - } else { - mHotwordDetectorSession = new SoftwareTrustedHotwordDetectorSession( - mRemoteHotwordDetectionService, mLock, mContext, callback, - mVoiceInteractionServiceUid, mVoiceInteractorIdentity, - mScheduledExecutorService, mDebugHotwordLogging); - } - mHotwordDetectorSession.initialize(options, sharedMemory); - if (mReStartPeriodSeconds <= 0) { mCancellationTaskFuture = null; } else { @@ -210,7 +199,10 @@ final class HotwordDetectionConnection { void cancelLocked() { Slog.v(TAG, "cancelLocked"); clearDebugHotwordLoggingTimeoutLocked(); - mHotwordDetectorSession.destroyLocked(); + runForEachHotwordDetectorSessionLocked((session) -> { + session.destroyLocked(); + }); + mHotwordDetectorSessions.clear(); mDebugHotwordLogging = false; mRemoteHotwordDetectionService.unbind(); LocalServices.getService(PermissionManagerServiceInternal.class) @@ -220,7 +212,7 @@ final class HotwordDetectionConnection { } mIdentity = null; if (mCancellationTaskFuture != null) { - mCancellationTaskFuture.cancel(/* may interrupt */ true); + mCancellationTaskFuture.cancel(/* mayInterruptIfRunning= */ true); } if (mAudioFlinger != null) { mAudioFlinger.unlinkToDeath(mAudioServerDeathRecipient, /* flags= */ 0); @@ -228,57 +220,65 @@ final class HotwordDetectionConnection { } @SuppressWarnings("GuardedBy") - void updateStateLocked(PersistableBundle options, SharedMemory sharedMemory) { - mHotwordDetectorSession.updateStateLocked(options, sharedMemory, mLastRestartInstant); + void updateStateLocked(PersistableBundle options, SharedMemory sharedMemory, + @NonNull IBinder token) { + final HotwordDetectorSession session = getDetectorSessionByTokenLocked(token); + if (session == null) { + Slog.v(TAG, "Not found the detector by token"); + return; + } + session.updateStateLocked(options, sharedMemory, mLastRestartInstant); } /** * This method is only used by SoftwareHotwordDetector. */ - void startListeningFromMic( + void startListeningFromMicLocked( AudioFormat audioFormat, IMicrophoneHotwordDetectionVoiceInteractionCallback callback) { if (DEBUG) { - Slog.d(TAG, "startListeningFromMic"); + Slog.d(TAG, "startListeningFromMicLocked"); } - synchronized (mLock) { - if (!(mHotwordDetectorSession instanceof SoftwareTrustedHotwordDetectorSession)) { - Slog.d(TAG, "It is not a software detector"); - return; - } - ((SoftwareTrustedHotwordDetectorSession) mHotwordDetectorSession) - .startListeningFromMicLocked(audioFormat, callback); + // We only support one Dsp trusted hotword detector and one software hotword detector at + // the same time, so we can reuse original single software trusted hotword mechanism. + final SoftwareTrustedHotwordDetectorSession session = + getSoftwareTrustedHotwordDetectorSessionLocked(); + if (session == null) { + return; } + session.startListeningFromMicLocked(audioFormat, callback); } - public void startListeningFromExternalSource( + public void startListeningFromExternalSourceLocked( ParcelFileDescriptor audioStream, AudioFormat audioFormat, @Nullable PersistableBundle options, + @NonNull IBinder token, IMicrophoneHotwordDetectionVoiceInteractionCallback callback) { if (DEBUG) { - Slog.d(TAG, "startListeningFromExternalSource"); + Slog.d(TAG, "startListeningFromExternalSourceLocked"); } - synchronized (mLock) { - mHotwordDetectorSession.startListeningFromExternalSourceLocked(audioStream, audioFormat, - options, callback); + final HotwordDetectorSession session = getDetectorSessionByTokenLocked(token); + if (session == null) { + Slog.v(TAG, "Not found the detector by token"); + return; } + session.startListeningFromExternalSourceLocked(audioStream, audioFormat, options, callback); } /** * This method is only used by SoftwareHotwordDetector. */ - void stopListening() { + void stopListeningFromMicLocked() { if (DEBUG) { - Slog.d(TAG, "stopListening"); + Slog.d(TAG, "stopListeningFromMicLocked"); } - synchronized (mLock) { - if (!(mHotwordDetectorSession instanceof SoftwareTrustedHotwordDetectorSession)) { - Slog.d(TAG, "It is not a software detector"); - return; - } - ((SoftwareTrustedHotwordDetectorSession) mHotwordDetectorSession).stopListeningLocked(); + final SoftwareTrustedHotwordDetectorSession session = + getSoftwareTrustedHotwordDetectorSessionLocked(); + if (session == null) { + return; } + session.stopListeningFromMicLocked(); } void triggerHardwareRecognitionEventForTestLocked( @@ -295,13 +295,16 @@ final class HotwordDetectionConnection { if (DEBUG) { Slog.d(TAG, "detectFromDspSource"); } + // We only support one Dsp trusted hotword detector and one software hotword detector at + // the same time, so we can reuse original single Dsp trusted hotword mechanism. synchronized (mLock) { - if (!(mHotwordDetectorSession instanceof DspTrustedHotwordDetectorSession)) { - Slog.d(TAG, "It is not a Dsp detector"); + final DspTrustedHotwordDetectorSession session = + getDspTrustedHotwordDetectorSessionLocked(); + if (session == null || !session.isSameCallback(externalCallback)) { + Slog.v(TAG, "Not found the Dsp detector by callback"); return; } - ((DspTrustedHotwordDetectorSession) mHotwordDetectorSession).detectFromDspSourceLocked( - recognitionEvent, externalCallback); + session.detectFromDspSourceLocked(recognitionEvent, externalCallback); } } @@ -317,7 +320,9 @@ final class HotwordDetectionConnection { Slog.v(TAG, "setDebugHotwordLoggingLocked: " + logging); clearDebugHotwordLoggingTimeoutLocked(); mDebugHotwordLogging = logging; - mHotwordDetectorSession.setDebugHotwordLoggingLocked(logging); + runForEachHotwordDetectorSessionLocked((session) -> { + session.setDebugHotwordLoggingLocked(logging); + }); if (logging) { // Reset mDebugHotwordLogging to false after one hour @@ -325,7 +330,9 @@ final class HotwordDetectionConnection { Slog.v(TAG, "Timeout to reset mDebugHotwordLogging to false"); synchronized (mLock) { mDebugHotwordLogging = false; - mHotwordDetectorSession.setDebugHotwordLoggingLocked(false); + runForEachHotwordDetectorSessionLocked((session) -> { + session.setDebugHotwordLoggingLocked(false); + }); } }, RESET_DEBUG_HOTWORD_LOGGING_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); } @@ -333,7 +340,7 @@ final class HotwordDetectionConnection { private void clearDebugHotwordLoggingTimeoutLocked() { if (mDebugHotwordLoggingTimeoutFuture != null) { - mDebugHotwordLoggingTimeoutFuture.cancel(/* mayInterruptIfRunning= */true); + mDebugHotwordLoggingTimeoutFuture.cancel(/* mayInterruptIfRunning= */ true); mDebugHotwordLoggingTimeoutFuture = null; } } @@ -350,12 +357,14 @@ final class HotwordDetectionConnection { mRemoteHotwordDetectionService = mServiceConnectionFactory.createLocked(); Slog.v(TAG, "Started the new process, dispatching processRestarted to detector"); - mHotwordDetectorSession.updateRemoteHotwordDetectionServiceLocked( - mRemoteHotwordDetectionService); - mHotwordDetectorSession.informRestartProcessLocked(); + runForEachHotwordDetectorSessionLocked((session) -> { + session.updateRemoteHotwordDetectionServiceLocked(mRemoteHotwordDetectionService); + session.informRestartProcessLocked(); + }); if (DEBUG) { Slog.i(TAG, "processRestarted is dispatched done, unbinding from the old process"); } + oldConnection.ignoreConnectionStatusEvents(); oldConnection.unbind(); if (previousIdentity != null) { @@ -431,8 +440,10 @@ final class HotwordDetectionConnection { pw.print(prefix); pw.print("mLastRestartInstant="); pw.println(mLastRestartInstant); pw.print(prefix); pw.print("mDetectorType="); pw.println(HotwordDetector.detectorTypeToString(mDetectorType)); - pw.print(prefix); pw.println("HotwordDetectorSession"); - mHotwordDetectorSession.dumpLocked(prefix, pw); + pw.print(prefix); pw.println("HotwordDetectorSession(s)"); + runForEachHotwordDetectorSessionLocked((session) -> { + session.dumpLocked(prefix, pw); + }); } } @@ -450,7 +461,7 @@ final class HotwordDetectionConnection { ServiceConnection createLocked() { ServiceConnection connection = new ServiceConnection(mContext, mIntent, mBindingFlags, mUser, - IHotwordDetectionService.Stub::asInterface, + ISandboxedDetectionService.Stub::asInterface, mRestartCount++ % MAX_ISOLATED_PROCESS_NUMBER); connection.connect(); @@ -462,7 +473,7 @@ final class HotwordDetectionConnection { } } - class ServiceConnection extends ServiceConnector.Impl<IHotwordDetectionService> { + class ServiceConnection extends ServiceConnector.Impl<ISandboxedDetectionService> { private final Object mLock = new Object(); private final Intent mIntent; @@ -475,7 +486,7 @@ final class HotwordDetectionConnection { ServiceConnection(@NonNull Context context, @NonNull Intent intent, int bindingFlags, int userId, - @Nullable Function<IBinder, IHotwordDetectionService> binderAsInterface, + @Nullable Function<IBinder, ISandboxedDetectionService> binderAsInterface, int instanceNumber) { super(context, intent, bindingFlags, userId, binderAsInterface); this.mIntent = intent; @@ -484,7 +495,7 @@ final class HotwordDetectionConnection { } @Override // from ServiceConnector.Impl - protected void onServiceConnectionStatusChanged(IHotwordDetectionService service, + protected void onServiceConnectionStatusChanged(ISandboxedDetectionService service, boolean connected) { if (DEBUG) { Slog.d(TAG, "onServiceConnectionStatusChanged connected = " + connected); @@ -525,8 +536,10 @@ final class HotwordDetectionConnection { } } synchronized (HotwordDetectionConnection.this.mLock) { - mHotwordDetectorSession.reportErrorLocked( - HotwordDetectorSession.HOTWORD_DETECTION_SERVICE_DIED); + runForEachHotwordDetectorSessionLocked((session) -> { + session.reportErrorLocked( + HotwordDetectorSession.HOTWORD_DETECTION_SERVICE_DIED); + }); } } @@ -571,6 +584,93 @@ final class HotwordDetectionConnection { } } + @SuppressWarnings("GuardedBy") + void createDetectorLocked( + @Nullable PersistableBundle options, + @Nullable SharedMemory sharedMemory, + @NonNull IBinder token, + @NonNull IHotwordRecognitionStatusCallback callback, + int detectorType) { + // We only support one Dsp trusted hotword detector and one software hotword detector at + // the same time, remove existing one. + HotwordDetectorSession removeSession = mHotwordDetectorSessions.get(detectorType); + if (removeSession != null) { + removeSession.destroyLocked(); + mHotwordDetectorSessions.remove(detectorType); + } + final HotwordDetectorSession session; + if (detectorType == HotwordDetector.DETECTOR_TYPE_TRUSTED_HOTWORD_DSP) { + session = new DspTrustedHotwordDetectorSession(mRemoteHotwordDetectionService, + mLock, mContext, token, callback, mVoiceInteractionServiceUid, + mVoiceInteractorIdentity, mScheduledExecutorService, mDebugHotwordLogging); + } else { + session = new SoftwareTrustedHotwordDetectorSession( + mRemoteHotwordDetectionService, mLock, mContext, token, callback, + mVoiceInteractionServiceUid, mVoiceInteractorIdentity, + mScheduledExecutorService, mDebugHotwordLogging); + } + mHotwordDetectorSessions.put(detectorType, session); + session.initialize(options, sharedMemory); + } + + @SuppressWarnings("GuardedBy") + void destroyDetectorLocked(@NonNull IBinder token) { + final HotwordDetectorSession session = getDetectorSessionByTokenLocked(token); + if (session != null) { + session.destroyLocked(); + final int index = mHotwordDetectorSessions.indexOfValue(session); + if (index < 0 || index > mHotwordDetectorSessions.size() - 1) { + return; + } + mHotwordDetectorSessions.removeAt(index); + } + } + + @SuppressWarnings("GuardedBy") + private HotwordDetectorSession getDetectorSessionByTokenLocked(IBinder token) { + if (token == null) { + return null; + } + for (int i = 0; i < mHotwordDetectorSessions.size(); i++) { + final HotwordDetectorSession session = mHotwordDetectorSessions.valueAt(i); + if (!session.isDestroyed() && session.isSameToken(token)) { + return session; + } + } + return null; + } + + @SuppressWarnings("GuardedBy") + private DspTrustedHotwordDetectorSession getDspTrustedHotwordDetectorSessionLocked() { + final HotwordDetectorSession session = mHotwordDetectorSessions.get( + HotwordDetector.DETECTOR_TYPE_TRUSTED_HOTWORD_DSP); + if (session == null || session.isDestroyed()) { + Slog.v(TAG, "Not found the Dsp detector"); + return null; + } + return (DspTrustedHotwordDetectorSession) session; + } + + @SuppressWarnings("GuardedBy") + private SoftwareTrustedHotwordDetectorSession getSoftwareTrustedHotwordDetectorSessionLocked() { + final HotwordDetectorSession session = mHotwordDetectorSessions.get( + HotwordDetector.DETECTOR_TYPE_TRUSTED_HOTWORD_SOFTWARE); + if (session == null || session.isDestroyed()) { + Slog.v(TAG, "Not found the software detector"); + return null; + } + return (SoftwareTrustedHotwordDetectorSession) session; + } + + @SuppressWarnings("GuardedBy") + private void runForEachHotwordDetectorSessionLocked( + @NonNull Consumer<HotwordDetectorSession> action) { + for (int i = 0; i < mHotwordDetectorSessions.size(); i++) { + HotwordDetectorSession session = mHotwordDetectorSessions.valueAt(i); + action.accept(session); + } + } + private static void updateAudioFlinger(ServiceConnection connection, IBinder audioFlinger) { // TODO: Consider using a proxy that limits the exposed API surface. connection.run(service -> service.updateAudioFlinger(audioFlinger)); diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectorSession.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectorSession.java index 742c324fa66f..689423ad3e3e 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectorSession.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectorSession.java @@ -57,6 +57,7 @@ import android.media.permission.Identity; import android.media.permission.PermissionUtil; import android.os.Binder; import android.os.Bundle; +import android.os.IBinder; import android.os.IRemoteCallback; import android.os.ParcelFileDescriptor; import android.os.PersistableBundle; @@ -182,16 +183,18 @@ abstract class HotwordDetectorSession { private boolean mDestroyed = false; @GuardedBy("mLock") boolean mPerformingExternalSourceHotwordDetection; + @NonNull final IBinder mToken; HotwordDetectorSession( @NonNull HotwordDetectionConnection.ServiceConnection remoteHotwordDetectionService, - @NonNull Object lock, @NonNull Context context, + @NonNull Object lock, @NonNull Context context, @NonNull IBinder token, @NonNull IHotwordRecognitionStatusCallback callback, int voiceInteractionServiceUid, Identity voiceInteractorIdentity, @NonNull ScheduledExecutorService scheduledExecutorService, boolean logging) { mRemoteHotwordDetectionService = remoteHotwordDetectionService; mLock = lock; mContext = context; + mToken = token; mCallback = callback; mVoiceInteractionServiceUid = voiceInteractionServiceUid; mVoiceInteractorIdentity = voiceInteractorIdentity; @@ -474,8 +477,8 @@ abstract class HotwordDetectorSession { callback.onError(); return; } - callback.onDetected(newResult, null /* audioFormat */, - null /* audioStream */); + callback.onDetected(newResult, /* audioFormat= */ null, + /* audioStream= */ null); Slog.i(TAG, "Egressed " + HotwordDetectedResult.getUsageSize(newResult) + " bits from hotword trusted process"); @@ -542,6 +545,30 @@ abstract class HotwordDetectorSession { */ abstract void informRestartProcessLocked(); + boolean isSameCallback(@Nullable IHotwordRecognitionStatusCallback callback) { + synchronized (mLock) { + if (callback == null) { + return false; + } + return mCallback.asBinder().equals(callback.asBinder()); + } + } + + boolean isSameToken(@NonNull IBinder token) { + synchronized (mLock) { + if (token == null) { + return false; + } + return mToken == token; + } + } + + boolean isDestroyed() { + synchronized (mLock) { + return mDestroyed; + } + } + private static Pair<ParcelFileDescriptor, ParcelFileDescriptor> createPipe() { ParcelFileDescriptor[] fileDescriptors; try { diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/SoftwareTrustedHotwordDetectorSession.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/SoftwareTrustedHotwordDetectorSession.java index 00aec71309c8..4eb997a610a4 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/SoftwareTrustedHotwordDetectorSession.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/SoftwareTrustedHotwordDetectorSession.java @@ -27,6 +27,7 @@ import android.annotation.NonNull; import android.content.Context; import android.media.AudioFormat; import android.media.permission.Identity; +import android.os.IBinder; import android.os.PersistableBundle; import android.os.RemoteException; import android.os.SharedMemory; @@ -35,8 +36,8 @@ import android.service.voice.HotwordDetectionService; import android.service.voice.HotwordDetector; import android.service.voice.HotwordRejectedResult; import android.service.voice.IDspHotwordDetectionCallback; -import android.service.voice.IHotwordDetectionService; import android.service.voice.IMicrophoneHotwordDetectionVoiceInteractionCallback; +import android.service.voice.ISandboxedDetectionService; import android.util.Slog; import com.android.internal.annotations.GuardedBy; @@ -63,12 +64,13 @@ final class SoftwareTrustedHotwordDetectorSession extends HotwordDetectorSession SoftwareTrustedHotwordDetectorSession( @NonNull HotwordDetectionConnection.ServiceConnection remoteHotwordDetectionService, - @NonNull Object lock, @NonNull Context context, + @NonNull Object lock, @NonNull Context context, @NonNull IBinder token, @NonNull IHotwordRecognitionStatusCallback callback, int voiceInteractionServiceUid, Identity voiceInteractorIdentity, @NonNull ScheduledExecutorService scheduledExecutorService, boolean logging) { - super(remoteHotwordDetectionService, lock, context, callback, voiceInteractionServiceUid, - voiceInteractorIdentity, scheduledExecutorService, logging); + super(remoteHotwordDetectionService, lock, context, token, callback, + voiceInteractionServiceUid, voiceInteractorIdentity, scheduledExecutorService, + logging); } @SuppressWarnings("GuardedBy") @@ -167,9 +169,9 @@ final class SoftwareTrustedHotwordDetectorSession extends HotwordDetectorSession } @SuppressWarnings("GuardedBy") - void stopListeningLocked() { + void stopListeningFromMicLocked() { if (DEBUG) { - Slog.d(TAG, "stopListeningLocked"); + Slog.d(TAG, "stopListeningFromMicLocked"); } if (!mPerformingSoftwareHotwordDetection) { Slog.i(TAG, "Hotword detection is not running"); @@ -177,7 +179,7 @@ final class SoftwareTrustedHotwordDetectorSession extends HotwordDetectorSession } mPerformingSoftwareHotwordDetection = false; - mRemoteHotwordDetectionService.run(IHotwordDetectionService::stopDetection); + mRemoteHotwordDetectionService.run(ISandboxedDetectionService::stopDetection); closeExternalAudioStreamLocked("stopping requested"); } diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java index 7207e3738d77..9a0218845038 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java @@ -1245,14 +1245,15 @@ public class VoiceInteractionManagerService extends SystemService { @Override public void updateState( @Nullable PersistableBundle options, - @Nullable SharedMemory sharedMemory) { + @Nullable SharedMemory sharedMemory, + @NonNull IBinder token) { super.updateState_enforcePermission(); synchronized (this) { enforceIsCurrentVoiceInteractionService(); Binder.withCleanCallingIdentity( - () -> mImpl.updateStateLocked(options, sharedMemory)); + () -> mImpl.updateStateLocked(options, sharedMemory, token)); } } @@ -1262,6 +1263,7 @@ public class VoiceInteractionManagerService extends SystemService { @NonNull Identity voiceInteractorIdentity, @Nullable PersistableBundle options, @Nullable SharedMemory sharedMemory, + @NonNull IBinder token, IHotwordRecognitionStatusCallback callback, int detectorType) { super.initAndVerifyDetector_enforcePermission(); @@ -1274,7 +1276,20 @@ public class VoiceInteractionManagerService extends SystemService { Binder.withCleanCallingIdentity( () -> mImpl.initAndVerifyDetectorLocked(voiceInteractorIdentity, options, - sharedMemory, callback, detectorType)); + sharedMemory, token, callback, detectorType)); + } + } + + @Override + public void destroyDetector(@NonNull IBinder token) { + synchronized (this) { + if (mImpl == null) { + Slog.w(TAG, "destroyDetector without running voice interaction service"); + return; + } + + Binder.withCleanCallingIdentity( + () -> mImpl.destroyDetectorLocked(token)); } } @@ -1326,6 +1341,7 @@ public class VoiceInteractionManagerService extends SystemService { ParcelFileDescriptor audioStream, AudioFormat audioFormat, PersistableBundle options, + @NonNull IBinder token, IMicrophoneHotwordDetectionVoiceInteractionCallback callback) throws RemoteException { synchronized (this) { @@ -1338,8 +1354,8 @@ public class VoiceInteractionManagerService extends SystemService { } final long caller = Binder.clearCallingIdentity(); try { - mImpl.startListeningFromExternalSourceLocked( - audioStream, audioFormat, options, callback); + mImpl.startListeningFromExternalSourceLocked(audioStream, audioFormat, options, + token, callback); } finally { Binder.restoreCallingIdentity(caller); } diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java index 6674520091c1..f041adcc7d77 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java @@ -56,7 +56,6 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.os.SharedMemory; import android.os.UserHandle; -import android.service.voice.HotwordDetector; import android.service.voice.IMicrophoneHotwordDetectionVoiceInteractionCallback; import android.service.voice.IVoiceInteractionService; import android.service.voice.IVoiceInteractionSession; @@ -113,7 +112,6 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne VoiceInteractionSessionConnection mActiveSession; int mDisabledShowContext; - int mDetectorType; final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { @Override @@ -552,7 +550,8 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne public void updateStateLocked( @Nullable PersistableBundle options, - @Nullable SharedMemory sharedMemory) { + @Nullable SharedMemory sharedMemory, + @NonNull IBinder token) { Slog.v(TAG, "updateStateLocked"); if (sharedMemory != null && !sharedMemory.setProtect(OsConstants.PROT_READ)) { @@ -565,7 +564,7 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne throw new IllegalStateException("Hotword detection connection not found"); } synchronized (mHotwordDetectionConnection.mLock) { - mHotwordDetectionConnection.updateStateLocked(options, sharedMemory); + mHotwordDetectionConnection.updateStateLocked(options, sharedMemory, token); } } @@ -573,6 +572,7 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne @NonNull Identity voiceInteractorIdentity, @Nullable PersistableBundle options, @Nullable SharedMemory sharedMemory, + @NonNull IBinder token, IHotwordRecognitionStatusCallback callback, int detectorType) { Slog.v(TAG, "initAndVerifyDetectorLocked"); @@ -624,16 +624,26 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne throw new IllegalStateException("Can't set sharedMemory to be read-only"); } - mDetectorType = detectorType; - logDetectorCreateEventIfNeeded(callback, detectorType, true, voiceInteractionServiceUid); if (mHotwordDetectionConnection == null) { mHotwordDetectionConnection = new HotwordDetectionConnection(mServiceStub, mContext, mInfo.getServiceInfo().applicationInfo.uid, voiceInteractorIdentity, mHotwordDetectionComponentName, mUser, /* bindInstantServiceAllowed= */ false, - options, sharedMemory, callback, detectorType); + detectorType); } + mHotwordDetectionConnection.createDetectorLocked(options, sharedMemory, token, callback, + detectorType); + } + + public void destroyDetectorLocked(IBinder token) { + Slog.v(TAG, "destroyDetectorLocked"); + + if (mHotwordDetectionConnection == null) { + Slog.w(TAG, "destroy detector callback, but no hotword detection connection"); + return; + } + mHotwordDetectionConnection.destroyDetectorLocked(token); } private void logDetectorCreateEventIfNeeded(IHotwordRecognitionStatusCallback callback, @@ -642,19 +652,16 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne HotwordMetricsLogger.writeDetectorCreateEvent(detectorType, isCreated, voiceInteractionServiceUid); } - } public void shutdownHotwordDetectionServiceLocked() { if (DEBUG) { Slog.d(TAG, "shutdownHotwordDetectionServiceLocked"); } - if (mHotwordDetectionConnection == null) { Slog.w(TAG, "shutdown, but no hotword detection connection"); return; } - mHotwordDetectionConnection.cancelLocked(); mHotwordDetectionConnection = null; } @@ -663,7 +670,7 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne AudioFormat audioFormat, IMicrophoneHotwordDetectionVoiceInteractionCallback callback) { if (DEBUG) { - Slog.d(TAG, "startListeningFromMic"); + Slog.d(TAG, "startListeningFromMicLocked"); } if (mHotwordDetectionConnection == null) { @@ -671,16 +678,17 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne return; } - mHotwordDetectionConnection.startListeningFromMic(audioFormat, callback); + mHotwordDetectionConnection.startListeningFromMicLocked(audioFormat, callback); } public void startListeningFromExternalSourceLocked( ParcelFileDescriptor audioStream, AudioFormat audioFormat, @Nullable PersistableBundle options, + @NonNull IBinder token, IMicrophoneHotwordDetectionVoiceInteractionCallback callback) { if (DEBUG) { - Slog.d(TAG, "startListeningFromExternalSource"); + Slog.d(TAG, "startListeningFromExternalSourceLocked"); } if (mHotwordDetectionConnection == null) { @@ -693,21 +701,21 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne throw new IllegalStateException("External source is null for hotword detector"); } - mHotwordDetectionConnection - .startListeningFromExternalSource(audioStream, audioFormat, options, callback); + mHotwordDetectionConnection.startListeningFromExternalSourceLocked(audioStream, audioFormat, + options, token, callback); } public void stopListeningFromMicLocked() { if (DEBUG) { - Slog.d(TAG, "stopListeningFromMic"); + Slog.d(TAG, "stopListeningFromMicLocked"); } if (mHotwordDetectionConnection == null) { - Slog.w(TAG, "stopListeningFromMic() called but connection isn't established"); + Slog.w(TAG, "stopListeningFromMicLocked() called but connection isn't established"); return; } - mHotwordDetectionConnection.stopListening(); + mHotwordDetectionConnection.stopListeningFromMicLocked(); } public void triggerHardwareRecognitionEventForTestLocked( @@ -809,8 +817,6 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne pw.println(Integer.toHexString(mDisabledShowContext)); } pw.print(" mBound="); pw.print(mBound); pw.print(" mService="); pw.println(mService); - pw.print(" mDetectorType="); - pw.println(HotwordDetector.detectorTypeToString(mDetectorType)); if (mHotwordDetectionConnection != null) { pw.println(" Hotword detection connection:"); mHotwordDetectionConnection.dump(" ", pw); @@ -899,5 +905,8 @@ class VoiceInteractionManagerServiceImpl implements VoiceInteractionSessionConne @Override public void onSessionHidden(VoiceInteractionSessionConnection connection) { mServiceStub.onSessionHidden(); + // Notifies visibility change here can cause duplicate events, it is added to make sure + // client always get the callback even if session is unexpectedly closed. + mServiceStub.setSessionWindowVisible(connection.mToken, false); } } diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java index 8cd5a85011b9..c4744ef21ead 100644 --- a/telephony/java/android/telephony/CarrierConfigManager.java +++ b/telephony/java/android/telephony/CarrierConfigManager.java @@ -9987,7 +9987,8 @@ public class CarrierConfigManager { * Gets the configuration values of the specified keys for a particular subscription. * * <p>If an invalid subId is used, the returned configuration will contain default values for - * the specified keys. + * the specified keys. If the value for the key can't be found, the returned configuration will + * filter the key out. * * <p>After using this method to get the configuration bundle, * {@link #isConfigForIdentifiedCarrier(PersistableBundle)} should be called to confirm whether @@ -10005,8 +10006,8 @@ public class CarrierConfigManager { * @param subId The subscription ID on which the carrier config should be retrieved. * @param keys The carrier config keys to retrieve values. * @return A {@link PersistableBundle} with key/value mapping for the specified configuration - * on success, or an empty (but never null) bundle on failure (for example, when no value for - * the specified key can be found). + * on success, or an empty (but never null) bundle on failure (for example, when the calling app + * has no permission). */ @RequiresPermission(anyOf = { Manifest.permission.READ_PHONE_STATE, @@ -10124,6 +10125,8 @@ public class CarrierConfigManager { * Gets the configuration values of the specified config keys applied for the default * subscription. * + * <p>If the value for the key can't be found, the returned bundle will filter the key out. + * * <p>After using this method to get the configuration bundle, {@link * #isConfigForIdentifiedCarrier(PersistableBundle)} should be called to confirm whether any * carrier specific configuration has been applied. diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java index 9b566fbeb0bb..8e8755d9434e 100644 --- a/telephony/java/android/telephony/SubscriptionManager.java +++ b/telephony/java/android/telephony/SubscriptionManager.java @@ -58,6 +58,7 @@ import android.os.UserHandle; import android.provider.Telephony.SimInfo; import android.telephony.euicc.EuiccManager; import android.telephony.ims.ImsMmTelManager; +import android.text.TextUtils; import android.util.Base64; import android.util.Log; import android.util.Pair; @@ -370,7 +371,7 @@ public class SubscriptionManager { /** * A content {@link Uri} used to receive updates on advanced calling user setting - * @see ImsMmTelManager#isAdvancedCallingSettingEnabled(). + * * <p> * Use this {@link Uri} with a {@link ContentObserver} to be notified of changes to the * subscription advanced calling enabled @@ -381,6 +382,9 @@ public class SubscriptionManager { * delivery of updates to the {@link Uri}. * To be notified of changes to a specific subId, append subId to the URI * {@link Uri#withAppendedPath(Uri, String)}. + * + * @see ImsMmTelManager#isAdvancedCallingSettingEnabled() + * * @hide */ @NonNull @@ -1165,7 +1169,7 @@ public class SubscriptionManager { * * An opportunistic subscription will default to data-centric. * - * {@see SubscriptionInfo#isOpportunistic} + * @see SubscriptionInfo#isOpportunistic */ public static final int USAGE_SETTING_DEFAULT = 0; @@ -1949,7 +1953,7 @@ public class SubscriptionManager { * * <p>Requires the {@link android.Manifest.permission#WRITE_EMBEDDED_SUBSCRIPTIONS} permission. * - * @see {@link TelephonyManager#getCardIdForDefaultEuicc()} for more information on the card ID. + * @see TelephonyManager#getCardIdForDefaultEuicc() for more information on the card ID. * * @hide */ @@ -1979,7 +1983,7 @@ public class SubscriptionManager { * * @param cardId the card ID of the eUICC. * - * @see {@link TelephonyManager#getCardIdForDefaultEuicc()} for more information on the card ID. + * @see TelephonyManager#getCardIdForDefaultEuicc() for more information on the card ID. * * @hide */ @@ -2103,10 +2107,15 @@ public class SubscriptionManager { } /** - * Remove SubscriptionInfo record from the SubscriptionInfo database + * Remove subscription info record from the subscription database. + * * @param uniqueId This is the unique identifier for the subscription within the specific - * subscription type. - * @param subscriptionType the {@link #SUBSCRIPTION_TYPE} + * subscription type. + * @param subscriptionType the type of subscription to be removed. + * + * @throws NullPointerException if {@code uniqueId} is {@code null}. + * @throws SecurityException if callers do not hold the required permission. + * * @hide */ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) @@ -2457,20 +2466,6 @@ public class SubscriptionManager { return getActiveSubscriptionInfo(getDefaultDataSubscriptionId()); } - /** @hide */ - public void clearSubscriptionInfo() { - try { - ISub iSub = TelephonyManager.getSubscriptionService(); - if (iSub != null) { - iSub.clearSubInfo(); - } - } catch (RemoteException ex) { - // ignore it - } - - return; - } - /** * Check if the supplied subscription ID is valid. * @@ -2614,17 +2609,27 @@ public class SubscriptionManager { } /** - * Store properties associated with SubscriptionInfo in database - * @param subId Subscription Id of Subscription - * @param propKey Column name in database associated with SubscriptionInfo - * @param propValue Value to store in DB for particular subId & column name + * Set a field in the subscription database. Note not all fields are supported. + * + * @param subscriptionId Subscription Id of Subscription. + * @param columnName Column name in the database. Note not all fields are supported. + * @param value Value to store in the database. + * + * @throws IllegalArgumentException if {@code subscriptionId} is invalid, or the field is not + * exposed. + * @throws SecurityException if callers do not hold the required permission. + * + * @see android.provider.Telephony.SimInfo for all the columns. + * * @hide */ - public static void setSubscriptionProperty(int subId, String propKey, String propValue) { + @RequiresPermission(Manifest.permission.MODIFY_PHONE_STATE) + public static void setSubscriptionProperty(int subscriptionId, @NonNull String columnName, + @NonNull String value) { try { ISub iSub = TelephonyManager.getSubscriptionService(); if (iSub != null) { - iSub.setSubscriptionProperty(subId, propKey, propValue); + iSub.setSubscriptionProperty(subscriptionId, columnName, value); } } catch (RemoteException ex) { // ignore it @@ -2653,118 +2658,149 @@ public class SubscriptionManager { } /** - * Return list of contacts uri corresponding to query result. - * @param subId Subscription Id of Subscription - * @param propKey Column name in SubscriptionInfo database - * @return list of contacts uri to be returned - * @hide - */ - private static List<Uri> getContactsFromSubscriptionProperty(int subId, String propKey, - Context context) { - String result = getSubscriptionProperty(subId, propKey, context); - if (result != null) { - try { - byte[] b = Base64.decode(result, Base64.DEFAULT); - ByteArrayInputStream bis = new ByteArrayInputStream(b); - ObjectInputStream ois = new ObjectInputStream(bis); - List<String> contacts = ArrayList.class.cast(ois.readObject()); - List<Uri> uris = new ArrayList<>(); - for (String contact : contacts) { - uris.add(Uri.parse(contact)); - } - return uris; - } catch (IOException e) { - logd("getContactsFromSubscriptionProperty IO exception"); - } catch (ClassNotFoundException e) { - logd("getContactsFromSubscriptionProperty ClassNotFound exception"); - } - } - return new ArrayList<>(); - } - - /** - * Store properties associated with SubscriptionInfo in database - * @param subId Subscription Id of Subscription - * @param propKey Column name in SubscriptionInfo database - * @return Value associated with subId and propKey column in database + * Get specific field in string format from the subscription info database. + * + * @param context The calling context. + * @param subscriptionId Subscription id of the subscription. + * @param columnName Column name in subscription database. + * + * @return Value in string format associated with {@code subscriptionId} and {@code columnName} + * from the database. + * + * @throws IllegalArgumentException if {@code subscriptionId} is invalid, or the field is not + * exposed. + * + * @see android.provider.Telephony.SimInfo for all the columns. + * * @hide */ - private static String getSubscriptionProperty(int subId, String propKey, - Context context) { + @NonNull + @RequiresPermission(anyOf = { + Manifest.permission.READ_PHONE_STATE, + Manifest.permission.READ_PRIVILEGED_PHONE_STATE, + "carrier privileges", + }) + private static String getStringSubscriptionProperty(@NonNull Context context, + int subscriptionId, @NonNull String columnName) { String resultValue = null; try { ISub iSub = TelephonyManager.getSubscriptionService(); if (iSub != null) { - resultValue = iSub.getSubscriptionProperty(subId, propKey, + resultValue = iSub.getSubscriptionProperty(subscriptionId, columnName, context.getOpPackageName(), context.getAttributionTag()); } } catch (RemoteException ex) { // ignore it } - return resultValue; + return TextUtils.emptyIfNull(resultValue); } /** - * Returns boolean value corresponding to query result. - * @param subId Subscription Id of Subscription - * @param propKey Column name in SubscriptionInfo database - * @param defValue Default boolean value to be returned - * @return boolean result value to be returned + * Get specific field in {@code boolean} format from the subscription info database. + * + * @param subscriptionId Subscription id of the subscription. + * @param columnName Column name in subscription database. + * @param defaultValue Default value in case not found or error. + * @param context The calling context. + * + * @return Value in {@code boolean} format associated with {@code subscriptionId} and + * {@code columnName} from the database, or {@code defaultValue} if not found or error. + * + * @throws IllegalArgumentException if {@code subscriptionId} is invalid, or the field is not + * exposed. + * + * @see android.provider.Telephony.SimInfo for all the columns. + * * @hide */ - public static boolean getBooleanSubscriptionProperty(int subId, String propKey, - boolean defValue, Context context) { - String result = getSubscriptionProperty(subId, propKey, context); - if (result != null) { + @RequiresPermission(anyOf = { + Manifest.permission.READ_PHONE_STATE, + Manifest.permission.READ_PRIVILEGED_PHONE_STATE, + "carrier privileges", + }) + public static boolean getBooleanSubscriptionProperty(int subscriptionId, + @NonNull String columnName, boolean defaultValue, @NonNull Context context) { + String result = getStringSubscriptionProperty(context, subscriptionId, columnName); + if (!result.isEmpty()) { try { return Integer.parseInt(result) == 1; } catch (NumberFormatException err) { logd("getBooleanSubscriptionProperty NumberFormat exception"); } } - return defValue; + return defaultValue; } /** - * Returns integer value corresponding to query result. - * @param subId Subscription Id of Subscription - * @param propKey Column name in SubscriptionInfo database - * @param defValue Default integer value to be returned - * @return integer result value to be returned + * Get specific field in {@code integer} format from the subscription info database. + * + * @param subscriptionId Subscription id of the subscription. + * @param columnName Column name in subscription database. + * @param defaultValue Default value in case not found or error. + * @param context The calling context. + * + * @return Value in {@code integer} format associated with {@code subscriptionId} and + * {@code columnName} from the database, or {@code defaultValue} if not found or error. + * + * @throws IllegalArgumentException if {@code subscriptionId} is invalid, or the field is not + * exposed. + * + * @see android.provider.Telephony.SimInfo for all the columns. + * * @hide */ - public static int getIntegerSubscriptionProperty(int subId, String propKey, int defValue, - Context context) { - String result = getSubscriptionProperty(subId, propKey, context); - if (result != null) { + @RequiresPermission(anyOf = { + Manifest.permission.READ_PHONE_STATE, + Manifest.permission.READ_PRIVILEGED_PHONE_STATE, + "carrier privileges", + }) + public static int getIntegerSubscriptionProperty(int subscriptionId, @NonNull String columnName, + int defaultValue, @NonNull Context context) { + String result = getStringSubscriptionProperty(context, subscriptionId, columnName); + if (!result.isEmpty()) { try { return Integer.parseInt(result); } catch (NumberFormatException err) { logd("getIntegerSubscriptionProperty NumberFormat exception"); } } - return defValue; + return defaultValue; } /** - * Returns long value corresponding to query result. - * @param subId Subscription Id of Subscription - * @param propKey Column name in SubscriptionInfo database - * @param defValue Default long value to be returned - * @return long result value to be returned + * Get specific field in {@code long} format from the subscription info database. + * + * @param subscriptionId Subscription id of the subscription. + * @param columnName Column name in subscription database. + * @param defaultValue Default value in case not found or error. + * @param context The calling context. + * + * @return Value in {@code long} format associated with {@code subscriptionId} and + * {@code columnName} from the database, or {@code defaultValue} if not found or error. + * + * @throws IllegalArgumentException if {@code subscriptionId} is invalid, or the field is not + * exposed. + * + * @see android.provider.Telephony.SimInfo for all the columns. + * * @hide */ - public static long getLongSubscriptionProperty(int subId, String propKey, long defValue, - Context context) { - String result = getSubscriptionProperty(subId, propKey, context); - if (result != null) { + @RequiresPermission(anyOf = { + Manifest.permission.READ_PHONE_STATE, + Manifest.permission.READ_PRIVILEGED_PHONE_STATE, + "carrier privileges", + }) + public static long getLongSubscriptionProperty(int subscriptionId, @NonNull String columnName, + long defaultValue, @NonNull Context context) { + String result = getStringSubscriptionProperty(context, subscriptionId, columnName); + if (!result.isEmpty()) { try { return Long.parseLong(result); } catch (NumberFormatException err) { logd("getLongSubscriptionProperty NumberFormat exception"); } } - return defValue; + return defaultValue; } /** @@ -3002,7 +3038,6 @@ public class SubscriptionManager { * considered unmetered. * @param networkTypes the network types this override applies to. If no * network types are specified, override values will be ignored. - * {@see TelephonyManager#getAllNetworkTypes()} * @param expirationDurationMillis the duration after which the requested override * will be automatically cleared, or {@code 0} to leave in the * requested state until explicitly cleared, or the next reboot, @@ -3063,17 +3098,14 @@ public class SubscriptionManager { * </ul> * * @param subId the subscriber this override applies to. - * @param overrideCongested set if the subscription should be considered - * congested. - * @param networkTypes the network types this override applies to. If no - * network types are specified, override values will be ignored. - * {@see TelephonyManager#getAllNetworkTypes()} + * @param overrideCongested set if the subscription should be considered congested. + * @param networkTypes the network types this override applies to. If no network types are + * specified, override values will be ignored. * @param expirationDurationMillis the duration after which the requested override - * will be automatically cleared, or {@code 0} to leave in the - * requested state until explicitly cleared, or the next reboot, - * whichever happens first. - * @throws SecurityException if the caller doesn't meet the requirements - * outlined above. + * will be automatically cleared, or {@code 0} to leave in the requested state until explicitly + * cleared, or the next reboot, whichever happens first. + * + * @throws SecurityException if the caller doesn't meet the requirements outlined above. */ public void setSubscriptionOverrideCongested(int subId, boolean overrideCongested, @NonNull @Annotation.NetworkType int[] networkTypes, @@ -3089,10 +3121,11 @@ public class SubscriptionManager { * * Only supported for embedded subscriptions (if {@link SubscriptionInfo#isEmbedded} returns * true). To check for permissions for non-embedded subscription as well, - * {@see android.telephony.TelephonyManager#hasCarrierPrivileges}. * * @param info The subscription to check. * @return whether the app is authorized to manage this subscription per its metadata. + * + * @see android.telephony.TelephonyManager#hasCarrierPrivileges */ public boolean canManageSubscription(SubscriptionInfo info) { return canManageSubscription(info, mContext.getPackageName()); @@ -3105,11 +3138,13 @@ public class SubscriptionManager { * * Only supported for embedded subscriptions (if {@link SubscriptionInfo#isEmbedded} returns * true). To check for permissions for non-embedded subscription as well, - * {@see android.telephony.TelephonyManager#hasCarrierPrivileges}. * * @param info The subscription to check. * @param packageName Package name of the app to check. + * * @return whether the app is authorized to manage this subscription per its access rules. + * + * @see android.telephony.TelephonyManager#hasCarrierPrivileges * @hide */ @SystemApi @@ -3423,21 +3458,20 @@ public class SubscriptionManager { /** * Remove a list of subscriptions from their subscription group. - * See {@link #createSubscriptionGroup(List)} for more details. * * Caller will either have {@link android.Manifest.permission#MODIFY_PHONE_STATE} - * permission or had carrier privilege permission on the subscriptions: - * {@link TelephonyManager#hasCarrierPrivileges()} or - * {@link #canManageSubscription(SubscriptionInfo)} - * - * @throws SecurityException if the caller doesn't meet the requirements - * outlined above. - * @throws IllegalArgumentException if the some subscriptions in the list doesn't belong - * the specified group. - * @throws IllegalStateException if Telephony service is in bad state. + * permission or has carrier privilege permission on all of the subscriptions provided in + * {@code subIdList}. * * @param subIdList list of subId that need removing from their groups. + * @param groupUuid The UUID of the subscription group. + * + * @throws SecurityException if the caller doesn't meet the requirements outlined above. + * @throws IllegalArgumentException if the some subscriptions in the list doesn't belong the + * specified group. + * @throws IllegalStateException if Telephony service is in bad state. * + * @see #createSubscriptionGroup(List) */ @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges @RequiresPermission(Manifest.permission.MODIFY_PHONE_STATE) @@ -3445,7 +3479,7 @@ public class SubscriptionManager { @NonNull ParcelUuid groupUuid) { Preconditions.checkNotNull(subIdList, "subIdList can't be null."); Preconditions.checkNotNull(groupUuid, "groupUuid can't be null."); - String pkgForDebug = mContext != null ? mContext.getOpPackageName() : "<unknown>"; + String callingPackage = mContext != null ? mContext.getOpPackageName() : "<unknown>"; if (VDBG) { logd("[removeSubscriptionsFromGroup]"); } @@ -3455,7 +3489,7 @@ public class SubscriptionManager { try { ISub iSub = TelephonyManager.getSubscriptionService(); if (iSub != null) { - iSub.removeSubscriptionsFromGroup(subIdArray, groupUuid, pkgForDebug); + iSub.removeSubscriptionsFromGroup(subIdArray, groupUuid, callingPackage); } else { if (!isSystemProcess()) { throw new IllegalStateException("telephony service is null."); @@ -3493,7 +3527,6 @@ public class SubscriptionManager { * @param groupUuid of which list of subInfo will be returned. * @return list of subscriptionInfo that belong to the same group, including the given * subscription itself. It will return an empty list if no subscription belongs to the group. - * */ @SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges @RequiresPermission(Manifest.permission.READ_PHONE_STATE) @@ -3533,7 +3566,8 @@ public class SubscriptionManager { * want to see their own hidden subscriptions. * * @param info the subscriptionInfo to check against. - * @return true if this subscription should be visible to the API caller. + * + * @return {@code true} if this subscription should be visible to the API caller. * * @hide */ @@ -3606,9 +3640,9 @@ public class SubscriptionManager { * <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 enable whether user is turning it on or off. - * @param subscriptionId Subscription to be enabled or disabled. - * It could be a eSIM or pSIM subscription. * * @return whether the operation is successful. * @@ -3641,8 +3675,6 @@ public class SubscriptionManager { * available from SubscriptionInfo.areUiccApplicationsEnabled() will be updated * immediately.) * - * Permissions android.Manifest.permission.MODIFY_PHONE_STATE is required - * * @param subscriptionId which subscription to operate on. * @param enabled whether uicc applications are enabled or disabled. * @hide @@ -3675,8 +3707,6 @@ public class SubscriptionManager { * It provides whether a physical SIM card can be disabled without taking it out, which is done * via {@link #setSubscriptionEnabled(int, boolean)} API. * - * Requires Permission: READ_PRIVILEGED_PHONE_STATE. - * * @return whether can disable subscriptions on physical SIMs. * * @hide @@ -3704,13 +3734,9 @@ public class SubscriptionManager { } /** - * Check if a subscription is active. - * - * @param subscriptionId The subscription id to check. - * - * @return {@code true} if the subscription is active. + * Check if the subscription is currently active in any slot. * - * @throws IllegalArgumentException if the provided slot index is invalid. + * @param subscriptionId The subscription id. * * @hide */ @@ -3730,15 +3756,14 @@ public class SubscriptionManager { } /** - * Set the device to device status sharing user preference for a subscription ID. The setting + * Set the device to device status sharing user preference for a subscription id. The setting * app uses this method to indicate with whom they wish to share device to device status * information. * - * @param subscriptionId the unique Subscription ID in database. - * @param sharing the status sharing preference. + * @param subscriptionId The subscription id. + * @param sharing The status sharing preference. * - * @throws IllegalArgumentException if the subscription does not exist, or the sharing - * preference is invalid. + * @throws SecurityException if the caller doesn't have permissions required. */ @RequiresPermission(Manifest.permission.MODIFY_PHONE_STATE) public void setDeviceToDeviceStatusSharingPreference(int subscriptionId, @@ -3755,6 +3780,8 @@ public class SubscriptionManager { * Returns the user-chosen device to device status sharing preference * @param subscriptionId Subscription id of subscription * @return The device to device status sharing preference + * + * @throws SecurityException if the caller doesn't have permissions required. */ public @DeviceToDeviceStatusSharingPreference int getDeviceToDeviceStatusSharingPreference( int subscriptionId) { @@ -3766,15 +3793,14 @@ public class SubscriptionManager { } /** - * Set the list of contacts that allow device to device status sharing for a subscription ID. + * Set the list of contacts that allow device to device status sharing for a subscription id. * The setting app uses this method to indicate with whom they wish to share device to device * status information. * - * @param subscriptionId The unique Subscription ID in database. + * @param subscriptionId The subscription id. * @param contacts The list of contacts that allow device to device status sharing. * - * @throws IllegalArgumentException if the subscription does not exist, or contacts is - * {@code null}. + * @throws SecurityException if the caller doesn't have permissions required. */ @RequiresPermission(Manifest.permission.MODIFY_PHONE_STATE) public void setDeviceToDeviceStatusSharingContacts(int subscriptionId, @@ -3790,38 +3816,46 @@ public class SubscriptionManager { } /** - * Returns the list of contacts that allow device to device status sharing. - * @param subscriptionId Subscription id of subscription - * @return The list of contacts that allow device to device status sharing + * Get the list of contacts that allow device to device status sharing. + * + * @param subscriptionId Subscription id. + * + * @return The list of contacts that allow device to device status sharing. */ - public @NonNull List<Uri> getDeviceToDeviceStatusSharingContacts( - int subscriptionId) { - if (VDBG) { - logd("[getDeviceToDeviceStatusSharingContacts] + subId: " + subscriptionId); + public @NonNull List<Uri> getDeviceToDeviceStatusSharingContacts(int subscriptionId) { + String result = getStringSubscriptionProperty(mContext, subscriptionId, + D2D_STATUS_SHARING_SELECTED_CONTACTS); + if (result != null) { + try { + byte[] b = Base64.decode(result, Base64.DEFAULT); + ByteArrayInputStream bis = new ByteArrayInputStream(b); + ObjectInputStream ois = new ObjectInputStream(bis); + List<String> contacts = ArrayList.class.cast(ois.readObject()); + List<Uri> uris = new ArrayList<>(); + for (String contact : contacts) { + uris.add(Uri.parse(contact)); + } + return uris; + } catch (IOException e) { + logd("getDeviceToDeviceStatusSharingContacts IO exception"); + } catch (ClassNotFoundException e) { + logd("getDeviceToDeviceStatusSharingContacts ClassNotFound exception"); + } } - return getContactsFromSubscriptionProperty(subscriptionId, - D2D_STATUS_SHARING_SELECTED_CONTACTS, mContext); + return new ArrayList<>(); } /** - * 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. - * + * DO NOT USE. + * This API is designed for features that are not finished at this point. Do not call this API. * @hide + * TODO b/135547512: further clean up */ @SystemApi @RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public int getEnabledSubscriptionId(int slotIndex) { int subId = INVALID_SUBSCRIPTION_ID; - if (!isValidSlotIndex(slotIndex)) { - throw new IllegalArgumentException("Invalid slot index " + slotIndex); - } - try { ISub iSub = TelephonyManager.getSubscriptionService(); if (iSub != null) { @@ -3863,7 +3897,7 @@ public class SubscriptionManager { /** * Get active data subscription id. Active data subscription refers to the subscription * currently chosen to provide cellular internet connection to the user. This may be - * different from getDefaultDataSubscriptionId(). + * different from {@link #getDefaultDataSubscriptionId()}. * * @return Active data subscription id if any is chosen, or {@link #INVALID_SUBSCRIPTION_ID} if * not. @@ -4061,12 +4095,15 @@ public class SubscriptionManager { * security-related or other sensitive scenarios. * * @param subscriptionId the subscription ID, or {@link #DEFAULT_SUBSCRIPTION_ID} - * for the default one. + * for the default one. * @param source the source of the phone number, one of the PHONE_NUMBER_SOURCE_* constants. + * * @return the phone number, or an empty string if not available. + * * @throws IllegalArgumentException if {@code source} is invalid. * @throws IllegalStateException if the telephony process is not currently available. * @throws SecurityException if the caller doesn't have permissions required. + * * @see #PHONE_NUMBER_SOURCE_UICC * @see #PHONE_NUMBER_SOURCE_CARRIER * @see #PHONE_NUMBER_SOURCE_IMS @@ -4123,8 +4160,10 @@ public class SubscriptionManager { * @param subscriptionId the subscription ID, or {@link #DEFAULT_SUBSCRIPTION_ID} * for the default one. * @return the phone number, or an empty string if not available. + * * @throws IllegalStateException if the telephony process is not currently available. * @throws SecurityException if the caller doesn't have permissions required. + * * @see #getPhoneNumber(int, int) */ @RequiresPermission(anyOf = { diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java index c926a23f55ce..5d49413e93da 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -17808,11 +17808,11 @@ public class TelephonyManager { * @hide */ @RequiresPermission(Manifest.permission.READ_PHONE_STATE) - public void isNullCipherAndIntegrityPreferenceEnabled() { + public boolean isNullCipherAndIntegrityPreferenceEnabled() { try { ITelephony telephony = getITelephony(); if (telephony != null) { - telephony.isNullCipherAndIntegrityPreferenceEnabled(); + return telephony.isNullCipherAndIntegrityPreferenceEnabled(); } else { throw new IllegalStateException("telephony service is null."); } @@ -17820,5 +17820,6 @@ public class TelephonyManager { Rlog.e(TAG, "isNullCipherAndIntegrityPreferenceEnabled RemoteException", ex); ex.rethrowFromSystemServer(); } + return true; } } diff --git a/telephony/java/android/telephony/ims/aidl/IImsMmTelListener.aidl b/telephony/java/android/telephony/ims/aidl/IImsMmTelListener.aidl index 640426b45ba3..c8c8724f3f13 100644 --- a/telephony/java/android/telephony/ims/aidl/IImsMmTelListener.aidl +++ b/telephony/java/android/telephony/ims/aidl/IImsMmTelListener.aidl @@ -35,4 +35,5 @@ interface IImsMmTelListener { void onRejectedCall(in ImsCallProfile callProfile, in ImsReasonInfo reason); oneway void onVoiceMessageCountUpdate(int count); oneway void onAudioModeIsVoipChanged(int imsAudioHandler); + oneway void onTriggerEpsFallback(int reason); } diff --git a/telephony/java/android/telephony/ims/feature/MmTelFeature.java b/telephony/java/android/telephony/ims/feature/MmTelFeature.java index 4710c1f4dbf3..1c7e9b9b281f 100644 --- a/telephony/java/android/telephony/ims/feature/MmTelFeature.java +++ b/telephony/java/android/telephony/ims/feature/MmTelFeature.java @@ -590,6 +590,17 @@ public class MmTelFeature extends ImsFeature { public void onAudioModeIsVoipChanged(int imsAudioHandler) { } + + /** + * Called when the IMS triggers EPS fallback procedure. + * + * @param reason specifies the reason that causes EPS fallback. + * @hide + */ + @Override + public void onTriggerEpsFallback(@EpsFallbackReason int reason) { + + } } /** @@ -662,6 +673,48 @@ public class MmTelFeature extends ImsFeature { @SystemApi public static final int AUDIO_HANDLER_BASEBAND = 1; + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef( + prefix = "EPS_FALLBACK_REASON_", + value = { + EPS_FALLBACK_REASON_INVALID, + EPS_FALLBACK_REASON_NO_NETWORK_TRIGGER, + EPS_FALLBACK_REASON_NO_NETWORK_RESPONSE, + }) + public @interface EpsFallbackReason {} + + /** + * Default value. Internal use only. + * This value should not be used to trigger EPS fallback. + * @hide + */ + public static final int EPS_FALLBACK_REASON_INVALID = -1; + + /** + * If the network only supports the EPS fallback in 5G NR SA for voice calling and the EPS + * Fallback procedure by the network during the call setup is not triggered, UE initiated + * fallback will be triggered with this reason. The modem shall locally release the 5G NR + * SA RRC connection and acquire the LTE network and perform a tracking area update + * procedure. After the EPS fallback procedure is completed, the call setup for voice will + * be established if there is no problem. + * + * @hide + */ + public static final int EPS_FALLBACK_REASON_NO_NETWORK_TRIGGER = 1; + + /** + * If the UE doesn't receive any response for SIP INVITE within a certain timeout in 5G NR + * SA for MO voice calling, the device determines that voice call is not available in 5G and + * terminates all active SIP dialogs and SIP requests and enters IMS non-registered state. + * In that case, UE initiated fallback will be triggered with this reason. The modem shall + * reset modem's data buffer of IMS PDU to prevent the ghost call. After the EPS fallback + * procedure is completed, VoLTE call could be tried if there is no problem. + * + * @hide + */ + public static final int EPS_FALLBACK_REASON_NO_NETWORK_RESPONSE = 2; + private IImsMmTelListener mListener; /** @@ -830,6 +883,24 @@ public class MmTelFeature extends ImsFeature { } /** + * Triggers the EPS fallback procedure. + * + * @param reason specifies the reason that causes EPS fallback. + * @hide + */ + public final void triggerEpsFallback(@EpsFallbackReason int reason) { + IImsMmTelListener listener = getListener(); + if (listener == null) { + throw new IllegalStateException("Session is not available."); + } + try { + listener.onTriggerEpsFallback(reason); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + /** * Provides the MmTelFeature with the ability to return the framework Capability Configuration * for a provided Capability. If the framework calls {@link #changeEnabledCapabilities} and * includes a capability A to enable or disable, this method should return the correct enabled diff --git a/telephony/java/android/telephony/ims/stub/ImsConfigImplBase.java b/telephony/java/android/telephony/ims/stub/ImsConfigImplBase.java index 897b57f48dad..ad8a936c3c27 100644 --- a/telephony/java/android/telephony/ims/stub/ImsConfigImplBase.java +++ b/telephony/java/android/telephony/ims/stub/ImsConfigImplBase.java @@ -22,6 +22,7 @@ import android.annotation.SystemApi; import android.content.Context; import android.os.PersistableBundle; import android.os.RemoteException; +import android.telephony.ims.ImsService; import android.telephony.ims.ProvisioningManager; import android.telephony.ims.RcsClientConfiguration; import android.telephony.ims.RcsConfig; @@ -553,7 +554,7 @@ public class ImsConfigImplBase { ImsConfigStub mImsConfigStub; /** - * Create a ImsConfig using the Executor specified for methods being called by the + * Create an ImsConfig using the Executor specified for methods being called by the * framework. * @param executor The executor for the framework to use when executing the methods overridden * by the implementation of ImsConfig. @@ -569,6 +570,9 @@ public class ImsConfigImplBase { mImsConfigStub = new ImsConfigStub(this, null); } + /** + * Create an ImsConfig using the Executor defined in {@link ImsService#getExecutor} + */ public ImsConfigImplBase() { mImsConfigStub = new ImsConfigStub(this, null); } diff --git a/telephony/java/com/android/internal/telephony/ISub.aidl b/telephony/java/com/android/internal/telephony/ISub.aidl index 280d25950228..c5f6902062ff 100644 --- a/telephony/java/com/android/internal/telephony/ISub.aidl +++ b/telephony/java/com/android/internal/telephony/ISub.aidl @@ -242,8 +242,6 @@ interface ISub { int getDefaultSubId(); - int clearSubInfo(); - int getPhoneId(int subId); /** diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/TaskTransitionTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/TaskTransitionTest.kt index 4e7ab7a24f65..b064695554bb 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/TaskTransitionTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/TaskTransitionTest.kt @@ -20,6 +20,7 @@ import android.app.Instrumentation import android.app.WallpaperManager import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Postsubmit +import android.platform.test.annotations.Presubmit import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.BaseTest import com.android.server.wm.flicker.FlickerBuilder @@ -29,11 +30,15 @@ import com.android.server.wm.flicker.helpers.NewTasksAppHelper import com.android.server.wm.flicker.helpers.SimpleAppHelper import com.android.server.wm.flicker.helpers.WindowUtils import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory +import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled import com.android.server.wm.traces.common.ComponentNameMatcher +import com.android.server.wm.traces.common.ComponentNameMatcher.Companion.DEFAULT_TASK_DISPLAY_AREA import com.android.server.wm.traces.common.ComponentNameMatcher.Companion.SPLASH_SCREEN import com.android.server.wm.traces.common.ComponentNameMatcher.Companion.WALLPAPER_BBQ_WRAPPER +import com.android.server.wm.traces.common.ComponentSplashScreenMatcher import com.android.server.wm.traces.common.IComponentMatcher import com.android.server.wm.traces.parser.toFlickerComponent +import org.junit.Assume import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith @@ -43,7 +48,7 @@ import org.junit.runners.Parameterized /** * Test the back and forward transition between 2 activities. * - * To run this test: `atest FlickerTests:ActivitiesTransitionTest` + * To run this test: `atest FlickerTests:TaskTransitionTest` * * Actions: * ``` @@ -57,7 +62,7 @@ import org.junit.runners.Parameterized @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) class TaskTransitionTest(flicker: FlickerTest) : BaseTest(flicker) { - private val testApp = NewTasksAppHelper(instrumentation) + private val launchNewTaskApp = NewTasksAppHelper(instrumentation) private val simpleApp = SimpleAppHelper(instrumentation) private val wallpaper by lazy { getWallpaperPackage(instrumentation) ?: error("Unable to obtain wallpaper") @@ -65,10 +70,10 @@ class TaskTransitionTest(flicker: FlickerTest) : BaseTest(flicker) { /** {@inheritDoc} */ override val transition: FlickerBuilder.() -> Unit = { - setup { testApp.launchViaIntent(wmHelper) } - teardown { testApp.exit(wmHelper) } + setup { launchNewTaskApp.launchViaIntent(wmHelper) } + teardown { launchNewTaskApp.exit(wmHelper) } transitions { - testApp.openNewTask(device, wmHelper) + launchNewTaskApp.openNewTask(device, wmHelper) tapl.pressBack() wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify() } @@ -101,7 +106,7 @@ class TaskTransitionTest(flicker: FlickerTest) : BaseTest(flicker) { * Check that the [ComponentNameMatcher.LAUNCHER] window is never visible when performing task * transitions. A solid color background should be shown above it. */ - @Postsubmit + @Presubmit @Test fun launcherWindowIsNeverVisible() { flicker.assertWm { this.isAppWindowInvisible(ComponentNameMatcher.LAUNCHER) } @@ -111,42 +116,76 @@ class TaskTransitionTest(flicker: FlickerTest) : BaseTest(flicker) { * Checks that the [ComponentNameMatcher.LAUNCHER] layer is never visible when performing task * transitions. A solid color background should be shown above it. */ - @Postsubmit + @Presubmit @Test fun launcherLayerIsNeverVisible() { flicker.assertLayers { this.isInvisible(ComponentNameMatcher.LAUNCHER) } } /** Checks that a color background is visible while the task transition is occurring. */ - @FlakyTest(bugId = 240570652) + @Presubmit @Test - fun colorLayerIsVisibleDuringTransition() { - val bgColorLayer = ComponentNameMatcher("", "colorBackgroundLayer") - val displayBounds = WindowUtils.getDisplayBounds(flicker.scenario.startRotation) + fun transitionHasColorBackground_legacy() { + Assume.assumeFalse(isShellTransitionsEnabled) + transitionHasColorBackground(DEFAULT_TASK_DISPLAY_AREA) + } + /** Checks that a color background is visible while the task transition is occurring. */ + @Presubmit + @Test + fun transitionHasColorBackground_shellTransit() { + Assume.assumeTrue(isShellTransitionsEnabled) + transitionHasColorBackground(ComponentNameMatcher("", "Animation Background")) + } + + private fun transitionHasColorBackground(backgroundColorLayer: IComponentMatcher) { + Assume.assumeTrue(isShellTransitionsEnabled) + + val displayBounds = WindowUtils.getDisplayBounds(flicker.scenario.startRotation) flicker.assertLayers { - this.invoke("LAUNCH_NEW_TASK_ACTIVITY coversExactly displayBounds") { - it.visibleRegion(testApp.componentMatcher).coversExactly(displayBounds) + this + .invoke("LAUNCH_NEW_TASK_ACTIVITY coversExactly displayBounds") { + it.visibleRegion(launchNewTaskApp.componentMatcher).coversExactly(displayBounds) } - .isInvisible(bgColorLayer) + .isInvisible(backgroundColorLayer) + .hasNoColor(backgroundColorLayer) .then() // Transitioning - .isVisible(bgColorLayer) + .isVisible(backgroundColorLayer) + .hasColor(backgroundColorLayer) .then() // Fully transitioned to simple SIMPLE_ACTIVITY + .invoke( + "SIMPLE_ACTIVITY's splashscreen coversExactly displayBounds", + isOptional = true + ) { + it.visibleRegion(ComponentSplashScreenMatcher( simpleApp.componentMatcher)) + .coversExactly(displayBounds) + } .invoke("SIMPLE_ACTIVITY coversExactly displayBounds") { it.visibleRegion(simpleApp.componentMatcher).coversExactly(displayBounds) } - .isInvisible(bgColorLayer) + .isInvisible(backgroundColorLayer) + .hasNoColor(backgroundColorLayer) .then() // Transitioning back - .isVisible(bgColorLayer) + .isVisible(backgroundColorLayer) + .hasColor(backgroundColorLayer) .then() // Fully transitioned back to LAUNCH_NEW_TASK_ACTIVITY + .invoke( + "LAUNCH_NEW_TASK_ACTIVITY's splashscreen coversExactly displayBounds", + isOptional = true + ) { + it.visibleRegion( + ComponentSplashScreenMatcher(launchNewTaskApp.componentMatcher)) + .coversExactly(displayBounds) + } .invoke("LAUNCH_NEW_TASK_ACTIVITY coversExactly displayBounds") { - it.visibleRegion(testApp.componentMatcher).coversExactly(displayBounds) + it.visibleRegion(launchNewTaskApp.componentMatcher).coversExactly(displayBounds) } - .isInvisible(bgColorLayer) + .isInvisible(backgroundColorLayer) + .hasNoColor(backgroundColorLayer) } } @@ -158,7 +197,7 @@ class TaskTransitionTest(flicker: FlickerTest) : BaseTest(flicker) { @Test fun newTaskOpensOnTopAndThenCloses() { flicker.assertWm { - this.isAppWindowOnTop(testApp.componentMatcher) + this.isAppWindowOnTop(launchNewTaskApp.componentMatcher) .then() .isAppWindowOnTop(SPLASH_SCREEN, isOptional = true) .then() @@ -166,66 +205,15 @@ class TaskTransitionTest(flicker: FlickerTest) : BaseTest(flicker) { .then() .isAppWindowOnTop(SPLASH_SCREEN, isOptional = true) .then() - .isAppWindowOnTop(testApp.componentMatcher) + .isAppWindowOnTop(launchNewTaskApp.componentMatcher) } } /** {@inheritDoc} */ - @Postsubmit @Test override fun entireScreenCovered() = super.entireScreenCovered() - - /** {@inheritDoc} */ @Postsubmit @Test override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd() - /** {@inheritDoc} */ - @Postsubmit - @Test - override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd() - - /** {@inheritDoc} */ - @Postsubmit - @Test - override fun statusBarLayerIsVisibleAtStartAndEnd() = - super.statusBarLayerIsVisibleAtStartAndEnd() - - /** {@inheritDoc} */ - @Postsubmit - @Test - override fun statusBarLayerPositionAtStartAndEnd() = super.statusBarLayerPositionAtStartAndEnd() - - /** {@inheritDoc} */ - @Postsubmit - @Test - override fun taskBarLayerIsVisibleAtStartAndEnd() = super.taskBarLayerIsVisibleAtStartAndEnd() - - /** {@inheritDoc} */ - @Postsubmit - @Test - override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible() - - /** {@inheritDoc} */ - @Postsubmit - @Test - override fun statusBarWindowIsAlwaysVisible() = super.statusBarWindowIsAlwaysVisible() - - /** {@inheritDoc} */ - @Postsubmit - @Test - override fun taskBarWindowIsAlwaysVisible() = super.taskBarWindowIsAlwaysVisible() - - /** {@inheritDoc} */ - @Postsubmit - @Test - override fun visibleLayersShownMoreThanOneConsecutiveEntry() = - super.visibleLayersShownMoreThanOneConsecutiveEntry() - - /** {@inheritDoc} */ - @Postsubmit - @Test - override fun visibleWindowsShownMoreThanOneConsecutiveEntry() = - super.visibleWindowsShownMoreThanOneConsecutiveEntry() - companion object { private fun getWallpaperPackage(instrumentation: Instrumentation): IComponentMatcher? { val wallpaperManager = WallpaperManager.getInstance(instrumentation.targetContext) diff --git a/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java b/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java index 2aef9ae7ca32..40408880a2c6 100644 --- a/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java +++ b/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java @@ -19,9 +19,11 @@ package android.net.vcn; import static android.net.ipsec.ike.IkeSessionParams.IKE_OPTION_MOBIKE; import static android.net.vcn.VcnGatewayConnectionConfig.DEFAULT_UNDERLYING_NETWORK_TEMPLATES; import static android.net.vcn.VcnGatewayConnectionConfig.UNDERLYING_NETWORK_TEMPLATES_KEY; +import static android.net.vcn.VcnGatewayConnectionConfig.VCN_GATEWAY_OPTION_ENABLE_DATA_STALL_RECOVERY_WITH_MOBILITY; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertTrue; @@ -42,7 +44,9 @@ import org.junit.runner.RunWith; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; +import java.util.Set; import java.util.concurrent.TimeUnit; @RunWith(AndroidJUnit4.class) @@ -79,6 +83,9 @@ public class VcnGatewayConnectionConfigTest { }; public static final int MAX_MTU = 1360; + private static final Set<Integer> GATEWAY_OPTIONS = + Collections.singleton(VCN_GATEWAY_OPTION_ENABLE_DATA_STALL_RECOVERY_WITH_MOBILITY); + public static final IkeTunnelConnectionParams TUNNEL_CONNECTION_PARAMS = TunnelConnectionParamsUtilsTest.buildTestParams(); @@ -109,10 +116,16 @@ public class VcnGatewayConnectionConfigTest { TUNNEL_CONNECTION_PARAMS); } - private static VcnGatewayConnectionConfig buildTestConfigWithExposedCaps( - VcnGatewayConnectionConfig.Builder builder, int... exposedCaps) { + private static VcnGatewayConnectionConfig buildTestConfigWithExposedCapsAndOptions( + VcnGatewayConnectionConfig.Builder builder, + Set<Integer> gatewayOptions, + int... exposedCaps) { builder.setRetryIntervalsMillis(RETRY_INTERVALS_MS).setMaxMtu(MAX_MTU); + for (int option : gatewayOptions) { + builder.addGatewayOption(option); + } + for (int caps : exposedCaps) { builder.addExposedCapability(caps); } @@ -120,11 +133,28 @@ public class VcnGatewayConnectionConfigTest { return builder.build(); } + private static VcnGatewayConnectionConfig buildTestConfigWithExposedCaps( + VcnGatewayConnectionConfig.Builder builder, int... exposedCaps) { + return buildTestConfigWithExposedCapsAndOptions( + builder, Collections.emptySet(), exposedCaps); + } + // Public for use in VcnGatewayConnectionTest public static VcnGatewayConnectionConfig buildTestConfigWithExposedCaps(int... exposedCaps) { return buildTestConfigWithExposedCaps(newBuilder(), exposedCaps); } + private static VcnGatewayConnectionConfig buildTestConfigWithGatewayOptions( + VcnGatewayConnectionConfig.Builder builder, Set<Integer> gatewayOptions) { + return buildTestConfigWithExposedCapsAndOptions(builder, gatewayOptions, EXPOSED_CAPS); + } + + // Public for use in VcnGatewayConnectionTest + public static VcnGatewayConnectionConfig buildTestConfigWithGatewayOptions( + Set<Integer> gatewayOptions) { + return buildTestConfigWithExposedCapsAndOptions(newBuilder(), gatewayOptions, EXPOSED_CAPS); + } + @Test public void testBuilderRequiresNonNullGatewayConnectionName() { try { @@ -211,6 +241,15 @@ public class VcnGatewayConnectionConfigTest { } @Test + public void testBuilderRequiresValidOption() { + try { + newBuilder().addGatewayOption(-1); + fail("Expected exception due to the invalid VCN gateway option"); + } catch (IllegalArgumentException e) { + } + } + + @Test public void testBuilderAndGetters() { final VcnGatewayConnectionConfig config = buildTestConfig(); @@ -225,6 +264,20 @@ public class VcnGatewayConnectionConfigTest { assertArrayEquals(RETRY_INTERVALS_MS, config.getRetryIntervalsMillis()); assertEquals(MAX_MTU, config.getMaxMtu()); + + assertFalse( + config.hasGatewayOption( + VCN_GATEWAY_OPTION_ENABLE_DATA_STALL_RECOVERY_WITH_MOBILITY)); + } + + @Test + public void testBuilderAndGettersWithOptions() { + final VcnGatewayConnectionConfig config = + buildTestConfigWithGatewayOptions(GATEWAY_OPTIONS); + + for (int option : GATEWAY_OPTIONS) { + assertTrue(config.hasGatewayOption(option)); + } } @Test @@ -235,6 +288,14 @@ public class VcnGatewayConnectionConfigTest { } @Test + public void testPersistableBundleWithOptions() { + final VcnGatewayConnectionConfig config = + buildTestConfigWithGatewayOptions(GATEWAY_OPTIONS); + + assertEquals(config, new VcnGatewayConnectionConfig(config.toPersistableBundle())); + } + + @Test public void testParsePersistableBundleWithoutVcnUnderlyingNetworkTemplates() { PersistableBundle configBundle = buildTestConfig().toPersistableBundle(); configBundle.putPersistableBundle(UNDERLYING_NETWORK_TEMPLATES_KEY, null); @@ -318,4 +379,27 @@ public class VcnGatewayConnectionConfigTest { assertNotEquals(UNDERLYING_NETWORK_TEMPLATES, networkTemplatesNotEqual); assertNotEquals(config, configNotEqual); } + + private static VcnGatewayConnectionConfig buildConfigWithGatewayOptionsForEqualityTest( + Set<Integer> gatewayOptions) { + return buildTestConfigWithGatewayOptions( + new VcnGatewayConnectionConfig.Builder( + "buildConfigWithGatewayOptionsForEqualityTest", TUNNEL_CONNECTION_PARAMS), + gatewayOptions); + } + + @Test + public void testVcnGatewayOptionsEquality() throws Exception { + final VcnGatewayConnectionConfig config = + buildConfigWithGatewayOptionsForEqualityTest(GATEWAY_OPTIONS); + + final VcnGatewayConnectionConfig configEqual = + buildConfigWithGatewayOptionsForEqualityTest(GATEWAY_OPTIONS); + + final VcnGatewayConnectionConfig configNotEqual = + buildConfigWithGatewayOptionsForEqualityTest(Collections.emptySet()); + + assertEquals(config, configEqual); + assertNotEquals(config, configNotEqual); + } } diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java index 15d4f1097108..1c21a067bde8 100644 --- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java +++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java @@ -50,9 +50,11 @@ import static org.mockito.Mockito.when; import static java.util.Collections.singletonList; +import android.net.ConnectivityDiagnosticsManager.DataStallReport; import android.net.ConnectivityManager; import android.net.LinkAddress; import android.net.LinkProperties; +import android.net.Network; import android.net.NetworkAgent; import android.net.NetworkCapabilities; import android.net.ipsec.ike.ChildSaProposal; @@ -63,10 +65,12 @@ import android.net.ipsec.ike.exceptions.IkeProtocolException; import android.net.vcn.VcnGatewayConnectionConfig; import android.net.vcn.VcnGatewayConnectionConfigTest; import android.net.vcn.VcnManager.VcnErrorCode; +import android.os.PersistableBundle; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import com.android.server.vcn.routeselection.UnderlyingNetworkRecord; import com.android.server.vcn.util.MtuUtils; import org.junit.Before; @@ -88,6 +92,7 @@ import java.util.function.Consumer; public class VcnGatewayConnectionConnectedStateTest extends VcnGatewayConnectionTestBase { private VcnIkeSession mIkeSession; private VcnNetworkAgent mNetworkAgent; + private Network mVcnNetwork; @Before public void setUp() throws Exception { @@ -98,6 +103,9 @@ public class VcnGatewayConnectionConnectedStateTest extends VcnGatewayConnection .when(mDeps) .newNetworkAgent(any(), any(), any(), any(), any(), any(), any(), any(), any()); + mVcnNetwork = mock(Network.class); + doReturn(mVcnNetwork).when(mNetworkAgent).getNetwork(); + mGatewayConnection.setUnderlyingNetwork(TEST_UNDERLYING_NETWORK_RECORD_1); mIkeSession = mGatewayConnection.buildIkeSession(TEST_UNDERLYING_NETWORK_RECORD_1.network); @@ -166,6 +174,56 @@ public class VcnGatewayConnectionConnectedStateTest extends VcnGatewayConnection assertEquals(mGatewayConnection.mConnectedState, mGatewayConnection.getCurrentState()); } + private void verifyDataStallTriggersMigration( + UnderlyingNetworkRecord networkRecord, + Network networkWithDataStall, + boolean expectMobilityUpdate) + throws Exception { + mGatewayConnection.setUnderlyingNetwork(networkRecord); + triggerChildOpened(); + mTestLooper.dispatchAll(); + + final DataStallReport report = + new DataStallReport( + networkWithDataStall, + 1234 /* reportTimestamp */, + 1 /* detectionMethod */, + new LinkProperties(), + new NetworkCapabilities(), + new PersistableBundle()); + + mGatewayConnection.getConnectivityDiagnosticsCallback().onDataStallSuspected(report); + mTestLooper.dispatchAll(); + + assertEquals(mGatewayConnection.mConnectedState, mGatewayConnection.getCurrentState()); + + if (expectMobilityUpdate) { + verify(mIkeSession).setNetwork(networkRecord.network); + } else { + verify(mIkeSession, never()).setNetwork(any(Network.class)); + } + } + + @Test + public void testDataStallTriggersMigration() throws Exception { + verifyDataStallTriggersMigration( + TEST_UNDERLYING_NETWORK_RECORD_1, mVcnNetwork, true /* expectMobilityUpdate */); + } + + @Test + public void testDataStallWontTriggerMigrationWhenOnOtherNetwork() throws Exception { + verifyDataStallTriggersMigration( + TEST_UNDERLYING_NETWORK_RECORD_1, + mock(Network.class), + false /* expectMobilityUpdate */); + } + + @Test + public void testDataStallWontTriggerMigrationWhenUnderlyingNetworkLost() throws Exception { + verifyDataStallTriggersMigration( + null /* networkRecord */, mock(Network.class), false /* expectMobilityUpdate */); + } + private void verifyVcnTransformsApplied( VcnGatewayConnection vcnGatewayConnection, boolean expectForwardTransform) throws Exception { diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java index 6a9a1e22cab1..a4ee2de9f433 100644 --- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java +++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java @@ -24,6 +24,7 @@ import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING; import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED; import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; import static android.net.NetworkCapabilities.TRANSPORT_WIFI; +import static android.net.vcn.VcnGatewayConnectionConfig.VCN_GATEWAY_OPTION_ENABLE_DATA_STALL_RECOVERY_WITH_MOBILITY; import static com.android.server.vcn.VcnGatewayConnection.DUMMY_ADDR; import static com.android.server.vcn.VcnGatewayConnection.VcnChildSessionConfiguration; @@ -34,20 +35,25 @@ import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.CALLS_REAL_METHODS; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import android.net.ConnectivityDiagnosticsManager.ConnectivityDiagnosticsCallback; import android.net.IpSecManager; import android.net.LinkAddress; import android.net.LinkProperties; import android.net.Network; import android.net.NetworkCapabilities; +import android.net.NetworkRequest; import android.net.TelephonyNetworkSpecifier; +import android.net.vcn.VcnGatewayConnectionConfig; import android.net.vcn.VcnGatewayConnectionConfigTest; import android.net.vcn.VcnTransportInfo; import android.net.wifi.WifiInfo; @@ -64,6 +70,7 @@ import com.android.server.vcn.routeselection.UnderlyingNetworkRecord; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import java.net.InetAddress; import java.util.Arrays; @@ -71,7 +78,9 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.UUID; +import java.util.concurrent.Executor; /** Tests for TelephonySubscriptionTracker */ @RunWith(AndroidJUnit4.class) @@ -287,5 +296,60 @@ public class VcnGatewayConnectionTest extends VcnGatewayConnectionTestBase { verify(vcnNetworkAgent).unregister(); verifyWakeLockReleased(); + + verify(mConnDiagMgr) + .unregisterConnectivityDiagnosticsCallback( + mGatewayConnection.getConnectivityDiagnosticsCallback()); + } + + private VcnGatewayConnection buildConnectionWithDataStallHandling( + boolean datatStallHandlingEnabled) throws Exception { + Set<Integer> options = + datatStallHandlingEnabled + ? Collections.singleton( + VCN_GATEWAY_OPTION_ENABLE_DATA_STALL_RECOVERY_WITH_MOBILITY) + : Collections.emptySet(); + final VcnGatewayConnectionConfig gatewayConfig = + VcnGatewayConnectionConfigTest.buildTestConfigWithGatewayOptions(options); + final VcnGatewayConnection gatewayConnection = + new VcnGatewayConnection( + mVcnContext, + TEST_SUB_GRP, + TEST_SUBSCRIPTION_SNAPSHOT, + gatewayConfig, + mGatewayStatusCallback, + true /* isMobileDataEnabled */, + mDeps); + return gatewayConnection; + } + + @Test + public void testDataStallHandlingEnabled() throws Exception { + final VcnGatewayConnection gatewayConnection = + buildConnectionWithDataStallHandling(true /* datatStallHandlingEnabled */); + + final ArgumentCaptor<NetworkRequest> networkRequestCaptor = + ArgumentCaptor.forClass(NetworkRequest.class); + verify(mConnDiagMgr) + .registerConnectivityDiagnosticsCallback( + networkRequestCaptor.capture(), + any(Executor.class), + eq(gatewayConnection.getConnectivityDiagnosticsCallback())); + + final NetworkRequest nr = networkRequestCaptor.getValue(); + final NetworkRequest expected = + new NetworkRequest.Builder().addTransportType(TRANSPORT_CELLULAR).build(); + assertEquals(expected, nr); + } + + @Test + public void testDataStallHandlingDisabled() throws Exception { + buildConnectionWithDataStallHandling(false /* datatStallHandlingEnabled */); + + verify(mConnDiagMgr, never()) + .registerConnectivityDiagnosticsCallback( + any(NetworkRequest.class), + any(Executor.class), + any(ConnectivityDiagnosticsCallback.class)); } } diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java index 785bff167ad2..7bafd243799f 100644 --- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java +++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java @@ -35,6 +35,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import android.annotation.NonNull; import android.content.Context; +import android.net.ConnectivityDiagnosticsManager; import android.net.ConnectivityManager; import android.net.InetAddresses; import android.net.IpSecConfig; @@ -157,6 +158,7 @@ public class VcnGatewayConnectionTestBase { @NonNull protected final IpSecService mIpSecSvc; @NonNull protected final ConnectivityManager mConnMgr; + @NonNull protected final ConnectivityDiagnosticsManager mConnDiagMgr; @NonNull protected final IkeSessionConnectionInfo mIkeConnectionInfo; @NonNull protected final IkeSessionConfiguration mIkeSessionConfiguration; @@ -186,6 +188,13 @@ public class VcnGatewayConnectionTestBase { VcnTestUtils.setupSystemService( mContext, mConnMgr, Context.CONNECTIVITY_SERVICE, ConnectivityManager.class); + mConnDiagMgr = mock(ConnectivityDiagnosticsManager.class); + VcnTestUtils.setupSystemService( + mContext, + mConnDiagMgr, + Context.CONNECTIVITY_DIAGNOSTICS_SERVICE, + ConnectivityDiagnosticsManager.class); + mIkeConnectionInfo = new IkeSessionConnectionInfo(TEST_ADDR, TEST_ADDR_2, mock(Network.class)); mIkeSessionConfiguration = new IkeSessionConfiguration.Builder(mIkeConnectionInfo).build(); |