Merge "Revert^2 "Modernize alternate bouncer logic""
diff --git a/apct-tests/perftests/contentcapture/AndroidManifest.xml b/apct-tests/perftests/contentcapture/AndroidManifest.xml
index 80957c7..6b566af 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 9b853fe..0ea2daf 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 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 @@
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 @@
}
}
+ @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 @@
}
/**
+ * Returns the intent which will launch CustomTestActivity.
+ */
+ 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 intent;
+ }
+
+ /**
* Launch test activity with give layout and parameter
*/
protected CustomTestActivity launchActivity(int layoutId, int numViews) {
- final Intent intent = new Intent(sContext, CustomTestActivity.class);
- intent.putExtra(INTENT_EXTRA_LAYOUT_ID, layoutId);
- intent.putExtra(INTENT_EXTRA_CUSTOM_VIEWS, numViews);
- return mActivityRule.launchActivity(intent);
- }
-
- protected void finishActivity() {
- try {
- mActivityRule.finishActivity();
- } catch (IllegalStateException e) {
- // no op
- }
+ 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 e509837..c24e79f 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 @@
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 @@
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 @@
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 7257509..aa95dfd 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 @@
}
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 @@
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/apct-tests/perftests/settingsprovider/AndroidManifest.xml b/apct-tests/perftests/settingsprovider/AndroidManifest.xml
index 140c280..9509c83 100644
--- a/apct-tests/perftests/settingsprovider/AndroidManifest.xml
+++ b/apct-tests/perftests/settingsprovider/AndroidManifest.xml
@@ -15,7 +15,7 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.android.perftests.multiuser">
+ package="com.android.perftests.settingsprovider">
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
<uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
@@ -26,6 +26,6 @@
</application>
<instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
- android:targetPackage="com.android.perftests.multiuser"/>
+ android:targetPackage="com.android.perftests.settingsprovider"/>
</manifest>
diff --git a/apex/jobscheduler/framework/java/android/app/job/JobInfo.java b/apex/jobscheduler/framework/java/android/app/job/JobInfo.java
index 9caf99e..19ab5bc 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.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 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 @@
}
/**
+ * @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 @@
*
* <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 @@
}
/**
+ * 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 @@
}
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 @@
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 @@
"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 30986dd..651853b 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 @@
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 @@
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 @@
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 @@
}
}
}
- 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 @@
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 @@
projectedRunningCount--;
}
if (selectedContext.newJob != null) {
+ selectedContext.newJob.startedWithImmediacyPrivilege = hasImmediacyPrivilege;
projectedRunningCount++;
minChangedWaitingTimeMs = Math.min(minChangedWaitingTimeMs,
mService.getMinJobExecutionGuaranteeMs(selectedContext.newJob));
@@ -1103,6 +1105,18 @@
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 @@
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 @@
}
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 @@
}
} 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 @@
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 @@
@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 535f8d4..e9b9660 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.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 @@
}
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 @@
}
}
+ 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 27268d2..36ba8dd 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 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 @@
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 @@
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 @@
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 @@
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 @@
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 df47f17..0dcfd24 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.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 @@
/** 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 @@
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 @@
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 @@
}
}
- 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 @@
}
}
+ 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 @@
}
}
- 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 6166921..3610b0a 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 @@
// 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 @@
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 @@
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 @@
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 @@
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 @@
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 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 @@
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 419127e..251a4da 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 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 @@
* @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 @@
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/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
index 1a775b4..6869936 100644
--- a/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
+++ b/apex/jobscheduler/service/java/com/android/server/usage/AppStandbyController.java
@@ -2187,8 +2187,10 @@
}
// component-level enable/disable can affect bucketing, so we always
// reevaluate that for any PACKAGE_CHANGED
- mHandler.obtainMessage(MSG_CHECK_PACKAGE_IDLE_STATE, userId, -1, pkgName)
- .sendToTarget();
+ if (Intent.ACTION_PACKAGE_CHANGED.equals(action)) {
+ mHandler.obtainMessage(MSG_CHECK_PACKAGE_IDLE_STATE, userId, -1, pkgName)
+ .sendToTarget();
+ }
}
if ((Intent.ACTION_PACKAGE_REMOVED.equals(action) ||
Intent.ACTION_PACKAGE_ADDED.equals(action))) {
diff --git a/core/api/current.txt b/core/api/current.txt
index 1ba99f9..39ac0d2e 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -4150,6 +4150,8 @@
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();
@@ -4292,6 +4294,7 @@
method @Deprecated public final void removeDialog(int);
method public void reportFullyDrawn();
method public android.view.DragAndDropPermissions requestDragAndDropPermissions(android.view.DragEvent);
+ method public void requestFullscreenMode(@NonNull int, @Nullable android.os.OutcomeReceiver<java.lang.Void,java.lang.Throwable>);
method public final void requestPermissions(@NonNull String[], int);
method public final void requestShowKeyboardShortcuts();
method @Deprecated public boolean requestVisibleBehind(boolean);
@@ -4378,6 +4381,8 @@
field public static final int DEFAULT_KEYS_SEARCH_LOCAL = 3; // 0x3
field public static final int DEFAULT_KEYS_SHORTCUT = 2; // 0x2
field protected static final int[] FOCUSED_STATE_SET;
+ field public static final int FULLSCREEN_MODE_REQUEST_ENTER = 1; // 0x1
+ field public static final int FULLSCREEN_MODE_REQUEST_EXIT = 0; // 0x0
field public static final int RESULT_CANCELED = 0; // 0x0
field public static final int RESULT_FIRST_USER = 1; // 0x1
field public static final int RESULT_OK = -1; // 0xffffffff
@@ -4619,6 +4624,7 @@
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);
@@ -10589,6 +10595,7 @@
field public static final String EXTRA_MIME_TYPES = "android.intent.extra.MIME_TYPES";
field public static final String EXTRA_NOT_UNKNOWN_SOURCE = "android.intent.extra.NOT_UNKNOWN_SOURCE";
field public static final String EXTRA_ORIGINATING_URI = "android.intent.extra.ORIGINATING_URI";
+ field public static final String EXTRA_PACKAGES = "android.intent.extra.PACKAGES";
field public static final String EXTRA_PACKAGE_NAME = "android.intent.extra.PACKAGE_NAME";
field public static final String EXTRA_PERMISSION_GROUP_NAME = "android.intent.extra.PERMISSION_GROUP_NAME";
field public static final String EXTRA_PHONE_NUMBER = "android.intent.extra.PHONE_NUMBER";
@@ -11718,9 +11725,12 @@
method public void unregisterSessionCallback(@NonNull android.content.pm.PackageInstaller.SessionCallback);
method public void updateSessionAppIcon(int, @Nullable android.graphics.Bitmap);
method public void updateSessionAppLabel(int, @Nullable CharSequence);
+ method public void waitForInstallConstraints(@NonNull java.util.List<java.lang.String>, @NonNull android.content.pm.PackageInstaller.InstallConstraints, @NonNull android.content.IntentSender, long);
field public static final String ACTION_SESSION_COMMITTED = "android.content.pm.action.SESSION_COMMITTED";
field public static final String ACTION_SESSION_DETAILS = "android.content.pm.action.SESSION_DETAILS";
field public static final String ACTION_SESSION_UPDATED = "android.content.pm.action.SESSION_UPDATED";
+ field public static final String EXTRA_INSTALL_CONSTRAINTS = "android.content.pm.extra.INSTALL_CONSTRAINTS";
+ field public static final String EXTRA_INSTALL_CONSTRAINTS_RESULT = "android.content.pm.extra.INSTALL_CONSTRAINTS_RESULT";
field public static final String EXTRA_OTHER_PACKAGE_NAME = "android.content.pm.extra.OTHER_PACKAGE_NAME";
field public static final String EXTRA_PACKAGE_NAME = "android.content.pm.extra.PACKAGE_NAME";
field public static final String EXTRA_PRE_APPROVAL = "android.content.pm.extra.PRE_APPROVAL";
@@ -12175,6 +12185,7 @@
field public static final String FEATURE_RAM_NORMAL = "android.hardware.ram.normal";
field public static final String FEATURE_SCREEN_LANDSCAPE = "android.hardware.screen.landscape";
field public static final String FEATURE_SCREEN_PORTRAIT = "android.hardware.screen.portrait";
+ field public static final String FEATURE_SEAMLESS_REFRESH_RATE_SWITCHING = "android.software.seamless_refresh_rate_switching";
field public static final String FEATURE_SECURELY_REMOVES_USERS = "android.software.securely_removes_users";
field public static final String FEATURE_SECURE_LOCK_SCREEN = "android.software.secure_lock_screen";
field public static final String FEATURE_SECURITY_MODEL_COMPATIBLE = "android.hardware.security.model.compatible";
@@ -17936,6 +17947,7 @@
method public int capture(@NonNull android.hardware.camera2.CaptureRequest, @NonNull java.util.concurrent.Executor, @NonNull android.hardware.camera2.CameraExtensionSession.ExtensionCaptureCallback) throws android.hardware.camera2.CameraAccessException;
method public void close() throws android.hardware.camera2.CameraAccessException;
method @NonNull public android.hardware.camera2.CameraDevice getDevice();
+ method @Nullable public android.util.Pair<java.lang.Long,java.lang.Long> getRealtimeStillCaptureLatency() throws android.hardware.camera2.CameraAccessException;
method public int setRepeatingRequest(@NonNull android.hardware.camera2.CaptureRequest, @NonNull java.util.concurrent.Executor, @NonNull android.hardware.camera2.CameraExtensionSession.ExtensionCaptureCallback) throws android.hardware.camera2.CameraAccessException;
method public void stopRepeating() throws android.hardware.camera2.CameraAccessException;
}
@@ -20518,6 +20530,7 @@
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
@@ -20911,6 +20924,7 @@
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;
}
@@ -25452,11 +25466,21 @@
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);
}
@@ -26785,6 +26809,7 @@
method public boolean onKeyUp(int, @NonNull android.view.KeyEvent);
method public void onMediaViewSizeChanged(@Px int, @Px int);
method public void onRecordingStarted(@NonNull String);
+ method public void onRecordingStopped(@NonNull String);
method public abstract void onRelease();
method public void onResetInteractiveApp();
method public abstract boolean onSetSurface(@Nullable android.view.Surface);
@@ -26811,6 +26836,7 @@
method @CallSuper public void requestCurrentTvInputId();
method @CallSuper public void requestSigning(@NonNull String, @NonNull String, @NonNull String, @NonNull byte[]);
method @CallSuper public void requestStartRecording(@Nullable android.net.Uri);
+ method @CallSuper public void requestStopRecording(@NonNull String);
method @CallSuper public void requestStreamVolume();
method @CallSuper public void requestTrackInfoList();
method @CallSuper public void sendPlaybackCommandRequest(@NonNull String, @Nullable android.os.Bundle);
@@ -26843,6 +26869,7 @@
method @Nullable public android.media.tv.interactive.TvInteractiveAppView.OnUnhandledInputEventListener getOnUnhandledInputEventListener();
method public void notifyError(@NonNull String, @NonNull android.os.Bundle);
method public void notifyRecordingStarted(@NonNull String);
+ method public void notifyRecordingStopped(@NonNull String);
method public void onAttachedToWindow();
method public void onDetachedFromWindow();
method public void onLayout(boolean, int, int, int, int);
@@ -26885,6 +26912,7 @@
method public void onRequestCurrentTvInputId(@NonNull String);
method public void onRequestSigning(@NonNull String, @NonNull String, @NonNull String, @NonNull String, @NonNull byte[]);
method public void onRequestStartRecording(@NonNull String, @Nullable android.net.Uri);
+ method public void onRequestStopRecording(@NonNull String, @NonNull String);
method public void onRequestStreamVolume(@NonNull String);
method public void onRequestTrackInfoList(@NonNull String);
method public void onSetVideoBounds(@NonNull String, @NonNull android.graphics.Rect);
@@ -27851,6 +27879,7 @@
public final class VcnConfig implements android.os.Parcelable {
method public int describeContents();
method @NonNull public java.util.Set<android.net.vcn.VcnGatewayConnectionConfig> getGatewayConnectionConfigs();
+ method @NonNull public java.util.Set<java.lang.Integer> getRestrictedUnderlyingNetworkTransports();
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.net.vcn.VcnConfig> CREATOR;
}
@@ -27859,6 +27888,7 @@
ctor public VcnConfig.Builder(@NonNull android.content.Context);
method @NonNull public android.net.vcn.VcnConfig.Builder addGatewayConnectionConfig(@NonNull android.net.vcn.VcnGatewayConnectionConfig);
method @NonNull public android.net.vcn.VcnConfig build();
+ method @NonNull public android.net.vcn.VcnConfig.Builder setRestrictedUnderlyingNetworkTransports(@NonNull java.util.Set<java.lang.Integer>);
}
public final class VcnGatewayConnectionConfig {
@@ -27867,13 +27897,17 @@
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>);
@@ -35114,6 +35148,7 @@
field public static final String CONTENT_TYPE = "vnd.android.cursor.dir/phone_v2";
field public static final android.net.Uri CONTENT_URI;
field public static final android.net.Uri ENTERPRISE_CONTENT_FILTER_URI;
+ field @NonNull public static final android.net.Uri ENTERPRISE_CONTENT_URI;
field public static final String EXTRA_ADDRESS_BOOK_INDEX = "android.provider.extra.ADDRESS_BOOK_INDEX";
field public static final String EXTRA_ADDRESS_BOOK_INDEX_COUNTS = "android.provider.extra.ADDRESS_BOOK_INDEX_COUNTS";
field public static final String EXTRA_ADDRESS_BOOK_INDEX_TITLES = "android.provider.extra.ADDRESS_BOOK_INDEX_TITLES";
@@ -35294,6 +35329,7 @@
field public static final String CONTENT_VCARD_TYPE = "text/x-vcard";
field public static final android.net.Uri CONTENT_VCARD_URI;
field public static final android.net.Uri ENTERPRISE_CONTENT_FILTER_URI;
+ field @NonNull public static final android.net.Uri ENTERPRISE_CONTENT_URI;
field public static final String EXTRA_ADDRESS_BOOK_INDEX = "android.provider.extra.ADDRESS_BOOK_INDEX";
field public static final String EXTRA_ADDRESS_BOOK_INDEX_COUNTS = "android.provider.extra.ADDRESS_BOOK_INDEX_COUNTS";
field public static final String EXTRA_ADDRESS_BOOK_INDEX_TITLES = "android.provider.extra.ADDRESS_BOOK_INDEX_TITLES";
@@ -36267,7 +36303,7 @@
field public static final String PARENTAL_CONTROL_LAST_UPDATE = "parental_control_last_update";
field public static final String PARENTAL_CONTROL_REDIRECT_URL = "parental_control_redirect_url";
field public static final String RTT_CALLING_MODE = "rtt_calling_mode";
- field public static final String SECURE_FRP_MODE = "secure_frp_mode";
+ field @Deprecated public static final String SECURE_FRP_MODE = "secure_frp_mode";
field public static final String SELECTED_INPUT_METHOD_SUBTYPE = "selected_input_method_subtype";
field public static final String SETTINGS_CLASSNAME = "settings_classname";
field public static final String SKIP_FIRST_USE_HINTS = "skip_first_use_hints";
@@ -38263,7 +38299,7 @@
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();
}
@@ -42017,11 +42053,15 @@
}
public class CarrierConfigManager {
- method @Nullable @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public android.os.PersistableBundle getConfig();
+ method @Deprecated @Nullable @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public android.os.PersistableBundle getConfig();
+ method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.READ_PHONE_STATE, "carrier privileges"}) public android.os.PersistableBundle getConfig(@NonNull java.lang.String...);
method @Nullable @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public android.os.PersistableBundle getConfigByComponentForSubId(@NonNull String, int);
- method @Nullable @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public android.os.PersistableBundle getConfigForSubId(int);
+ method @Deprecated @Nullable @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public android.os.PersistableBundle getConfigForSubId(int);
+ method @NonNull @RequiresPermission(anyOf={android.Manifest.permission.READ_PHONE_STATE, "carrier privileges"}) public android.os.PersistableBundle getConfigForSubId(int, @NonNull java.lang.String...);
method public static boolean isConfigForIdentifiedCarrier(android.os.PersistableBundle);
method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void notifyConfigChangedForSubId(int);
+ method public void registerCarrierConfigChangeListener(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.CarrierConfigManager.CarrierConfigChangeListener);
+ method public void unregisterCarrierConfigChangeListener(@NonNull android.telephony.CarrierConfigManager.CarrierConfigChangeListener);
field public static final String ACTION_CARRIER_CONFIG_CHANGED = "android.telephony.action.CARRIER_CONFIG_CHANGED";
field public static final int CARRIER_NR_AVAILABILITY_NSA = 1; // 0x1
field public static final int CARRIER_NR_AVAILABILITY_SA = 2; // 0x2
@@ -42351,6 +42391,10 @@
field public static final String KEY_PREFIX = "bsf.";
}
+ public static interface CarrierConfigManager.CarrierConfigChangeListener {
+ method public void onCarrierConfigChanged(int, int, int, int);
+ }
+
public static final class CarrierConfigManager.Gps {
field public static final String KEY_PERSIST_LPP_MODE_BOOL = "gps.persist_lpp_mode_bool";
field public static final String KEY_PREFIX = "gps.";
@@ -49564,6 +49608,7 @@
field public static final int KEYCODE_PROG_YELLOW = 185; // 0xb9
field public static final int KEYCODE_Q = 45; // 0x2d
field public static final int KEYCODE_R = 46; // 0x2e
+ field public static final int KEYCODE_RECENT_APPS = 312; // 0x138
field public static final int KEYCODE_REFRESH = 285; // 0x11d
field public static final int KEYCODE_RIGHT_BRACKET = 72; // 0x48
field public static final int KEYCODE_RO = 217; // 0xd9
@@ -52762,12 +52807,14 @@
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();
@@ -52779,6 +52826,7 @@
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
@@ -52801,6 +52849,10 @@
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);
@@ -54541,6 +54593,8 @@
method @NonNull public android.text.SegmentFinder getGraphemeSegmentFinder();
method @NonNull public android.text.SegmentFinder getLineSegmentFinder();
method @NonNull public android.graphics.Matrix getMatrix();
+ method public int getOffsetForPosition(float, float);
+ method @Nullable public int[] getRangeForRect(@NonNull android.graphics.RectF, @NonNull android.text.SegmentFinder, @NonNull android.text.Layout.TextInclusionStrategy);
method public int getStart();
method @NonNull public android.text.SegmentFinder getWordSegmentFinder();
method public void writeToParcel(@NonNull android.os.Parcel, int);
diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt
index 98c78fe..314fd03 100644
--- a/core/api/module-lib-current.txt
+++ b/core/api/module-lib-current.txt
@@ -99,6 +99,10 @@
public class ApplicationInfo extends android.content.pm.PackageItemInfo implements android.os.Parcelable {
method @NonNull public java.util.List<android.content.pm.SharedLibraryInfo> getSharedLibraryInfos();
+ field public static final int HIDDEN_API_ENFORCEMENT_DEFAULT = -1; // 0xffffffff
+ field public static final int HIDDEN_API_ENFORCEMENT_DISABLED = 0; // 0x0
+ field public static final int HIDDEN_API_ENFORCEMENT_ENABLED = 2; // 0x2
+ field public static final int HIDDEN_API_ENFORCEMENT_JUST_WARN = 1; // 0x1
}
public abstract class PackageManager {
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 6f341f2..fb74260 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -352,6 +352,7 @@
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 @@
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 @@
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();
@@ -3319,7 +3322,6 @@
field public static final String EXTRA_INSTANT_APP_TOKEN = "android.intent.extra.INSTANT_APP_TOKEN";
field public static final String EXTRA_LONG_VERSION_CODE = "android.intent.extra.LONG_VERSION_CODE";
field public static final String EXTRA_ORIGINATING_UID = "android.intent.extra.ORIGINATING_UID";
- field public static final String EXTRA_PACKAGES = "android.intent.extra.PACKAGES";
field public static final String EXTRA_PERMISSION_NAME = "android.intent.extra.PERMISSION_NAME";
field public static final String EXTRA_REASON = "android.intent.extra.REASON";
field public static final String EXTRA_REMOTE_CALLBACK = "android.intent.extra.REMOTE_CALLBACK";
@@ -3608,6 +3610,7 @@
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;
@@ -4784,6 +4787,24 @@
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();
@@ -5275,11 +5296,12 @@
method public void writeToParcel(android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.hardware.radio.ProgramSelector> CREATOR;
field public static final int IDENTIFIER_TYPE_AMFM_FREQUENCY = 1; // 0x1
+ field public static final int IDENTIFIER_TYPE_DAB_DMB_SID_EXT = 14; // 0xe
field public static final int IDENTIFIER_TYPE_DAB_ENSEMBLE = 6; // 0x6
field public static final int IDENTIFIER_TYPE_DAB_FREQUENCY = 8; // 0x8
field public static final int IDENTIFIER_TYPE_DAB_SCID = 7; // 0x7
- field public static final int IDENTIFIER_TYPE_DAB_SIDECC = 5; // 0x5
- field public static final int IDENTIFIER_TYPE_DAB_SID_EXT = 5; // 0x5
+ field @Deprecated public static final int IDENTIFIER_TYPE_DAB_SIDECC = 5; // 0x5
+ field @Deprecated public static final int IDENTIFIER_TYPE_DAB_SID_EXT = 5; // 0x5
field public static final int IDENTIFIER_TYPE_DRMO_FREQUENCY = 10; // 0xa
field @Deprecated public static final int IDENTIFIER_TYPE_DRMO_MODULATION = 11; // 0xb
field public static final int IDENTIFIER_TYPE_DRMO_SERVICE_ID = 9; // 0x9
@@ -5316,7 +5338,7 @@
field @NonNull public static final android.os.Parcelable.Creator<android.hardware.radio.ProgramSelector.Identifier> CREATOR;
}
- @IntDef(prefix={"IDENTIFIER_TYPE_"}, value={android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_INVALID, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_RDS_PI, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_HD_STATION_ID_EXT, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_HD_SUBCHANNEL, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_HD_STATION_NAME, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_DAB_SIDECC, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_DAB_SCID, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_DRMO_SERVICE_ID, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_DRMO_FREQUENCY, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_DRMO_MODULATION, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_SXM_SERVICE_ID, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_SXM_CHANNEL}) @IntRange(from=android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_VENDOR_START, to=android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_VENDOR_END) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface ProgramSelector.IdentifierType {
+ @IntDef(prefix={"IDENTIFIER_TYPE_"}, value={android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_INVALID, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_RDS_PI, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_HD_STATION_ID_EXT, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_HD_SUBCHANNEL, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_HD_STATION_NAME, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_DAB_SID_EXT, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_DAB_SIDECC, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_DAB_SCID, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_DRMO_SERVICE_ID, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_DRMO_FREQUENCY, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_DRMO_MODULATION, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_SXM_SERVICE_ID, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_SXM_CHANNEL, android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT}) @IntRange(from=android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_VENDOR_START, to=android.hardware.radio.ProgramSelector.IDENTIFIER_TYPE_VENDOR_END) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface ProgramSelector.IdentifierType {
}
@Deprecated @IntDef(prefix={"PROGRAM_TYPE_"}, value={android.hardware.radio.ProgramSelector.PROGRAM_TYPE_INVALID, android.hardware.radio.ProgramSelector.PROGRAM_TYPE_AM, android.hardware.radio.ProgramSelector.PROGRAM_TYPE_FM, android.hardware.radio.ProgramSelector.PROGRAM_TYPE_AM_HD, android.hardware.radio.ProgramSelector.PROGRAM_TYPE_FM_HD, android.hardware.radio.ProgramSelector.PROGRAM_TYPE_DAB, android.hardware.radio.ProgramSelector.PROGRAM_TYPE_DRMO, android.hardware.radio.ProgramSelector.PROGRAM_TYPE_SXM}) @IntRange(from=android.hardware.radio.ProgramSelector.PROGRAM_TYPE_VENDOR_START, to=android.hardware.radio.ProgramSelector.PROGRAM_TYPE_VENDOR_END) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface ProgramSelector.ProgramType {
@@ -5531,30 +5553,31 @@
public abstract class RadioTuner {
ctor public RadioTuner();
- method public abstract int cancel();
- method public abstract void cancelAnnouncement();
- method public abstract void close();
- method @Deprecated public abstract int getConfiguration(android.hardware.radio.RadioManager.BandConfig[]);
- method @Nullable public android.hardware.radio.ProgramList getDynamicProgramList(@Nullable android.hardware.radio.ProgramList.Filter);
- method public abstract boolean getMute();
- method @NonNull public java.util.Map<java.lang.String,java.lang.String> getParameters(@NonNull java.util.List<java.lang.String>);
- method @Deprecated public abstract int getProgramInformation(android.hardware.radio.RadioManager.ProgramInfo[]);
- method @Deprecated @NonNull public abstract java.util.List<android.hardware.radio.RadioManager.ProgramInfo> getProgramList(@Nullable java.util.Map<java.lang.String,java.lang.String>);
- method public abstract boolean hasControl();
- method @Deprecated public abstract boolean isAnalogForced();
- method @Deprecated public abstract boolean isAntennaConnected();
- method public boolean isConfigFlagSet(int);
- method public boolean isConfigFlagSupported(int);
- method public abstract int scan(int, boolean);
- method @Deprecated public abstract void setAnalogForced(boolean);
- method public void setConfigFlag(int, boolean);
- method @Deprecated public abstract int setConfiguration(android.hardware.radio.RadioManager.BandConfig);
- method public abstract int setMute(boolean);
- method @NonNull public java.util.Map<java.lang.String,java.lang.String> setParameters(@NonNull java.util.Map<java.lang.String,java.lang.String>);
- method public abstract boolean startBackgroundScan();
- method public abstract int step(int, boolean);
- method @Deprecated public abstract int tune(int, int);
- method public abstract void tune(@NonNull android.hardware.radio.ProgramSelector);
+ method @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract int cancel();
+ method @Deprecated @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract void cancelAnnouncement();
+ method @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract void close();
+ method @Deprecated @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract int getConfiguration(android.hardware.radio.RadioManager.BandConfig[]);
+ method @Nullable @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public android.hardware.radio.ProgramList getDynamicProgramList(@Nullable android.hardware.radio.ProgramList.Filter);
+ method @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract boolean getMute();
+ method @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public java.util.Map<java.lang.String,java.lang.String> getParameters(@NonNull java.util.List<java.lang.String>);
+ method @Deprecated @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract int getProgramInformation(android.hardware.radio.RadioManager.ProgramInfo[]);
+ method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract java.util.List<android.hardware.radio.RadioManager.ProgramInfo> getProgramList(@Nullable java.util.Map<java.lang.String,java.lang.String>);
+ method @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract boolean hasControl();
+ method @Deprecated @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract boolean isAnalogForced();
+ method @Deprecated @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract boolean isAntennaConnected();
+ method @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public boolean isConfigFlagSet(int);
+ method @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public boolean isConfigFlagSupported(int);
+ method @Deprecated @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract int scan(int, boolean);
+ method @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public int seek(int, boolean);
+ method @Deprecated @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract void setAnalogForced(boolean);
+ method @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public void setConfigFlag(int, boolean);
+ method @Deprecated @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract int setConfiguration(android.hardware.radio.RadioManager.BandConfig);
+ method @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract int setMute(boolean);
+ method @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public java.util.Map<java.lang.String,java.lang.String> setParameters(@NonNull java.util.Map<java.lang.String,java.lang.String>);
+ method @Deprecated @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract boolean startBackgroundScan();
+ method @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract int step(int, boolean);
+ method @Deprecated @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract int tune(int, int);
+ method @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract void tune(@NonNull android.hardware.radio.ProgramSelector);
field public static final int DIRECTION_DOWN = 1; // 0x1
field public static final int DIRECTION_UP = 0; // 0x0
field @Deprecated public static final int ERROR_BACKGROUND_SCAN_FAILED = 6; // 0x6
@@ -5564,6 +5587,14 @@
field @Deprecated public static final int ERROR_HARDWARE_FAILURE = 0; // 0x0
field @Deprecated public static final int ERROR_SCAN_TIMEOUT = 3; // 0x3
field @Deprecated public static final int ERROR_SERVER_DIED = 1; // 0x1
+ field public static final int TUNER_RESULT_CANCELED = 6; // 0x6
+ field public static final int TUNER_RESULT_INTERNAL_ERROR = 1; // 0x1
+ field public static final int TUNER_RESULT_INVALID_ARGUMENTS = 2; // 0x2
+ field public static final int TUNER_RESULT_INVALID_STATE = 3; // 0x3
+ field public static final int TUNER_RESULT_NOT_SUPPORTED = 4; // 0x4
+ field public static final int TUNER_RESULT_OK = 0; // 0x0
+ field public static final int TUNER_RESULT_TIMEOUT = 5; // 0x5
+ field public static final int TUNER_RESULT_UNKNOWN_ERROR = 7; // 0x7
}
public abstract static class RadioTuner.Callback {
@@ -5571,6 +5602,7 @@
method public void onAntennaState(boolean);
method public void onBackgroundScanAvailabilityChange(boolean);
method public void onBackgroundScanComplete();
+ method public void onConfigFlagUpdated(int, boolean);
method @Deprecated public void onConfigurationChanged(android.hardware.radio.RadioManager.BandConfig);
method public void onControlChanged(boolean);
method public void onEmergencyAnnouncement(boolean);
@@ -10671,10 +10703,6 @@
field public static final int ERROR_UNKNOWN = 0; // 0x0
}
- public static final class ContactsContract.CommonDataKinds.Phone implements android.provider.ContactsContract.CommonDataKinds.CommonColumns android.provider.ContactsContract.DataColumnsWithJoins {
- field @NonNull @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS) public static final android.net.Uri ENTERPRISE_CONTENT_URI;
- }
-
@Deprecated public static final class ContactsContract.MetadataSync implements android.provider.BaseColumns android.provider.ContactsContract.MetadataSyncColumns {
field @Deprecated public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/contact_metadata";
field @Deprecated public static final String CONTENT_TYPE = "vnd.android.cursor.dir/contact_metadata";
@@ -10883,6 +10911,7 @@
field public static final String INSTALL_CARRIER_APP_NOTIFICATION_SLEEP_MILLIS = "install_carrier_app_notification_sleep_millis";
field public static final String OTA_DISABLE_AUTOMATIC_UPDATE = "ota_disable_automatic_update";
field public static final String REQUIRE_PASSWORD_TO_DECRYPT = "require_password_to_decrypt";
+ field public static final String SECURE_FRP_MODE = "secure_frp_mode";
field public static final String TETHER_OFFLOAD_DISABLED = "tether_offload_disabled";
field public static final String TETHER_SUPPORTED = "tether_supported";
field public static final String THEATER_MODE_ON = "theater_mode_on";
@@ -15737,9 +15766,12 @@
method public void onFeatureRemoved();
method public boolean queryCapabilityConfiguration(int, int);
method @NonNull public final android.telephony.ims.feature.MmTelFeature.MmTelCapabilities queryCapabilityStatus();
+ method public final void setCallAudioHandler(int);
method public void setTerminalBasedCallWaitingStatus(boolean);
method public void setUiTtyMode(int, @Nullable android.os.Message);
method public int shouldProcessCall(@NonNull String[]);
+ field public static final int AUDIO_HANDLER_ANDROID = 0; // 0x0
+ field public static final int AUDIO_HANDLER_BASEBAND = 1; // 0x1
field public static final String EXTRA_IS_UNKNOWN_CALL = "android.telephony.ims.feature.extra.IS_UNKNOWN_CALL";
field public static final String EXTRA_IS_USSD = "android.telephony.ims.feature.extra.IS_USSD";
field public static final int PROCESS_CALL_CSFB = 1; // 0x1
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 2820ef9..c36aa61 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -2203,10 +2203,6 @@
field public static final String HIDDEN_COLUMN_PREFIX = "x_";
}
- public static final class ContactsContract.CommonDataKinds.Phone implements android.provider.ContactsContract.CommonDataKinds.CommonColumns android.provider.ContactsContract.DataColumnsWithJoins {
- field @NonNull @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS) public static final android.net.Uri ENTERPRISE_CONTENT_URI;
- }
-
public static final class ContactsContract.PinnedPositions {
field public static final String UNDEMOTE_METHOD = "undemote";
}
@@ -2930,7 +2926,7 @@
method public static String actionToString(int);
method public final void setDisplayId(int);
field public static final int FLAG_IS_ACCESSIBILITY_EVENT = 2048; // 0x800
- field public static final int LAST_KEYCODE = 311; // 0x137
+ field public static final int LAST_KEYCODE = 312; // 0x138
}
public final class KeyboardShortcutGroup implements android.os.Parcelable {
@@ -2944,6 +2940,7 @@
}
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 30ff052..b9eb443 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -83,6 +83,7 @@
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
+import android.os.OutcomeReceiver;
import android.os.Parcelable;
import android.os.PersistableBundle;
import android.os.Process;
@@ -987,6 +988,17 @@
/** @hide */
boolean mIsInPictureInPictureMode;
+ /** @hide */
+ @IntDef(prefix = { "FULLSCREEN_REQUEST_" }, value = {
+ FULLSCREEN_MODE_REQUEST_EXIT,
+ FULLSCREEN_MODE_REQUEST_ENTER
+ })
+ public @interface FullscreenModeRequest {}
+
+ public static final int FULLSCREEN_MODE_REQUEST_EXIT = 0;
+
+ public static final int FULLSCREEN_MODE_REQUEST_ENTER = 1;
+
private boolean mShouldDockBigOverlays;
private UiTranslationController mUiTranslationController;
@@ -3001,6 +3013,36 @@
}
/**
+ * Request to put the a freeform activity into fullscreen. This will only be allowed if the
+ * activity is on a freeform display, such as a desktop device. The requester has to be the
+ * top-most activity and the request should be a response to a user input. When getting
+ * fullscreen and receiving corresponding {@link #onConfigurationChanged(Configuration)} and
+ * {@link #onMultiWindowModeChanged(boolean, Configuration)}, the activity should relayout
+ * itself and the system bars' visibilities can be controlled as usual fullscreen apps.
+ *
+ * Calling it again with the exit request can restore the activity to the previous status.
+ * This will only happen when it got into fullscreen through this API.
+ *
+ * If an app wants to be in fullscreen always, it should claim as not being resizable
+ * by setting
+ * <a href="https://developer.android.com/guide/topics/large-screens/multi-window-support#resizeableActivity">
+ * {@code android:resizableActivity="false"}</a> instead of calling this API.
+ *
+ * @param request Can be {@link #FULLSCREEN_MODE_REQUEST_ENTER} or
+ * {@link #FULLSCREEN_MODE_REQUEST_EXIT} to indicate this request is to get
+ * fullscreen or get restored.
+ * @param approvalCallback Optional callback, use {@code null} when not necessary. When the
+ * request is approved or rejected, the callback will be triggered. This
+ * will happen before any configuration change. The callback will be
+ * dispatched on the main thread.
+ */
+ public void requestFullscreenMode(@NonNull @FullscreenModeRequest int request,
+ @Nullable OutcomeReceiver<Void, Throwable> approvalCallback) {
+ FullscreenRequestHandler.requestFullscreenMode(
+ request, approvalCallback, mCurrentConfig, getActivityToken());
+ }
+
+ /**
* Specifies a preference to dock big overlays like the expanded picture-in-picture on TV
* (see {@link PictureInPictureParams.Builder#setExpandedAspectRatio}). Docking puts the
* big overlay side-by-side next to this activity, so that both windows are fully visible to
@@ -6612,16 +6654,64 @@
}
/**
- * 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/ActivityClient.java b/core/java/android/app/ActivityClient.java
index 324b8e7..ce99119 100644
--- a/core/java/android/app/ActivityClient.java
+++ b/core/java/android/app/ActivityClient.java
@@ -24,6 +24,7 @@
import android.content.res.Resources;
import android.os.Bundle;
import android.os.IBinder;
+import android.os.IRemoteCallback;
import android.os.PersistableBundle;
import android.os.RemoteException;
import android.util.Singleton;
@@ -372,6 +373,14 @@
}
}
+ void requestMultiwindowFullscreen(IBinder token, int request, IRemoteCallback callback) {
+ try {
+ getActivityClientController().requestMultiwindowFullscreen(token, request, callback);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
void startLockTaskModeByToken(IBinder token) {
try {
getActivityClientController().startLockTaskModeByToken(token);
diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java
index 52ef7fb..1b92312 100644
--- a/core/java/android/app/ActivityOptions.java
+++ b/core/java/android/app/ActivityOptions.java
@@ -202,6 +202,12 @@
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 @@
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 @@
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 @@
}
/**
+ * 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 @@
}
/**
+ * 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 @@
break;
}
mLockTaskMode = otherOptions.mLockTaskMode;
+ mShareIdentity = otherOptions.mShareIdentity;
mAnimSpecs = otherOptions.mAnimSpecs;
mAnimationFinishedListener = otherOptions.mAnimationFinishedListener;
mSpecsFuture = otherOptions.mSpecsFuture;
@@ -2123,6 +2173,9 @@
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/ActivityTaskManager.java b/core/java/android/app/ActivityTaskManager.java
index 7cfca97..be8f48d 100644
--- a/core/java/android/app/ActivityTaskManager.java
+++ b/core/java/android/app/ActivityTaskManager.java
@@ -62,6 +62,12 @@
public static final int INVALID_TASK_ID = -1;
/**
+ * Invalid windowing mode.
+ * @hide
+ */
+ public static final int INVALID_WINDOWING_MODE = -1;
+
+ /**
* Input parameter to {@link IActivityTaskManager#resizeTask} which indicates
* that the resize doesn't need to preserve the window, and can be skipped if bounds
* is unchanged. This mode is used by window manager in most cases.
diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java
index 569f4dd..4d3f9e4 100644
--- a/core/java/android/app/ApplicationPackageManager.java
+++ b/core/java/android/app/ApplicationPackageManager.java
@@ -3816,8 +3816,17 @@
@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/FullscreenRequestHandler.java b/core/java/android/app/FullscreenRequestHandler.java
new file mode 100644
index 0000000..52f461d
--- /dev/null
+++ b/core/java/android/app/FullscreenRequestHandler.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import static android.app.Activity.FULLSCREEN_MODE_REQUEST_ENTER;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.IRemoteCallback;
+import android.os.OutcomeReceiver;
+
+/**
+ * @hide
+ */
+public class FullscreenRequestHandler {
+ @IntDef(prefix = { "RESULT_" }, value = {
+ RESULT_APPROVED,
+ RESULT_FAILED_NOT_IN_FREEFORM,
+ RESULT_FAILED_NOT_IN_FULLSCREEN_WITH_HISTORY,
+ RESULT_FAILED_NOT_DEFAULT_FREEFORM,
+ RESULT_FAILED_NOT_TOP_FOCUSED
+ })
+ public @interface RequestResult {}
+
+ public static final int RESULT_APPROVED = 0;
+ public static final int RESULT_FAILED_NOT_IN_FREEFORM = 1;
+ public static final int RESULT_FAILED_NOT_IN_FULLSCREEN_WITH_HISTORY = 2;
+ public static final int RESULT_FAILED_NOT_DEFAULT_FREEFORM = 3;
+ public static final int RESULT_FAILED_NOT_TOP_FOCUSED = 4;
+
+ public static final String REMOTE_CALLBACK_RESULT_KEY = "result";
+
+ static void requestFullscreenMode(@NonNull @Activity.FullscreenModeRequest int request,
+ @Nullable OutcomeReceiver<Void, Throwable> approvalCallback, Configuration config,
+ IBinder token) {
+ int earlyCheck = earlyCheckRequestMatchesWindowingMode(
+ request, config.windowConfiguration.getWindowingMode());
+ if (earlyCheck != RESULT_APPROVED) {
+ if (approvalCallback != null) {
+ notifyFullscreenRequestResult(approvalCallback, earlyCheck);
+ }
+ return;
+ }
+ try {
+ if (approvalCallback != null) {
+ ActivityClient.getInstance().requestMultiwindowFullscreen(token, request,
+ new IRemoteCallback.Stub() {
+ @Override
+ public void sendResult(Bundle res) {
+ notifyFullscreenRequestResult(
+ approvalCallback, res.getInt(REMOTE_CALLBACK_RESULT_KEY));
+ }
+ });
+ } else {
+ ActivityClient.getInstance().requestMultiwindowFullscreen(token, request, null);
+ }
+ } catch (Throwable e) {
+ if (approvalCallback != null) {
+ approvalCallback.onError(e);
+ }
+ }
+ }
+
+ private static void notifyFullscreenRequestResult(
+ OutcomeReceiver<Void, Throwable> callback, int result) {
+ Throwable e = null;
+ switch (result) {
+ case RESULT_FAILED_NOT_IN_FREEFORM:
+ e = new IllegalStateException("The window is not a freeform window, the request "
+ + "to get into fullscreen cannot be approved.");
+ break;
+ case RESULT_FAILED_NOT_IN_FULLSCREEN_WITH_HISTORY:
+ e = new IllegalStateException("The window is not in fullscreen by calling the "
+ + "requestFullscreenMode API before, such that cannot be restored.");
+ break;
+ case RESULT_FAILED_NOT_DEFAULT_FREEFORM:
+ e = new IllegalStateException("The window is not launched in freeform by default.");
+ break;
+ case RESULT_FAILED_NOT_TOP_FOCUSED:
+ e = new IllegalStateException("The window is not the top focused window.");
+ break;
+ default:
+ callback.onResult(null);
+ break;
+ }
+ if (e != null) {
+ callback.onError(e);
+ }
+ }
+
+ private static int earlyCheckRequestMatchesWindowingMode(int request, int windowingMode) {
+ if (request == FULLSCREEN_MODE_REQUEST_ENTER) {
+ if (windowingMode != WINDOWING_MODE_FREEFORM) {
+ return RESULT_FAILED_NOT_IN_FREEFORM;
+ }
+ } else {
+ if (windowingMode != WINDOWING_MODE_FULLSCREEN) {
+ return RESULT_FAILED_NOT_IN_FULLSCREEN_WITH_HISTORY;
+ }
+ }
+ return RESULT_APPROVED;
+ }
+}
diff --git a/core/java/android/app/IActivityClientController.aidl b/core/java/android/app/IActivityClientController.aidl
index 8b655b9..286b84c 100644
--- a/core/java/android/app/IActivityClientController.aidl
+++ b/core/java/android/app/IActivityClientController.aidl
@@ -24,6 +24,7 @@
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Bundle;
+import android.os.IRemoteCallback;
import android.os.PersistableBundle;
import android.view.RemoteAnimationDefinition;
import android.window.SizeConfigurationBuckets;
@@ -102,6 +103,8 @@
void setPictureInPictureParams(in IBinder token, in PictureInPictureParams params);
oneway void setShouldDockBigOverlays(in IBinder token, in boolean shouldDockBigOverlays);
void toggleFreeformWindowingMode(in IBinder token);
+ oneway void requestMultiwindowFullscreen(in IBinder token, in int request,
+ in IRemoteCallback callback);
oneway void startLockTaskModeByToken(in IBinder token);
oneway void stopLockTaskModeByToken(in IBinder token);
diff --git a/core/java/android/app/StatusBarManager.java b/core/java/android/app/StatusBarManager.java
index f63f406..6c43010 100644
--- a/core/java/android/app/StatusBarManager.java
+++ b/core/java/android/app/StatusBarManager.java
@@ -511,10 +511,26 @@
@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/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 1e6412f..128a872 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -3918,34 +3918,46 @@
public static @interface MtePolicy {}
/**
- * Set MTE policy for device. MTE_ENABLED does not necessarily enable MTE if set on a device
- * that does not support MTE.
- *
- * The default policy is MTE_NOT_CONTROLLED_BY_POLICY.
- *
- * Memory Tagging Extension (MTE) is a CPU extension that allows to protect against certain
+ * Called by a device owner or profile owner of an organization-owned device to set the Memory
+ * Tagging Extension (MTE) policy. MTE is a CPU extension that allows to protect against certain
* classes of security problems at a small runtime performance cost overhead.
*
- * @param policy the policy to be set
+ * <p>The MTE policy can only be set to {@link #MTE_DISABLED} if called by a device owner.
+ * Otherwise a {@link SecurityException} will be thrown.
+ *
+ * @throws SecurityException if caller is not device owner or profile owner of org-owned device
+ * or if called on a parent instance
+ * @param policy the MTE policy to be set
*/
public void setMtePolicy(@MtePolicy int policy) {
- // TODO(b/244290023): implement
- // This is SecurityException to temporarily make ParentProfileTest happy.
- // This is not used.
- throw new SecurityException("not implemented");
+ throwIfParentInstance("setMtePolicy");
+ if (mService != null) {
+ try {
+ mService.setMtePolicy(policy);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
}
/**
- * Get currently set MTE policy. This is not necessarily the same as the state of MTE on the
- * device, as the device might not support MTE.
+ * Called by a device owner, a profile owner of an organization-owned device or the system to
+ * get the Memory Tagging Extension (MTE) policy
*
- * @return the currently set policy
+ * @throws SecurityException if caller is not device owner or profile owner of org-owned device
+ * or system uid, or if called on a parent instance
+ * @return the currently set MTE policy
*/
public @MtePolicy int getMtePolicy() {
- // TODO(b/244290023): implement
- // This is SecurityException to temporarily make ParentProfileTest happy.
- // This is not used.
- throw new SecurityException("not implemented");
+ throwIfParentInstance("setMtePolicy");
+ if (mService != null) {
+ try {
+ return mService.getMtePolicy();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return MTE_NOT_CONTROLLED_BY_POLICY;
}
// TODO: Expose this as SystemAPI once we add the query API
diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl
index 8a40265..5383dca 100644
--- a/core/java/android/app/admin/IDevicePolicyManager.aidl
+++ b/core/java/android/app/admin/IDevicePolicyManager.aidl
@@ -571,4 +571,7 @@
void setApplicationExemptions(String packageName, in int[]exemptions);
int[] getApplicationExemptions(String packageName);
+
+ void setMtePolicy(int flag);
+ int getMtePolicy();
}
diff --git a/core/java/android/companion/virtual/IVirtualDevice.aidl b/core/java/android/companion/virtual/IVirtualDevice.aidl
index 5c47ea2..f17d186 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.VirtualMouseScrollEvent;
import android.hardware.input.VirtualTouchEvent;
import android.hardware.input.VirtualTouchscreenConfig;
+import android.hardware.input.VirtualNavigationTouchpadConfig;
import android.os.ResultReceiver;
/**
@@ -84,6 +85,10 @@
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 57bdf2d..dba7c8e 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.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 @@
}
/**
+ * 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 @@
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/Intent.java b/core/java/android/content/Intent.java
index ebc00a7..8aa0454 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -6117,9 +6117,8 @@
public static final String EXTRA_UID = "android.intent.extra.UID";
/**
- * @hide String array of package names.
+ * String array of package names.
*/
- @SystemApi
public static final String EXTRA_PACKAGES = "android.intent.extra.PACKAGES";
/**
diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java
index 2c28268..84811ea 100644
--- a/core/java/android/content/pm/ApplicationInfo.java
+++ b/core/java/android/content/pm/ApplicationInfo.java
@@ -1528,12 +1528,14 @@
* the application, e.g. the target SDK version.
* @hide
*/
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
public static final int HIDDEN_API_ENFORCEMENT_DEFAULT = -1;
/**
* No API enforcement; the app can access the entire internal private API. Only for use by
* system apps.
* @hide
*/
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
public static final int HIDDEN_API_ENFORCEMENT_DISABLED = 0;
/**
* No API enforcement, but enable the detection logic and warnings. Observed behaviour is the
@@ -1541,11 +1543,13 @@
* APIs are accessed.
* @hide
* */
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
public static final int HIDDEN_API_ENFORCEMENT_JUST_WARN = 1;
/**
* Dark grey list enforcement. Enforces the dark grey and black lists
* @hide
*/
+ @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
public static final int HIDDEN_API_ENFORCEMENT_ENABLED = 2;
private static final int HIDDEN_API_ENFORCEMENT_MIN = HIDDEN_API_ENFORCEMENT_DEFAULT;
diff --git a/core/java/android/content/pm/IPackageInstaller.aidl b/core/java/android/content/pm/IPackageInstaller.aidl
index 1e928bd..115d4b0 100644
--- a/core/java/android/content/pm/IPackageInstaller.aidl
+++ b/core/java/android/content/pm/IPackageInstaller.aidl
@@ -69,4 +69,7 @@
void setSilentUpdatesThrottleTime(long throttleTimeInSeconds);
void checkInstallConstraints(String installerPackageName, in List<String> packageNames,
in PackageInstaller.InstallConstraints constraints, in RemoteCallback callback);
+ void waitForInstallConstraints(String installerPackageName, in List<String> packageNames,
+ in PackageInstaller.InstallConstraints constraints, in IntentSender callback,
+ long timeout);
}
diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl
index 35afe9f..81bea2e 100644
--- a/core/java/android/content/pm/IPackageManager.aidl
+++ b/core/java/android/content/pm/IPackageManager.aidl
@@ -797,5 +797,5 @@
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/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java
index 1f01ae9..f17d8fa 100644
--- a/core/java/android/content/pm/PackageInstaller.java
+++ b/core/java/android/content/pm/PackageInstaller.java
@@ -30,6 +30,7 @@
import android.Manifest;
import android.annotation.CallbackExecutor;
import android.annotation.CurrentTimeMillisLong;
+import android.annotation.DurationMillisLong;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -248,6 +249,24 @@
*/
public static final String EXTRA_STORAGE_PATH = "android.content.pm.extra.STORAGE_PATH";
+ /**
+ * The {@link InstallConstraints} object.
+ *
+ * @see Intent#getParcelableExtra(String, Class)
+ * @see #waitForInstallConstraints(List, InstallConstraints, IntentSender, long)
+ */
+ public static final String EXTRA_INSTALL_CONSTRAINTS =
+ "android.content.pm.extra.INSTALL_CONSTRAINTS";
+
+ /**
+ * The {@link InstallConstraintsResult} object.
+ *
+ * @see Intent#getParcelableExtra(String, Class)
+ * @see #waitForInstallConstraints(List, InstallConstraints, IntentSender, long)
+ */
+ public static final String EXTRA_INSTALL_CONSTRAINTS_RESULT =
+ "android.content.pm.extra.INSTALL_CONSTRAINTS_RESULT";
+
/** {@hide} */
@Deprecated
public static final String EXTRA_PACKAGE_NAMES = "android.content.pm.extra.PACKAGE_NAMES";
@@ -885,6 +904,32 @@
}
/**
+ * Similar to {@link #checkInstallConstraints(List, InstallConstraints, Executor, Consumer)},
+ * but the callback is invoked only when the constraints are satisfied or after timeout.
+ *
+ * @param callback Called when the constraints are satisfied or after timeout.
+ * Intents sent to this callback contain:
+ * {@link Intent#EXTRA_PACKAGES} for the input package names,
+ * {@link #EXTRA_INSTALL_CONSTRAINTS} for the input constraints,
+ * {@link #EXTRA_INSTALL_CONSTRAINTS_RESULT} for the result.
+ * @param timeoutMillis The maximum time to wait, in milliseconds until the constraints are
+ * satisfied. Valid range is from 0 to one week. {@code 0} means the
+ * callback will be invoked immediately no matter constraints are
+ * satisfied or not.
+ */
+ public void waitForInstallConstraints(@NonNull List<String> packageNames,
+ @NonNull InstallConstraints constraints,
+ @NonNull IntentSender callback,
+ @DurationMillisLong long timeoutMillis) {
+ try {
+ mInstaller.waitForInstallConstraints(
+ mInstallerPackageName, packageNames, constraints, callback, timeoutMillis);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Events for observing session lifecycle.
* <p>
* A typical session lifecycle looks like this:
@@ -3807,7 +3852,7 @@
* Note the constraints are applied transitively. If app Foo is used by app Bar (via shared
* library or bounded service), the constraints will also be applied to Bar.
*/
- @DataClass(genParcelable = true, genHiddenConstructor = true)
+ @DataClass(genParcelable = true, genHiddenConstructor = true, genEqualsHashCode=true)
public static final class InstallConstraints implements Parcelable {
/**
* Preset constraints suitable for gentle update.
@@ -3968,6 +4013,41 @@
@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(InstallConstraints other) { ... }
+ // boolean fieldNameEquals(FieldType otherValue) { ... }
+
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ @SuppressWarnings("unchecked")
+ InstallConstraints that = (InstallConstraints) o;
+ //noinspection PointlessBooleanExpression
+ return true
+ && mRequireDeviceIdle == that.mRequireDeviceIdle
+ && mRequireAppNotForeground == that.mRequireAppNotForeground
+ && mRequireAppNotInteracting == that.mRequireAppNotInteracting
+ && mRequireAppNotTopVisible == that.mRequireAppNotTopVisible
+ && mRequireNotInCall == that.mRequireNotInCall;
+ }
+
+ @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 + Boolean.hashCode(mRequireDeviceIdle);
+ _hash = 31 * _hash + Boolean.hashCode(mRequireAppNotForeground);
+ _hash = 31 * _hash + Boolean.hashCode(mRequireAppNotInteracting);
+ _hash = 31 * _hash + Boolean.hashCode(mRequireAppNotTopVisible);
+ _hash = 31 * _hash + Boolean.hashCode(mRequireNotInCall);
+ return _hash;
+ }
+
+ @Override
+ @DataClass.Generated.Member
public void writeToParcel(@NonNull Parcel dest, int flags) {
// You can override field parcelling by defining methods like:
// void parcelFieldName(Parcel dest, int flags) { ... }
@@ -4023,10 +4103,10 @@
};
@DataClass.Generated(
- time = 1668650523752L,
+ time = 1670207178734L,
codegenVersion = "1.0.23",
sourceFile = "frameworks/base/core/java/android/content/pm/PackageInstaller.java",
- inputSignatures = "public static final @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints GENTLE_UPDATE\nprivate final boolean mRequireDeviceIdle\nprivate final boolean mRequireAppNotForeground\nprivate final boolean mRequireAppNotInteracting\nprivate final boolean mRequireAppNotTopVisible\nprivate final boolean mRequireNotInCall\nclass InstallConstraints extends java.lang.Object implements [android.os.Parcelable]\nprivate boolean mRequireDeviceIdle\nprivate boolean mRequireAppNotForeground\nprivate boolean mRequireAppNotInteracting\nprivate boolean mRequireAppNotTopVisible\nprivate boolean mRequireNotInCall\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder requireDeviceIdle()\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder requireAppNotForeground()\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder requireAppNotInteracting()\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder requireAppNotTopVisible()\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder requireNotInCall()\npublic @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints build()\nclass Builder extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genParcelable=true, genHiddenConstructor=true)")
+ inputSignatures = "public static final @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints GENTLE_UPDATE\nprivate final boolean mRequireDeviceIdle\nprivate final boolean mRequireAppNotForeground\nprivate final boolean mRequireAppNotInteracting\nprivate final boolean mRequireAppNotTopVisible\nprivate final boolean mRequireNotInCall\nclass InstallConstraints extends java.lang.Object implements [android.os.Parcelable]\nprivate boolean mRequireDeviceIdle\nprivate boolean mRequireAppNotForeground\nprivate boolean mRequireAppNotInteracting\nprivate boolean mRequireAppNotTopVisible\nprivate boolean mRequireNotInCall\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder requireDeviceIdle()\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder requireAppNotForeground()\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder requireAppNotInteracting()\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder requireAppNotTopVisible()\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder requireNotInCall()\npublic @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints build()\nclass Builder extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genParcelable=true, genHiddenConstructor=true, genEqualsHashCode=true)")
@Deprecated
private void __metadata() {}
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index ec490d1..f3ccfb0 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -2979,6 +2979,14 @@
"android.software.virtualization_framework";
/**
+ * Feature for {@link #getSystemAvailableFeatures()} and {@link #hasSystemFeature(String)}.
+ * This feature indicates whether device supports seamless refresh rate switching.
+ */
+ @SdkConstant(SdkConstantType.FEATURE)
+ public static final String FEATURE_SEAMLESS_REFRESH_RATE_SWITCHING
+ = "android.software.seamless_refresh_rate_switching";
+
+ /**
* Feature for {@link #getSystemAvailableFeatures} and
* {@link #hasSystemFeature(String, int)}: If this feature is supported, the Vulkan
* implementation on this device is hardware accelerated, and the Vulkan native API will
@@ -10388,6 +10396,30 @@
}
/**
+ * 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/content/pm/UserProperties.java b/core/java/android/content/pm/UserProperties.java
index fd35378..fb61b37 100644
--- a/core/java/android/content/pm/UserProperties.java
+++ b/core/java/android/content/pm/UserProperties.java
@@ -46,6 +46,8 @@
private static final String ATTR_SHOW_IN_SETTINGS = "showInSettings";
private static final String ATTR_INHERIT_DEVICE_POLICY = "inheritDevicePolicy";
private static final String ATTR_USE_PARENTS_CONTACTS = "useParentsContacts";
+ private static final String ATTR_UPDATE_CROSS_PROFILE_INTENT_FILTERS_ON_OTA =
+ "updateCrossProfileIntentFiltersOnOTA";
/** Index values of each property (to indicate whether they are present in this object). */
@IntDef(prefix = "INDEX_", value = {
@@ -54,6 +56,7 @@
INDEX_SHOW_IN_SETTINGS,
INDEX_INHERIT_DEVICE_POLICY,
INDEX_USE_PARENTS_CONTACTS,
+ INDEX_UPDATE_CROSS_PROFILE_INTENT_FILTERS_ON_OTA
})
@Retention(RetentionPolicy.SOURCE)
private @interface PropertyIndex {
@@ -63,6 +66,7 @@
private static final int INDEX_SHOW_IN_SETTINGS = 2;
private static final int INDEX_INHERIT_DEVICE_POLICY = 3;
private static final int INDEX_USE_PARENTS_CONTACTS = 4;
+ private static final int INDEX_UPDATE_CROSS_PROFILE_INTENT_FILTERS_ON_OTA = 5;
/** A bit set, mapping each PropertyIndex to whether it is present (1) or absent (0). */
private long mPropertiesPresent = 0;
@@ -199,6 +203,7 @@
// Add items that require exposeAllFields to be true (strictest permission level).
setStartWithParent(orig.getStartWithParent());
setInheritDevicePolicy(orig.getInheritDevicePolicy());
+ setUpdateCrossProfileIntentFiltersOnOTA(orig.getUpdateCrossProfileIntentFiltersOnOTA());
}
if (hasManagePermission) {
// Add items that require MANAGE_USERS or stronger.
@@ -354,6 +359,34 @@
*/
private boolean mUseParentsContacts;
+ /**
+ * Returns true if user needs to update default
+ * {@link com.android.server.pm.CrossProfileIntentFilter} with its parents during an OTA update
+ * @hide
+ */
+ public boolean getUpdateCrossProfileIntentFiltersOnOTA() {
+ if (isPresent(INDEX_UPDATE_CROSS_PROFILE_INTENT_FILTERS_ON_OTA)) {
+ return mUpdateCrossProfileIntentFiltersOnOTA;
+ }
+ if (mDefaultProperties != null) {
+ return mDefaultProperties.mUpdateCrossProfileIntentFiltersOnOTA;
+ }
+ throw new SecurityException("You don't have permission to query "
+ + "updateCrossProfileIntentFiltersOnOTA");
+ }
+
+ /** @hide */
+ public void setUpdateCrossProfileIntentFiltersOnOTA(boolean val) {
+ this.mUpdateCrossProfileIntentFiltersOnOTA = val;
+ setPresent(INDEX_UPDATE_CROSS_PROFILE_INTENT_FILTERS_ON_OTA);
+ }
+
+ /*
+ Indicate if {@link com.android.server.pm.CrossProfileIntentFilter}s need to be updated during
+ OTA update between user-parent
+ */
+ private boolean mUpdateCrossProfileIntentFiltersOnOTA;
+
@Override
public String toString() {
// Please print in increasing order of PropertyIndex.
@@ -364,6 +397,8 @@
+ ", mShowInSettings=" + getShowInSettings()
+ ", mInheritDevicePolicy=" + getInheritDevicePolicy()
+ ", mUseParentsContacts=" + getUseParentsContacts()
+ + ", mUpdateCrossProfileIntentFiltersOnOTA="
+ + getUpdateCrossProfileIntentFiltersOnOTA()
+ "}";
}
@@ -380,6 +415,8 @@
pw.println(prefix + " mShowInSettings=" + getShowInSettings());
pw.println(prefix + " mInheritDevicePolicy=" + getInheritDevicePolicy());
pw.println(prefix + " mUseParentsContacts=" + getUseParentsContacts());
+ pw.println(prefix + " mUpdateCrossProfileIntentFiltersOnOTA="
+ + getUpdateCrossProfileIntentFiltersOnOTA());
}
/**
@@ -428,6 +465,9 @@
case ATTR_USE_PARENTS_CONTACTS:
setUseParentsContacts(parser.getAttributeBoolean(i));
break;
+ case ATTR_UPDATE_CROSS_PROFILE_INTENT_FILTERS_ON_OTA:
+ setUpdateCrossProfileIntentFiltersOnOTA(parser.getAttributeBoolean(i));
+ break;
default:
Slog.w(LOG_TAG, "Skipping unknown property " + attributeName);
}
@@ -462,6 +502,11 @@
serializer.attributeBoolean(null, ATTR_USE_PARENTS_CONTACTS,
mUseParentsContacts);
}
+ if (isPresent(INDEX_UPDATE_CROSS_PROFILE_INTENT_FILTERS_ON_OTA)) {
+ serializer.attributeBoolean(null,
+ ATTR_UPDATE_CROSS_PROFILE_INTENT_FILTERS_ON_OTA,
+ mUpdateCrossProfileIntentFiltersOnOTA);
+ }
}
// For use only with an object that has already had any permission-lacking fields stripped out.
@@ -473,6 +518,7 @@
dest.writeInt(mShowInSettings);
dest.writeInt(mInheritDevicePolicy);
dest.writeBoolean(mUseParentsContacts);
+ dest.writeBoolean(mUpdateCrossProfileIntentFiltersOnOTA);
}
/**
@@ -488,6 +534,7 @@
mShowInSettings = source.readInt();
mInheritDevicePolicy = source.readInt();
mUseParentsContacts = source.readBoolean();
+ mUpdateCrossProfileIntentFiltersOnOTA = source.readBoolean();
}
@Override
@@ -517,6 +564,7 @@
private @ShowInSettings int mShowInSettings = SHOW_IN_SETTINGS_WITH_PARENT;
private @InheritDevicePolicy int mInheritDevicePolicy = INHERIT_DEVICE_POLICY_NO;
private boolean mUseParentsContacts = false;
+ private boolean mUpdateCrossProfileIntentFiltersOnOTA = false;
public Builder setShowInLauncher(@ShowInLauncher int showInLauncher) {
mShowInLauncher = showInLauncher;
@@ -546,6 +594,13 @@
return this;
}
+ /** Sets the value for {@link #mUpdateCrossProfileIntentFiltersOnOTA} */
+ public Builder setUpdateCrossProfileIntentFiltersOnOTA(boolean
+ updateCrossProfileIntentFiltersOnOTA) {
+ mUpdateCrossProfileIntentFiltersOnOTA = updateCrossProfileIntentFiltersOnOTA;
+ return this;
+ }
+
/** Builds a UserProperties object with *all* values populated. */
public UserProperties build() {
return new UserProperties(
@@ -553,7 +608,8 @@
mStartWithParent,
mShowInSettings,
mInheritDevicePolicy,
- mUseParentsContacts);
+ mUseParentsContacts,
+ mUpdateCrossProfileIntentFiltersOnOTA);
}
} // end Builder
@@ -563,7 +619,7 @@
boolean startWithParent,
@ShowInSettings int showInSettings,
@InheritDevicePolicy int inheritDevicePolicy,
- boolean useParentsContacts) {
+ boolean useParentsContacts, boolean updateCrossProfileIntentFiltersOnOTA) {
mDefaultProperties = null;
setShowInLauncher(showInLauncher);
@@ -571,5 +627,6 @@
setShowInSettings(showInSettings);
setInheritDevicePolicy(inheritDevicePolicy);
setUseParentsContacts(useParentsContacts);
+ setUpdateCrossProfileIntentFiltersOnOTA(updateCrossProfileIntentFiltersOnOTA);
}
}
diff --git a/core/java/android/hardware/camera2/CameraExtensionSession.java b/core/java/android/hardware/camera2/CameraExtensionSession.java
index 6f895d5..b0fafea 100644
--- a/core/java/android/hardware/camera2/CameraExtensionSession.java
+++ b/core/java/android/hardware/camera2/CameraExtensionSession.java
@@ -18,6 +18,11 @@
import android.annotation.IntRange;
import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.hardware.camera2.impl.PublicKey;
+import android.hardware.camera2.utils.TypeReference;
+import android.util.Pair;
+import android.util.Range;
import java.util.concurrent.Executor;
@@ -424,6 +429,28 @@
}
/**
+ * Return the realtime still {@link #capture} latency.
+ *
+ * <p>The pair will be in milliseconds with the first value indicating the capture latency from
+ * the {@link ExtensionCaptureCallback#onCaptureStarted} until
+ * {@link ExtensionCaptureCallback#onCaptureProcessStarted}
+ * and the second value containing the estimated post-processing latency from
+ * {@link ExtensionCaptureCallback#onCaptureProcessStarted} until the processed frame returns
+ * to the client.</p>
+ *
+ * <p>The estimations will take into account the current environment conditions, the camera
+ * state and will include the time spent processing the multi-frame capture request along with
+ * any additional time for encoding of the processed buffer if necessary.</p>
+ *
+ * @return The realtime still capture latency,
+ * or {@code null} if the estimation is not supported.
+ */
+ @Nullable
+ public Pair<Long, Long> getRealtimeStillCaptureLatency() throws CameraAccessException {
+ throw new UnsupportedOperationException("Subclasses must override this method");
+ }
+
+ /**
* Close this capture session asynchronously.
*
* <p>Closing a session frees up the target output Surfaces of the session
diff --git a/core/java/android/hardware/camera2/extension/IImageCaptureExtenderImpl.aidl b/core/java/android/hardware/camera2/extension/IImageCaptureExtenderImpl.aidl
index 615536b..360f809 100644
--- a/core/java/android/hardware/camera2/extension/IImageCaptureExtenderImpl.aidl
+++ b/core/java/android/hardware/camera2/extension/IImageCaptureExtenderImpl.aidl
@@ -19,6 +19,7 @@
import android.hardware.camera2.extension.CaptureStageImpl;
import android.hardware.camera2.extension.ICaptureProcessorImpl;
+import android.hardware.camera2.extension.LatencyPair;
import android.hardware.camera2.extension.LatencyRange;
import android.hardware.camera2.extension.Size;
import android.hardware.camera2.extension.SizeList;
@@ -43,4 +44,5 @@
CameraMetadataNative getAvailableCaptureRequestKeys();
CameraMetadataNative getAvailableCaptureResultKeys();
boolean isCaptureProcessProgressAvailable();
+ @nullable LatencyPair getRealtimeCaptureLatency();
}
diff --git a/core/java/android/hardware/camera2/extension/ISessionProcessorImpl.aidl b/core/java/android/hardware/camera2/extension/ISessionProcessorImpl.aidl
index 0eca5a7..e0f1b64 100644
--- a/core/java/android/hardware/camera2/extension/ISessionProcessorImpl.aidl
+++ b/core/java/android/hardware/camera2/extension/ISessionProcessorImpl.aidl
@@ -19,6 +19,8 @@
import android.hardware.camera2.extension.CameraSessionConfig;
import android.hardware.camera2.extension.ICaptureCallback;
import android.hardware.camera2.extension.IRequestProcessorImpl;
+import android.hardware.camera2.extension.LatencyPair;
+import android.hardware.camera2.extension.LatencyRange;
import android.hardware.camera2.extension.OutputSurface;
/** @hide */
@@ -34,4 +36,5 @@
int startCapture(in ICaptureCallback callback);
void setParameters(in CaptureRequest captureRequest);
int startTrigger(in CaptureRequest captureRequest, in ICaptureCallback callback);
+ @nullable LatencyPair getRealtimeCaptureLatency();
}
diff --git a/core/java/android/hardware/camera2/extension/LatencyPair.aidl b/core/java/android/hardware/camera2/extension/LatencyPair.aidl
new file mode 100644
index 0000000..5174f1d
--- /dev/null
+++ b/core/java/android/hardware/camera2/extension/LatencyPair.aidl
@@ -0,0 +1,23 @@
+/**
+ * Copyright (c) 2022, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.hardware.camera2.extension;
+
+/** @hide */
+parcelable LatencyPair
+{
+ long first;
+ long second;
+}
diff --git a/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java
index 3a8dc03..42c4411 100644
--- a/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java
@@ -43,6 +43,7 @@
import android.hardware.camera2.extension.IRequestCallback;
import android.hardware.camera2.extension.IRequestProcessorImpl;
import android.hardware.camera2.extension.ISessionProcessorImpl;
+import android.hardware.camera2.extension.LatencyPair;
import android.hardware.camera2.extension.OutputConfigId;
import android.hardware.camera2.extension.OutputSurface;
import android.hardware.camera2.extension.ParcelCaptureResult;
@@ -61,6 +62,8 @@
import android.os.HandlerThread;
import android.os.RemoteException;
import android.util.Log;
+import android.util.Pair;
+import android.util.Range;
import android.util.Size;
import android.view.Surface;
@@ -329,6 +332,28 @@
}
@Override
+ public Pair<Long, Long> getRealtimeStillCaptureLatency() throws CameraAccessException {
+ synchronized (mInterfaceLock) {
+ if (!mInitialized) {
+ throw new IllegalStateException("Uninitialized component");
+ }
+
+ try {
+ LatencyPair latency = mSessionProcessor.getRealtimeCaptureLatency();
+ if (latency != null) {
+ return new Pair<>(latency.first, latency.second);
+ }
+
+ return null;
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to query realtime latency! Extension service does not "
+ + "respond");
+ throw new CameraAccessException(CameraAccessException.CAMERA_ERROR);
+ }
+ }
+ }
+
+ @Override
public int setRepeatingRequest(@NonNull CaptureRequest request, @NonNull Executor executor,
@NonNull ExtensionCaptureCallback listener) throws CameraAccessException {
int seqId = -1;
diff --git a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java
index 389c214..259bd7b 100644
--- a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java
@@ -43,6 +43,7 @@
import android.hardware.camera2.extension.IPreviewExtenderImpl;
import android.hardware.camera2.extension.IProcessResultImpl;
import android.hardware.camera2.extension.IRequestUpdateProcessorImpl;
+import android.hardware.camera2.extension.LatencyPair;
import android.hardware.camera2.extension.ParcelImage;
import android.hardware.camera2.params.DynamicRangeProfiles;
import android.hardware.camera2.params.ExtensionSessionConfiguration;
@@ -59,6 +60,7 @@
import android.util.Log;
import android.util.LongSparseArray;
import android.util.Pair;
+import android.util.Range;
import android.util.Size;
import android.view.Surface;
@@ -466,6 +468,28 @@
}
@Override
+ public Pair<Long, Long> getRealtimeStillCaptureLatency() throws CameraAccessException {
+ synchronized (mInterfaceLock) {
+ if (!mInitialized) {
+ throw new IllegalStateException("Uninitialized component");
+ }
+
+ try {
+ LatencyPair latency = mImageExtender.getRealtimeCaptureLatency();
+ if (latency != null) {
+ return new Pair<>(latency.first, latency.second);
+ }
+
+ return null;
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to query realtime latency! Extension service does not "
+ + "respond");
+ throw new CameraAccessException(CameraAccessException.CAMERA_ERROR);
+ }
+ }
+ }
+
+ @Override
public int setRepeatingRequest(@NonNull CaptureRequest request,
@NonNull Executor executor,
@NonNull ExtensionCaptureCallback listener)
diff --git a/core/java/android/hardware/display/AmbientBrightnessDayStats.java b/core/java/android/hardware/display/AmbientBrightnessDayStats.java
index 8aff911..2816706 100644
--- a/core/java/android/hardware/display/AmbientBrightnessDayStats.java
+++ b/core/java/android/hardware/display/AmbientBrightnessDayStats.java
@@ -208,7 +208,7 @@
}
private int getBucketIndex(float ambientBrightness) {
- if (ambientBrightness < mBucketBoundaries[0]) {
+ if (ambientBrightness < mBucketBoundaries[0] || Float.isNaN(ambientBrightness)) {
return -1;
}
int low = 0;
diff --git a/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java
index 829908f..7409187 100644
--- a/core/java/android/hardware/display/DisplayManagerInternal.java
+++ b/core/java/android/hardware/display/DisplayManagerInternal.java
@@ -439,9 +439,6 @@
// 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 @@
policy = POLICY_BRIGHT;
useProximitySensor = false;
screenBrightnessOverride = PowerManager.BRIGHTNESS_INVALID_FLOAT;
- useAutoBrightness = false;
screenAutoBrightnessAdjustmentOverride = Float.NaN;
screenLowPowerBrightnessFactor = 0.5f;
blockScreenOn = false;
@@ -491,7 +487,6 @@
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 @@
&& useProximitySensor == other.useProximitySensor
&& floatEquals(screenBrightnessOverride,
other.screenBrightnessOverride)
- && useAutoBrightness == other.useAutoBrightness
&& floatEquals(screenAutoBrightnessAdjustmentOverride,
other.screenAutoBrightnessAdjustmentOverride)
&& screenLowPowerBrightnessFactor
@@ -539,7 +533,6 @@
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 0000000..2854034
--- /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 0000000..d912491
--- /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 0000000..f2805bb
--- /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/hardware/radio/IRadioService.aidl b/core/java/android/hardware/radio/IRadioService.aidl
index 9349cf7..c7131a7 100644
--- a/core/java/android/hardware/radio/IRadioService.aidl
+++ b/core/java/android/hardware/radio/IRadioService.aidl
@@ -31,7 +31,7 @@
List<RadioManager.ModuleProperties> listModules();
ITuner openTuner(int moduleId, in RadioManager.BandConfig bandConfig, boolean withAudio,
- in ITunerCallback callback);
+ in ITunerCallback callback, int targetSdkVersion);
ICloseHandle addAnnouncementListener(in int[] enabledTypes,
in IAnnouncementListener listener);
diff --git a/core/java/android/hardware/radio/ITuner.aidl b/core/java/android/hardware/radio/ITuner.aidl
index 7bf234b..e68c3cc 100644
--- a/core/java/android/hardware/radio/ITuner.aidl
+++ b/core/java/android/hardware/radio/ITuner.aidl
@@ -49,7 +49,7 @@
/**
* @throws IllegalStateException if called out of sequence
*/
- void scan(boolean directionDown, boolean skipSubChannel);
+ void seek(boolean directionDown, boolean skipSubChannel);
/**
* @throws IllegalArgumentException if invalid arguments are passed
diff --git a/core/java/android/hardware/radio/ITunerCallback.aidl b/core/java/android/hardware/radio/ITunerCallback.aidl
index f98947b..13092cc 100644
--- a/core/java/android/hardware/radio/ITunerCallback.aidl
+++ b/core/java/android/hardware/radio/ITunerCallback.aidl
@@ -24,6 +24,13 @@
/** {@hide} */
oneway interface ITunerCallback {
void onError(int status);
+
+ /**
+ * Callback called when tuning operations, such as tune, step, seek, failed.
+ *
+ * @param result Tuning result of {@link RadioTuner#TunerResultType} type.
+ * @param selector Program selector used for the tuning operation.
+ */
void onTuneFailed(int result, in ProgramSelector selector);
void onConfigurationChanged(in RadioManager.BandConfig config);
void onCurrentProgramInfoChanged(in RadioManager.ProgramInfo info);
@@ -36,6 +43,18 @@
void onProgramListUpdated(in ProgramList.Chunk chunk);
/**
+ * Callback for passing updates to config flags from {@link IRadioService} to
+ * {@link RadioTuner}.
+ *
+ * @param flag Config flag (defined in {@link RadioManager.ConfigFlag}) updated
+ * @param value Updated value for the config flag
+ */
+ void onConfigFlagUpdated(int flag, boolean value);
+
+ /**
+ * Callback for passing updates to vendor-specific parameter values from
+ * {@link IRadioService} to {@link RadioTuner}.
+ *
* @param parameters Vendor-specific key-value pairs
*/
void onParametersUpdated(in Map<String, String> parameters);
diff --git a/core/java/android/hardware/radio/ProgramSelector.java b/core/java/android/hardware/radio/ProgramSelector.java
index 8a92135..7faa285 100644
--- a/core/java/android/hardware/radio/ProgramSelector.java
+++ b/core/java/android/hardware/radio/ProgramSelector.java
@@ -167,7 +167,10 @@
public static final int IDENTIFIER_TYPE_HD_STATION_NAME = 10004;
/**
* @see {@link IDENTIFIER_TYPE_DAB_SID_EXT}
+ *
+ * @deprecated use {@link #IDENTIFIER_TYPE_DAB_DMB_SID_EXT} instead
*/
+ @Deprecated
public static final int IDENTIFIER_TYPE_DAB_SIDECC = 5;
/**
* 28bit compound primary identifier for Digital Audio Broadcasting.
@@ -183,7 +186,10 @@
*
* The remaining bits should be set to zeros when writing on the chip side
* and ignored when read.
+ *
+ * @deprecated use {@link #IDENTIFIER_TYPE_DAB_DMB_SID_EXT} instead
*/
+ @Deprecated
public static final int IDENTIFIER_TYPE_DAB_SID_EXT = IDENTIFIER_TYPE_DAB_SIDECC;
/** 16bit */
public static final int IDENTIFIER_TYPE_DAB_ENSEMBLE = 6;
@@ -197,7 +203,7 @@
public static final int IDENTIFIER_TYPE_DRMO_FREQUENCY = 10;
/**
* 1: AM, 2:FM
- * @deprecated use {@link IDENTIFIER_TYPE_DRMO_FREQUENCY} instead
+ * @deprecated use {@link #IDENTIFIER_TYPE_DRMO_FREQUENCY} instead
*/
@Deprecated
public static final int IDENTIFIER_TYPE_DRMO_MODULATION = 11;
@@ -206,6 +212,23 @@
/** 0-999 range */
public static final int IDENTIFIER_TYPE_SXM_CHANNEL = 13;
/**
+ * 44bit compound primary identifier for Digital Audio Broadcasting and
+ * Digital Multimedia Broadcasting.
+ *
+ * <p>Consists of (from the LSB):
+ * - 32bit: SId;
+ * - 8bit: ECC code;
+ * - 4bit: SCIdS.
+ *
+ * <p>SCIdS (Service Component Identifier within the Service) value
+ * of 0 represents the main service, while 1 and above represents
+ * secondary services.
+ *
+ * The remaining bits should be set to zeros when writing on the chip side
+ * and ignored when read.
+ */
+ public static final int IDENTIFIER_TYPE_DAB_DMB_SID_EXT = 14;
+ /**
* Primary identifier for vendor-specific radio technology.
* The value format is determined by a vendor.
*
@@ -219,12 +242,12 @@
*/
public static final int IDENTIFIER_TYPE_VENDOR_END = PROGRAM_TYPE_VENDOR_END;
/**
- * @deprecated use {@link IDENTIFIER_TYPE_VENDOR_START} instead
+ * @deprecated use {@link #IDENTIFIER_TYPE_VENDOR_START} instead
*/
@Deprecated
public static final int IDENTIFIER_TYPE_VENDOR_PRIMARY_START = IDENTIFIER_TYPE_VENDOR_START;
/**
- * @deprecated use {@link IDENTIFIER_TYPE_VENDOR_END} instead
+ * @deprecated use {@link #IDENTIFIER_TYPE_VENDOR_END} instead
*/
@Deprecated
public static final int IDENTIFIER_TYPE_VENDOR_PRIMARY_END = IDENTIFIER_TYPE_VENDOR_END;
@@ -245,6 +268,7 @@
IDENTIFIER_TYPE_DRMO_MODULATION,
IDENTIFIER_TYPE_SXM_SERVICE_ID,
IDENTIFIER_TYPE_SXM_CHANNEL,
+ IDENTIFIER_TYPE_DAB_DMB_SID_EXT,
})
@IntRange(from = IDENTIFIER_TYPE_VENDOR_START, to = IDENTIFIER_TYPE_VENDOR_END)
@Retention(RetentionPolicy.SOURCE)
@@ -285,7 +309,7 @@
* Type of a radio technology.
*
* @return program type.
- * @deprecated use {@link getPrimaryId} instead
+ * @deprecated use {@link #getPrimaryId} instead
*/
@Deprecated
public @ProgramType int getProgramType() {
diff --git a/core/java/android/hardware/radio/RadioManager.java b/core/java/android/hardware/radio/RadioManager.java
index 9a217f9..f072e3b 100644
--- a/core/java/android/hardware/radio/RadioManager.java
+++ b/core/java/android/hardware/radio/RadioManager.java
@@ -82,6 +82,24 @@
/** Method return status: time out before operation completion */
public static final int STATUS_TIMED_OUT = -110;
+ /**
+ * Radio operation status types
+ *
+ * @hide
+ */
+ @IntDef(prefix = { "STATUS_" }, value = {
+ STATUS_OK,
+ STATUS_ERROR,
+ STATUS_PERMISSION_DENIED,
+ STATUS_NO_INIT,
+ STATUS_BAD_VALUE,
+ STATUS_DEAD_OBJECT,
+ STATUS_INVALID_OPERATION,
+ STATUS_TIMED_OUT,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface RadioStatusType{}
+
// keep in sync with radio_class_t in /system/core/incluse/system/radio.h
/** Radio module class supporting FM (including HD radio) and AM */
@@ -330,6 +348,7 @@
* program list.
* @return the number of audio sources available.
*/
+ @RadioStatusType
public int getNumAudioSources() {
return mNumAudioSources;
}
@@ -1724,6 +1743,7 @@
* </ul>
*/
@RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
+ @RadioStatusType
public int listModules(List<ModuleProperties> modules) {
if (modules == null) {
Log.e(TAG, "the output list must not be empty");
@@ -1776,7 +1796,7 @@
ITuner tuner;
TunerCallbackAdapter halCallback = new TunerCallbackAdapter(callback, handler);
try {
- tuner = mService.openTuner(moduleId, config, withAudio, halCallback);
+ tuner = mService.openTuner(moduleId, config, withAudio, halCallback, mTargetSdkVersion);
} catch (RemoteException | IllegalArgumentException | IllegalStateException ex) {
Log.e(TAG, "Failed to open tuner", ex);
return null;
@@ -1853,6 +1873,7 @@
@NonNull private final Context mContext;
@NonNull private final IRadioService mService;
+ private final int mTargetSdkVersion;
/**
* @hide
@@ -1869,5 +1890,6 @@
public RadioManager(Context context, IRadioService service) {
mContext = context;
mService = service;
+ mTargetSdkVersion = mContext.getApplicationInfo().targetSdkVersion;
}
}
diff --git a/core/java/android/hardware/radio/RadioTuner.java b/core/java/android/hardware/radio/RadioTuner.java
index 969db96..9b2bcde 100644
--- a/core/java/android/hardware/radio/RadioTuner.java
+++ b/core/java/android/hardware/radio/RadioTuner.java
@@ -16,14 +16,20 @@
package android.hardware.radio;
+import android.Manifest;
+import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.graphics.Bitmap;
import android.os.Handler;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.Executor;
/**
* RadioTuner interface provides methods to control a radio tuner on the device: selecting and
@@ -43,16 +49,21 @@
public static final int DIRECTION_DOWN = 1;
/**
- * Close the tuner interface. The {@link Callback} callback will not be called
- * anymore and associated resources will be released.
- * Must be called when the tuner is not needed to make hardware resources available to others.
+ * Close the tuner interface.
+ *
+ * <p>The {@link Callback} callback will not be called anymore and associated resources will be
+ * released. Must be called when the tuner is not needed to make hardware resources available
+ * to others.
* */
+ @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
public abstract void close();
/**
* Set the active band configuration for this module.
- * Must be a valid configuration obtained via buildConfig() from a valid BandDescriptor listed
- * in the ModuleProperties of the module with the specified ID.
+ *
+ * <p>Must be a valid configuration obtained via buildConfig() from a valid BandDescriptor
+ * listed in the ModuleProperties of the module with the specified ID.
+ *
* @param config The desired band configuration (FmBandConfig or AmBandConfig).
* @return
* <ul>
@@ -67,10 +78,13 @@
* @deprecated Only applicable for HAL 1.x.
*/
@Deprecated
+ @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
+ @RadioManager.RadioStatusType
public abstract int setConfiguration(RadioManager.BandConfig config);
/**
* Get current configuration.
+ *
* @param config a BandConfig array of lengh 1 where the configuration is returned.
* @return
* <ul>
@@ -86,11 +100,15 @@
* @deprecated Only applicable for HAL 1.x.
*/
@Deprecated
+ @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
+ @RadioManager.RadioStatusType
public abstract int getConfiguration(RadioManager.BandConfig[] config);
/**
- * Set mute state. When muted, the radio tuner audio source is not available for playback on
+ * Set mute state.
+ *
+ * <p>When muted, the radio tuner audio source is not available for playback on
* any audio device. when unmuted, the radio tuner audio source is output as a media source
* and renderd over the audio device selected for media use case.
* The radio tuner audio source is muted by default when the tuner is first attached.
@@ -107,6 +125,8 @@
* service fails, </li>
* </ul>
*/
+ @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
+ @RadioManager.RadioStatusType
public abstract int setMute(boolean mute);
/**
@@ -115,13 +135,19 @@
* @return {@code true} if the radio tuner audio source is muted or a problem occured
* retrieving the mute state, {@code false} otherwise.
*/
+ @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
public abstract boolean getMute();
/**
* Step up or down by one channel spacing.
- * The operation is asynchronous and {@link Callback}
- * onProgramInfoChanged() will be called when step completes or
- * onError() when cancelled or timeout.
+ *
+ * <p>The operation is asynchronous and {@link Callback#onProgramInfoChanged}
+ * will be called when step completes or {@link Callback#onTuneFailed}
+ * when timeout or canceled.
+ *
+ * <p>When this operation is called by users other than current user or system user, it is
+ * ignored silently.
+ *
* @param direction {@link #DIRECTION_UP} or {@link #DIRECTION_DOWN}.
* @param skipSubChannel indicates to skip sub channels when the configuration currently
* selected supports sub channel (e.g HD Radio). N/A otherwise.
@@ -136,13 +162,50 @@
* service fails, </li>
* </ul>
*/
+ @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
+ @RadioManager.RadioStatusType
public abstract int step(int direction, boolean skipSubChannel);
/**
* Scan up or down to next valid station.
- * The operation is asynchronous and {@link Callback}
- * onProgramInfoChanged() will be called when scan completes or
- * onError() when cancelled or timeout.
+ *
+ * <p>The operation is asynchronous and {@link Callback#onProgramInfoChanged}
+ * will be called when scan completes or {@link Callback#onTuneFailed}
+ * when timeout or canceled.
+ *
+ * <p>When this operation is called by users other than current user or system user, it is
+ * ignored silently.
+ *
+ * @param direction {@link #DIRECTION_UP} or {@link #DIRECTION_DOWN}.
+ * @param skipSubChannel indicates to skip sub channels when the configuration currently
+ * selected supports sub channel (e.g HD Radio). N/A otherwise.
+ * @return
+ * <ul>
+ * <li>{@link RadioManager#STATUS_OK} in case of success, </li>
+ * <li>{@link RadioManager#STATUS_ERROR} in case of unspecified error, </li>
+ * <li>{@link RadioManager#STATUS_NO_INIT} if the native service cannot be reached, </li>
+ * <li>{@link RadioManager#STATUS_BAD_VALUE} if parameters are invalid, </li>
+ * <li>{@link RadioManager#STATUS_INVALID_OPERATION} if the call is out of sequence, </li>
+ * <li>{@link RadioManager#STATUS_DEAD_OBJECT} if the binder transaction to the native
+ * service fails, </li>
+ * </ul>
+ * @deprecated Use {@link #seek(int, boolean)} instead.
+ */
+ @Deprecated
+ @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
+ @RadioManager.RadioStatusType
+ public abstract int scan(int direction, boolean skipSubChannel);
+
+ /**
+ * Seek up or down to next valid station.
+ *
+ * <p>The operation is asynchronous and {@link Callback#onProgramInfoChanged}
+ * will be called when seek completes or {@link Callback#onTuneFailed}
+ * when timeout or canceled.
+ *
+ * <p>When this operation is called by users other than current user or system user, it is
+ * ignore silently.
+ *
* @param direction {@link #DIRECTION_UP} or {@link #DIRECTION_DOWN}.
* @param skipSubChannel indicates to skip sub channels when the configuration currently
* selected supports sub channel (e.g HD Radio). N/A otherwise.
@@ -157,13 +220,22 @@
* service fails, </li>
* </ul>
*/
- public abstract int scan(int direction, boolean skipSubChannel);
+ @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
+ @RadioManager.RadioStatusType
+ public int seek(int direction, boolean skipSubChannel) {
+ throw new UnsupportedOperationException("Seeking is not supported");
+ }
/**
* Tune to a specific frequency.
- * The operation is asynchronous and {@link Callback}
- * onProgramInfoChanged() will be called when tune completes or
- * onError() when cancelled or timeout.
+ *
+ * <p>The operation is asynchronous and {@link Callback#onProgramInfoChanged}
+ * will be called when tune completes or {@link Callback#onTuneFailed}
+ * when timeout or canceled.
+ *
+ * <p>When this operation is called by users other than current user or system user, it is
+ * ignored silently.
+ *
* @param channel the specific channel or frequency to tune to.
* @param subChannel the specific sub-channel to tune to. N/A if the selected configuration
* does not support cub channels.
@@ -177,25 +249,37 @@
* <li>{@link RadioManager#STATUS_DEAD_OBJECT} if the binder transaction to the native
* service fails, </li>
* </ul>
- * @deprecated Use {@link tune(ProgramSelector)} instead.
+ * @deprecated Use {@link #tune(ProgramSelector)} instead.
*/
@Deprecated
+ @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
+ @RadioManager.RadioStatusType
public abstract int tune(int channel, int subChannel);
/**
* Tune to a program.
*
- * The operation is asynchronous and {@link Callback} onProgramInfoChanged() will be called
- * when tune completes or onError() when cancelled or on timeout.
+ * <p>The operation is asynchronous and {@link Callback#onProgramInfoChanged}
+ * will be called when tune completes or {@link Callback#onTuneFailed}
+ * when timeout or canceled.
+ *
+ * <p>When this operation is called by users other than current user or system user, it is
+ * ignored silently.
*
* @throws IllegalArgumentException if the provided selector is invalid
*/
+ @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
public abstract void tune(@NonNull ProgramSelector selector);
/**
* Cancel a pending scan or tune operation.
- * If an operation is pending, {@link Callback} onError() will be called with
+ *
+ * <p>If an operation is pending, {@link Callback#onTuneFailed} will be called with
* {@link #ERROR_CANCELLED}.
+ *
+ * <p>When this operation is called by users other than current user or system
+ * user, it is ignored silently.
+ *
* @return
* <ul>
* <li>{@link RadioManager#STATUS_OK} in case of success, </li>
@@ -207,21 +291,27 @@
* service fails, </li>
* </ul>
*/
+ @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
+ @RadioManager.RadioStatusType
public abstract int cancel();
/**
* Cancels traffic or emergency announcement.
*
- * If there was no announcement to cancel, no action is taken.
+ * <p>If there was no announcement to cancel, no action is taken.
*
- * There is a race condition between calling cancelAnnouncement and the actual announcement
+ * <p>There is a race condition between calling cancelAnnouncement and the actual announcement
* being finished, so onTrafficAnnouncement / onEmergencyAnnouncement callback should be
* tracked with proper locking.
+ * @deprecated Only applicable for HAL 1.x.
*/
+ @Deprecated
+ @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
public abstract void cancelAnnouncement();
/**
* Get current station information.
+ *
* @param info a ProgramInfo array of lengh 1 where the information is returned.
* @return
* <ul>
@@ -233,23 +323,27 @@
* <li>{@link RadioManager#STATUS_DEAD_OBJECT} if the binder transaction to the native
* service fails, </li>
* </ul>
- * @deprecated Use {@link onProgramInfoChanged} callback instead.
+ * @deprecated Use {@link Callback#onProgramInfoChanged(RadioManager.ProgramInfo)} callback
+ * instead.
*/
@Deprecated
+ @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
+ @RadioManager.RadioStatusType
public abstract int getProgramInformation(RadioManager.ProgramInfo[] info);
/**
* Retrieves a {@link Bitmap} for the given image ID or null,
* if the image was missing from the tuner.
*
- * This involves doing a call to the tuner, so the bitmap should be cached
+ * <p>This involves doing a call to the tuner, so the bitmap should be cached
* on the application side.
*
- * If the method returns null for non-zero ID, it means the image was
+ * <p>If the method returns null for non-zero ID, it means the image was
* updated on the tuner side. There is a race conditon between fetching
* image for an old ID and tuner updating the image (and cleaning up the
* old image). In such case, a new ProgramInfo with updated image id will
- * be sent with a {@link onProgramInfoChanged} callback.
+ * be sent with a {@link Callback#onProgramInfoChanged(RadioManager.ProgramInfo)}
+ * callback.
*
* @param id The image identifier, retrieved with
* {@link RadioMetadata#getBitmapId(String)}.
@@ -258,14 +352,16 @@
* @hide This API is not thoroughly elaborated yet
*/
@SuppressWarnings("HiddenAbstractMethod")
+ @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
public abstract @Nullable Bitmap getMetadataImage(int id);
/**
* Initiates a background scan to update internally cached program list.
*
- * It may not be necessary to initiate the scan explicitly - the scan MAY be performed on boot.
+ * <p>It may not be necessary to initiate the scan explicitly - the scan MAY be performed on
+ * boot.
*
- * The operation is asynchronous and {@link Callback} backgroundScanComplete or onError will
+ * <p>The operation is asynchronous and {@link Callback} backgroundScanComplete or onError will
* be called if the return value of this call was {@code true}. As result of this call
* programListChanged may be triggered (if the scanned list differs).
*
@@ -273,13 +369,16 @@
* is unavailable; ie. temporarily due to ongoing foreground playback in single-tuner device
* or permanently if the feature is not supported
* (see ModuleProperties#isBackgroundScanningSupported()).
+ * @deprecated Only applicable for HAL 1.x.
*/
+ @Deprecated
+ @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
public abstract boolean startBackgroundScan();
/**
* Get the list of discovered radio stations.
*
- * To get the full list, set filter to null or empty map.
+ * <p>To get the full list, set filter to null or empty map.
* Keys must be prefixed with unique vendor Java-style namespace,
* eg. 'com.somecompany.parameter1'.
*
@@ -288,24 +387,27 @@
* @throws IllegalStateException if the scan is in progress or has not been started,
* startBackgroundScan() call may fix it.
* @throws IllegalArgumentException if the vendorFilter argument is not valid.
- * @deprecated Use {@link getDynamicProgramList} instead.
+ * @deprecated Use {@link #getDynamicProgramList(ProgramList.Filter)} instead.
*/
@Deprecated
+ @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
public abstract @NonNull List<RadioManager.ProgramInfo>
getProgramList(@Nullable Map<String, String> vendorFilter);
/**
* Get the dynamic list of discovered radio stations.
*
- * The list object is updated asynchronously; to get the updates register
- * with {@link ProgramList#addListCallback}.
+ * <p>The list object is updated asynchronously; to get the updates register
+ * with {@link ProgramList#registerListCallback(ProgramList.ListCallback)}
+ * or {@link ProgramList#registerListCallback(Executor, ProgramList.ListCallback)}.
*
- * When the returned object is no longer used, it must be closed.
+ * <p>When the returned object is no longer used, it must be closed.
*
* @param filter filter for the list, or null to get the full list.
* @return the dynamic program list object, close it after use
* or {@code null} if program list is not supported by the tuner
*/
+ @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
public @Nullable ProgramList getDynamicProgramList(@Nullable ProgramList.Filter filter) {
return null;
}
@@ -316,26 +418,28 @@
* @throws IllegalStateException if the switch is not supported at current
* configuration.
* @return {@code true} if analog is forced, {@code false} otherwise.
- * @deprecated Use {@link isConfigFlagSet(int)} instead.
+ * @deprecated Use {@link #isConfigFlagSet(int)} instead.
*/
@Deprecated
+ @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
public abstract boolean isAnalogForced();
/**
* Forces the analog playback for the supporting radio technology.
*
- * User may disable digital playback for FM HD Radio or hybrid FM/DAB with
+ * <p>User may disable digital playback for FM HD Radio or hybrid FM/DAB with
* this option. This is purely user choice, ie. does not reflect digital-
* analog handover managed from the HAL implementation side.
*
- * Some radio technologies may not support this, ie. DAB.
+ * <p>Some radio technologies may not support this, ie. DAB.
*
* @param isForced {@code true} to force analog, {@code false} for a default behaviour.
* @throws IllegalStateException if the switch is not supported at current
* configuration.
- * @deprecated Use {@link setConfigFlag(int, boolean)} instead.
+ * @deprecated Use {@link #setConfigFlag(int, boolean)} instead.
*/
@Deprecated
+ @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
public abstract void setAnalogForced(boolean isForced);
/**
@@ -344,6 +448,7 @@
* @param flag Flag to check.
* @return True, if the flag is supported.
*/
+ @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
public boolean isConfigFlagSupported(@RadioManager.ConfigFlag int flag) {
return false;
}
@@ -351,29 +456,34 @@
/**
* Fetches the current setting of a given config flag.
*
- * The success/failure result is consistent with isConfigFlagSupported.
+ * <p>The success/failure result is consistent with isConfigFlagSupported.
*
* @param flag Flag to fetch.
* @return The current value of the flag.
* @throws IllegalStateException if the flag is not applicable right now.
* @throws UnsupportedOperationException if the flag is not supported at all.
*/
+ @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
public boolean isConfigFlagSet(@RadioManager.ConfigFlag int flag) {
- throw new UnsupportedOperationException();
+ throw new UnsupportedOperationException("isConfigFlagSet is not supported");
}
/**
* Sets the config flag.
*
- * The success/failure result is consistent with isConfigFlagSupported.
+ * <p>The success/failure result is consistent with isConfigFlagSupported.
+ *
+ * <p>When this operation is called by users other than current user or system user,
+ * it is ignored silently.
*
* @param flag Flag to set.
* @param value The new value of a given flag.
* @throws IllegalStateException if the flag is not applicable right now.
* @throws UnsupportedOperationException if the flag is not supported at all.
*/
+ @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
public void setConfigFlag(@RadioManager.ConfigFlag int flag, boolean value) {
- throw new UnsupportedOperationException();
+ throw new UnsupportedOperationException("Setting config flag is not supported");
}
/**
@@ -381,30 +491,37 @@
* The framework does not interpret the parameters, they are passed
* in an opaque manner between a vendor application and HAL.
*
- * Framework does not make any assumptions on the keys or values, other than
+ * <p>Framework does not make any assumptions on the keys or values, other than
* ones stated in VendorKeyValue documentation (a requirement of key
* prefixes).
- * See VendorKeyValue at hardware/interfaces/broadcastradio/2.0/types.hal.
+ * See VendorKeyValue at hardware/interfaces/broadcastradio/2.0/types.hal for
+ * HIDL 2.0 HAL or
+ * hardware/interfaces/broadcastradio/aidl/android/hardware/broadcastradio/VendorKeyValue.aidl
+ * for AIDL HAL.
*
- * For each pair in the result map, the key will be one of the keys
+ * <p>For each pair in the result map, the key will be one of the keys
* contained in the input (possibly with wildcards expanded), and the value
* will be a vendor-specific result status (such as "OK" or an error code).
* The implementation may choose to return an empty map, or only return
* a status for a subset of the provided inputs, at its discretion.
*
- * Application and HAL must not use keys with unknown prefix. In particular,
+ * <p>Application and HAL must not use keys with unknown prefix. In particular,
* it must not place a key-value pair in results vector for unknown key from
* parameters vector - instead, an unknown key should simply be ignored.
* In other words, results vector may contain a subset of parameter keys
* (however, the framework doesn't enforce a strict subset - the only
* formal requirement is vendor domain prefix for keys).
*
+ * <p>When this operation is called by users other than current user or system user,
+ * it is ignored silently.
+ *
* @param parameters Vendor-specific key-value pairs.
* @return Operation completion status for parameters being set.
*/
+ @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
public @NonNull Map<String, String>
setParameters(@NonNull Map<String, String> parameters) {
- throw new UnsupportedOperationException();
+ throw new UnsupportedOperationException("Setting parameters is not supported");
}
/**
@@ -412,23 +529,24 @@
* The framework does not interpret the parameters, they are passed
* in an opaque manner between a vendor application and HAL.
*
- * Framework does not cache set/get requests, so it's possible for
+ * <p>Framework does not cache set/get requests, so it's possible for
* getParameter to return a different value than previous setParameter call.
*
- * The syntax and semantics of keys are up to the vendor (as long as prefix
+ * <p>The syntax and semantics of keys are up to the vendor (as long as prefix
* rules are obeyed). For instance, vendors may include some form of
* wildcard support. In such case, result vector may be of different size
* than requested keys vector. However, wildcards are not recognized by
* framework and they are passed as-is to the HAL implementation.
*
- * Unknown keys must be ignored and not placed into results vector.
+ * <p>Unknown keys must be ignored and not placed into results vector.
*
* @param keys Parameter keys to fetch.
* @return Vendor-specific key-value pairs.
*/
+ @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
public @NonNull Map<String, String>
getParameters(@NonNull List<String> keys) {
- throw new UnsupportedOperationException();
+ throw new UnsupportedOperationException("Getting parameters is not supported");
}
/**
@@ -436,14 +554,16 @@
* Only valid if a configuration has been applied.
* @return {@code true} if the antenna is connected, {@code false} otherwise.
*
- * @deprecated Use {@link onAntennaState} callback instead
+ * @deprecated Use {@link Callback#onAntennaState(boolean)} callback instead
*/
@Deprecated
+ @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
public abstract boolean isAntennaConnected();
/**
* Indicates if this client actually controls the tuner.
- * Control is always granted after
+ *
+ * <p>Control is always granted after
* {@link RadioManager#openTuner(int,
* RadioManager.BandConfig, boolean, Callback, Handler)}
* returns a non null tuner interface.
@@ -451,48 +571,102 @@
* When this happens, {@link Callback#onControlChanged(boolean)} is received.
* The client can either wait for control to be returned (which is indicated by the same
* callback) or close and reopen the tuner interface.
+ *
* @return {@code true} if this interface controls the tuner,
* {@code false} otherwise or if a problem occured retrieving the state.
*/
+ @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
public abstract boolean hasControl();
/** Indicates a failure of radio IC or driver.
- * The application must close and re open the tuner
- * @deprecated See {@link onError} callback.
+ *
+ * <p>The application must close and re open the tuner
+ *
+ * @deprecated See {@link RadioTuner.Callback#onError(int)} callback.
*/
@Deprecated
public static final int ERROR_HARDWARE_FAILURE = 0;
/** Indicates a failure of the radio service.
- * The application must close and re open the tuner
- * @deprecated See {@link onError} callback.
+ *
+ * <p>The application must close and re open the tuner
+ * @deprecated See {@link RadioTuner.Callback#onError(int)} callback.
*/
@Deprecated
public static final int ERROR_SERVER_DIED = 1;
/** A pending seek or tune operation was cancelled
- * @deprecated See {@link onError} callback.
+ * @deprecated See {@link RadioTuner.Callback#onError(int)} callback.
*/
@Deprecated
public static final int ERROR_CANCELLED = 2;
/** A pending seek or tune operation timed out
- * @deprecated See {@link onError} callback.
+ * @deprecated See {@link RadioTuner.Callback#onError(int)} callback.
*/
@Deprecated
public static final int ERROR_SCAN_TIMEOUT = 3;
/** The requested configuration could not be applied
- * @deprecated See {@link onError} callback.
+ * @deprecated See {@link RadioTuner.Callback#onError(int)} callback.
*/
@Deprecated
public static final int ERROR_CONFIG = 4;
/** Background scan was interrupted due to hardware becoming temporarily unavailable.
- * @deprecated See {@link onError} callback.
+ * @deprecated See {@link RadioTuner.Callback#onError(int)} callback.
*/
@Deprecated
public static final int ERROR_BACKGROUND_SCAN_UNAVAILABLE = 5;
/** Background scan failed due to other error, ie. HW failure.
- * @deprecated See {@link onError} callback.
+ * @deprecated See {@link RadioTuner.Callback#onError(int)} callback.
*/
@Deprecated
public static final int ERROR_BACKGROUND_SCAN_FAILED = 6;
+ /** Result when a tune, seek, or step operation runs without error.
+ */
+ public static final int TUNER_RESULT_OK = 0;
+ /** Result when internal error occurs in HAL.
+ * See {@link RadioTuner.Callback#onTuneFailed(int, ProgramSelector)} callback.
+ */
+ public static final int TUNER_RESULT_INTERNAL_ERROR = 1;
+ /** Result used when the input argument for the method is invalid.
+ * See {@link RadioTuner.Callback#onTuneFailed(int, ProgramSelector)} callback.
+ */
+ public static final int TUNER_RESULT_INVALID_ARGUMENTS = 2;
+ /** Result when HAL is of invalid state.
+ * See {@link RadioTuner.Callback#onTuneFailed(int, ProgramSelector)} callback.
+ */
+ public static final int TUNER_RESULT_INVALID_STATE = 3;
+ /** Result when the operation is not supported.
+ * See {@link RadioTuner.Callback#onTuneFailed(int, ProgramSelector)} callback.
+ */
+ public static final int TUNER_RESULT_NOT_SUPPORTED = 4;
+ /** Result when a tune, seek, or step operation is timeout
+ * See {@link RadioTuner.Callback#onTuneFailed(int, ProgramSelector)} callback.
+ */
+ public static final int TUNER_RESULT_TIMEOUT = 5;
+ /** Result when a tune, seek, or step operation is canceled before processed.
+ * See {@link RadioTuner.Callback#onTuneFailed(int, ProgramSelector)} callback.
+ */
+ public static final int TUNER_RESULT_CANCELED = 6;
+ /** Result when a tune, seek, or step operation fails due to unknown error.
+ * See {@link RadioTuner.Callback#onTuneFailed(int, ProgramSelector)} callback.
+ */
+ public static final int TUNER_RESULT_UNKNOWN_ERROR = 7;
+
+ /**
+ * Tuning operation result types
+ *
+ * @hide
+ */
+ @IntDef(prefix = { "TUNER_RESULT_" }, value = {
+ TUNER_RESULT_OK,
+ TUNER_RESULT_INTERNAL_ERROR,
+ TUNER_RESULT_INVALID_ARGUMENTS,
+ TUNER_RESULT_INVALID_STATE,
+ TUNER_RESULT_NOT_SUPPORTED,
+ TUNER_RESULT_TIMEOUT,
+ TUNER_RESULT_CANCELED,
+ TUNER_RESULT_UNKNOWN_ERROR,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface TunerResultType{}
/**
* Callback provided by the client application when opening a {@link RadioTuner}
@@ -506,8 +680,9 @@
* {@link #ERROR_CANCELLED}, {@link #ERROR_SCAN_TIMEOUT},
* {@link #ERROR_CONFIG}
*
- * @deprecated Use {@link onTuneFailed} for tune, scan and step;
- * other use cases (configuration, background scan) are already deprecated.
+ * @deprecated Use {@link RadioTuner.Callback#onTuneFailed(int, ProgramSelector)} for
+ * tune, scan and step; other use cases (configuration, background scan)
+ * are already deprecated.
*/
public void onError(int status) {}
@@ -518,7 +693,7 @@
* @param selector ProgramSelector argument of tune that failed;
* null for scan and step.
*/
- public void onTuneFailed(int result, @Nullable ProgramSelector selector) {}
+ public void onTuneFailed(@TunerResultType int result, @Nullable ProgramSelector selector) {}
/**
* onConfigurationChanged() is called upon successful completion of
@@ -533,7 +708,7 @@
/**
* Called when program info (including metadata) for the current program has changed.
*
- * It happens either upon successful completion of {@link RadioTuner#step(int, boolean)},
+ * <p>It happens either upon successful completion of {@link RadioTuner#step(int, boolean)},
* {@link RadioTuner#scan(int, boolean)}, {@link RadioTuner#tune(int, int)}; when
* a switching to alternate frequency occurs; or when metadata is updated.
*/
@@ -589,16 +764,29 @@
/**
* Called when available program list changed.
*
- * Use {@link RadioTuner#getProgramList(String)} to get an actual list.
+ * Use {@link RadioTuner#getProgramList(Map)} to get an actual list.
*/
public void onProgramListChanged() {}
/**
+ * Called when config flags are updated asynchronously due to internal events
+ * in broadcast radio HAL.
+ *
+ * {@link RadioTuner#setConfigFlag(int, boolean)} must not trigger this
+ * callback.
+ *
+ * @param flag Config flag updated
+ * @param value Value of the updated config flag
+ */
+ public void onConfigFlagUpdated(@RadioManager.ConfigFlag int flag, boolean value) {}
+
+ /**
* Generic callback for passing updates to vendor-specific parameter values.
- * The framework does not interpret the parameters, they are passed
+ *
+ * <p>The framework does not interpret the parameters, they are passed
* in an opaque manner between a vendor application and HAL.
*
- * It's up to the HAL implementation if and how to implement this callback,
+ * <p>It's up to the HAL implementation if and how to implement this callback,
* as long as it obeys the prefix rule. In particular, only selected keys
* may be notified this way. However, setParameters must not trigger
* this callback, while an internal event can change parameters
diff --git a/core/java/android/hardware/radio/TunerAdapter.java b/core/java/android/hardware/radio/TunerAdapter.java
index 4a18333..bdbca91 100644
--- a/core/java/android/hardware/radio/TunerAdapter.java
+++ b/core/java/android/hardware/radio/TunerAdapter.java
@@ -154,7 +154,7 @@
@Override
public int scan(int direction, boolean skipSubChannel) {
try {
- mTuner.scan(/* directionDown= */ direction == RadioTuner.DIRECTION_DOWN,
+ mTuner.seek(/* directionDown= */ direction == RadioTuner.DIRECTION_DOWN,
skipSubChannel);
} catch (IllegalStateException e) {
Log.e(TAG, "Can't scan", e);
@@ -167,6 +167,21 @@
}
@Override
+ public int seek(int direction, boolean skipSubChannel) {
+ try {
+ mTuner.seek(/* directionDown= */ direction == RadioTuner.DIRECTION_DOWN,
+ skipSubChannel);
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "Can't seek", e);
+ return RadioManager.STATUS_INVALID_OPERATION;
+ } catch (RemoteException e) {
+ Log.e(TAG, "Service died", e);
+ return RadioManager.STATUS_DEAD_OBJECT;
+ }
+ return RadioManager.STATUS_OK;
+ }
+
+ @Override
public int tune(int channel, int subChannel) {
try {
int band;
diff --git a/core/java/android/hardware/radio/TunerCallbackAdapter.java b/core/java/android/hardware/radio/TunerCallbackAdapter.java
index b9782a8..22f5902 100644
--- a/core/java/android/hardware/radio/TunerCallbackAdapter.java
+++ b/core/java/android/hardware/radio/TunerCallbackAdapter.java
@@ -246,6 +246,11 @@
}
@Override
+ public void onConfigFlagUpdated(@RadioManager.ConfigFlag int flag, boolean value) {
+ mHandler.post(() -> mCallback.onConfigFlagUpdated(flag, value));
+ }
+
+ @Override
public void onParametersUpdated(Map<String, String> parameters) {
mHandler.post(() -> mCallback.onParametersUpdated(parameters));
}
diff --git a/core/java/android/net/vcn/VcnConfig.java b/core/java/android/net/vcn/VcnConfig.java
index fd3fe37..dcf0026 100644
--- a/core/java/android/net/vcn/VcnConfig.java
+++ b/core/java/android/net/vcn/VcnConfig.java
@@ -15,16 +15,23 @@
*/
package android.net.vcn;
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+
import static com.android.internal.annotations.VisibleForTesting.Visibility;
+import static com.android.server.vcn.util.PersistableBundleUtils.INTEGER_DESERIALIZER;
+import static com.android.server.vcn.util.PersistableBundleUtils.INTEGER_SERIALIZER;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
+import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.PersistableBundle;
import android.util.ArraySet;
+import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Preconditions;
@@ -32,6 +39,7 @@
import java.util.ArrayList;
import java.util.Collections;
+import java.util.Iterator;
import java.util.Objects;
import java.util.Set;
@@ -46,22 +54,36 @@
public final class VcnConfig implements Parcelable {
@NonNull private static final String TAG = VcnConfig.class.getSimpleName();
+ private static final Set<Integer> ALLOWED_TRANSPORTS = new ArraySet<>();
+
+ static {
+ ALLOWED_TRANSPORTS.add(TRANSPORT_WIFI);
+ ALLOWED_TRANSPORTS.add(TRANSPORT_CELLULAR);
+ }
+
private static final String PACKAGE_NAME_KEY = "mPackageName";
@NonNull private final String mPackageName;
private static final String GATEWAY_CONNECTION_CONFIGS_KEY = "mGatewayConnectionConfigs";
@NonNull private final Set<VcnGatewayConnectionConfig> mGatewayConnectionConfigs;
+ private static final Set<Integer> RESTRICTED_TRANSPORTS_DEFAULT =
+ Collections.singleton(TRANSPORT_WIFI);
+ private static final String RESTRICTED_TRANSPORTS_KEY = "mRestrictedTransports";
+ @NonNull private final Set<Integer> mRestrictedTransports;
+
private static final String IS_TEST_MODE_PROFILE_KEY = "mIsTestModeProfile";
private final boolean mIsTestModeProfile;
private VcnConfig(
@NonNull String packageName,
@NonNull Set<VcnGatewayConnectionConfig> gatewayConnectionConfigs,
+ @NonNull Set<Integer> restrictedTransports,
boolean isTestModeProfile) {
mPackageName = packageName;
mGatewayConnectionConfigs =
Collections.unmodifiableSet(new ArraySet<>(gatewayConnectionConfigs));
+ mRestrictedTransports = Collections.unmodifiableSet(new ArraySet<>(restrictedTransports));
mIsTestModeProfile = isTestModeProfile;
validate();
@@ -82,6 +104,20 @@
new ArraySet<>(
PersistableBundleUtils.toList(
gatewayConnectionConfigsBundle, VcnGatewayConnectionConfig::new));
+
+ final PersistableBundle restrictedTransportsBundle =
+ in.getPersistableBundle(RESTRICTED_TRANSPORTS_KEY);
+ if (restrictedTransportsBundle == null) {
+ // RESTRICTED_TRANSPORTS_KEY was added in U and does not exist in VcnConfigs created in
+ // older platforms
+ mRestrictedTransports = RESTRICTED_TRANSPORTS_DEFAULT;
+ } else {
+ mRestrictedTransports =
+ new ArraySet<Integer>(
+ PersistableBundleUtils.toList(
+ restrictedTransportsBundle, INTEGER_DESERIALIZER));
+ }
+
mIsTestModeProfile = in.getBoolean(IS_TEST_MODE_PROFILE_KEY);
validate();
@@ -91,6 +127,19 @@
Objects.requireNonNull(mPackageName, "packageName was null");
Preconditions.checkCollectionNotEmpty(
mGatewayConnectionConfigs, "gatewayConnectionConfigs was empty");
+
+ final Iterator<Integer> iterator = mRestrictedTransports.iterator();
+ while (iterator.hasNext()) {
+ final int transport = iterator.next();
+ if (!ALLOWED_TRANSPORTS.contains(transport)) {
+ iterator.remove();
+ Log.w(
+ TAG,
+ "Found invalid transport "
+ + transport
+ + " which might be from a new version of VcnConfig");
+ }
+ }
}
/**
@@ -110,6 +159,16 @@
}
/**
+ * Retrieve the transports that will be restricted by the VCN.
+ *
+ * @see Builder#setRestrictedUnderlyingNetworkTransports(Set)
+ */
+ @NonNull
+ public Set<Integer> getRestrictedUnderlyingNetworkTransports() {
+ return Collections.unmodifiableSet(mRestrictedTransports);
+ }
+
+ /**
* Returns whether or not this VcnConfig is restricted to test networks.
*
* @hide
@@ -134,6 +193,12 @@
new ArrayList<>(mGatewayConnectionConfigs),
VcnGatewayConnectionConfig::toPersistableBundle);
result.putPersistableBundle(GATEWAY_CONNECTION_CONFIGS_KEY, gatewayConnectionConfigsBundle);
+
+ final PersistableBundle restrictedTransportsBundle =
+ PersistableBundleUtils.fromList(
+ new ArrayList<>(mRestrictedTransports), INTEGER_SERIALIZER);
+ result.putPersistableBundle(RESTRICTED_TRANSPORTS_KEY, restrictedTransportsBundle);
+
result.putBoolean(IS_TEST_MODE_PROFILE_KEY, mIsTestModeProfile);
return result;
@@ -141,7 +206,8 @@
@Override
public int hashCode() {
- return Objects.hash(mPackageName, mGatewayConnectionConfigs, mIsTestModeProfile);
+ return Objects.hash(
+ mPackageName, mGatewayConnectionConfigs, mRestrictedTransports, mIsTestModeProfile);
}
@Override
@@ -153,6 +219,7 @@
final VcnConfig rhs = (VcnConfig) other;
return mPackageName.equals(rhs.mPackageName)
&& mGatewayConnectionConfigs.equals(rhs.mGatewayConnectionConfigs)
+ && mRestrictedTransports.equals(rhs.mRestrictedTransports)
&& mIsTestModeProfile == rhs.mIsTestModeProfile;
}
@@ -189,12 +256,15 @@
@NonNull
private final Set<VcnGatewayConnectionConfig> mGatewayConnectionConfigs = new ArraySet<>();
+ @NonNull private final Set<Integer> mRestrictedTransports = new ArraySet<>();
+
private boolean mIsTestModeProfile = false;
public Builder(@NonNull Context context) {
Objects.requireNonNull(context, "context was null");
mPackageName = context.getOpPackageName();
+ mRestrictedTransports.addAll(RESTRICTED_TRANSPORTS_DEFAULT);
}
/**
@@ -225,6 +295,36 @@
return this;
}
+ private void validateRestrictedTransportsOrThrow(Set<Integer> restrictedTransports) {
+ Objects.requireNonNull(restrictedTransports, "transports was null");
+
+ for (int transport : restrictedTransports) {
+ if (!ALLOWED_TRANSPORTS.contains(transport)) {
+ throw new IllegalArgumentException("Invalid transport " + transport);
+ }
+ }
+ }
+
+ /**
+ * Sets transports that will be restricted by the VCN.
+ *
+ * @param transports transports that will be restricted by VCN. Networks that include any
+ * of the transports will be marked as restricted. Only {@link
+ * NetworkCapabilities#TRANSPORT_WIFI} and {@link
+ * NetworkCapabilities#TRANSPORT_CELLULAR} are allowed. {@link
+ * NetworkCapabilities#TRANSPORT_WIFI} is marked restricted by default.
+ * @return this {@link Builder} instance, for chaining
+ * @throws IllegalArgumentException if the input contains unsupported transport types.
+ */
+ @NonNull
+ public Builder setRestrictedUnderlyingNetworkTransports(@NonNull Set<Integer> transports) {
+ validateRestrictedTransportsOrThrow(transports);
+
+ mRestrictedTransports.clear();
+ mRestrictedTransports.addAll(transports);
+ return this;
+ }
+
/**
* Restricts this VcnConfig to matching with test networks (only).
*
@@ -248,7 +348,11 @@
*/
@NonNull
public VcnConfig build() {
- return new VcnConfig(mPackageName, mGatewayConnectionConfigs, mIsTestModeProfile);
+ return new VcnConfig(
+ mPackageName,
+ mGatewayConnectionConfigs,
+ mRestrictedTransports,
+ mIsTestModeProfile);
}
}
}
diff --git a/core/java/android/net/vcn/VcnGatewayConnectionConfig.java b/core/java/android/net/vcn/VcnGatewayConnectionConfig.java
index 2339656..b8850f4 100644
--- a/core/java/android/net/vcn/VcnGatewayConnectionConfig.java
+++ b/core/java/android/net/vcn/VcnGatewayConnectionConfig.java
@@ -42,6 +42,7 @@
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 @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 @@
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 @@
@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 @@
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 @@
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 @@
}
}
+ 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 @@
}
/**
+ * 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 @@
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 @@
mExposedCapabilities,
mUnderlyingNetworkTemplates,
Arrays.hashCode(mRetryIntervalsMs),
- mMaxMtu);
+ mMaxMtu,
+ mGatewayOptions);
}
@Override
@@ -452,7 +525,8 @@
&& 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 @@
@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 @@
}
/**
+ * 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 @@
mExposedCapabilities,
mUnderlyingNetworkTemplates,
mRetryIntervalsMs,
- mMaxMtu);
+ mMaxMtu,
+ mGatewayOptions);
}
}
}
diff --git a/core/java/android/nfc/NfcAdapter.java b/core/java/android/nfc/NfcAdapter.java
index f545c30..0c7f529 100644
--- a/core/java/android/nfc/NfcAdapter.java
+++ b/core/java/android/nfc/NfcAdapter.java
@@ -30,6 +30,7 @@
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
import android.content.IntentFilter;
+import android.content.pm.IPackageManager;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.nfc.tech.MifareClassic;
@@ -525,6 +526,66 @@
}
/**
+ * Helper to check if this device has FEATURE_NFC_BEAM, but without using
+ * a context.
+ * Equivalent to
+ * context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_NFC_BEAM)
+ */
+ private static boolean hasBeamFeature() {
+ IPackageManager pm = ActivityThread.getPackageManager();
+ if (pm == null) {
+ Log.e(TAG, "Cannot get package manager, assuming no Android Beam feature");
+ return false;
+ }
+ try {
+ return pm.hasSystemFeature(PackageManager.FEATURE_NFC_BEAM, 0);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Package manager query failed, assuming no Android Beam feature", e);
+ return false;
+ }
+ }
+
+ /**
+ * Helper to check if this device has FEATURE_NFC, but without using
+ * a context.
+ * Equivalent to
+ * context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_NFC)
+ */
+ private static boolean hasNfcFeature() {
+ IPackageManager pm = ActivityThread.getPackageManager();
+ if (pm == null) {
+ Log.e(TAG, "Cannot get package manager, assuming no NFC feature");
+ return false;
+ }
+ try {
+ return pm.hasSystemFeature(PackageManager.FEATURE_NFC, 0);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Package manager query failed, assuming no NFC feature", e);
+ return false;
+ }
+ }
+
+ /**
+ * Helper to check if this device is NFC HCE capable, by checking for
+ * FEATURE_NFC_HOST_CARD_EMULATION and/or FEATURE_NFC_HOST_CARD_EMULATION_NFCF,
+ * but without using a context.
+ */
+ private static boolean hasNfcHceFeature() {
+ IPackageManager pm = ActivityThread.getPackageManager();
+ if (pm == null) {
+ Log.e(TAG, "Cannot get package manager, assuming no NFC feature");
+ return false;
+ }
+ try {
+ return pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION, 0)
+ || pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION_NFCF, 0);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Package manager query failed, assuming no NFC feature", e);
+ return false;
+ }
+ }
+
+ /**
* Return list of Secure Elements which support off host card emulation.
*
* @return List<String> containing secure elements on the device which supports
@@ -533,21 +594,23 @@
* @hide
*/
public @NonNull List<String> getSupportedOffHostSecureElements() {
- if (mContext == null) {
- throw new UnsupportedOperationException("You need a context on NfcAdapter to use the "
- + " getSupportedOffHostSecureElements APIs");
- }
List<String> offHostSE = new ArrayList<String>();
- PackageManager pm = mContext.getPackageManager();
+ IPackageManager pm = ActivityThread.getPackageManager();
if (pm == null) {
Log.e(TAG, "Cannot get package manager, assuming no off-host CE feature");
return offHostSE;
}
- if (pm.hasSystemFeature(PackageManager.FEATURE_NFC_OFF_HOST_CARD_EMULATION_UICC)) {
- offHostSE.add("SIM");
- }
- if (pm.hasSystemFeature(PackageManager.FEATURE_NFC_OFF_HOST_CARD_EMULATION_ESE)) {
- offHostSE.add("eSE");
+ try {
+ if (pm.hasSystemFeature(PackageManager.FEATURE_NFC_OFF_HOST_CARD_EMULATION_UICC, 0)) {
+ offHostSE.add("SIM");
+ }
+ if (pm.hasSystemFeature(PackageManager.FEATURE_NFC_OFF_HOST_CARD_EMULATION_ESE, 0)) {
+ offHostSE.add("eSE");
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Package manager query failed, assuming no off-host CE feature", e);
+ offHostSE.clear();
+ return offHostSE;
}
return offHostSE;
}
@@ -559,19 +622,10 @@
*/
@UnsupportedAppUsage
public static synchronized NfcAdapter getNfcAdapter(Context context) {
- if (context == null) {
- if (sNullContextNfcAdapter == null) {
- sNullContextNfcAdapter = new NfcAdapter(null);
- }
- return sNullContextNfcAdapter;
- }
if (!sIsInitialized) {
- PackageManager pm = context.getPackageManager();
- sHasNfcFeature = pm.hasSystemFeature(PackageManager.FEATURE_NFC);
- sHasBeamFeature = pm.hasSystemFeature(PackageManager.FEATURE_NFC_BEAM);
- boolean hasHceFeature =
- pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION)
- || pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION_NFCF);
+ sHasNfcFeature = hasNfcFeature();
+ sHasBeamFeature = hasBeamFeature();
+ boolean hasHceFeature = hasNfcHceFeature();
/* is this device meant to have NFC */
if (!sHasNfcFeature && !hasHceFeature) {
Log.v(TAG, "this device does not have NFC support");
@@ -607,6 +661,12 @@
sIsInitialized = true;
}
+ if (context == null) {
+ if (sNullContextNfcAdapter == null) {
+ sNullContextNfcAdapter = new NfcAdapter(null);
+ }
+ return sNullContextNfcAdapter;
+ }
NfcAdapter adapter = sNfcAdapters.get(context);
if (adapter == null) {
adapter = new NfcAdapter(context);
@@ -617,12 +677,8 @@
/** get handle to NFC service interface */
private static INfcAdapter getServiceInterface() {
- if (!sHasNfcFeature) {
- /* NFC is not supported */
- return null;
- }
/* get a handle to NFC service */
- IBinder b = ServiceManager.waitForService("nfc");
+ IBinder b = ServiceManager.getService("nfc");
if (b == null) {
return null;
}
@@ -652,15 +708,6 @@
"context not associated with any application (using a mock context?)");
}
- synchronized (NfcAdapter.class) {
- if (!sIsInitialized) {
- PackageManager pm = context.getPackageManager();
- sHasNfcFeature = pm.hasSystemFeature(PackageManager.FEATURE_NFC);
- }
- if (!sHasNfcFeature) {
- return null;
- }
- }
if (getServiceInterface() == null) {
// NFC is not available
return null;
diff --git a/core/java/android/nfc/cardemulation/CardEmulation.java b/core/java/android/nfc/cardemulation/CardEmulation.java
index 6a42091..0b56d19 100644
--- a/core/java/android/nfc/cardemulation/CardEmulation.java
+++ b/core/java/android/nfc/cardemulation/CardEmulation.java
@@ -22,9 +22,11 @@
import android.annotation.SdkConstant;
import android.annotation.SdkConstant.SdkConstantType;
import android.app.Activity;
+import android.app.ActivityThread;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.IPackageManager;
import android.content.pm.PackageManager;
import android.nfc.INfcCardEmulation;
import android.nfc.NfcAdapter;
@@ -156,13 +158,18 @@
throw new UnsupportedOperationException();
}
if (!sIsInitialized) {
- PackageManager pm = context.getPackageManager();
+ IPackageManager pm = ActivityThread.getPackageManager();
if (pm == null) {
Log.e(TAG, "Cannot get PackageManager");
throw new UnsupportedOperationException();
}
- if (!pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION)) {
- Log.e(TAG, "This device does not support card emulation");
+ try {
+ if (!pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION, 0)) {
+ Log.e(TAG, "This device does not support card emulation");
+ throw new UnsupportedOperationException();
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "PackageManager query failed.");
throw new UnsupportedOperationException();
}
sIsInitialized = true;
diff --git a/core/java/android/nfc/cardemulation/NfcFCardEmulation.java b/core/java/android/nfc/cardemulation/NfcFCardEmulation.java
index 48bbf5b6..3c92455 100644
--- a/core/java/android/nfc/cardemulation/NfcFCardEmulation.java
+++ b/core/java/android/nfc/cardemulation/NfcFCardEmulation.java
@@ -17,8 +17,10 @@
package android.nfc.cardemulation;
import android.app.Activity;
+import android.app.ActivityThread;
import android.content.ComponentName;
import android.content.Context;
+import android.content.pm.IPackageManager;
import android.content.pm.PackageManager;
import android.nfc.INfcFCardEmulation;
import android.nfc.NfcAdapter;
@@ -68,13 +70,18 @@
throw new UnsupportedOperationException();
}
if (!sIsInitialized) {
- PackageManager pm = context.getPackageManager();
+ IPackageManager pm = ActivityThread.getPackageManager();
if (pm == null) {
Log.e(TAG, "Cannot get PackageManager");
throw new UnsupportedOperationException();
}
- if (!pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION_NFCF)) {
- Log.e(TAG, "This device does not support NFC-F card emulation");
+ try {
+ if (!pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION_NFCF, 0)) {
+ Log.e(TAG, "This device does not support NFC-F card emulation");
+ throw new UnsupportedOperationException();
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "PackageManager query failed.");
throw new UnsupportedOperationException();
}
sIsInitialized = true;
diff --git a/core/java/android/os/BatteryStats.java b/core/java/android/os/BatteryStats.java
index e915d98..2a4c861 100644
--- a/core/java/android/os/BatteryStats.java
+++ b/core/java/android/os/BatteryStats.java
@@ -44,6 +44,7 @@
import android.util.MutableBoolean;
import android.util.Pair;
import android.util.Printer;
+import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseDoubleArray;
import android.util.SparseIntArray;
@@ -52,6 +53,7 @@
import android.view.Display;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.os.BatteryStatsHistoryIterator;
import com.google.android.collect.Lists;
@@ -2398,9 +2400,6 @@
public abstract int getHistoryUsedSize();
- @UnsupportedAppUsage
- public abstract boolean startIteratingHistoryLocked();
-
public abstract int getHistoryStringPoolSize();
public abstract int getHistoryStringPoolBytes();
@@ -2409,10 +2408,11 @@
public abstract int getHistoryTagPoolUid(int index);
- @UnsupportedAppUsage
- public abstract boolean getNextHistoryLocked(HistoryItem out);
-
- public abstract void finishIteratingHistoryLocked();
+ /**
+ * Returns a BatteryStatsHistoryIterator. Battery history will remain immutable until the
+ * {@link BatteryStatsHistoryIterator#close()} method is invoked.
+ */
+ public abstract BatteryStatsHistoryIterator iterateBatteryStatsHistory();
/**
* Returns the number of times the device has been started.
@@ -7451,80 +7451,88 @@
private void dumpHistoryLocked(PrintWriter pw, int flags, long histStart, boolean checkin) {
final HistoryPrinter hprinter = new HistoryPrinter();
- final HistoryItem rec = new HistoryItem();
long lastTime = -1;
long baseTime = -1;
boolean printed = false;
HistoryEventTracker tracker = null;
- while (getNextHistoryLocked(rec)) {
- lastTime = rec.time;
- if (baseTime < 0) {
- baseTime = lastTime;
- }
- if (rec.time >= histStart) {
- if (histStart >= 0 && !printed) {
- if (rec.cmd == HistoryItem.CMD_CURRENT_TIME
- || rec.cmd == HistoryItem.CMD_RESET
- || rec.cmd == HistoryItem.CMD_START
- || rec.cmd == HistoryItem.CMD_SHUTDOWN) {
- printed = true;
- hprinter.printNextItem(pw, rec, baseTime, checkin,
- (flags&DUMP_VERBOSE) != 0);
- rec.cmd = HistoryItem.CMD_UPDATE;
- } else if (rec.currentTime != 0) {
- printed = true;
- byte cmd = rec.cmd;
- rec.cmd = HistoryItem.CMD_CURRENT_TIME;
- hprinter.printNextItem(pw, rec, baseTime, checkin,
- (flags&DUMP_VERBOSE) != 0);
- rec.cmd = cmd;
+ try (BatteryStatsHistoryIterator iterator = iterateBatteryStatsHistory()) {
+ HistoryItem rec;
+ while ((rec = iterator.next()) != null) {
+ try {
+ lastTime = rec.time;
+ if (baseTime < 0) {
+ baseTime = lastTime;
}
- if (tracker != null) {
- if (rec.cmd != HistoryItem.CMD_UPDATE) {
- hprinter.printNextItem(pw, rec, baseTime, checkin,
- (flags&DUMP_VERBOSE) != 0);
- rec.cmd = HistoryItem.CMD_UPDATE;
- }
- int oldEventCode = rec.eventCode;
- HistoryTag oldEventTag = rec.eventTag;
- rec.eventTag = new HistoryTag();
- for (int i=0; i<HistoryItem.EVENT_COUNT; i++) {
- HashMap<String, SparseIntArray> active
- = tracker.getStateForEvent(i);
- if (active == null) {
- continue;
+ if (rec.time >= histStart) {
+ if (histStart >= 0 && !printed) {
+ if (rec.cmd == HistoryItem.CMD_CURRENT_TIME
+ || rec.cmd == HistoryItem.CMD_RESET
+ || rec.cmd == HistoryItem.CMD_START
+ || rec.cmd == HistoryItem.CMD_SHUTDOWN) {
+ printed = true;
+ hprinter.printNextItem(pw, rec, baseTime, checkin,
+ (flags & DUMP_VERBOSE) != 0);
+ rec.cmd = HistoryItem.CMD_UPDATE;
+ } else if (rec.currentTime != 0) {
+ printed = true;
+ byte cmd = rec.cmd;
+ rec.cmd = HistoryItem.CMD_CURRENT_TIME;
+ hprinter.printNextItem(pw, rec, baseTime, checkin,
+ (flags & DUMP_VERBOSE) != 0);
+ rec.cmd = cmd;
}
- for (HashMap.Entry<String, SparseIntArray> ent
- : active.entrySet()) {
- SparseIntArray uids = ent.getValue();
- for (int j=0; j<uids.size(); j++) {
- rec.eventCode = i;
- rec.eventTag.string = ent.getKey();
- rec.eventTag.uid = uids.keyAt(j);
- rec.eventTag.poolIdx = uids.valueAt(j);
+ if (tracker != null) {
+ if (rec.cmd != HistoryItem.CMD_UPDATE) {
hprinter.printNextItem(pw, rec, baseTime, checkin,
- (flags&DUMP_VERBOSE) != 0);
- rec.wakeReasonTag = null;
- rec.wakelockTag = null;
+ (flags & DUMP_VERBOSE) != 0);
+ rec.cmd = HistoryItem.CMD_UPDATE;
}
+ int oldEventCode = rec.eventCode;
+ HistoryTag oldEventTag = rec.eventTag;
+ rec.eventTag = new HistoryTag();
+ for (int i = 0; i < HistoryItem.EVENT_COUNT; i++) {
+ Map<String, SparseIntArray> active =
+ tracker.getStateForEvent(i);
+ if (active == null) {
+ continue;
+ }
+ for (Map.Entry<String, SparseIntArray> ent :
+ active.entrySet()) {
+ SparseIntArray uids = ent.getValue();
+ for (int j = 0; j < uids.size(); j++) {
+ rec.eventCode = i;
+ rec.eventTag.string = ent.getKey();
+ rec.eventTag.uid = uids.keyAt(j);
+ rec.eventTag.poolIdx = uids.valueAt(j);
+ hprinter.printNextItem(pw, rec, baseTime, checkin,
+ (flags & DUMP_VERBOSE) != 0);
+ rec.wakeReasonTag = null;
+ rec.wakelockTag = null;
+ }
+ }
+ }
+ rec.eventCode = oldEventCode;
+ rec.eventTag = oldEventTag;
+ tracker = null;
}
}
- rec.eventCode = oldEventCode;
- rec.eventTag = oldEventTag;
- tracker = null;
+ hprinter.printNextItem(pw, rec, baseTime, checkin,
+ (flags & DUMP_VERBOSE) != 0);
+ } else if (false/* && rec.eventCode != HistoryItem.EVENT_NONE */) {
+ // This is an attempt to aggregate the previous state and generate
+ // fake events to reflect that state at the point where we start
+ // printing real events. It doesn't really work right, so is turned off.
+ if (tracker == null) {
+ tracker = new HistoryEventTracker();
+ }
+ tracker.updateState(rec.eventCode, rec.eventTag.string,
+ rec.eventTag.uid, rec.eventTag.poolIdx);
}
+ } catch (Throwable t) {
+ t.printStackTrace(pw);
+ Slog.wtf(TAG, "Corrupted battery history", t);
+ break;
}
- hprinter.printNextItem(pw, rec, baseTime, checkin,
- (flags&DUMP_VERBOSE) != 0);
- } else if (false && rec.eventCode != HistoryItem.EVENT_NONE) {
- // This is an attempt to aggregate the previous state and generate
- // fake events to reflect that state at the point where we start
- // printing real events. It doesn't really work right, so is turned off.
- if (tracker == null) {
- tracker = new HistoryEventTracker();
- }
- tracker.updateState(rec.eventCode, rec.eventTag.string,
- rec.eventTag.uid, rec.eventTag.poolIdx);
}
}
if (histStart >= 0) {
@@ -7595,25 +7603,19 @@
if ((flags&DUMP_HISTORY_ONLY) != 0 || !filtering) {
final long historyTotalSize = getHistoryTotalSize();
final long historyUsedSize = getHistoryUsedSize();
- if (startIteratingHistoryLocked()) {
- try {
- pw.print("Battery History (");
- pw.print((100*historyUsedSize)/historyTotalSize);
- pw.print("% used, ");
- printSizeValue(pw, historyUsedSize);
- pw.print(" used of ");
- printSizeValue(pw, historyTotalSize);
- pw.print(", ");
- pw.print(getHistoryStringPoolSize());
- pw.print(" strings using ");
- printSizeValue(pw, getHistoryStringPoolBytes());
- pw.println("):");
- dumpHistoryLocked(pw, flags, histStart, false);
- pw.println();
- } finally {
- finishIteratingHistoryLocked();
- }
- }
+ pw.print("Battery History (");
+ pw.print((100 * historyUsedSize) / historyTotalSize);
+ pw.print("% used, ");
+ printSizeValue(pw, historyUsedSize);
+ pw.print(" used of ");
+ printSizeValue(pw, historyTotalSize);
+ pw.print(", ");
+ pw.print(getHistoryStringPoolSize());
+ pw.print(" strings using ");
+ printSizeValue(pw, getHistoryStringPoolBytes());
+ pw.println("):");
+ dumpHistoryLocked(pw, flags, histStart, false);
+ pw.println();
}
if (filtering && (flags&(DUMP_CHARGED_ONLY|DUMP_DAILY_ONLY)) == 0) {
@@ -7770,28 +7772,24 @@
getEndPlatformVersion());
if ((flags & (DUMP_INCLUDE_HISTORY | DUMP_HISTORY_ONLY)) != 0) {
- if (startIteratingHistoryLocked()) {
- try {
- for (int i=0; i<getHistoryStringPoolSize(); i++) {
- pw.print(BATTERY_STATS_CHECKIN_VERSION); pw.print(',');
- pw.print(HISTORY_STRING_POOL); pw.print(',');
- pw.print(i);
- pw.print(",");
- pw.print(getHistoryTagPoolUid(i));
- pw.print(",\"");
- String str = getHistoryTagPoolString(i);
- if (str != null) {
- str = str.replace("\\", "\\\\");
- str = str.replace("\"", "\\\"");
- pw.print(str);
- }
- pw.print("\"");
- pw.println();
- }
- dumpHistoryLocked(pw, flags, histStart, true);
- } finally {
- finishIteratingHistoryLocked();
+ for (int i = 0; i < getHistoryStringPoolSize(); i++) {
+ pw.print(BATTERY_STATS_CHECKIN_VERSION);
+ pw.print(',');
+ pw.print(HISTORY_STRING_POOL);
+ pw.print(',');
+ pw.print(i);
+ pw.print(",");
+ pw.print(getHistoryTagPoolUid(i));
+ pw.print(",\"");
+ String str = getHistoryTagPoolString(i);
+ if (str != null) {
+ str = str.replace("\\", "\\\\");
+ str = str.replace("\"", "\\\"");
+ pw.print(str);
}
+ pw.print("\"");
+ pw.println();
+ dumpHistoryLocked(pw, flags, histStart, true);
}
}
@@ -8331,17 +8329,12 @@
}
private void dumpProtoHistoryLocked(ProtoOutputStream proto, int flags, long histStart) {
- if (!startIteratingHistoryLocked()) {
- return;
- }
-
proto.write(BatteryStatsServiceDumpHistoryProto.REPORT_VERSION, CHECKIN_VERSION);
proto.write(BatteryStatsServiceDumpHistoryProto.PARCEL_VERSION, getParcelVersion());
proto.write(BatteryStatsServiceDumpHistoryProto.START_PLATFORM_VERSION,
getStartPlatformVersion());
proto.write(BatteryStatsServiceDumpHistoryProto.END_PLATFORM_VERSION,
getEndPlatformVersion());
- try {
long token;
// History string pool (HISTORY_STRING_POOL)
for (int i = 0; i < getHistoryStringPoolSize(); ++i) {
@@ -8353,14 +8346,15 @@
proto.end(token);
}
- // History data (HISTORY_DATA)
- final HistoryPrinter hprinter = new HistoryPrinter();
- final HistoryItem rec = new HistoryItem();
- long lastTime = -1;
- long baseTime = -1;
- boolean printed = false;
- HistoryEventTracker tracker = null;
- while (getNextHistoryLocked(rec)) {
+ // History data (HISTORY_DATA)
+ final HistoryPrinter hprinter = new HistoryPrinter();
+ long lastTime = -1;
+ long baseTime = -1;
+ boolean printed = false;
+ HistoryEventTracker tracker = null;
+ try (BatteryStatsHistoryIterator iterator = iterateBatteryStatsHistory()) {
+ HistoryItem rec;
+ while ((rec = iterator.next()) != null) {
lastTime = rec.time;
if (baseTime < 0) {
baseTime = lastTime;
@@ -8427,8 +8421,6 @@
proto.write(BatteryStatsServiceDumpHistoryProto.CSV_LINES,
"NEXT: " + (lastTime + 1));
}
- } finally {
- finishIteratingHistoryLocked();
}
}
diff --git a/core/java/android/os/Looper.java b/core/java/android/os/Looper.java
index a529ac6..712d328 100644
--- a/core/java/android/os/Looper.java
+++ b/core/java/android/os/Looper.java
@@ -177,12 +177,15 @@
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 @@
SystemProperties.getInt("log.looper."
+ Process.myUid() + "."
+ Thread.currentThread().getName()
- + ".slow", 0);
+ + ".slow", -1);
me.mSlowDeliveryDetected = false;
diff --git a/core/java/android/provider/ContactsContract.java b/core/java/android/provider/ContactsContract.java
index d50ba8d..363d035 100644
--- a/core/java/android/provider/ContactsContract.java
+++ b/core/java/android/provider/ContactsContract.java
@@ -435,25 +435,27 @@
Uri.withAppendedPath(AUTHORITY_URI, "directories");
/**
- * URI used for getting all directories from primary and managed profile.
- * It supports the same semantics as {@link #CONTENT_URI} and returns the same columns.
- * If the device has no managed profile that is linked to the current profile, it behaves
- * in the exact same way as {@link #CONTENT_URI}.
- * If there is a managed profile linked to the current profile, it will merge
- * managed profile and current profile's results and return.
- *
- * Note: this query returns primary profile results before managed profile results,
- * and this order is not affected by sorting parameter.
+ * URI used for getting all directories from both the calling user and the managed profile
+ * that is linked to it.
+ * <p>
+ * It supports the same semantics as {@link #CONTENT_URI} and returns the same columns.<br>
+ * If the device has no managed profile that is linked to the calling user, it behaves
+ * in the exact same way as {@link #CONTENT_URI}.<br>
+ * If there is a managed profile linked to the calling user, it will return merged results
+ * from both.
+ * <p>
+ * Note: this query returns the calling user results before the managed profile results,
+ * and this order is not affected by the sorting parameter.
*
*/
public static final Uri ENTERPRISE_CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI,
"directories_enterprise");
/**
- * Access file provided by remote directory. It allows both personal and work remote
- * directory, but not local and invisible diretory.
- *
- * It's supported only by a few specific places for referring to contact pictures in the
+ * Access file provided by remote directory. It allows both calling user and managed profile
+ * remote directory, but not local and invisible directory.
+ * <p>
+ * It is supported only by a few specific places for referring to contact pictures in the
* remote directory. Contact picture URIs, e.g.
* {@link PhoneLookup#ENTERPRISE_CONTENT_FILTER_URI}, may contain this kind of URI.
*
@@ -490,13 +492,13 @@
public static final long LOCAL_INVISIBLE = 1;
/**
- * _ID of the work profile default directory, which represents locally stored contacts.
+ * _ID of the managed profile default directory, which represents locally stored contacts.
*/
public static final long ENTERPRISE_DEFAULT = Directory.ENTERPRISE_DIRECTORY_ID_BASE
+ DEFAULT;
/**
- * _ID of the work profile directory that represents locally stored invisible contacts.
+ * _ID of the managed profile directory that represents locally stored invisible contacts.
*/
public static final long ENTERPRISE_LOCAL_INVISIBLE = Directory.ENTERPRISE_DIRECTORY_ID_BASE
+ LOCAL_INVISIBLE;
@@ -557,8 +559,8 @@
public static final String ACCOUNT_NAME = "accountName";
/**
- * Mimimal ID for corp directory returned from
- * {@link Directory#CORP_CONTENT_URI}.
+ * Mimimal ID for managed profile directory returned from
+ * {@link Directory#ENTERPRISE_CONTENT_URI}.
*
* @hide
*/
@@ -1537,12 +1539,42 @@
public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "contacts");
/**
- * Special contacts URI to refer to contacts on the corp profile from the personal
- * profile.
- *
+ * URI used for getting all contacts from both the calling user and the managed profile
+ * that is linked to it.
+ * <p>
+ * It supports the same semantics as {@link #CONTENT_URI} and returns the same columns.<br>
+ * If the calling user has no managed profile, it behaves in the exact same way as
+ * {@link #CONTENT_URI}.<br>
+ * If there is a managed profile linked to the calling user, it will return merged results
+ * from both.
+ * <p>
+ * Note: this query returns the calling user results before the managed profile results,
+ * and this order is not affected by the sorting parameter.
+ * <p>
+ * If a result is from the managed profile, the following changes are made to the data:
+ * <ul>
+ * <li>{@link #PHOTO_THUMBNAIL_URI} and {@link #PHOTO_URI} will be rewritten to special
+ * URIs. Use {@link ContentResolver#openAssetFileDescriptor} or its siblings to
+ * load pictures from them.
+ * <li>{@link #PHOTO_ID} and {@link #PHOTO_FILE_ID} will be set to null. Don't use them.
+ * <li>{@link #_ID} and {@link #LOOKUP_KEY} will be replaced with artificial values.
+ * These values will be consistent across multiple queries, but do not use them in
+ * places that do not explicitly say they accept them. If they are used in the
+ * {@code selection} param in {@link android.content.ContentProvider#query}, the result
+ * is undefined.
+ * <li>In order to tell whether a contact is from the managed profile, use
+ * {@link ContactsContract.Contacts#isEnterpriseContactId(long)}.
+ */
+ public static final @NonNull Uri ENTERPRISE_CONTENT_URI = Uri.withAppendedPath(
+ CONTENT_URI, "enterprise");
+
+ /**
+ * Special contacts URI to refer to contacts on the managed profile from the calling user.
+ * <p>
* It's supported only by a few specific places for referring to contact pictures that
- * are in the corp provider for enterprise caller-ID. Contact picture URIs returned from
- * {@link PhoneLookup#ENTERPRISE_CONTENT_FILTER_URI} may contain this kind of URI.
+ * are in the managed profile provider for enterprise caller-ID. Contact picture URIs
+ * returned from {@link PhoneLookup#ENTERPRISE_CONTENT_FILTER_URI} and similar APIs may
+ * contain this kind of URI.
*
* @hide
*/
@@ -1736,7 +1768,8 @@
/**
* It supports the similar semantics as {@link #CONTENT_FILTER_URI} and returns the same
* columns. This URI requires {@link ContactsContract#DIRECTORY_PARAM_KEY} in parameters,
- * otherwise it will throw IllegalArgumentException.
+ * otherwise it will throw IllegalArgumentException. The passed directory can belong either
+ * to the calling user or to a managed profile that is linked to it.
*/
public static final Uri ENTERPRISE_CONTENT_FILTER_URI = Uri.withAppendedPath(
CONTENT_URI, "filter_enterprise");
@@ -1807,25 +1840,25 @@
public static final String CONTENT_VCARD_TYPE = "text/x-vcard";
/**
- * Mimimal ID for corp contacts returned from
- * {@link PhoneLookup#ENTERPRISE_CONTENT_FILTER_URI}.
+ * Mimimal ID for managed profile contacts returned from
+ * {@link PhoneLookup#ENTERPRISE_CONTENT_FILTER_URI} and similar APIs
*
* @hide
*/
public static long ENTERPRISE_CONTACT_ID_BASE = 1000000000; // slightly smaller than 2 ** 30
/**
- * Prefix for corp contacts returned from
- * {@link PhoneLookup#ENTERPRISE_CONTENT_FILTER_URI}.
+ * Prefix for managed profile contacts returned from
+ * {@link PhoneLookup#ENTERPRISE_CONTENT_FILTER_URI} and similar APIs.
*
* @hide
*/
public static String ENTERPRISE_CONTACT_LOOKUP_PREFIX = "c-";
/**
- * Return TRUE if a contact ID is from the contacts provider on the enterprise profile.
+ * Return {@code true} if a contact ID is from the contacts provider on the managed profile.
*
- * {@link PhoneLookup#ENTERPRISE_CONTENT_FILTER_URI} may return such a contact.
+ * {@link PhoneLookup#ENTERPRISE_CONTENT_FILTER_URI} and similar APIs may return such IDs.
*/
public static boolean isEnterpriseContactId(long contactId) {
return (contactId >= ENTERPRISE_CONTACT_ID_BASE) && (contactId < Profile.MIN_ID);
@@ -5167,7 +5200,7 @@
Uri.withAppendedPath(AUTHORITY_URI, "raw_contact_entities");
/**
- * The content:// style URI for this table in corp profile
+ * The content:// style URI for this table in the managed profile
*
* @hide
*/
@@ -5209,13 +5242,13 @@
public static final String DATA_ID = "data_id";
/**
- * Query raw contacts entity by a contact ID, which can potentially be a corp profile
- * contact ID
+ * Query raw contacts entity by a contact ID, which can potentially be a managed profile
+ * contact ID.
+ * <p>
+ * @param contentResolver The content resolver to query
+ * @param contactId Contact ID, which can potentially be a managed profile contact ID.
+ * @return A map from a mimetype to a list of the entity content values.
*
- * @param context A context to get the ContentResolver from
- * @param contactId Contact ID, which can potentialy be a corp profile contact ID.
- *
- * @return A map from a mimetype to a List of the entity content values.
* {@hide}
*/
@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
@@ -5452,55 +5485,44 @@
"phone_lookup");
/**
- * <p>URI used for the "enterprise caller-id".</p>
- *
- * <p class="caution"><b>Caution: </b>If you publish your app to the Google Play Store, this
- * field doesn't sort results based on contacts frequency. For more information, see the
- * <a href="/guide/topics/providers/contacts-provider#ObsoleteData">Contacts Provider</a>
- * page.
- *
+ * URI used for looking up contacts by phone number on the contact databases of both the
+ * calling user and the managed profile that is linked to it.
* <p>
* It supports the same semantics as {@link #CONTENT_FILTER_URI} and returns the same
- * columns. If the device has no corp profile that is linked to the current profile, it
- * behaves in the exact same way as {@link #CONTENT_FILTER_URI}. If there is a corp profile
- * linked to the current profile, it first queries against the personal contact database,
- * and if no matching contacts are found there, then queries against the
- * corp contacts database.
- * </p>
+ * columns.<br>
+ * If the device has no managed profile that is linked to the calling user, it behaves in
+ * the exact same way as {@link #CONTENT_FILTER_URI}.<br>
+ * If there is a managed profile linked to the calling user, it first queries the calling
+ * user's contact database, and only if no matching contacts are found there it then queries
+ * the managed profile database.
+ * <p class="caution">
+ * <b>Caution: </b>If you publish your app to the Google Play Store, this field doesn't sort
+ * results based on contacts frequency. For more information, see the
+ * <a href="/guide/topics/providers/contacts-provider#ObsoleteData">Contacts Provider</a>
+ * page.
* <p>
- * If a result is from the corp profile, it makes the following changes to the data:
+ * If a result is from the managed profile, the following changes are made to the data:
* <ul>
- * <li>
- * {@link #PHOTO_THUMBNAIL_URI} and {@link #PHOTO_URI} will be rewritten to special
- * URIs. Use {@link ContentResolver#openAssetFileDescriptor} or its siblings to
+ * <li>{@link #PHOTO_THUMBNAIL_URI} and {@link #PHOTO_URI} will be rewritten to special
+ * URIs. Use {@link ContentResolver#openAssetFileDescriptor} or its siblings to
* load pictures from them.
- * {@link #PHOTO_ID} and {@link #PHOTO_FILE_ID} will be set to null. Do not use them.
- * </li>
- * <li>
- * Corp contacts will get artificial {@link #_ID}s. In order to tell whether a contact
- * is from the corp profile, use
+ * <li>{@link #PHOTO_ID} and {@link #PHOTO_FILE_ID} will be set to null. Don't use them.
+ * <li>{@link #CONTACT_ID} and {@link #LOOKUP_KEY} will be replaced with artificial
+ * values. These values will be consistent across multiple queries, but do not use them
+ * in places that do not explicitly say they accept them. If they are used in the
+ * {@code selection} param in {@link android.content.ContentProvider#query}, the result
+ * is undefined.
+ * <li>In order to tell whether a contact is from the managed profile, use
* {@link ContactsContract.Contacts#isEnterpriseContactId(long)}.
- * </li>
- * <li>
- * Corp contacts will get artificial {@link #LOOKUP_KEY}s too.
- * </li>
- * <li>
- * Returned work contact IDs and lookup keys are not accepted in places that not
- * explicitly say to accept them.
- * </li>
- * </ul>
* <p>
* A contact lookup URL built by
* {@link ContactsContract.Contacts#getLookupUri(long, String)}
- * with an {@link #_ID} and a {@link #LOOKUP_KEY} returned by this API can be passed to
- * {@link ContactsContract.QuickContact#showQuickContact} even if a contact is from the
- * corp profile.
- * </p>
- *
+ * with a {@link #CONTACT_ID} and a {@link #LOOKUP_KEY} returned by this API can be passed
+ * to {@link ContactsContract.QuickContact#showQuickContact} even if a contact is from the
+ * managed profile.
* <pre>
* Uri lookupUri = Uri.withAppendedPath(PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI,
* Uri.encode(phoneNumber));
- * </pre>
*/
public static final Uri ENTERPRISE_CONTENT_FILTER_URI = Uri.withAppendedPath(AUTHORITY_URI,
"phone_lookup_enterprise");
@@ -6236,19 +6258,32 @@
"phones");
/**
- * URI used for getting all contacts from primary and managed profile.
- *
- * It supports the same semantics as {@link #CONTENT_URI} and returns the same
- * columns. If the device has no corp profile that is linked to the current profile, it
- * behaves in the exact same way as {@link #CONTENT_URI}. If there is a corp profile
- * linked to the current profile, it will merge corp profile and current profile's
- * results and return
- *
- * @hide
- */
- @SystemApi
- @TestApi
- @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS)
+ * URI used for getting all data records of the {@link #CONTENT_ITEM_TYPE} MIME type,
+ * combined with the associated raw contact and aggregate contact data, from both the
+ * calling user and the managed profile that is linked to it.
+ * <p>
+ * It supports the same semantics as {@link #CONTENT_URI} and returns the same
+ * columns.<br>
+ * If the device has no managed profile that is linked to the calling user, it behaves
+ * in the exact same way as {@link #CONTENT_URI}.<br>
+ * If there is a managed profile linked to the calling user, it will return merged
+ * results from both.
+ * <p>
+ * If a result is from the managed profile, the following changes are made to the data:
+ * <ul>
+ * <li>{@link #PHOTO_THUMBNAIL_URI} and {@link #PHOTO_URI} will be rewritten to
+ * special URIs. Use {@link ContentResolver#openAssetFileDescriptor} or its siblings
+ * to load pictures from them.
+ * <li>{@link #PHOTO_ID} and {@link #PHOTO_FILE_ID} will be set to null. Don't use
+ * them.
+ * <li>{@link #CONTACT_ID} and {@link #LOOKUP_KEY} will be replaced with artificial
+ * values. These values will be consistent across multiple queries, but do not use
+ * them in places that don't explicitly say they accept them. If they are used in
+ * the {@code selection} param in {@link android.content.ContentProvider#query}, the
+ * result is undefined.
+ * <li>In order to tell whether a contact is from the managed profile, use
+ * {@link ContactsContract.Contacts#isEnterpriseContactId(long)}.
+ */
public static final @NonNull Uri ENTERPRISE_CONTENT_URI =
Uri.withAppendedPath(Data.ENTERPRISE_CONTENT_URI, "phones");
@@ -6483,53 +6518,41 @@
"lookup");
/**
- * <p>URI used for enterprise email lookup.</p>
- *
- * <p>
- * It supports the same semantics as {@link #CONTENT_LOOKUP_URI} and returns the same
- * columns. If the device has no corp profile that is linked to the current profile, it
- * behaves in the exact same way as {@link #CONTENT_LOOKUP_URI}. If there is a
- * corp profile linked to the current profile, it first queries against the personal contact database,
- * and if no matching contacts are found there, then queries against the
- * corp contacts database.
- * </p>
- * <p>
- * If a result is from the corp profile, it makes the following changes to the data:
- * <ul>
- * <li>
- * {@link #PHOTO_THUMBNAIL_URI} and {@link #PHOTO_URI} will be rewritten to special
- * URIs. Use {@link ContentResolver#openAssetFileDescriptor} or its siblings to
- * load pictures from them.
- * {@link #PHOTO_ID} and {@link #PHOTO_FILE_ID} will be set to null. Do not
- * use them.
- * </li>
- * <li>
- * Corp contacts will get artificial {@link #CONTACT_ID}s. In order to tell whether
- * a contact
- * is from the corp profile, use
- * {@link ContactsContract.Contacts#isEnterpriseContactId(long)}.
- * </li>
- * <li>
- * Corp contacts will get artificial {@link #LOOKUP_KEY}s too.
- * </li>
- * <li>
- * Returned work contact IDs and lookup keys are not accepted in places that not
- * explicitly say to accept them.
- * </li>
- * </ul>
+ * URI used for looking up contacts by email on the contact databases of both the
+ * calling user and the managed profile that is linked to it.
+ * <p>
+ * It supports the same semantics as {@link #CONTENT_LOOKUP_URI} and returns the same
+ * columns.<br>
+ * If the device has no managed profile that is linked to the calling user, it behaves
+ * in the exact same way as {@link #CONTENT_LOOKUP_URI}.<br>
+ * If there is a managed profile linked to the calling user, it first queries the
+ * calling user's contact database, and only if no matching contacts are found there it
+ * then queries the managed profile database.
+ * <p class="caution">
+ * If a result is from the managed profile, the following changes are made to the data:
+ * <ul>
+ * <li>{@link #PHOTO_THUMBNAIL_URI} and {@link #PHOTO_URI} will be rewritten to
+ * specialURIs. Use {@link ContentResolver#openAssetFileDescriptor} or its siblings
+ * to load pictures from them.
+ * <li>{@link #PHOTO_ID} and {@link #PHOTO_FILE_ID} will be set to null. Don't use
+ * them.
+ * <li>{@link #CONTACT_ID} and {@link #LOOKUP_KEY} will be replaced with artificial
+ * values. These values will be consistent across multiple queries, but do not use
+ * them in places that do not explicitly say they accept them. If they are used in
+ * the {@code selection} param in {@link android.content.ContentProvider#query}, the
+ * result is undefined.
+ * <li>In order to tell whether a contact is from the managed profile, use
+ * {@link ContactsContract.Contacts#isEnterpriseContactId(long)}.
* <p>
* A contact lookup URL built by
* {@link ContactsContract.Contacts#getLookupUri(long, String)}
- * with an {@link #_ID} and a {@link #LOOKUP_KEY} returned by this API can be passed to
- * {@link ContactsContract.QuickContact#showQuickContact} even if a contact is from the
- * corp profile.
- * </p>
- *
- * <pre>
- * Uri lookupUri = Uri.withAppendedPath(Email.ENTERPRISE_CONTENT_LOOKUP_URI,
- * Uri.encode(email));
- * </pre>
- */
+ * with a {@link #CONTACT_ID} and a {@link #LOOKUP_KEY} returned by this API can be
+ * passed to {@link ContactsContract.QuickContact#showQuickContact} even if a contact is
+ * from the managed profile.
+ * <pre>
+ * Uri lookupUri = Uri.withAppendedPath(Email.ENTERPRISE_CONTENT_LOOKUP_URI,
+ * Uri.encode(email));
+ */
public static final Uri ENTERPRISE_CONTENT_LOOKUP_URI =
Uri.withAppendedPath(CONTENT_URI, "lookup_enterprise");
@@ -6562,9 +6585,10 @@
/**
* <p>It supports the similar semantics as {@link #CONTENT_FILTER_URI} and returns the
* same columns. This URI requires {@link ContactsContract#DIRECTORY_PARAM_KEY} in
- * parameters, otherwise it will throw IllegalArgumentException.
- *
- * <p class="caution"><b>Caution: </b>If you publish your app to the Google Play Store,
+ * parameters, otherwise it will throw IllegalArgumentException. The passed directory
+ * can belong either to the calling user or to a managed profile that is linked to it.
+ * <p class="caution">
+ * <b>Caution: </b>If you publish your app to the Google Play Store,
* this field doesn't sort results based on contacts frequency. For more information,
* see the
* <a href="/guide/topics/providers/contacts-provider#ObsoleteData">Contacts Provider</a>
@@ -9261,7 +9285,7 @@
* around this {@link View}.
* @param lookupUri A {@link ContactsContract.Contacts#CONTENT_LOOKUP_URI} style
* {@link Uri} that describes a specific contact to feature
- * in this dialog. A work lookup uri is supported here,
+ * in this dialog. A managed profile lookup uri is supported here,
* see {@link CommonDataKinds.Email#ENTERPRISE_CONTENT_LOOKUP_URI} and
* {@link PhoneLookup#ENTERPRISE_CONTENT_FILTER_URI}.
* @param mode Any of {@link #MODE_SMALL}, {@link #MODE_MEDIUM}, or
@@ -9297,7 +9321,7 @@
* @param lookupUri A
* {@link ContactsContract.Contacts#CONTENT_LOOKUP_URI} style
* {@link Uri} that describes a specific contact to feature
- * in this dialog. A work lookup uri is supported here,
+ * in this dialog. A managed profile lookup uri is supported here,
* see {@link CommonDataKinds.Email#ENTERPRISE_CONTENT_LOOKUP_URI} and
* {@link PhoneLookup#ENTERPRISE_CONTENT_FILTER_URI}.
* @param mode Any of {@link #MODE_SMALL}, {@link #MODE_MEDIUM}, or
@@ -9330,7 +9354,7 @@
* @param lookupUri A
* {@link ContactsContract.Contacts#CONTENT_LOOKUP_URI} style
* {@link Uri} that describes a specific contact to feature
- * in this dialog. A work lookup uri is supported here,
+ * in this dialog. A managed profile lookup uri is supported here,
* see {@link CommonDataKinds.Email#ENTERPRISE_CONTENT_LOOKUP_URI} and
* {@link PhoneLookup#ENTERPRISE_CONTENT_FILTER_URI}.
* @param excludeMimes Optional list of {@link Data#MIMETYPE} MIME-types
@@ -9370,7 +9394,7 @@
* @param lookupUri A
* {@link ContactsContract.Contacts#CONTENT_LOOKUP_URI} style
* {@link Uri} that describes a specific contact to feature
- * in this dialog. A work lookup uri is supported here,
+ * in this dialog. A managed profile lookup uri is supported here,
* see {@link CommonDataKinds.Email#ENTERPRISE_CONTENT_LOOKUP_URI} and
* {@link PhoneLookup#ENTERPRISE_CONTENT_FILTER_URI}.
* @param excludeMimes Optional list of {@link Data#MIMETYPE} MIME-types
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index e90ba6e..25f15d8 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -6210,6 +6210,7 @@
MOVED_TO_GLOBAL.add(Settings.Global.DEFAULT_DNS_SERVER);
MOVED_TO_GLOBAL.add(Settings.Global.PREFERRED_NETWORK_MODE);
MOVED_TO_GLOBAL.add(Settings.Global.WEBVIEW_DATA_REDUCTION_PROXY_KEY);
+ MOVED_TO_GLOBAL.add(Settings.Global.SECURE_FRP_MODE);
}
/** @hide */
@@ -7072,7 +7073,10 @@
* device is removed from this mode.
* <p>
* Type: int (0 for false, 1 for true)
+ *
+ * @deprecated Use Global.SECURE_FRP_MODE
*/
+ @Deprecated
@Readable
public static final String SECURE_FRP_MODE = "secure_frp_mode";
@@ -10719,6 +10723,49 @@
"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.
@@ -11897,7 +11944,21 @@
public static final String DEVICE_PROVISIONED = "device_provisioned";
/**
- * Whether bypassing the device policy management role holder qualifcation is allowed,
+ * Indicates whether the device is under restricted secure FRP mode.
+ * Secure FRP mode is enabled when the device is under FRP. On solving of FRP challenge,
+ * device is removed from this mode.
+ * <p>
+ * Type: int (0 for false, 1 for true)
+ *
+ * @hide
+ */
+ @SystemApi
+ @Readable
+ @SuppressLint("NoSettingsProvider")
+ public static final String SECURE_FRP_MODE = "secure_frp_mode";
+
+ /**
+ * Whether bypassing the device policy management role holder qualification is allowed,
* (0 = false, 1 = true).
*
* @hide
diff --git a/core/java/android/service/voice/AbstractHotwordDetector.java b/core/java/android/service/voice/AbstractHotwordDetector.java
index 28357ef..c90ab67 100644
--- a/core/java/android/service/voice/AbstractHotwordDetector.java
+++ b/core/java/android/service/voice/AbstractHotwordDetector.java
@@ -25,7 +25,9 @@
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 @@
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 @@
audioStream,
audioFormat,
options,
+ mToken,
new BinderCallback(mHandler, mCallback));
} catch (RemoteException e) {
e.rethrowFromSystemServer();
@@ -111,7 +115,7 @@
}
throwIfDetectorIsNoLongerActive();
try {
- mManagerService.updateState(options, sharedMemory);
+ mManagerService.updateState(options, sharedMemory, mToken);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -128,7 +132,7 @@
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 @@
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 320280b..9008bf7 100644
--- a/core/java/android/service/voice/AlwaysOnHotwordDetector.java
+++ b/core/java/android/service/voice/AlwaysOnHotwordDetector.java
@@ -293,7 +293,7 @@
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 @@
* @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 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 @@
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 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 @@
* @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 552a793..a47c096 100644
--- a/core/java/android/service/voice/HotwordDetectionService.java
+++ b/core/java/android/service/voice/HotwordDetectionService.java
@@ -140,7 +140,7 @@
@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 7112dc6..b7f7d54 100644
--- a/core/java/android/service/voice/HotwordDetector.java
+++ b/core/java/android/service/voice/HotwordDetector.java
@@ -178,7 +178,7 @@
/**
* @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
similarity index 94%
rename from core/java/android/service/voice/IHotwordDetectionService.aidl
rename to core/java/android/service/voice/ISandboxedDetectionService.aidl
index 9ef9307..5537fd1 100644
--- a/core/java/android/service/voice/IHotwordDetectionService.aidl
+++ b/core/java/android/service/voice/ISandboxedDetectionService.aidl
@@ -29,11 +29,11 @@
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 11688df..f1b7745 100644
--- a/core/java/android/service/voice/SoftwareHotwordDetector.java
+++ b/core/java/android/service/voice/SoftwareHotwordDetector.java
@@ -59,7 +59,7 @@
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 @@
* @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 7c125c7..a59578e 100644
--- a/core/java/android/service/voice/VoiceInteractionService.java
+++ b/core/java/android/service/voice/VoiceInteractionService.java
@@ -430,7 +430,7 @@
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 @@
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 @@
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 ff03cc1..af29961 100644
--- a/core/java/android/service/voice/VoiceInteractionServiceInfo.java
+++ b/core/java/android/service/voice/VoiceInteractionServiceInfo.java
@@ -46,6 +46,7 @@
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 @@
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 String getHotwordDetectionService() {
return mHotwordDetectionService;
}
+
+ @Nullable
+ public String getVisualQueryDetectionService() {
+ return mVisualQueryDetectionService;
+ }
}
diff --git a/core/java/android/telephony/TelephonyRegistryManager.java b/core/java/android/telephony/TelephonyRegistryManager.java
index 0a1538de..519647d 100644
--- a/core/java/android/telephony/TelephonyRegistryManager.java
+++ b/core/java/android/telephony/TelephonyRegistryManager.java
@@ -45,6 +45,7 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.listeners.ListenerExecutor;
+import com.android.internal.telephony.ICarrierConfigChangeListener;
import com.android.internal.telephony.ICarrierPrivilegesCallback;
import com.android.internal.telephony.IOnSubscriptionsChangedListener;
import com.android.internal.telephony.ITelephonyRegistry;
@@ -54,8 +55,10 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
import java.util.WeakHashMap;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.stream.Collectors;
@@ -89,6 +92,14 @@
IOnSubscriptionsChangedListener> mOpportunisticSubscriptionChangedListenerMap
= new HashMap<>();
+ /**
+ * A mapping between {@link CarrierConfigManager.CarrierConfigChangeListener} and its callback
+ * ICarrierConfigChangeListener.
+ */
+ private final ConcurrentHashMap<CarrierConfigManager.CarrierConfigChangeListener,
+ ICarrierConfigChangeListener>
+ mCarrierConfigChangeListenerMap = new ConcurrentHashMap<>();
+
/** @hide **/
public TelephonyRegistryManager(@NonNull Context context) {
@@ -1412,4 +1423,94 @@
throw e.rethrowFromSystemServer();
}
}
+
+ /**
+ * Register a {@link android.telephony.CarrierConfigManager.CarrierConfigChangeListener} to get
+ * notification when carrier configurations have changed.
+ *
+ * @param executor The executor on which the callback will be executed.
+ * @param listener The CarrierConfigChangeListener to be registered with.
+ */
+ public void addCarrierConfigChangedListener(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull CarrierConfigManager.CarrierConfigChangeListener listener) {
+ Objects.requireNonNull(executor, "Executor should be non-null.");
+ Objects.requireNonNull(listener, "Listener should be non-null.");
+ if (mCarrierConfigChangeListenerMap.get(listener) != null) {
+ Log.e(TAG, "registerCarrierConfigChangeListener: listener already present");
+ return;
+ }
+
+ ICarrierConfigChangeListener callback = new ICarrierConfigChangeListener.Stub() {
+ @Override
+ public void onCarrierConfigChanged(int slotIndex, int subId, int carrierId,
+ int specificCarrierId) {
+ Log.d(TAG, "onCarrierConfigChanged call in ICarrierConfigChangeListener callback");
+ final long identify = Binder.clearCallingIdentity();
+ try {
+ executor.execute(() -> listener.onCarrierConfigChanged(slotIndex, subId,
+ carrierId, specificCarrierId));
+ } finally {
+ Binder.restoreCallingIdentity(identify);
+ }
+ }
+ };
+
+ try {
+ sRegistry.addCarrierConfigChangeListener(callback,
+ mContext.getOpPackageName(), mContext.getAttributionTag());
+ mCarrierConfigChangeListenerMap.put(listener, callback);
+ } catch (RemoteException re) {
+ // system server crashes
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Unregister to stop the notification when carrier configurations changed.
+ *
+ * @param listener The CarrierConfigChangeListener to be unregistered with.
+ */
+ public void removeCarrierConfigChangedListener(
+ @NonNull CarrierConfigManager.CarrierConfigChangeListener listener) {
+ Objects.requireNonNull(listener, "Listener should be non-null.");
+ if (mCarrierConfigChangeListenerMap.get(listener) == null) {
+ Log.e(TAG, "removeCarrierConfigChangedListener: listener was not present");
+ return;
+ }
+
+ try {
+ sRegistry.removeCarrierConfigChangeListener(
+ mCarrierConfigChangeListenerMap.get(listener), mContext.getOpPackageName());
+ mCarrierConfigChangeListenerMap.remove(listener);
+ } catch (RemoteException re) {
+ // System sever crashes
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Notify the registrants the carrier configurations have changed.
+ *
+ * @param slotIndex The SIM slot index on which to monitor and get notification.
+ * @param subId The subscription on the SIM slot. May be
+ * {@link SubscriptionManager#INVALID_SUBSCRIPTION_ID}.
+ * @param carrierId The optional carrier Id, may be
+ * {@link TelephonyManager#UNKNOWN_CARRIER_ID}.
+ * @param specificCarrierId The optional specific carrier Id, may be {@link
+ * TelephonyManager#UNKNOWN_CARRIER_ID}.
+ */
+ public void notifyCarrierConfigChanged(int slotIndex, int subId, int carrierId,
+ int specificCarrierId) {
+ // Only validate slotIndex, all others are optional and allowed to be invalid
+ if (!SubscriptionManager.isValidPhoneId(slotIndex)) {
+ Log.e(TAG, "notifyCarrierConfigChanged, ignored: invalid slotIndex " + slotIndex);
+ return;
+ }
+ try {
+ sRegistry.notifyCarrierConfigChanged(slotIndex, subId, carrierId, specificCarrierId);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
}
diff --git a/core/java/android/util/FeatureFlagUtils.java b/core/java/android/util/FeatureFlagUtils.java
index 2c4f38d..897e23a 100644
--- a/core/java/android/util/FeatureFlagUtils.java
+++ b/core/java/android/util/FeatureFlagUtils.java
@@ -196,6 +196,7 @@
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 c692981..3b082bc 100644
--- a/core/java/android/view/AttachedSurfaceControl.java
+++ b/core/java/android/view/AttachedSurfaceControl.java
@@ -140,13 +140,13 @@
}
/**
- * 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/KeyEvent.java b/core/java/android/view/KeyEvent.java
index 06c1b25..61582cd 100644
--- a/core/java/android/view/KeyEvent.java
+++ b/core/java/android/view/KeyEvent.java
@@ -892,13 +892,18 @@
* The use of this button does not usually correspond to the function of an eraser.
*/
public static final int KEYCODE_STYLUS_BUTTON_TAIL = 311;
+ /**
+ * Key code constant: To open recent apps view (a.k.a. Overview).
+ * This key is handled by the framework and is never delivered to applications.
+ */
+ public static final int KEYCODE_RECENT_APPS = 312;
/**
* Integer value of the last KEYCODE. Increases as new keycodes are added to KeyEvent.
* @hide
*/
@TestApi
- public static final int LAST_KEYCODE = KEYCODE_STYLUS_BUTTON_TAIL;
+ public static final int LAST_KEYCODE = KEYCODE_RECENT_APPS;
// NOTE: If you add a new keycode here you must also add it to:
// isSystem()
@@ -2021,6 +2026,7 @@
case KeyEvent.KEYCODE_MENU:
case KeyEvent.KEYCODE_SOFT_RIGHT:
case KeyEvent.KEYCODE_HOME:
+ case KeyEvent.KEYCODE_RECENT_APPS:
case KeyEvent.KEYCODE_BACK:
case KeyEvent.KEYCODE_CALL:
case KeyEvent.KEYCODE_ENDCALL:
diff --git a/core/java/android/view/MotionEvent.java b/core/java/android/view/MotionEvent.java
index c8a5d8d..4fbb249 100644
--- a/core/java/android/view/MotionEvent.java
+++ b/core/java/android/view/MotionEvent.java
@@ -2199,6 +2199,7 @@
}
/** @hide */
+ @TestApi
@Override
public int getDisplayId() {
return nativeGetDisplayId(mNativePtr);
diff --git a/core/java/android/view/ScrollCaptureConnection.java b/core/java/android/view/ScrollCaptureConnection.java
index c50f70a..f0c7909 100644
--- a/core/java/android/view/ScrollCaptureConnection.java
+++ b/core/java/android/view/ScrollCaptureConnection.java
@@ -129,7 +129,7 @@
close();
}
mCancellation = null;
- Trace.asyncTraceForTrackEnd(TRACE_TAG_GRAPHICS, TRACE_TRACK, START_CAPTURE, mTraceId);
+ Trace.asyncTraceForTrackEnd(TRACE_TAG_GRAPHICS, TRACE_TRACK, mTraceId);
}
@BinderThread
@@ -164,7 +164,7 @@
} finally {
mCancellation = null;
}
- Trace.asyncTraceForTrackEnd(TRACE_TAG_GRAPHICS, TRACE_TRACK, REQUEST_IMAGE, mTraceId);
+ Trace.asyncTraceForTrackEnd(TRACE_TAG_GRAPHICS, TRACE_TRACK, mTraceId);
}
@BinderThread
@@ -200,8 +200,8 @@
mCancellation = null;
close();
}
- Trace.asyncTraceForTrackEnd(TRACE_TAG_GRAPHICS, TRACE_TRACK, END_CAPTURE, mTraceId);
- Trace.asyncTraceForTrackEnd(TRACE_TAG_GRAPHICS, TRACE_TRACK, SESSION, mTraceId);
+ Trace.asyncTraceForTrackEnd(TRACE_TAG_GRAPHICS, TRACE_TRACK, mTraceId);
+ Trace.asyncTraceForTrackEnd(TRACE_TAG_GRAPHICS, TRACE_TRACK, mTraceId);
}
@Override
diff --git a/core/java/android/view/SurfaceControlViewHost.java b/core/java/android/view/SurfaceControlViewHost.java
index 1889772..0a134be 100644
--- a/core/java/android/view/SurfaceControlViewHost.java
+++ b/core/java/android/view/SurfaceControlViewHost.java
@@ -404,14 +404,6 @@
}
/**
- * @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 @@
}
/**
+ * @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 33ea92d..9db084e 100644
--- a/core/java/android/view/SurfaceView.java
+++ b/core/java/android/view/SurfaceView.java
@@ -982,8 +982,8 @@
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 @@
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 @@
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 33b58d7..b2973ef 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -595,19 +595,21 @@
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;
@@ -849,8 +851,19 @@
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
@@ -3643,6 +3656,12 @@
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;
}
@@ -3656,8 +3675,8 @@
mPendingTransitions.clear();
}
- if (mTransactionReadyCallback != null) {
- mTransactionReadyCallback.onTransactionReady(null);
+ if (mActiveSurfaceSyncGroup != null) {
+ mActiveSurfaceSyncGroup.onTransactionReady(null);
}
} else if (cancelAndRedraw) {
mLastPerformTraversalsSkipDrawReason = cancelDueToPreDrawListener
@@ -3672,8 +3691,8 @@
}
mPendingTransitions.clear();
}
- if (!performDraw() && mTransactionReadyCallback != null) {
- mTransactionReadyCallback.onTransactionReady(null);
+ if (!performDraw() && mActiveSurfaceSyncGroup != null) {
+ mActiveSurfaceSyncGroup.onTransactionReady(null);
}
}
@@ -3687,39 +3706,40 @@
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();
}
@@ -4370,19 +4390,11 @@
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() {
@@ -4449,7 +4461,7 @@
return false;
}
- final boolean fullRedrawNeeded = mFullRedrawNeeded || mTransactionReadyCallback != null;
+ final boolean fullRedrawNeeded = mFullRedrawNeeded || mActiveSurfaceSyncGroup != null;
mFullRedrawNeeded = false;
mIsDrawing = true;
@@ -4457,9 +4469,9 @@
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
@@ -4510,11 +4522,10 @@
}
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();
@@ -4525,8 +4536,8 @@
}
}
}
- if (mTransactionReadyCallback != null && !usingAsyncReport) {
- mTransactionReadyCallback.onTransactionReady(null);
+ if (mActiveSurfaceSyncGroup != null && !usingAsyncReport) {
+ mActiveSurfaceSyncGroup.onTransactionReady(null);
}
if (mPerformContentCapture) {
performContentCaptureInitialReport();
@@ -8616,8 +8627,8 @@
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);
@@ -11205,7 +11216,7 @@
}
private void registerCallbacksForSync(boolean syncBuffer,
- final SurfaceSyncGroup.TransactionReadyCallback transactionReadyCallback) {
+ final SurfaceSyncGroup surfaceSyncGroup) {
if (!isHardwareEnabled()) {
return;
}
@@ -11232,7 +11243,7 @@
// pendingDrawFinished.
if ((syncResult
& (SYNC_LOST_SURFACE_REWARD_IF_FOUND | SYNC_CONTEXT_IS_STOPPED)) != 0) {
- transactionReadyCallback.onTransactionReady(
+ surfaceSyncGroup.onTransactionReady(
mBlastBufferQueue.gatherPendingTransactions(frame));
return null;
}
@@ -11243,7 +11254,7 @@
if (syncBuffer) {
mBlastBufferQueue.syncNextTransaction(
- transactionReadyCallback::onTransactionReady);
+ surfaceSyncGroup::onTransactionReady);
}
return didProduceBuffer -> {
@@ -11263,7 +11274,7 @@
// 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;
}
@@ -11272,35 +11283,23 @@
// 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;
@@ -11325,15 +11324,10 @@
});
}
- @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/accessibility/AccessibilityDisplayProxy.java b/core/java/android/view/accessibility/AccessibilityDisplayProxy.java
index 44b6deb..a757236 100644
--- a/core/java/android/view/accessibility/AccessibilityDisplayProxy.java
+++ b/core/java/android/view/accessibility/AccessibilityDisplayProxy.java
@@ -242,11 +242,7 @@
super(context, executor, new AccessibilityService.Callbacks() {
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
- // TODO(254545943): Remove check when event processing is done more upstream in
- // AccessibilityManagerService.
- if (event.getDisplayId() == mDisplayId) {
- AccessibilityDisplayProxy.this.onAccessibilityEvent(event);
- }
+ AccessibilityDisplayProxy.this.onAccessibilityEvent(event);
}
@Override
diff --git a/core/java/android/view/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java
index 423c560..9abbba9 100644
--- a/core/java/android/view/accessibility/AccessibilityManager.java
+++ b/core/java/android/view/accessibility/AccessibilityManager.java
@@ -25,6 +25,7 @@
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.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.Objects;
import java.util.concurrent.Executor;
/**
@@ -138,6 +140,21 @@
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 @@
@UnsupportedAppUsage(trackingBug = 123768939L)
boolean mIsHighTextContrastEnabled;
+ private float mUiContrast;
+
boolean mIsAudioDescriptionByDefaultRequested;
// accessibility tracing state
@@ -270,6 +289,9 @@
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 @@
*
* @param manager The manager that is calling back
*/
- void onAccessibilityServicesStateChanged(@NonNull AccessibilityManager manager);
+ void onAccessibilityServicesStateChanged(@NonNull AccessibilityManager manager);
}
/**
@@ -358,6 +380,21 @@
}
/**
+ * 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 @@
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 @@
/**
* 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 @@
}
/**
+ * 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 @@
}
/**
+ * 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 @@
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 @@
}
/**
+ * 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 @@
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 @@
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 364c7c8..c2d899a 100644
--- a/core/java/android/view/accessibility/IAccessibilityManager.aidl
+++ b/core/java/android/view/accessibility/IAccessibilityManager.aidl
@@ -118,4 +118,6 @@
// 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 041399c..931f862 100644
--- a/core/java/android/view/accessibility/IAccessibilityManagerClient.aidl
+++ b/core/java/android/view/accessibility/IAccessibilityManagerClient.aidl
@@ -31,4 +31,6 @@
void setRelevantEventTypes(int eventTypes);
void setFocusAppearance(int strokeWidth, int color);
+
+ void setUiContrast(float contrast);
}
diff --git a/core/java/android/view/inputmethod/TextBoundsInfo.java b/core/java/android/view/inputmethod/TextBoundsInfo.java
index 4e87405..dd05543 100644
--- a/core/java/android/view/inputmethod/TextBoundsInfo.java
+++ b/core/java/android/view/inputmethod/TextBoundsInfo.java
@@ -25,6 +25,7 @@
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
+import android.text.Layout;
import android.text.SegmentFinder;
import com.android.internal.util.ArrayUtils;
@@ -314,6 +315,554 @@
}
/**
+ * Return the index of the closest character to the given position.
+ * It's similar to the text layout API {@link Layout#getOffsetForHorizontal(int, float)}.
+ * And it's mainly used to find the cursor index (the index of the character before which the
+ * cursor should be placed) for the given position. It's guaranteed that the returned index is
+ * a grapheme break. Check {@link #getGraphemeSegmentFinder()} for more information.
+ *
+ * <p>It's assumed that the editor lays out text in horizontal lines from top to bottom and each
+ * line is laid out according to the display algorithm specified in
+ * <a href="https://unicode.org/reports/tr9/#Basic_Display_Algorithm"> unicode bidirectional
+ * algorithm</a>.
+ * </p>
+ *
+ * <p> This method won't check the text ranges whose line information is missing. For example,
+ * the {@link TextBoundsInfo}'s range is from index 5 to 15. If the associated
+ * {@link SegmentFinder} only identifies one line range from 7 to 12. Then this method
+ * won't check the text in the ranges of [5, 7) and [12, 15).
+ * </p>
+ *
+ * @param x the x coordinates of the interested location, in the editor's coordinates.
+ * @param y the y coordinates of the interested location, in the editor's coordinates.
+ * @return the index of the character whose position is closest to the given location. It will
+ * return -1 if it can't find a character.
+ *
+ * @see Layout#getOffsetForHorizontal(int, float)
+ */
+ public int getOffsetForPosition(float x, float y) {
+ final int[] lineRange = new int[2];
+ final RectF lineBounds = new RectF();
+ getLineInfo(y, lineRange, lineBounds);
+ // No line is found, return -1;
+ if (lineRange[0] == -1 || lineRange[1] == -1) return -1;
+ final int lineStart = lineRange[0];
+ final int lineEnd = lineRange[1];
+
+ final boolean lineEndsWithLinefeed =
+ (getCharacterFlags(lineEnd - 1) & FLAG_CHARACTER_LINEFEED) != 0;
+
+ // Consider the following 2 cases:
+ // Case 1:
+ // Text: "AB\nCD"
+ // Layout: AB
+ // CD
+ // Case 2:
+ // Text: "ABCD"
+ // Layout: AB
+ // CD
+ // If user wants to insert a 'X' character at the end of the first line:
+ // In case 1, 'X' is inserted before the last character '\n'.
+ // In case 2, 'X' is inserted after the last character 'B'.
+ // So if a line ends with linefeed, it shouldn't check the cursor position after the last
+ // character.
+ final int lineLimit;
+ if (lineEndsWithLinefeed) {
+ lineLimit = lineEnd;
+ } else {
+ lineLimit = lineEnd + 1;
+ }
+ // Point graphemeStart to the start of the first grapheme segment intersects with the line.
+ int graphemeStart = mGraphemeSegmentFinder.nextEndBoundary(lineStart);
+ // The grapheme information is missing.
+ if (graphemeStart == SegmentFinder.DONE) return -1;
+ graphemeStart = mGraphemeSegmentFinder.previousStartBoundary(graphemeStart);
+
+ int target = -1;
+ float minDistance = Float.MAX_VALUE;
+ while (graphemeStart != SegmentFinder.DONE && graphemeStart < lineLimit) {
+ if (graphemeStart >= lineStart) {
+ float cursorPosition = getCursorHorizontalPosition(graphemeStart, lineStart,
+ lineEnd, lineBounds.left, lineBounds.right);
+ final float distance = Math.abs(cursorPosition - x);
+ if (distance < minDistance) {
+ minDistance = distance;
+ target = graphemeStart;
+ }
+ }
+ graphemeStart = mGraphemeSegmentFinder.nextStartBoundary(graphemeStart);
+ }
+
+ return target;
+ }
+
+ /**
+ * Whether the primary position at the given index is the previous character's trailing
+ * position. <br/>
+ *
+ * For LTR character, trailing position is its right edge. For RTL character, trailing position
+ * is its left edge.
+ *
+ * The primary position is defined as the position of a newly inserted character with the
+ * context direction at the given offset. In contrast, the secondary position is the position
+ * of a newly inserted character with the context's opposite direction at the given offset.
+ *
+ * In Android, the trailing position is used for primary position when the direction run after
+ * the given index has a higher level than the current direction run.
+ *
+ * <p>
+ * For example:
+ * (L represents LTR character, and R represents RTL character. The number is the index)
+ * <pre>
+ * input text: L0 L1 L2 R3 R4 R5 L6 L7 L8
+ * render result: L0 L1 L2 R5 R4 R3 L6 L7 L8
+ * BiDi Run: [ Run 0 ][ Run 1 ][ Run 2 ]
+ * BiDi Level: 0 0 0 1 1 1 0 0 0
+ * </pre>
+ *
+ * The index 3 is a BiDi transition point, the cursor can be placed either after L2 or before
+ * R3. Because the bidi level of run 1 is higher than the run 0, this method returns true. And
+ * the cursor should be placed after L2.
+ * <pre>
+ * render result: L0 L1 L2 R5 R4 R3 L6 L7 L8
+ * position after L2: |
+ * position before R3: |
+ * result position: |
+ * </pre>
+ *
+ * The index 6 is also a Bidi transition point, the 2 possible cursor positions are exactly the
+ * same as index 3. However, since the bidi level of run 2 is higher than the run 1, this
+ * method returns false. And the cursor should be placed before L6.
+ * <pre>
+ * render result: L0 L1 L2 R5 R4 R3 L6 L7 L8
+ * position after R5: |
+ * position before L6: |
+ * result position: |
+ * </pre>
+ *
+ * This method helps guarantee that the cursor index and the cursor position forms a one to
+ * one relation.
+ * </p>
+ *
+ * @param offset the offset of the character in front of which the cursor is placed. It must be
+ * the start index of a grapheme. And it must be in the range from lineStart to
+ * lineEnd. An offset equal to lineEnd is allowed. It indicates that the cursor is
+ * placed at the end of current line instead of the start of the following line.
+ * @param lineStart the start index of the line that index belongs to, inclusive.
+ * @param lineEnd the end index of the line that index belongs to, exclusive.
+ * @return true if primary position is the trailing position of the previous character.
+ *
+ * @see #getCursorHorizontalPosition(int, int, int, float, float)
+ */
+ private boolean primaryIsTrailingPrevious(int offset, int lineStart, int lineEnd) {
+ final int bidiLevel;
+ if (offset < lineEnd) {
+ bidiLevel = getCharacterBidiLevel(offset);
+ } else {
+ // index equals to lineEnd, use line's BiDi level for the BiDi run.
+ boolean lineIsRtl =
+ (getCharacterFlags(offset - 1) & FLAG_LINE_IS_RTL) == FLAG_LINE_IS_RTL;
+ bidiLevel = lineIsRtl ? 1 : 0;
+ }
+ final int bidiLevelBefore;
+ if (offset > lineStart) {
+ // Here it assumes index is always the start of a grapheme. And (index - 1) belongs to
+ // the previous grapheme.
+ bidiLevelBefore = getCharacterBidiLevel(offset - 1);
+ } else {
+ // index equals to lineStart, use line's BiDi level for previous BiDi run.
+ boolean lineIsRtl =
+ (getCharacterFlags(offset) & FLAG_LINE_IS_RTL) == FLAG_LINE_IS_RTL;
+ bidiLevelBefore = lineIsRtl ? 1 : 0;
+ }
+ return bidiLevelBefore < bidiLevel;
+ }
+
+ /**
+ * Returns the x coordinates of the cursor at the given index. (The index of the character
+ * before which the cursor should be placed.)
+ *
+ * @param index the character index before which the cursor is placed. It must be the start
+ * index of a grapheme. It must be in the range from lineStart to lineEnd.
+ * An index equal to lineEnd is allowed. It indicates that the cursor is
+ * placed at the end of current line instead of the start of the following line.
+ * @param lineStart start index of the line that index belongs to, inclusive.
+ * @param lineEnd end index of the line that index belongs, exclusive.
+ * @return the x coordinates of the cursor at the given index,
+ *
+ * @see #primaryIsTrailingPrevious(int, int, int)
+ */
+ private float getCursorHorizontalPosition(int index, int lineStart, int lineEnd,
+ float lineLeft, float lineRight) {
+ Preconditions.checkArgumentInRange(index, lineStart, lineEnd, "index");
+ final boolean lineIsRtl = (getCharacterFlags(lineStart) & FLAG_LINE_IS_RTL) != 0;
+ final boolean isPrimaryIsTrailingPrevious =
+ primaryIsTrailingPrevious(index, lineStart, lineEnd);
+
+ // The index of the character used to compute the cursor position.
+ final int targetIndex;
+ // Whether to use the start position of the character.
+ // For LTR character start is the left edge. For RTL character, start is the right edge.
+ final boolean isStart;
+ if (isPrimaryIsTrailingPrevious) {
+ // (index - 1) belongs to the previous line(if any), return the line start position.
+ if (index <= lineStart) {
+ return lineIsRtl ? lineRight : lineLeft;
+ }
+ targetIndex = index - 1;
+ isStart = false;
+ } else {
+ // index belongs to the next line(if any), return the line end position.
+ if (index >= lineEnd) {
+ return lineIsRtl ? lineLeft : lineRight;
+ }
+ targetIndex = index;
+ isStart = true;
+ }
+
+ // The BiDi level is odd when the character is RTL.
+ final boolean isRtl = (getCharacterBidiLevel(targetIndex) & 1) != 0;
+ final int offset = targetIndex - mStart;
+ // If the character is RTL, the start is the right edge. Otherwise, the start is the
+ // left edge:
+ // +-----------------------+
+ // | | start | end |
+ // |-------+-------+-------|
+ // | RTL | right | left |
+ // |-------+-------+-------|
+ // | LTR | left | right |
+ // +-------+-------+-------+
+ return (isRtl != isStart) ? mCharacterBounds[4 * offset] : mCharacterBounds[4 * offset + 2];
+ }
+
+ /**
+ * Return the minimal rectangle that contains all the characters in the given range.
+ *
+ * @param start the start index of the given range, inclusive.
+ * @param end the end index of the given range, exclusive.
+ * @param rectF the {@link RectF} to receive the bounds.
+ */
+ private void getBoundsForRange(int start, int end, @NonNull RectF rectF) {
+ Preconditions.checkArgumentInRange(start, mStart, mEnd - 1, "start");
+ Preconditions.checkArgumentInRange(end, start, mEnd, "end");
+ if (end <= start) {
+ rectF.setEmpty();
+ return;
+ }
+
+ rectF.left = Float.MAX_VALUE;
+ rectF.top = Float.MAX_VALUE;
+ rectF.right = Float.MIN_VALUE;
+ rectF.bottom = Float.MIN_VALUE;
+ for (int index = start; index < end; ++index) {
+ final int offset = index - mStart;
+ rectF.left = Math.min(rectF.left, mCharacterBounds[4 * offset]);
+ rectF.top = Math.min(rectF.top, mCharacterBounds[4 * offset + 1]);
+ rectF.right = Math.max(rectF.right, mCharacterBounds[4 * offset + 2]);
+ rectF.bottom = Math.max(rectF.bottom, mCharacterBounds[4 * offset + 3]);
+ }
+ }
+
+ /**
+ * Return the character range and bounds of the closest line to the given {@code y} coordinate,
+ * in the editor's local coordinates.
+ *
+ * If the given y is above the first line or below the last line -1 will be returned for line
+ * start and end.
+ *
+ * This method assumes that the lines are laid out from the top to bottom.
+ *
+ * @param y the y coordinates used to search for the line.
+ * @param characterRange a two element array used to receive the character range of the line.
+ * If no valid line is found -1 will be returned for both start and end.
+ * @param bounds {@link RectF} to receive the line bounds result, nullable. If given, it can
+ * still be modified even if no valid line is found.
+ */
+ private void getLineInfo(float y, @NonNull int[] characterRange, @Nullable RectF bounds) {
+ characterRange[0] = -1;
+ characterRange[1] = -1;
+
+ // Starting from the first line.
+ int currentLineEnd = mLineSegmentFinder.nextEndBoundary(mStart);
+ if (currentLineEnd == SegmentFinder.DONE) return;
+ int currentLineStart = mLineSegmentFinder.previousStartBoundary(currentLineEnd);
+
+ float top = Float.MAX_VALUE;
+ float bottom = Float.MIN_VALUE;
+ float minDistance = Float.MAX_VALUE;
+ final RectF currentLineBounds = new RectF();
+ while (currentLineStart != SegmentFinder.DONE && currentLineStart < mEnd) {
+ final int lineStartInRange = Math.max(mStart, currentLineStart);
+ final int lineEndInRange = Math.min(mEnd, currentLineEnd);
+ getBoundsForRange(lineStartInRange, lineEndInRange, currentLineBounds);
+
+ top = Math.min(currentLineBounds.top, top);
+ bottom = Math.max(currentLineBounds.bottom, bottom);
+
+ final float distance = verticalDistance(currentLineBounds, y);
+
+ if (distance == 0f) {
+ characterRange[0] = currentLineStart;
+ characterRange[1] = currentLineEnd;
+ if (bounds != null) {
+ bounds.set(currentLineBounds);
+ }
+ return;
+ }
+
+ if (distance < minDistance) {
+ minDistance = distance;
+ characterRange[0] = currentLineStart;
+ characterRange[1] = currentLineEnd;
+ if (bounds != null) {
+ bounds.set(currentLineBounds);
+ }
+ }
+ if (y < bounds.top) break;
+ currentLineStart = mLineSegmentFinder.nextStartBoundary(currentLineStart);
+ currentLineEnd = mLineSegmentFinder.nextEndBoundary(currentLineEnd);
+ }
+
+ // y is above the first line or below the last line. The founded line is still invalid,
+ // clear the result.
+ if (y < top || y > bottom) {
+ characterRange[0] = -1;
+ characterRange[1] = -1;
+ if (bounds != null) {
+ bounds.setEmpty();
+ }
+ }
+ }
+
+ /**
+ * Finds the range of text which is inside the specified rectangle area. This method is a
+ * counterpart of the
+ * {@link Layout#getRangeForRect(RectF, SegmentFinder, Layout.TextInclusionStrategy)}.
+ *
+ * <p>It's assumed that the editor lays out text in horizontal lines from top to bottom
+ * and each line is laid out according to the display algorithm specified in
+ * <a href="https://unicode.org/reports/tr9/#Basic_Display_Algorithm"> unicode bidirectional
+ * algorithm</a>.
+ * </p>
+ *
+ * <p> This method won't check the text ranges whose line information is missing. For example,
+ * the {@link TextBoundsInfo}'s range is from index 5 to 15. If the associated line
+ * {@link SegmentFinder} only identifies one line range from 7 to 12. Then this method
+ * won't check the text in the ranges of [5, 7) and [12, 15).
+ * </p>
+ *
+ * @param area area for which the text range will be found
+ * @param segmentFinder SegmentFinder for determining the ranges of text to be considered as a
+ * text segment
+ * @param inclusionStrategy strategy for determining whether a text segment is inside the
+ * specified area
+ * @return the text range stored in a two element int array. The first element is the
+ * start (inclusive) of the text range, and the second element is the end (exclusive) character
+ * offsets of the text range, or null if there are no text segments inside the area.
+ *
+ * @see Layout#getRangeForRect(RectF, SegmentFinder, Layout.TextInclusionStrategy)
+ */
+ @Nullable
+ public int[] getRangeForRect(@NonNull RectF area, @NonNull SegmentFinder segmentFinder,
+ @NonNull Layout.TextInclusionStrategy inclusionStrategy) {
+ int lineEnd = mLineSegmentFinder.nextEndBoundary(mStart);
+ // Line information is missing.
+ if (lineEnd == SegmentFinder.DONE) return null;
+ int lineStart = mLineSegmentFinder.previousStartBoundary(lineEnd);
+
+ int start = -1;
+ while (lineStart != SegmentFinder.DONE && start == -1) {
+ start = getStartForRectWithinLine(lineStart, lineEnd, area, segmentFinder,
+ inclusionStrategy);
+ lineStart = mLineSegmentFinder.nextStartBoundary(lineStart);
+ lineEnd = mLineSegmentFinder.nextEndBoundary(lineEnd);
+ }
+
+ // Can't find the start index; the specified contains no valid segment.
+ if (start == -1) return null;
+
+ lineStart = mLineSegmentFinder.previousStartBoundary(mEnd);
+ // Line information is missing.
+ if (lineStart == SegmentFinder.DONE) return null;
+ lineEnd = mLineSegmentFinder.nextEndBoundary(lineStart);
+ int end = -1;
+ while (lineEnd > start && end == -1) {
+ end = getEndForRectWithinLine(lineStart, lineEnd, area, segmentFinder,
+ inclusionStrategy);
+ lineStart = mLineSegmentFinder.previousStartBoundary(lineStart);
+ lineEnd = mLineSegmentFinder.previousEndBoundary(lineEnd);
+ }
+
+ // We've already found start, end is guaranteed to be found at this point.
+ start = segmentFinder.previousStartBoundary(start + 1);
+ end = segmentFinder.nextEndBoundary(end - 1);
+ return new int[] { start, end };
+ }
+
+ /**
+ * Find the start character index of the first text segments within a line inside the specified
+ * {@code area}.
+ *
+ * @param lineStart the start of this line, inclusive .
+ * @param lineEnd the end of this line, exclusive.
+ * @param area the area inside which the text segments will be found.
+ * @param segmentFinder SegmentFinder for determining the ranges of text to be considered a
+ * text segment.
+ * @param inclusionStrategy strategy for determining whether a text segment is inside the
+ * specified area.
+ * @return the start index of the first segment in the area.
+ */
+ private int getStartForRectWithinLine(int lineStart, int lineEnd, @NonNull RectF area,
+ @NonNull SegmentFinder segmentFinder,
+ @NonNull Layout.TextInclusionStrategy inclusionStrategy) {
+ if (lineStart >= lineEnd) return -1;
+
+ int runStart = lineStart;
+ int runLevel = -1;
+ // Check the BiDi runs and search for the start index.
+ for (int index = lineStart; index < lineEnd; ++index) {
+ final int level = getCharacterBidiLevel(index);
+ if (level != runLevel) {
+ final int start = getStartForRectWithinRun(runStart, index, area, segmentFinder,
+ inclusionStrategy);
+ if (start != -1) {
+ return start;
+ }
+
+ runStart = index;
+ runLevel = level;
+ }
+ }
+ return getStartForRectWithinRun(runStart, lineEnd, area, segmentFinder, inclusionStrategy);
+ }
+
+ /**
+ * Find the start character index of the first text segments within the directional run inside
+ * the specified {@code area}.
+ *
+ * @param runStart the start of this directional run, inclusive.
+ * @param runEnd the end of this directional run, exclusive.
+ * @param area the area inside which the text segments will be found.
+ * @param segmentFinder SegmentFinder for determining the ranges of text to be considered a
+ * text segment.
+ * @param inclusionStrategy strategy for determining whether a text segment is inside the
+ * specified area.
+ * @return the start index of the first segment in the area.
+ */
+ private int getStartForRectWithinRun(int runStart, int runEnd, @NonNull RectF area,
+ @NonNull SegmentFinder segmentFinder,
+ @NonNull Layout.TextInclusionStrategy inclusionStrategy) {
+ if (runStart >= runEnd) return -1;
+
+ int segmentEndOffset = segmentFinder.nextEndBoundary(runStart);
+ // No segment is found in run.
+ if (segmentEndOffset == SegmentFinder.DONE) return -1;
+ int segmentStartOffset = segmentFinder.previousStartBoundary(segmentEndOffset);
+
+ final RectF segmentBounds = new RectF();
+ while (segmentStartOffset != SegmentFinder.DONE && segmentStartOffset < runEnd) {
+ final int start = Math.max(runStart, segmentStartOffset);
+ final int end = Math.min(runEnd, segmentEndOffset);
+ getBoundsForRange(start, end, segmentBounds);
+ // Find the first segment inside the area, return the start.
+ if (inclusionStrategy.isSegmentInside(segmentBounds, area)) return start;
+
+ segmentStartOffset = segmentFinder.nextStartBoundary(segmentStartOffset);
+ segmentEndOffset = segmentFinder.nextEndBoundary(segmentEndOffset);
+ }
+ return -1;
+ }
+
+ /**
+ * Find the end character index of the last text segments within a line inside the specified
+ * {@code area}.
+ *
+ * @param lineStart the start of this line, inclusive .
+ * @param lineEnd the end of this line, exclusive.
+ * @param area the area inside which the text segments will be found.
+ * @param segmentFinder SegmentFinder for determining the ranges of text to be considered a
+ * text segment.
+ * @param inclusionStrategy strategy for determining whether a text segment is inside the
+ * specified area.
+ * @return the end index of the last segment in the area.
+ */
+ private int getEndForRectWithinLine(int lineStart, int lineEnd, @NonNull RectF area,
+ @NonNull SegmentFinder segmentFinder,
+ @NonNull Layout.TextInclusionStrategy inclusionStrategy) {
+ if (lineStart >= lineEnd) return -1;
+ lineStart = Math.max(lineStart, mStart);
+ lineEnd = Math.min(lineEnd, mEnd);
+
+ // The exclusive run end index.
+ int runEnd = lineEnd;
+ int runLevel = -1;
+ // Check the BiDi runs backwards and search for the end index.
+ for (int index = lineEnd - 1; index >= lineStart; --index) {
+ final int level = getCharacterBidiLevel(index);
+ if (level != runLevel) {
+ final int end = getEndForRectWithinRun(index + 1, runEnd, area, segmentFinder,
+ inclusionStrategy);
+ if (end != -1) return end;
+
+ runEnd = index + 1;
+ runLevel = level;
+ }
+ }
+ return getEndForRectWithinRun(lineStart, runEnd, area, segmentFinder, inclusionStrategy);
+ }
+
+ /**
+ * Find the end character index of the last text segments within the directional run inside the
+ * specified {@code area}.
+ *
+ * @param runStart the start of this directional run, inclusive.
+ * @param runEnd the end of this directional run, exclusive.
+ * @param area the area inside which the text segments will be found.
+ * @param segmentFinder SegmentFinder for determining the ranges of text to be considered a
+ * text segment.
+ * @param inclusionStrategy strategy for determining whether a text segment is inside the
+ * specified area.
+ * @return the end index of the last segment in the area.
+ */
+ private int getEndForRectWithinRun(int runStart, int runEnd, @NonNull RectF area,
+ @NonNull SegmentFinder segmentFinder,
+ @NonNull Layout.TextInclusionStrategy inclusionStrategy) {
+ if (runStart >= runEnd) return -1;
+
+ int segmentStart = segmentFinder.previousStartBoundary(runEnd);
+ // No segment is found before the runEnd.
+ if (segmentStart == SegmentFinder.DONE) return -1;
+ int segmentEnd = segmentFinder.nextEndBoundary(segmentStart);
+
+ final RectF segmentBounds = new RectF();
+ while (segmentEnd != SegmentFinder.DONE && segmentEnd > runStart) {
+ final int start = Math.max(runStart, segmentStart);
+ final int end = Math.min(runEnd, segmentEnd);
+ getBoundsForRange(start, end, segmentBounds);
+ // Find the last segment inside the area, return the end.
+ if (inclusionStrategy.isSegmentInside(segmentBounds, area)) return end;
+
+ segmentStart = segmentFinder.previousStartBoundary(segmentStart);
+ segmentEnd = segmentFinder.previousEndBoundary(segmentEnd);
+ }
+ return -1;
+ }
+
+ /**
+ * Get the vertical distance from the {@code pointF} to the {@code rectF}. It's useful to find
+ * the corresponding line for a given point.
+ */
+ private static float verticalDistance(@NonNull RectF rectF, float y) {
+ if (rectF.top <= y && y < rectF.bottom) {
+ return 0f;
+ }
+ if (y < rectF.top) {
+ return rectF.top - y;
+ }
+ return y - rectF.bottom;
+ }
+
+ /**
* Describe the kinds of special objects contained in this Parcelable
* instance's marshaled representation. For example, if the object will
* include a file descriptor in the output of {@link #writeToParcel(Parcel, int)},
diff --git a/core/java/android/window/SurfaceSyncGroup.java b/core/java/android/window/SurfaceSyncGroup.java
index 3950739..250652a 100644
--- a/core/java/android/window/SurfaceSyncGroup.java
+++ b/core/java/android/window/SurfaceSyncGroup.java
@@ -40,64 +40,65 @@
* 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;
-
- @GuardedBy("mLock")
- private final Set<SurfaceSyncGroup> mMergedSyncGroups = new ArraySet<>();
-
- @GuardedBy("mLock")
private boolean mFinished;
@GuardedBy("mLock")
+ private TransactionReadyCallback mTransactionReadyCallback;
+
+ @GuardedBy("mLock")
+ private SurfaceSyncGroup mParentSyncGroup;
+
+ @GuardedBy("mLock")
private final ArraySet<Pair<Executor, Runnable>> mSyncCompleteCallbacks = new ArraySet<>();
/**
@@ -122,16 +123,16 @@
/**
* 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 @@
}
/**
+ * 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 @@
@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 @@
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,48 +257,16 @@
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
- */
- public void markSyncReady() {
- synchronized (mLock) {
- mSyncReady = true;
- checkIfSyncIsComplete();
- }
- }
-
- @GuardedBy("mLock")
- private void checkIfSyncIsComplete() {
- if (!mSyncReady || !mPendingSyncs.isEmpty() || !mMergedSyncGroups.isEmpty()) {
- if (DEBUG) {
- Log.d(TAG, "Syncable is not complete. mSyncReady=" + mSyncReady
- + " mPendingSyncs=" + mPendingSyncs.size() + " mergedSyncs="
- + mMergedSyncGroups.size());
- }
- return;
- }
-
- if (DEBUG) {
- Log.d(TAG, "Successfully finished sync id=" + this);
- }
-
- mSyncRequestCompleteCallback.accept(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.
*/
@@ -267,99 +276,68 @@
}
}
- private void updateCallback(Consumer<Transaction> transactionConsumer) {
+ @GuardedBy("mLock")
+ private void checkIfSyncIsComplete() {
+ if (mFinished) {
+ if (DEBUG) {
+ 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;
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "Successfully finished sync id=" + this);
+ }
+ mTransactionReadyCallback.onTransactionReady(mTransaction);
+ mFinished = true;
+ }
+
+ 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 9f23f24..b9ca557 100644
--- a/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl
+++ b/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl
@@ -252,11 +252,13 @@
* @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 @@
* @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 @@
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 @@
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 6e40988..46f78e2 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/jank/InteractionJankMonitor.java b/core/java/com/android/internal/jank/InteractionJankMonitor.java
index 75f0bf5..7bd6ec8 100644
--- a/core/java/com/android/internal/jank/InteractionJankMonitor.java
+++ b/core/java/com/android/internal/jank/InteractionJankMonitor.java
@@ -780,17 +780,17 @@
// 2. The returned string should be the same with the name defined in atoms.proto.
switch (cujType) {
case CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE:
- return "SHADE_EXPAND_COLLAPSE";
+ return "NOTIFICATION_SHADE_EXPAND_COLLAPSE";
case CUJ_NOTIFICATION_SHADE_SCROLL_FLING:
- return "SHADE_SCROLL_FLING";
+ return "NOTIFICATION_SHADE_SCROLL_FLING";
case CUJ_NOTIFICATION_SHADE_ROW_EXPAND:
- return "SHADE_ROW_EXPAND";
+ return "NOTIFICATION_SHADE_ROW_EXPAND";
case CUJ_NOTIFICATION_SHADE_ROW_SWIPE:
- return "SHADE_ROW_SWIPE";
+ return "NOTIFICATION_SHADE_ROW_SWIPE";
case CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE:
- return "SHADE_QS_EXPAND_COLLAPSE";
+ return "NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE";
case CUJ_NOTIFICATION_SHADE_QS_SCROLL_SWIPE:
- return "SHADE_QS_SCROLL_SWIPE";
+ return "NOTIFICATION_SHADE_QS_SCROLL_SWIPE";
case CUJ_LAUNCHER_APP_LAUNCH_FROM_RECENTS:
return "LAUNCHER_APP_LAUNCH_FROM_RECENTS";
case CUJ_LAUNCHER_APP_LAUNCH_FROM_ICON:
@@ -880,9 +880,9 @@
case CUJ_SPLIT_SCREEN_EXIT:
return "SPLIT_SCREEN_EXIT";
case CUJ_LOCKSCREEN_LAUNCH_CAMERA:
- return "CUJ_LOCKSCREEN_LAUNCH_CAMERA";
+ return "LOCKSCREEN_LAUNCH_CAMERA";
case CUJ_SPLIT_SCREEN_RESIZE:
- return "CUJ_SPLIT_SCREEN_RESIZE";
+ return "SPLIT_SCREEN_RESIZE";
case CUJ_SETTINGS_SLIDER:
return "SETTINGS_SLIDER";
case CUJ_TAKE_SCREENSHOT:
diff --git a/core/java/com/android/internal/os/BatteryStatsHistory.java b/core/java/com/android/internal/os/BatteryStatsHistory.java
index c3361a2..66d64c4 100644
--- a/core/java/com/android/internal/os/BatteryStatsHistory.java
+++ b/core/java/com/android/internal/os/BatteryStatsHistory.java
@@ -49,6 +49,7 @@
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -163,10 +164,6 @@
*/
private int mCurrentParcelEnd;
/**
- * When iterating history files, the current record count.
- */
- private int mRecordCount = 0;
- /**
* Used when BatteryStatsImpl object is created from deserialization of a parcel,
* such as Settings app or checkin file, to iterate over history parcels.
*/
@@ -199,10 +196,8 @@
private boolean mMeasuredEnergyHeaderWritten = false;
private boolean mCpuUsageHeaderWritten = false;
private final VarintParceler mVarintParceler = new VarintParceler();
-
private byte mLastHistoryStepLevel = 0;
-
- private BatteryStatsHistoryIterator mBatteryStatsHistoryIterator;
+ private boolean mMutable = true;
/**
* A delegate responsible for computing additional details for a step in battery history.
@@ -493,25 +488,21 @@
* @return always return true.
*/
public BatteryStatsHistoryIterator iterate() {
- mRecordCount = 0;
mCurrentFileIndex = 0;
mCurrentParcel = null;
mCurrentParcelEnd = 0;
mParcelIndex = 0;
- mBatteryStatsHistoryIterator = new BatteryStatsHistoryIterator(this);
- return mBatteryStatsHistoryIterator;
+ mMutable = false;
+ return new BatteryStatsHistoryIterator(this);
}
/**
* Finish iterating history files and history buffer.
*/
- void finishIteratingHistory() {
+ void iteratorFinished() {
// setDataPosition so mHistoryBuffer Parcel can be written.
mHistoryBuffer.setDataPosition(mHistoryBuffer.dataSize());
- mBatteryStatsHistoryIterator = null;
- if (DEBUG) {
- Slog.d(TAG, "Battery history records iterated: " + mRecordCount);
- }
+ mMutable = true;
}
/**
@@ -519,17 +510,11 @@
* history file, when reached the mActiveFile (highest numbered history file), do not read from
* mActiveFile, read from history buffer instead because the buffer has more updated data.
*
- * @param out a history item.
* @return The parcel that has next record. null if finished all history files and history
* buffer
*/
- public Parcel getNextParcel(HistoryItem out) {
- if (mRecordCount == 0) {
- // reset out if it is the first record.
- out.clear();
- }
- ++mRecordCount;
-
+ @Nullable
+ public Parcel getNextParcel() {
// First iterate through all records in current parcel.
if (mCurrentParcel != null) {
if (mCurrentParcel.dataPosition() < mCurrentParcelEnd) {
@@ -1270,6 +1255,10 @@
return;
}
+ if (!mMutable) {
+ throw new ConcurrentModificationException("Battery history is not writable");
+ }
+
final long timeDiffMs = (mHistoryBaseTimeMs + elapsedRealtimeMs) - mHistoryLastWritten.time;
final int diffStates = mHistoryLastWritten.states ^ (cur.states & mActiveHistoryStates);
final int diffStates2 = mHistoryLastWritten.states2 ^ (cur.states2 & mActiveHistoryStates2);
@@ -1390,8 +1379,8 @@
private void writeHistoryItem(long elapsedRealtimeMs,
@SuppressWarnings("UnusedVariable") long uptimeMs, HistoryItem cur, byte cmd) {
- if (mBatteryStatsHistoryIterator != null) {
- throw new IllegalStateException("Can't do this while iterating history!");
+ if (!mMutable) {
+ throw new ConcurrentModificationException("Battery history is not writable");
}
mHistoryBufferLastPos = mHistoryBuffer.dataPosition();
mHistoryLastLastWritten.setTo(mHistoryLastWritten);
@@ -1687,7 +1676,10 @@
Slog.i(TAG, "WRITE DELTA: cpuUsageDetails=" + cur.cpuUsageDetails);
}
if (!mCpuUsageHeaderWritten) {
- dest.writeStringArray(cur.cpuUsageDetails.cpuBracketDescriptions);
+ dest.writeInt(cur.cpuUsageDetails.cpuBracketDescriptions.length);
+ for (String desc: cur.cpuUsageDetails.cpuBracketDescriptions) {
+ dest.writeString(desc);
+ }
mCpuUsageHeaderWritten = true;
}
dest.writeInt(cur.cpuUsageDetails.uid);
diff --git a/core/java/com/android/internal/os/BatteryStatsHistoryIterator.java b/core/java/com/android/internal/os/BatteryStatsHistoryIterator.java
index 09fe100..b88116d 100644
--- a/core/java/com/android/internal/os/BatteryStatsHistoryIterator.java
+++ b/core/java/com/android/internal/os/BatteryStatsHistoryIterator.java
@@ -23,10 +23,13 @@
import android.util.Slog;
import android.util.SparseArray;
+import java.util.Iterator;
+
/**
* An iterator for {@link BatteryStats.HistoryItem}'s.
*/
-public class BatteryStatsHistoryIterator {
+public class BatteryStatsHistoryIterator implements Iterator<BatteryStats.HistoryItem>,
+ AutoCloseable {
private static final boolean DEBUG = false;
private static final String TAG = "BatteryStatsHistoryItr";
private final BatteryStatsHistory mBatteryStatsHistory;
@@ -38,29 +41,51 @@
private final BatteryStatsHistory.VarintParceler mVarintParceler =
new BatteryStatsHistory.VarintParceler();
+ private final BatteryStats.HistoryItem mHistoryItem = new BatteryStats.HistoryItem();
+
+ private static final int MAX_ENERGY_CONSUMER_COUNT = 100;
+ private static final int MAX_CPU_BRACKET_COUNT = 100;
+
public BatteryStatsHistoryIterator(@NonNull BatteryStatsHistory history) {
mBatteryStatsHistory = history;
+ mHistoryItem.clear();
+ }
+
+ @Override
+ public boolean hasNext() {
+ Parcel p = mBatteryStatsHistory.getNextParcel();
+ if (p == null) {
+ close();
+ return false;
+ }
+ return true;
}
/**
- * Retrieves the next HistoryItem from battery history, if available. Returns false if there
+ * Retrieves the next HistoryItem from battery history, if available. Returns null if there
* are no more items.
*/
- public boolean next(BatteryStats.HistoryItem out) {
- Parcel p = mBatteryStatsHistory.getNextParcel(out);
+ @Override
+ public BatteryStats.HistoryItem next() {
+ Parcel p = mBatteryStatsHistory.getNextParcel();
if (p == null) {
- mBatteryStatsHistory.finishIteratingHistory();
- return false;
+ close();
+ return null;
}
- final long lastRealtimeMs = out.time;
- final long lastWalltimeMs = out.currentTime;
- readHistoryDelta(p, out);
- if (out.cmd != BatteryStats.HistoryItem.CMD_CURRENT_TIME
- && out.cmd != BatteryStats.HistoryItem.CMD_RESET && lastWalltimeMs != 0) {
- out.currentTime = lastWalltimeMs + (out.time - lastRealtimeMs);
+ final long lastRealtimeMs = mHistoryItem.time;
+ final long lastWalltimeMs = mHistoryItem.currentTime;
+ try {
+ readHistoryDelta(p, mHistoryItem);
+ } catch (Throwable t) {
+ Slog.wtf(TAG, "Corrupted battery history", t);
+ return null;
}
- return true;
+ if (mHistoryItem.cmd != BatteryStats.HistoryItem.CMD_CURRENT_TIME
+ && mHistoryItem.cmd != BatteryStats.HistoryItem.CMD_RESET && lastWalltimeMs != 0) {
+ mHistoryItem.currentTime = lastWalltimeMs + (mHistoryItem.time - lastRealtimeMs);
+ }
+ return mHistoryItem;
}
private void readHistoryDelta(Parcel src, BatteryStats.HistoryItem cur) {
@@ -210,6 +235,12 @@
}
final int consumerCount = src.readInt();
+ if (consumerCount > MAX_ENERGY_CONSUMER_COUNT) {
+ // Check to avoid a heap explosion in case the parcel is corrupted
+ throw new IllegalStateException(
+ "EnergyConsumer count too high: " + consumerCount
+ + ". Max = " + MAX_ENERGY_CONSUMER_COUNT);
+ }
mMeasuredEnergyDetails.consumers =
new BatteryStats.MeasuredEnergyDetails.EnergyConsumer[consumerCount];
mMeasuredEnergyDetails.chargeUC = new long[consumerCount];
@@ -236,7 +267,16 @@
if ((extensionFlags & BatteryStatsHistory.EXTENSION_CPU_USAGE_HEADER_FLAG) != 0) {
mCpuUsageDetails = new BatteryStats.CpuUsageDetails();
- mCpuUsageDetails.cpuBracketDescriptions = src.readStringArray();
+ final int cpuBracketCount = src.readInt();
+ if (cpuBracketCount > MAX_CPU_BRACKET_COUNT) {
+ // Check to avoid a heap explosion in case the parcel is corrupted
+ throw new IllegalStateException("Too many CPU brackets: " + cpuBracketCount
+ + ". Max = " + MAX_CPU_BRACKET_COUNT);
+ }
+ mCpuUsageDetails.cpuBracketDescriptions = new String[cpuBracketCount];
+ for (int i = 0; i < cpuBracketCount; i++) {
+ mCpuUsageDetails.cpuBracketDescriptions[i] = src.readString();
+ }
mCpuUsageDetails.cpuUsageMs =
new long[mCpuUsageDetails.cpuBracketDescriptions.length];
} else if (mCpuUsageDetails != null) {
@@ -294,7 +334,8 @@
/**
* Should be called when iteration is complete.
*/
+ @Override
public void close() {
- mBatteryStatsHistory.finishIteratingHistory();
+ mBatteryStatsHistory.iteratorFinished();
}
}
diff --git a/core/java/com/android/internal/security/TEST_MAPPING b/core/java/com/android/internal/security/TEST_MAPPING
index 803760c..b47ecec 100644
--- a/core/java/com/android/internal/security/TEST_MAPPING
+++ b/core/java/com/android/internal/security/TEST_MAPPING
@@ -14,6 +14,19 @@
{
"name": "ApkVerityTest",
"file_patterns": ["VerityUtils\\.java"]
+ },
+ {
+ "name": "UpdatableSystemFontTest",
+ "file_patterns": ["VerityUtils\\.java"]
+ },
+ {
+ "name": "CtsAppSecurityHostTestCases",
+ "options": [
+ {
+ "include-filter": "android.appsecurity.cts.ApkVerityInstallTest"
+ }
+ ],
+ "file_patterns": ["VerityUtils\\.java"]
}
]
}
diff --git a/core/java/com/android/internal/security/VerityUtils.java b/core/java/com/android/internal/security/VerityUtils.java
index 3ab11a8..786941f 100644
--- a/core/java/com/android/internal/security/VerityUtils.java
+++ b/core/java/com/android/internal/security/VerityUtils.java
@@ -17,7 +17,6 @@
package com.android.internal.security;
import android.annotation.NonNull;
-import android.annotation.Nullable;
import android.os.Build;
import android.os.SystemProperties;
import android.system.Os;
@@ -41,9 +40,6 @@
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
@@ -58,9 +54,6 @@
*/
public static final String FSVERITY_SIGNATURE_FILE_EXTENSION = ".fsv_sig";
- /** The maximum size of signature file. This is just to avoid potential abuse. */
- private static final int MAX_SIGNATURE_FILE_SIZE_BYTES = 8192;
-
/** SHA256 hash size. */
private static final int HASH_SIZE_BYTES = 32;
@@ -79,26 +72,9 @@
return filePath + FSVERITY_SIGNATURE_FILE_EXTENSION;
}
- /** Enables fs-verity for the file with an optional PKCS#7 detached signature file. */
- public static void setUpFsverity(@NonNull String filePath, @Nullable String signaturePath)
- throws IOException {
- byte[] rawSignature = null;
- if (signaturePath != null) {
- Path path = Paths.get(signaturePath);
- if (Files.size(path) > MAX_SIGNATURE_FILE_SIZE_BYTES) {
- throw new SecurityException("Signature file is unexpectedly large: "
- + signaturePath);
- }
- rawSignature = Files.readAllBytes(path);
- }
- setUpFsverity(filePath, rawSignature);
- }
-
- /** Enables fs-verity for the file with an optional PKCS#7 detached signature bytes. */
- public static void setUpFsverity(@NonNull String filePath, @Nullable byte[] pkcs7Signature)
- throws IOException {
- // This will fail if the public key is not already in .fs-verity kernel keyring.
- int errno = enableFsverityNative(filePath, pkcs7Signature);
+ /** Enables fs-verity for the file without signature. */
+ public static void setUpFsverity(@NonNull String filePath) throws IOException {
+ int errno = enableFsverityNative(filePath);
if (errno != 0) {
throw new IOException("Failed to enable fs-verity on " + filePath + ": "
+ Os.strerror(errno));
@@ -234,8 +210,7 @@
return buffer.array();
}
- private static native int enableFsverityNative(@NonNull String filePath,
- @Nullable byte[] pkcs7Signature);
+ private static native int enableFsverityNative(@NonNull String filePath);
private static native int measureFsverityNative(@NonNull String filePath,
@NonNull byte[] digest);
private static native int statxForFsverityNative(@NonNull String filePath);
diff --git a/core/java/com/android/internal/telephony/ICarrierConfigChangeListener.aidl b/core/java/com/android/internal/telephony/ICarrierConfigChangeListener.aidl
new file mode 100644
index 0000000..0f7ab0a
--- /dev/null
+++ b/core/java/com/android/internal/telephony/ICarrierConfigChangeListener.aidl
@@ -0,0 +1,21 @@
+/*
+ * 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.internal.telephony;
+
+oneway interface ICarrierConfigChangeListener {
+ void onCarrierConfigChanged(int slotIndex, int subId, int carrierId, int specificCarrierId);
+}
\ No newline at end of file
diff --git a/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl b/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl
index 7ba2686..54936c6 100644
--- a/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl
+++ b/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl
@@ -32,6 +32,7 @@
import android.telephony.ServiceState;
import android.telephony.SignalStrength;
import android.telephony.emergency.EmergencyNumber;
+import com.android.internal.telephony.ICarrierConfigChangeListener;
import com.android.internal.telephony.ICarrierPrivilegesCallback;
import com.android.internal.telephony.IPhoneStateListener;
import com.android.internal.telephony.IOnSubscriptionsChangedListener;
@@ -109,4 +110,8 @@
int phoneId, in List<String> privilegedPackageNames, in int[] privilegedUids);
void notifyCarrierServiceChanged(int phoneId, in String packageName, int uid);
+ void addCarrierConfigChangeListener(ICarrierConfigChangeListener listener,
+ String pkg, String featureId);
+ void removeCarrierConfigChangeListener(ICarrierConfigChangeListener listener, String pkg);
+ void notifyCarrierConfigChanged(int phoneId, int subId, int carrierId, int specificCarrierId);
}
diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java
index 953b36b..65f5522 100644
--- a/core/java/com/android/internal/widget/LockPatternUtils.java
+++ b/core/java/com/android/internal/widget/LockPatternUtils.java
@@ -1508,7 +1508,8 @@
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 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;
@@ -1742,7 +1750,7 @@
}
public static boolean userOwnsFrpCredential(Context context, UserInfo info) {
- return info != null && info.isPrimary() && info.isAdmin() && frpCredentialEnabled(context);
+ return info != null && info.isMain() && info.isAdmin() && frpCredentialEnabled(context);
}
public static boolean frpCredentialEnabled(Context context) {
diff --git a/core/jni/android_media_AudioFormat.h b/core/jni/android_media_AudioFormat.h
index 962b501..a9b1906 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 @@
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 @@
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 446bd64..5096250e 100644
--- a/core/jni/android_media_AudioProfile.h
+++ b/core/jni/android_media_AudioProfile.h
@@ -25,6 +25,7 @@
// 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 @@
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/jni/com_android_internal_security_VerityUtils.cpp b/core/jni/com_android_internal_security_VerityUtils.cpp
index dabee69..3e5689b 100644
--- a/core/jni/com_android_internal_security_VerityUtils.cpp
+++ b/core/jni/com_android_internal_security_VerityUtils.cpp
@@ -23,7 +23,6 @@
#include <linux/fsverity.h>
#include <linux/stat.h>
#include <nativehelper/JNIHelp.h>
-#include <nativehelper/ScopedPrimitiveArray.h>
#include <nativehelper/ScopedUtfChars.h>
#include <string.h>
#include <sys/ioctl.h>
@@ -39,7 +38,7 @@
namespace {
-int enableFsverity(JNIEnv *env, jobject /* clazz */, jstring filePath, jbyteArray signature) {
+int enableFsverity(JNIEnv *env, jobject /* clazz */, jstring filePath) {
ScopedUtfChars path(env, filePath);
if (path.c_str() == nullptr) {
return EINVAL;
@@ -56,18 +55,6 @@
arg.salt_size = 0;
arg.salt_ptr = reinterpret_cast<uintptr_t>(nullptr);
- if (signature != nullptr) {
- ScopedByteArrayRO signature_bytes(env, signature);
- if (signature_bytes.get() == nullptr) {
- return EINVAL;
- }
- arg.sig_size = signature_bytes.size();
- arg.sig_ptr = reinterpret_cast<uintptr_t>(signature_bytes.get());
- } else {
- arg.sig_size = 0;
- arg.sig_ptr = reinterpret_cast<uintptr_t>(nullptr);
- }
-
if (ioctl(rfd.get(), FS_IOC_ENABLE_VERITY, &arg) < 0) {
return errno;
}
@@ -138,7 +125,7 @@
return 0;
}
const JNINativeMethod sMethods[] = {
- {"enableFsverityNative", "(Ljava/lang/String;[B)I", (void *)enableFsverity},
+ {"enableFsverityNative", "(Ljava/lang/String;)I", (void *)enableFsverity},
{"statxForFsverityNative", "(Ljava/lang/String;)I", (void *)statxForFsverity},
{"measureFsverityNative", "(Ljava/lang/String;[B)I", (void *)measureFsverity},
};
diff --git a/core/proto/OWNERS b/core/proto/OWNERS
index 9070933..db391f7 100644
--- a/core/proto/OWNERS
+++ b/core/proto/OWNERS
@@ -16,7 +16,8 @@
per-file package_item_info.proto = toddke@google.com,patb@google.com
per-file usagestatsservice.proto, usagestatsservice_v2.proto = file:/core/java/android/app/usage/OWNERS
per-file apphibernationservice.proto = file:/core/java/android/apphibernation/OWNERS
-per-file android/hardware/sensorprivacy.proto = ntmyren@google.com,evanseverson@google.com,ewol@google.com
+per-file android/hardware/sensorprivacy.proto = ntmyren@google.com,evanseverson@google.com
+per-file background_install_control.proto = wenhaowang@google.com,georgechan@google.com,billylau@google.com
# Biometrics
jaggies@google.com
diff --git a/core/proto/android/providers/settings/secure.proto b/core/proto/android/providers/settings/secure.proto
index 556636dd..e6f942e 100644
--- a/core/proto/android/providers/settings/secure.proto
+++ b/core/proto/android/providers/settings/secure.proto
@@ -596,6 +596,15 @@
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 @@
// 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/core/proto/android/server/background_install_control.proto b/core/proto/android/server/background_install_control.proto
new file mode 100644
index 0000000..38e6b4d
--- /dev/null
+++ b/core/proto/android/server/background_install_control.proto
@@ -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.
+ */
+
+syntax = "proto2";
+package com.android.server.pm;
+
+option java_multiple_files = true;
+
+// 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;
+}
+
+// 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 5853686..fd4d4f8 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -4937,7 +4937,7 @@
<!-- Allows an application to manage the companion devices.
@hide -->
<permission android:name="android.permission.MANAGE_COMPANION_DEVICES"
- android:protectionLevel="signature|role" />
+ android:protectionLevel="module|signature|role" />
<!-- Allows an application to subscribe to notifications about the presence status change
of their associated companion device
@@ -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 a2ef400..b878189 100644
--- a/core/res/OWNERS
+++ b/core/res/OWNERS
@@ -21,6 +21,11 @@
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 2d832bc..c8b0601 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 2fb766e..f995a6e 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 78ec145..bd93aa9 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 90141e5..0aeee10 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/strings.xml b/core/res/res/values/strings.xml
index 8106e24..add7307 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -6435,4 +6435,8 @@
<string name="permlab_startForegroundServicesFromBackground">Start foreground services from background</string>
<!-- Description of start foreground services from background permission [CHAR LIMIT=NONE] -->
<string name="permdesc_startForegroundServicesFromBackground">Allows a companion app to start foreground services from background.</string>
+ <!-- Toast message that is shown when the user unmutes the microphone from the keyboard. [CHAR LIMIT=TOAST] -->
+ <string name="mic_access_on_toast">Microphone is available</string>
+ <!-- Toast message that is shown when the user mutes the microphone from the keyboard. [CHAR LIMIT=TOAST] -->
+ <string name="mic_access_off_toast">Microphone is blocked</string>
</resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index e8304d8..14c751f 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -836,6 +836,8 @@
<java-symbol type="string" name="lockscreen_emergency_call" />
<java-symbol type="string" name="lockscreen_return_to_call" />
<java-symbol type="string" name="low_memory" />
+ <java-symbol type="string" name="mic_access_off_toast" />
+ <java-symbol type="string" name="mic_access_on_toast" />
<java-symbol type="string" name="midnight" />
<java-symbol type="string" name="mismatchPin" />
<java-symbol type="string" name="mmiComplete" />
@@ -4397,6 +4399,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/BroadcastRadioTests/src/android/hardware/radio/tests/functional/RadioTunerTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/functional/RadioTunerTest.java
index 3f35e99..cabeb13 100644
--- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/functional/RadioTunerTest.java
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/functional/RadioTunerTest.java
@@ -17,6 +17,7 @@
import static org.junit.Assert.*;
import static org.junit.Assume.*;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Matchers.anyInt;
@@ -335,8 +336,10 @@
assertEquals(RadioManager.STATUS_OK, scanRet);
assertEquals(RadioManager.STATUS_OK, cancelRet);
- verify(mCallback, after(kCancelTimeoutMs).atMost(1)).onError(RadioTuner.ERROR_CANCELLED);
+ verify(mCallback, after(kCancelTimeoutMs).atMost(1))
+ .onTuneFailed(eq(RadioTuner.TUNER_RESULT_CANCELED), any());
verify(mCallback, atMost(1)).onProgramInfoChanged(any());
+ Mockito.reset(mCallback);
}
@Test
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/DefaultRadioTunerTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/DefaultRadioTunerTest.java
index 2fa3f876..65e55a2 100644
--- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/DefaultRadioTunerTest.java
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/DefaultRadioTunerTest.java
@@ -130,6 +130,18 @@
};
@Test
+ public void seek_forRadioTuner_throwsException() {
+ UnsupportedOperationException thrown = assertThrows(
+ UnsupportedOperationException.class, () -> {
+ DEFAULT_RADIO_TUNER.seek(RadioTuner.DIRECTION_DOWN,
+ /* skipSubChannel= */ false);
+ });
+
+ assertWithMessage("Exception for seeking on default radio tuner")
+ .that(thrown).hasMessageThat().contains("Seeking is not supported");
+ }
+
+ @Test
public void getDynamicProgramList_forRadioTuner_returnsNull() {
assertWithMessage("Dynamic program list obtained from default radio tuner")
.that(DEFAULT_RADIO_TUNER.getDynamicProgramList(new ProgramList.Filter())).isNull();
@@ -143,29 +155,45 @@
@Test
public void isConfigFlagSet_forRadioTuner_throwsException() {
- assertThrows(UnsupportedOperationException.class, () -> {
- DEFAULT_RADIO_TUNER.isConfigFlagSet(/* flag= */ 1);
- });
+ UnsupportedOperationException thrown = assertThrows(
+ UnsupportedOperationException.class, () -> {
+ DEFAULT_RADIO_TUNER.isConfigFlagSet(/* flag= */ 1);
+ });
+
+ assertWithMessage("Exception for isConfigFlagSet on default radio tuner")
+ .that(thrown).hasMessageThat().contains("isConfigFlagSet is not supported");
}
@Test
public void setConfigFlag_forRadioTuner_throwsException() {
- assertThrows(UnsupportedOperationException.class, () -> {
+ UnsupportedOperationException thrown = assertThrows(UnsupportedOperationException.class,
+ () -> {
DEFAULT_RADIO_TUNER.setConfigFlag(/* flag= */ 1, /* value= */ false);
});
+
+ assertWithMessage("Exception for setting config flag on default radio tuner")
+ .that(thrown).hasMessageThat().contains("Setting config flag is not supported");
}
@Test
public void setParameters_forRadioTuner_throwsException() {
- assertThrows(UnsupportedOperationException.class, () -> {
+ UnsupportedOperationException thrown = assertThrows(UnsupportedOperationException.class,
+ () -> {
DEFAULT_RADIO_TUNER.setParameters(Map.of("testKey", "testValue"));
});
+
+ assertWithMessage("Exception for setting parameters from default radio tuner")
+ .that(thrown).hasMessageThat().contains("Setting parameters is not supported");
}
@Test
public void getParameters_forRadioTuner_throwsException() {
- assertThrows(UnsupportedOperationException.class, () -> {
+ UnsupportedOperationException thrown = assertThrows(UnsupportedOperationException.class,
+ () -> {
DEFAULT_RADIO_TUNER.getParameters(List.of("testKey"));
});
+
+ assertWithMessage("Exception for getting parameters from default radio tuner")
+ .that(thrown).hasMessageThat().contains("Getting parameters is not supported");
}
}
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/ProgramListTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/ProgramListTest.java
index 2b9de18..87f91fa 100644
--- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/ProgramListTest.java
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/ProgramListTest.java
@@ -41,6 +41,8 @@
import android.os.RemoteException;
import android.util.ArraySet;
+import androidx.test.InstrumentationRegistry;
+
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
@@ -54,6 +56,8 @@
@RunWith(MockitoJUnitRunner.class)
public final class ProgramListTest {
+ public final Context mContext = InstrumentationRegistry.getContext();
+
private static final int CREATOR_ARRAY_SIZE = 3;
private static final VerificationWithTimeout CALLBACK_TIMEOUT = timeout(/* millis= */ 500);
@@ -109,8 +113,6 @@
@Mock
private IRadioService mRadioServiceMock;
@Mock
- private Context mContextMock;
- @Mock
private ITuner mTunerMock;
@Mock
private RadioTuner.Callback mTunerCallbackMock;
@@ -477,7 +479,7 @@
}
private void createRadioTuner() throws Exception {
- RadioManager radioManager = new RadioManager(mContextMock, mRadioServiceMock);
+ RadioManager radioManager = new RadioManager(mContext, mRadioServiceMock);
RadioManager.BandConfig band = new RadioManager.FmBandConfig(
new RadioManager.FmBandDescriptor(RadioManager.REGION_ITU_1, RadioManager.BAND_FM,
/* lowerLimit= */ 87500, /* upperLimit= */ 108000, /* spacing= */ 200,
@@ -487,7 +489,7 @@
doAnswer(invocation -> {
mTunerCallback = (ITunerCallback) invocation.getArguments()[3];
return mTunerMock;
- }).when(mRadioServiceMock).openTuner(anyInt(), any(), anyBoolean(), any());
+ }).when(mRadioServiceMock).openTuner(anyInt(), any(), anyBoolean(), any(), anyInt());
mRadioTuner = radioManager.openTuner(/* moduleId= */ 0, band,
/* withAudio= */ true, mTunerCallbackMock, /* handler= */ null);
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioManagerTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioManagerTest.java
index 44aa6d1..03742eb 100644
--- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioManagerTest.java
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/RadioManagerTest.java
@@ -18,14 +18,16 @@
import static com.google.common.truth.Truth.assertWithMessage;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.eq;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.annotation.Nullable;
import android.content.Context;
+import android.content.pm.ApplicationInfo;
import android.hardware.radio.Announcement;
import android.hardware.radio.IAnnouncementListener;
import android.hardware.radio.ICloseHandle;
@@ -34,6 +36,7 @@
import android.hardware.radio.RadioManager;
import android.hardware.radio.RadioMetadata;
import android.hardware.radio.RadioTuner;
+import android.os.Build;
import android.os.Parcel;
import android.os.RemoteException;
import android.util.ArrayMap;
@@ -53,6 +56,8 @@
@RunWith(MockitoJUnitRunner.class)
public final class RadioManagerTest {
+ private static final int TEST_TARGET_SDK_VERSION = Build.VERSION_CODES.CUR_DEVELOPMENT;
+
private static final int REGION = RadioManager.REGION_ITU_2;
private static final int FM_LOWER_LIMIT = 87500;
private static final int FM_UPPER_LIMIT = 108000;
@@ -126,6 +131,7 @@
/* vendorInfo= */ new ArrayMap<>()));
private RadioManager mRadioManager;
+ private final ApplicationInfo mApplicationInfo = new ApplicationInfo();
@Mock
private IRadioService mRadioServiceMock;
@@ -1008,7 +1014,8 @@
mRadioManager.openTuner(moduleId, FM_BAND_CONFIG, withAudio, mCallbackMock,
/* handler= */ null);
- verify(mRadioServiceMock).openTuner(eq(moduleId), eq(FM_BAND_CONFIG), eq(withAudio), any());
+ verify(mRadioServiceMock).openTuner(eq(moduleId), eq(FM_BAND_CONFIG), eq(withAudio), any(),
+ anyInt());
}
@Test
@@ -1103,6 +1110,8 @@
}
private void createRadioManager() throws RemoteException {
+ mApplicationInfo.targetSdkVersion = TEST_TARGET_SDK_VERSION;
+ when(mContextMock.getApplicationInfo()).thenReturn(mApplicationInfo);
when(mRadioServiceMock.listModules()).thenReturn(Arrays.asList(AMFM_PROPERTIES));
when(mRadioServiceMock.addAnnouncementListener(any(), any())).thenReturn(mCloseHandleMock);
diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/TunerAdapterTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/TunerAdapterTest.java
index bdba6a1..d851a7724 100644
--- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/TunerAdapterTest.java
+++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/tests/unittests/TunerAdapterTest.java
@@ -27,6 +27,7 @@
import static org.mockito.Mockito.when;
import android.content.Context;
+import android.content.pm.ApplicationInfo;
import android.graphics.Bitmap;
import android.hardware.radio.IRadioService;
import android.hardware.radio.ITuner;
@@ -35,6 +36,7 @@
import android.hardware.radio.RadioManager;
import android.hardware.radio.RadioMetadata;
import android.hardware.radio.RadioTuner;
+import android.os.Build;
import org.junit.After;
import org.junit.Before;
@@ -51,11 +53,12 @@
@RunWith(MockitoJUnitRunner.class)
public final class TunerAdapterTest {
+ private static final int TEST_TARGET_SDK_VERSION = Build.VERSION_CODES.CUR_DEVELOPMENT;
+
private static final int CALLBACK_TIMEOUT_MS = 30_000;
private static final int AM_LOWER_LIMIT_KHZ = 150;
private static final RadioManager.BandConfig TEST_BAND_CONFIG = createBandConfig();
-
private static final ProgramSelector.Identifier FM_IDENTIFIER =
new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY,
/* value= */ 94300);
@@ -66,6 +69,7 @@
private RadioTuner mRadioTuner;
private ITunerCallback mTunerCallback;
+ private final ApplicationInfo mApplicationInfo = new ApplicationInfo();
@Mock
private IRadioService mRadioServiceMock;
@@ -78,12 +82,14 @@
@Before
public void setUp() throws Exception {
+ mApplicationInfo.targetSdkVersion = TEST_TARGET_SDK_VERSION;
+ when(mContextMock.getApplicationInfo()).thenReturn(mApplicationInfo);
RadioManager radioManager = new RadioManager(mContextMock, mRadioServiceMock);
doAnswer(invocation -> {
mTunerCallback = (ITunerCallback) invocation.getArguments()[3];
return mTunerMock;
- }).when(mRadioServiceMock).openTuner(anyInt(), any(), anyBoolean(), any());
+ }).when(mRadioServiceMock).openTuner(anyInt(), any(), anyBoolean(), any(), anyInt());
doAnswer(invocation -> {
ProgramSelector program = (ProgramSelector) invocation.getArguments()[0];
@@ -92,7 +98,7 @@
throw new IllegalArgumentException();
}
if (program.getPrimaryId().getValue() < AM_LOWER_LIMIT_KHZ) {
- mTunerCallback.onTuneFailed(RadioManager.STATUS_BAD_VALUE, program);
+ mTunerCallback.onTuneFailed(RadioTuner.TUNER_RESULT_INVALID_ARGUMENTS, program);
} else {
mTunerCallback.onCurrentProgramInfoChanged(FM_PROGRAM_INFO);
}
@@ -170,15 +176,30 @@
}
@Test
+ public void scan_forTunerAdapter_succeeds() throws Exception {
+ doAnswer(invocation -> {
+ mTunerCallback.onCurrentProgramInfoChanged(FM_PROGRAM_INFO);
+ return RadioManager.STATUS_OK;
+ }).when(mTunerMock).seek(anyBoolean(), anyBoolean());
+
+ int scanStatus = mRadioTuner.scan(RadioTuner.DIRECTION_DOWN, /* skipSubChannel= */ false);
+
+ verify(mTunerMock).seek(/* directionDown= */ true, /* skipSubChannel= */ false);
+ assertWithMessage("Status for scaning")
+ .that(scanStatus).isEqualTo(RadioManager.STATUS_OK);
+ verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)).onProgramInfoChanged(FM_PROGRAM_INFO);
+ }
+
+ @Test
public void seek_forTunerAdapter_succeeds() throws Exception {
doAnswer(invocation -> {
mTunerCallback.onCurrentProgramInfoChanged(FM_PROGRAM_INFO);
return RadioManager.STATUS_OK;
- }).when(mTunerMock).scan(anyBoolean(), anyBoolean());
+ }).when(mTunerMock).seek(anyBoolean(), anyBoolean());
int scanStatus = mRadioTuner.scan(RadioTuner.DIRECTION_DOWN, /* skipSubChannel= */ false);
- verify(mTunerMock).scan(/* directionDown= */ true, /* skipSubChannel= */ false);
+ verify(mTunerMock).seek(/* directionDown= */ true, /* skipSubChannel= */ false);
assertWithMessage("Status for seeking")
.that(scanStatus).isEqualTo(RadioManager.STATUS_OK);
verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)).onProgramInfoChanged(FM_PROGRAM_INFO);
@@ -187,13 +208,14 @@
@Test
public void seek_forTunerAdapter_invokesOnErrorWhenTimeout() throws Exception {
doAnswer(invocation -> {
- mTunerCallback.onError(RadioTuner.ERROR_SCAN_TIMEOUT);
+ mTunerCallback.onTuneFailed(RadioTuner.TUNER_RESULT_TIMEOUT, FM_SELECTOR);
return RadioManager.STATUS_OK;
- }).when(mTunerMock).scan(anyBoolean(), anyBoolean());
+ }).when(mTunerMock).seek(anyBoolean(), anyBoolean());
mRadioTuner.scan(RadioTuner.DIRECTION_UP, /* skipSubChannel*/ true);
- verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)).onError(RadioTuner.ERROR_SCAN_TIMEOUT);
+ verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS)).onTuneFailed(
+ RadioTuner.TUNER_RESULT_TIMEOUT, FM_SELECTOR);
}
@Test
@@ -224,7 +246,7 @@
mRadioTuner.tune(invalidSelector);
verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS))
- .onTuneFailed(RadioManager.STATUS_BAD_VALUE, invalidSelector);
+ .onTuneFailed(RadioTuner.TUNER_RESULT_INVALID_ARGUMENTS, invalidSelector);
}
@Test
@@ -415,6 +437,17 @@
}
@Test
+ public void onConfigFlagUpdated_forTunerCallbackAdapter() throws Exception {
+ int configFlag = RadioManager.CONFIG_RDS_AF;
+ boolean configFlagValue = true;
+
+ mTunerCallback.onConfigFlagUpdated(configFlag, configFlagValue);
+
+ verify(mCallbackMock, timeout(CALLBACK_TIMEOUT_MS))
+ .onConfigFlagUpdated(configFlag, configFlagValue);
+ }
+
+ @Test
public void onParametersUpdated_forTunerCallbackAdapter() throws Exception {
Map<String, String> parametersExpected = Map.of("ParameterKeyMock", "ParameterValueMock");
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceAidlImplTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceAidlImplTest.java
index a2df426..9803474 100644
--- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceAidlImplTest.java
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceAidlImplTest.java
@@ -23,6 +23,7 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -33,6 +34,7 @@
import android.hardware.radio.ITuner;
import android.hardware.radio.ITunerCallback;
import android.hardware.radio.RadioManager;
+import android.os.Build;
import android.os.IBinder;
import android.os.ServiceManager;
@@ -55,6 +57,7 @@
"android.hardware.broadcastradio.IBroadcastRadio/amfm";
private static final String DAB_SERVICE_NAME =
"android.hardware.broadcastradio.IBroadcastRadio/dab";
+ private static final int TARGET_SDK_VERSION = Build.VERSION_CODES.CUR_DEVELOPMENT;
private IRadioServiceAidlImpl mAidlImpl;
@@ -82,7 +85,7 @@
doNothing().when(mServiceMock).enforcePolicyAccess();
when(mHalMock.listModules()).thenReturn(List.of(mModuleMock));
- when(mHalMock.openSession(anyInt(), any(), anyBoolean(), any()))
+ when(mHalMock.openSession(anyInt(), any(), anyBoolean(), any(), eq(TARGET_SDK_VERSION)))
.thenReturn(mTunerMock);
when(mHalMock.addAnnouncementListener(any(), any())).thenReturn(mICloseHandle);
@@ -114,7 +117,7 @@
@Test
public void openTuner_forAidlImpl() throws Exception {
ITuner tuner = mAidlImpl.openTuner(/* moduleId= */ 0, mBandConfigMock,
- /* withAudio= */ true, mTunerCallbackMock);
+ /* withAudio= */ true, mTunerCallbackMock, TARGET_SDK_VERSION);
assertWithMessage("Tuner opened in AIDL HAL")
.that(tuner).isEqualTo(mTunerMock);
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceHidlImplTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceHidlImplTest.java
index 5ab9435..cfff477 100644
--- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceHidlImplTest.java
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/IRadioServiceHidlImplTest.java
@@ -32,6 +32,7 @@
import android.hardware.radio.ITuner;
import android.hardware.radio.ITunerCallback;
import android.hardware.radio.RadioManager;
+import android.os.Build;
import org.junit.Before;
import org.junit.Test;
@@ -49,6 +50,7 @@
private static final int HAL1_MODULE_ID = 0;
private static final int[] ENABLE_TYPES = new int[]{Announcement.TYPE_TRAFFIC};
+ private static final int TARGET_SDK_VERSION = Build.VERSION_CODES.CUR_DEVELOPMENT;
private IRadioServiceHidlImpl mHidlImpl;
@@ -103,7 +105,7 @@
@Test
public void openTuner_withHal1ModuleId_forHidlImpl() throws Exception {
ITuner tuner = mHidlImpl.openTuner(HAL1_MODULE_ID, mBandConfigMock,
- /* withAudio= */ true, mTunerCallbackMock);
+ /* withAudio= */ true, mTunerCallbackMock, TARGET_SDK_VERSION);
assertWithMessage("Tuner opened in HAL 1")
.that(tuner).isEqualTo(mHal1TunerMock);
@@ -112,7 +114,7 @@
@Test
public void openTuner_withHal2ModuleId_forHidlImpl() throws Exception {
ITuner tuner = mHidlImpl.openTuner(HAL1_MODULE_ID + 1, mBandConfigMock,
- /* withAudio= */ true, mTunerCallbackMock);
+ /* withAudio= */ true, mTunerCallbackMock, TARGET_SDK_VERSION);
assertWithMessage("Tuner opened in HAL 2")
.that(tuner).isEqualTo(mHal2TunerMock);
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImplTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImplTest.java
index 1cc0a985..f404082 100644
--- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImplTest.java
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImplTest.java
@@ -36,6 +36,7 @@
import android.hardware.radio.ITunerCallback;
import android.hardware.radio.RadioManager;
import android.hardware.radio.RadioTuner;
+import android.os.Build;
import android.os.IBinder;
import android.os.IServiceCallback;
import android.os.RemoteException;
@@ -54,6 +55,8 @@
public final class BroadcastRadioServiceImplTest extends ExtendedRadioMockitoTestCase {
+ private static final int TARGET_SDK_VERSION = Build.VERSION_CODES.CUR_DEVELOPMENT;
+
private static final int FM_RADIO_MODULE_ID = 0;
private static final int DAB_RADIO_MODULE_ID = 1;
private static final ArrayList<String> SERVICE_LIST =
@@ -137,7 +140,8 @@
createBroadcastRadioService();
ITuner session = mBroadcastRadioService.openSession(FM_RADIO_MODULE_ID,
- /* legacyConfig= */ null, /* withAudio= */ true, mTunerCallbackMock);
+ /* legacyConfig= */ null, /* withAudio= */ true, mTunerCallbackMock,
+ TARGET_SDK_VERSION);
assertWithMessage("Session opened in FM radio module")
.that(session).isEqualTo(mFmTunerSessionMock);
@@ -148,7 +152,8 @@
createBroadcastRadioService();
ITuner session = mBroadcastRadioService.openSession(DAB_RADIO_MODULE_ID + 1,
- /* legacyConfig= */ null, /* withAudio= */ true, mTunerCallbackMock);
+ /* legacyConfig= */ null, /* withAudio= */ true, mTunerCallbackMock,
+ TARGET_SDK_VERSION);
assertWithMessage("Session opened with id not found").that(session).isNull();
}
@@ -160,7 +165,8 @@
IllegalStateException thrown = assertThrows(IllegalStateException.class,
() -> mBroadcastRadioService.openSession(FM_RADIO_MODULE_ID,
- /* legacyConfig= */ null, /* withAudio= */ true, mTunerCallbackMock));
+ /* legacyConfig= */ null, /* withAudio= */ true, mTunerCallbackMock,
+ TARGET_SDK_VERSION));
assertWithMessage("Exception for opening session by non-current user")
.that(thrown).hasMessageThat().contains("Cannot open session for non-current user");
@@ -228,7 +234,7 @@
return null;
}).when(mFmBinderMock).linkToDeath(any(), anyInt());
- when(mFmRadioModuleMock.openSession(eq(mTunerCallbackMock)))
+ when(mFmRadioModuleMock.openSession(eq(mTunerCallbackMock), eq(TARGET_SDK_VERSION)))
.thenReturn(mFmTunerSessionMock);
}
}
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsResultTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsResultTest.java
new file mode 100644
index 0000000..df3ddfd
--- /dev/null
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsResultTest.java
@@ -0,0 +1,65 @@
+/*
+ * 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.broadcastradio.aidl;
+
+import android.hardware.broadcastradio.Result;
+import android.hardware.radio.RadioTuner;
+
+import com.google.common.truth.Expect;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.Arrays;
+import java.util.List;
+
+@RunWith(Parameterized.class)
+public final class ConversionUtilsResultTest {
+
+ private final int mHalResult;
+ private final int mTunerResult;
+
+ @Rule
+ public final Expect expect = Expect.create();
+
+ public ConversionUtilsResultTest(int halResult, int tunerResult) {
+ this.mHalResult = halResult;
+ this.mTunerResult = tunerResult;
+ }
+
+ @Parameterized.Parameters
+ public static List<Object[]> inputParameters() {
+ return Arrays.asList(new Object[][]{
+ {Result.OK, RadioTuner.TUNER_RESULT_OK},
+ {Result.INTERNAL_ERROR, RadioTuner.TUNER_RESULT_INTERNAL_ERROR},
+ {Result.INVALID_ARGUMENTS, RadioTuner.TUNER_RESULT_INVALID_ARGUMENTS},
+ {Result.INVALID_STATE, RadioTuner.TUNER_RESULT_INVALID_STATE},
+ {Result.NOT_SUPPORTED, RadioTuner.TUNER_RESULT_NOT_SUPPORTED},
+ {Result.TIMEOUT, RadioTuner.TUNER_RESULT_TIMEOUT},
+ {Result.UNKNOWN_ERROR, RadioTuner.TUNER_RESULT_UNKNOWN_ERROR}
+ });
+ }
+
+ @Test
+ public void halResultToTunerResult() {
+ expect.withMessage("Tuner result converted from AIDL HAL result %s", mHalResult)
+ .that(ConversionUtils.halResultToTunerResult(mHalResult))
+ .isEqualTo(mTunerResult);
+ }
+}
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsTest.java
index 3119554..a1cebb6 100644
--- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsTest.java
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsTest.java
@@ -25,6 +25,7 @@
import android.hardware.radio.Announcement;
import android.hardware.radio.ProgramSelector;
import android.hardware.radio.RadioManager;
+import android.os.Build;
import com.google.common.truth.Expect;
@@ -69,6 +70,18 @@
public final Expect expect = Expect.create();
@Test
+ public void isAtLeastU_withTSdkVersion_returnsFalse() {
+ expect.withMessage("Target SDK version of T")
+ .that(ConversionUtils.isAtLeastU(Build.VERSION_CODES.TIRAMISU)).isFalse();
+ }
+
+ @Test
+ public void isAtLeastU_withCurrentSdkVersion_returnsTrue() {
+ expect.withMessage("Target SDK version of U")
+ .that(ConversionUtils.isAtLeastU(Build.VERSION_CODES.CUR_DEVELOPMENT)).isTrue();
+ }
+
+ @Test
public void propertiesFromHalProperties_idsMatch() {
expect.withMessage("Properties id")
.that(MODULE_PROPERTIES.getId()).isEqualTo(TEST_ID);
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/TunerSessionTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/TunerSessionTest.java
index 993ca77..c5c6349 100644
--- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/TunerSessionTest.java
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/TunerSessionTest.java
@@ -42,6 +42,7 @@
import android.hardware.radio.ProgramSelector;
import android.hardware.radio.RadioManager;
import android.hardware.radio.RadioTuner;
+import android.os.Build;
import android.os.ServiceSpecificException;
import android.util.ArrayMap;
import android.util.ArraySet;
@@ -65,6 +66,7 @@
*/
public final class TunerSessionTest extends ExtendedRadioMockitoTestCase {
+ private static final int TARGET_SDK_VERSION = Build.VERSION_CODES.CUR_DEVELOPMENT;
private static final VerificationWithTimeout CALLBACK_TIMEOUT =
timeout(/* millis= */ 200);
private static final int SIGNAL_QUALITY = 1;
@@ -299,6 +301,18 @@
}
@Test
+ public void tune_withLowerSdkVersion() throws Exception {
+ openAidlClients(/* numClients= */ 1, Build.VERSION_CODES.TIRAMISU);
+ ProgramSelector initialSel = AidlTestUtils.makeFmSelector(AM_FM_FREQUENCY_LIST[1]);
+ RadioManager.ProgramInfo tuneInfo =
+ AidlTestUtils.makeProgramInfo(initialSel, SIGNAL_QUALITY);
+
+ mTunerSessions[0].tune(initialSel);
+
+ verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT).onCurrentProgramInfoChanged(tuneInfo);
+ }
+
+ @Test
public void tune_withMultipleSessions() throws Exception {
int numSessions = 3;
openAidlClients(numSessions);
@@ -377,49 +391,49 @@
}
@Test
- public void scan_withDirectionUp() throws Exception {
+ public void seek_withDirectionUp() throws Exception {
long initFreq = AM_FM_FREQUENCY_LIST[2];
ProgramSelector initialSel = AidlTestUtils.makeFmSelector(initFreq);
- RadioManager.ProgramInfo scanUpInfo = AidlTestUtils.makeProgramInfo(
+ RadioManager.ProgramInfo seekUpInfo = AidlTestUtils.makeProgramInfo(
AidlTestUtils.makeFmSelector(getSeekFrequency(initFreq, /* seekDown= */ false)),
SIGNAL_QUALITY);
openAidlClients(/* numClients= */ 1);
mHalCurrentInfo = AidlTestUtils.makeHalProgramInfo(
ConversionUtils.programSelectorToHalProgramSelector(initialSel), SIGNAL_QUALITY);
- mTunerSessions[0].scan(/* directionDown= */ false, /* skipSubChannel= */ false);
+ mTunerSessions[0].seek(/* directionDown= */ false, /* skipSubChannel= */ false);
verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT)
- .onCurrentProgramInfoChanged(scanUpInfo);
+ .onCurrentProgramInfoChanged(seekUpInfo);
}
@Test
- public void scan_callsOnTuneFailedWhenTimeout() throws Exception {
+ public void seek_callsOnTuneFailedWhenTimeout() throws Exception {
int numSessions = 2;
openAidlClients(numSessions);
- mTunerSessions[0].scan(/* directionDown= */ false, /* skipSubChannel= */ false);
+ mTunerSessions[0].seek(/* directionDown= */ false, /* skipSubChannel= */ false);
for (int index = 0; index < numSessions; index++) {
verify(mAidlTunerCallbackMocks[index], CALLBACK_TIMEOUT)
- .onTuneFailed(eq(Result.TIMEOUT), any());
+ .onTuneFailed(eq(RadioTuner.TUNER_RESULT_TIMEOUT), any());
}
}
@Test
- public void scan_withDirectionDown() throws Exception {
+ public void seek_withDirectionDown() throws Exception {
long initFreq = AM_FM_FREQUENCY_LIST[2];
ProgramSelector initialSel = AidlTestUtils.makeFmSelector(initFreq);
- RadioManager.ProgramInfo scanUpInfo = AidlTestUtils.makeProgramInfo(
+ RadioManager.ProgramInfo seekUpInfo = AidlTestUtils.makeProgramInfo(
AidlTestUtils.makeFmSelector(getSeekFrequency(initFreq, /* seekDown= */ true)),
SIGNAL_QUALITY);
openAidlClients(/* numClients= */ 1);
mHalCurrentInfo = AidlTestUtils.makeHalProgramInfo(
ConversionUtils.programSelectorToHalProgramSelector(initialSel), SIGNAL_QUALITY);
- mTunerSessions[0].scan(/* directionDown= */ true, /* skipSubChannel= */ false);
+ mTunerSessions[0].seek(/* directionDown= */ true, /* skipSubChannel= */ false);
verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT)
- .onCurrentProgramInfoChanged(scanUpInfo);
+ .onCurrentProgramInfoChanged(seekUpInfo);
}
@Test
@@ -585,7 +599,7 @@
}
@Test
- public void onConfigFlagUpdated_forTunerCallback() throws Exception {
+ public void onAntennaStateChange_forTunerCallback() throws Exception {
int numSessions = 3;
openAidlClients(numSessions);
@@ -598,6 +612,21 @@
}
@Test
+ public void onConfigFlagUpdated_forTunerCallback() throws Exception {
+ int numSessions = 3;
+ openAidlClients(numSessions);
+ int flag = UNSUPPORTED_CONFIG_FLAG + 1;
+ boolean configFlagValue = true;
+
+ mHalTunerCallback.onConfigFlagUpdated(flag, configFlagValue);
+
+ for (int index = 0; index < numSessions; index++) {
+ verify(mAidlTunerCallbackMocks[index], CALLBACK_TIMEOUT)
+ .onConfigFlagUpdated(flag, configFlagValue);
+ }
+ }
+
+ @Test
public void onParametersUpdated_forTunerCallback() throws Exception {
int numSessions = 3;
openAidlClients(numSessions);
@@ -612,13 +641,17 @@
.onParametersUpdated(parametersExpected);
}
}
-
private void openAidlClients(int numClients) throws Exception {
+ openAidlClients(numClients, TARGET_SDK_VERSION);
+ }
+
+ private void openAidlClients(int numClients, int targetSdkVersion) throws Exception {
mAidlTunerCallbackMocks = new android.hardware.radio.ITunerCallback[numClients];
mTunerSessions = new TunerSession[numClients];
for (int index = 0; index < numClients; index++) {
mAidlTunerCallbackMocks[index] = mock(android.hardware.radio.ITunerCallback.class);
- mTunerSessions[index] = mRadioModule.openSession(mAidlTunerCallbackMocks[index]);
+ mTunerSessions[index] = mRadioModule.openSession(mAidlTunerCallbackMocks[index],
+ targetSdkVersion);
}
}
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/ConvertResultTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/ConvertResultTest.java
new file mode 100644
index 0000000..d106de9
--- /dev/null
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/ConvertResultTest.java
@@ -0,0 +1,66 @@
+/*
+ * 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.broadcastradio.hal2;
+
+import android.hardware.broadcastradio.V2_0.Result;
+import android.hardware.radio.RadioTuner;
+
+import com.google.common.truth.Expect;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.Arrays;
+import java.util.List;
+
+@RunWith(Parameterized.class)
+public final class ConvertResultTest {
+
+ private final int mHalResult;
+ private final int mTunerResult;
+
+ @Rule
+ public final Expect expect = Expect.create();
+
+ public ConvertResultTest(int halResult, int tunerResult) {
+ this.mHalResult = halResult;
+ this.mTunerResult = tunerResult;
+ }
+
+ @Parameterized.Parameters
+ public static List<Object[]> inputParameters() {
+ return Arrays.asList(new Object[][]{
+ {Result.OK, RadioTuner.TUNER_RESULT_OK},
+ {Result.INTERNAL_ERROR, RadioTuner.TUNER_RESULT_INTERNAL_ERROR},
+ {Result.INVALID_ARGUMENTS, RadioTuner.TUNER_RESULT_INVALID_ARGUMENTS},
+ {Result.INVALID_STATE, RadioTuner.TUNER_RESULT_INVALID_STATE},
+ {Result.NOT_SUPPORTED, RadioTuner.TUNER_RESULT_NOT_SUPPORTED},
+ {Result.TIMEOUT, RadioTuner.TUNER_RESULT_TIMEOUT},
+ {Result.UNKNOWN_ERROR, RadioTuner.TUNER_RESULT_UNKNOWN_ERROR}
+ });
+ }
+
+ @Test
+ public void halResultToTunerResult() {
+ expect.withMessage("Tuner result converted from HAL 2.0 result %s",
+ Result.toString(mHalResult))
+ .that(Convert.halResultToTunerResult(mHalResult))
+ .isEqualTo(mTunerResult);
+ }
+}
diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/TunerSessionHidlTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/TunerSessionHidlTest.java
index ff988a2..db16c03 100644
--- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/TunerSessionHidlTest.java
+++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/hal2/TunerSessionHidlTest.java
@@ -384,49 +384,49 @@
}
@Test
- public void scan_withDirectionUp() throws Exception {
+ public void seek_withDirectionUp() throws Exception {
long initFreq = AM_FM_FREQUENCY_LIST[2];
ProgramSelector initialSel = TestUtils.makeFmSelector(initFreq);
- RadioManager.ProgramInfo scanUpInfo = TestUtils.makeProgramInfo(
+ RadioManager.ProgramInfo seekUpInfo = TestUtils.makeProgramInfo(
TestUtils.makeFmSelector(getSeekFrequency(initFreq, /* seekDown= */ false)),
SIGNAL_QUALITY);
openAidlClients(/* numClients= */ 1);
mHalCurrentInfo = TestUtils.makeHalProgramInfo(
Convert.programSelectorToHal(initialSel), SIGNAL_QUALITY);
- mTunerSessions[0].scan(/* directionDown= */ false, /* skipSubChannel= */ false);
+ mTunerSessions[0].seek(/* directionDown= */ false, /* skipSubChannel= */ false);
verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT)
- .onCurrentProgramInfoChanged(scanUpInfo);
+ .onCurrentProgramInfoChanged(seekUpInfo);
}
@Test
- public void scan_callsOnTuneFailedWhenTimeout() throws Exception {
+ public void seek_callsOnTuneFailedWhenTimeout() throws Exception {
int numSessions = 2;
openAidlClients(numSessions);
- mTunerSessions[0].scan(/* directionDown= */ false, /* skipSubChannel= */ false);
+ mTunerSessions[0].seek(/* directionDown= */ false, /* skipSubChannel= */ false);
for (int index = 0; index < numSessions; index++) {
verify(mAidlTunerCallbackMocks[index], CALLBACK_TIMEOUT)
- .onTuneFailed(eq(Result.TIMEOUT), any());
+ .onTuneFailed(eq(RadioTuner.TUNER_RESULT_TIMEOUT), any());
}
}
@Test
- public void scan_withDirectionDown() throws Exception {
+ public void seek_withDirectionDown() throws Exception {
long initFreq = AM_FM_FREQUENCY_LIST[2];
ProgramSelector initialSel = TestUtils.makeFmSelector(initFreq);
- RadioManager.ProgramInfo scanUpInfo = TestUtils.makeProgramInfo(
+ RadioManager.ProgramInfo seekUpInfo = TestUtils.makeProgramInfo(
TestUtils.makeFmSelector(getSeekFrequency(initFreq, /* seekDown= */ true)),
SIGNAL_QUALITY);
openAidlClients(/* numClients= */ 1);
mHalCurrentInfo = TestUtils.makeHalProgramInfo(
Convert.programSelectorToHal(initialSel), SIGNAL_QUALITY);
- mTunerSessions[0].scan(/* directionDown= */ true, /* skipSubChannel= */ false);
+ mTunerSessions[0].seek(/* directionDown= */ true, /* skipSubChannel= */ false);
verify(mAidlTunerCallbackMocks[0], CALLBACK_TIMEOUT)
- .onCurrentProgramInfoChanged(scanUpInfo);
+ .onCurrentProgramInfoChanged(seekUpInfo);
}
@Test
diff --git a/core/tests/coretests/src/com/android/internal/jank/InteractionJankMonitorTest.java b/core/tests/coretests/src/com/android/internal/jank/InteractionJankMonitorTest.java
index 7674135..8f83461 100644
--- a/core/tests/coretests/src/com/android/internal/jank/InteractionJankMonitorTest.java
+++ b/core/tests/coretests/src/com/android/internal/jank/InteractionJankMonitorTest.java
@@ -20,7 +20,6 @@
import static com.android.internal.jank.FrameTracker.REASON_CANCEL_TIMEOUT;
import static com.android.internal.jank.FrameTracker.REASON_END_NORMAL;
-import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_LAUNCH_CAMERA;
import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_ADD;
import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_APP_START;
import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_HEADS_UP_APPEAR;
@@ -32,7 +31,6 @@
import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_ROW_EXPAND;
import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_ROW_SWIPE;
import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_SCROLL_FLING;
-import static com.android.internal.jank.InteractionJankMonitor.CUJ_SPLIT_SCREEN_RESIZE;
import static com.android.internal.jank.InteractionJankMonitor.CUJ_TO_STATSD_INTERACTION_TYPE;
import static com.android.internal.jank.InteractionJankMonitor.MAX_LENGTH_OF_CUJ_NAME;
import static com.android.internal.jank.InteractionJankMonitor.getNameOfCuj;
@@ -92,7 +90,6 @@
public class InteractionJankMonitorTest {
private static final String CUJ_POSTFIX = "";
private static final SparseArray<String> ENUM_NAME_EXCEPTION_MAP = new SparseArray<>();
- private static final SparseArray<String> CUJ_NAME_EXCEPTION_MAP = new SparseArray<>();
private static final String ENUM_NAME_PREFIX =
"UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__";
@@ -129,22 +126,6 @@
CUJ_NOTIFICATION_SHADE_ROW_SWIPE, getEnumName("SHADE_ROW_SWIPE"));
ENUM_NAME_EXCEPTION_MAP.put(
CUJ_NOTIFICATION_SHADE_SCROLL_FLING, getEnumName("SHADE_SCROLL_FLING"));
-
- CUJ_NAME_EXCEPTION_MAP.put(
- CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE, "CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE");
- CUJ_NAME_EXCEPTION_MAP.put(
- CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE,
- "CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE");
- CUJ_NAME_EXCEPTION_MAP.put(
- CUJ_NOTIFICATION_SHADE_QS_SCROLL_SWIPE, "CUJ_NOTIFICATION_SHADE_QS_SCROLL_SWIPE");
- CUJ_NAME_EXCEPTION_MAP.put(
- CUJ_NOTIFICATION_SHADE_ROW_EXPAND, "CUJ_NOTIFICATION_SHADE_ROW_EXPAND");
- CUJ_NAME_EXCEPTION_MAP.put(
- CUJ_NOTIFICATION_SHADE_ROW_SWIPE, "CUJ_NOTIFICATION_SHADE_ROW_SWIPE");
- CUJ_NAME_EXCEPTION_MAP.put(
- CUJ_NOTIFICATION_SHADE_SCROLL_FLING, "CUJ_NOTIFICATION_SHADE_SCROLL_FLING");
- CUJ_NAME_EXCEPTION_MAP.put(CUJ_LOCKSCREEN_LAUNCH_CAMERA, "CUJ_LOCKSCREEN_LAUNCH_CAMERA");
- CUJ_NAME_EXCEPTION_MAP.put(CUJ_SPLIT_SCREEN_RESIZE, "CUJ_SPLIT_SCREEN_RESIZE");
}
private static String getEnumName(String name) {
@@ -272,9 +253,8 @@
: formatSimple("%s%s", ENUM_NAME_PREFIX, cujName.substring(4));
final int enumKey = CUJ_TO_STATSD_INTERACTION_TYPE[cuj];
final String enumName = enumsMap.get(enumKey);
- final String expectedNameOfCuj = CUJ_NAME_EXCEPTION_MAP.contains(cuj)
- ? CUJ_NAME_EXCEPTION_MAP.get(cuj)
- : formatSimple("CUJ_%s", getNameOfCuj(cuj));
+ final String expectedNameOfCuj = formatSimple("CUJ_%s", getNameOfCuj(cuj));
+
mExpect
.withMessage(formatSimple(
"%s (%d) not matches %s (%d)", cujName, cuj, enumName, enumKey))
@@ -323,7 +303,7 @@
// Since the length of the cuj name is tested in another test, no need to test it here.
// Too long postfix case, should trim the postfix and keep the cuj name completed.
final String expectedTrimmedName = formatSimple("J<%s::%s>", cujName,
- "ThisIsTheCujTagThisIsTheCujTagThisIsTheCujTagThisIsTheCujTagThisIsTheCujTagT...");
+ "ThisIsTheCujTagThisIsTheCujTagThisIsTheCujTagThisIsTheCujTagThi...");
Session longPostfix = new Session(cujType, tooLongTag);
assertThat(longPostfix.getName()).isEqualTo(expectedTrimmedName);
}
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 4679a9e..0b70199 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 @@
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.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 @@
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 73f25b1..1ab5e4b 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",
@@ -619,6 +625,12 @@
"group": "WM_DEBUG_CONFIGURATION",
"at": "com\/android\/server\/wm\/ActivityStarter.java"
},
+ "-1484988952": {
+ "message": "Creating Pending Multiwindow Fullscreen Request: %s",
+ "level": "VERBOSE",
+ "group": "WM_DEBUG_WINDOW_TRANSITIONS",
+ "at": "com\/android\/server\/wm\/ActivityClientController.java"
+ },
"-1483435730": {
"message": "InsetsSource setWin %s for type %s",
"level": "DEBUG",
@@ -709,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/data/keyboards/Generic.kl b/data/keyboards/Generic.kl
index a43e225..0f8616d 100644
--- a/data/keyboards/Generic.kl
+++ b/data/keyboards/Generic.kl
@@ -139,7 +139,7 @@
key 117 NUMPAD_EQUALS
# key 118 "KEY_KPPLUSMINUS"
key 119 BREAK
-# key 120 (undefined)
+key 120 RECENT_APPS
key 121 NUMPAD_COMMA
key 122 KANA
key 123 EISU
diff --git a/identity/java/android/security/identity/AuthenticationKeyMetadata.java b/identity/java/android/security/identity/AuthenticationKeyMetadata.java
index d4c28f8..c6abc22 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 @@
/**
* 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/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
index 5afb1d1..5400164 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -431,12 +431,8 @@
if (container != null) {
// Cleanup if the TaskFragment vanished is not requested by the organizer.
removeContainer(container);
- // Make sure the top container is updated.
- final TaskFragmentContainer newTopContainer = getTopActiveContainer(
- container.getTaskId());
- if (newTopContainer != null) {
- updateContainer(wct, newTopContainer);
- }
+ // Make sure the containers in the Task are up-to-date.
+ updateContainersInTaskIfVisible(wct, container.getTaskId());
}
cleanupTaskFragment(taskFragmentInfo.getFragmentToken());
}
@@ -476,6 +472,13 @@
updateContainersInTask(wct, taskContainer);
}
+ void updateContainersInTaskIfVisible(@NonNull WindowContainerTransaction wct, int taskId) {
+ final TaskContainer taskContainer = getTaskContainer(taskId);
+ if (taskContainer != null && taskContainer.isVisible()) {
+ updateContainersInTask(wct, taskContainer);
+ }
+ }
+
private void updateContainersInTask(@NonNull WindowContainerTransaction wct,
@NonNull TaskContainer taskContainer) {
// Update all TaskFragments in the Task. Make a copy of the list since some may be
@@ -1328,9 +1331,6 @@
void removeContainer(@NonNull TaskFragmentContainer container) {
// Remove all split containers that included this one
final TaskContainer taskContainer = container.getTaskContainer();
- if (taskContainer == null) {
- return;
- }
taskContainer.mContainers.remove(container);
// Marked as a pending removal which will be removed after it is actually removed on the
// server side (#onTaskFragmentVanished).
@@ -1523,14 +1523,8 @@
}
final TaskFragmentContainer container = getContainerWithActivity(activity);
- // Don't launch placeholder if the container is occluded.
- if (container != null && container != getTopActiveContainer(container.getTaskId())) {
- return false;
- }
-
- final SplitContainer splitContainer = getActiveSplitForContainer(container);
- if (splitContainer != null && container.equals(splitContainer.getPrimaryContainer())) {
- // Don't launch placeholder in primary split container
+ if (container != null && !allowLaunchPlaceholder(container)) {
+ // We don't allow activity in this TaskFragment to launch placeholder.
return false;
}
@@ -1558,6 +1552,32 @@
return true;
}
+ /** Whether or not to allow activity in this container to launch placeholder. */
+ @GuardedBy("mLock")
+ private boolean allowLaunchPlaceholder(@NonNull TaskFragmentContainer container) {
+ final TaskFragmentContainer topContainer = getTopActiveContainer(container.getTaskId());
+ if (container != topContainer) {
+ // The container is not the top most.
+ if (!container.isVisible()) {
+ // In case the container is visible (the one on top may be transparent), we may
+ // still want to launch placeholder even if it is not the top most.
+ return false;
+ }
+ if (topContainer.isWaitingActivityAppear()) {
+ // When the top container appeared info is not sent by the server yet, the visible
+ // check above may not be reliable.
+ return false;
+ }
+ }
+
+ final SplitContainer splitContainer = getActiveSplitForContainer(container);
+ if (splitContainer != null && container.equals(splitContainer.getPrimaryContainer())) {
+ // Don't launch placeholder for primary split container.
+ return false;
+ }
+ return true;
+ }
+
/**
* Gets the activity options for starting the placeholder activity. In case the placeholder is
* launched when the Task is in the background, we don't want to bring the Task to the front.
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
index 47253d3..a432e2b 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
@@ -154,12 +154,8 @@
void cleanupContainer(@NonNull WindowContainerTransaction wct,
@NonNull TaskFragmentContainer container, boolean shouldFinishDependent) {
container.finish(shouldFinishDependent, this, wct, mController);
-
- final TaskFragmentContainer newTopContainer = mController.getTopActiveContainer(
- container.getTaskId());
- if (newTopContainer != null) {
- mController.updateContainer(wct, newTopContainer);
- }
+ // Make sure the containers in the Task is up-to-date.
+ mController.updateContainersInTaskIfVisible(wct, container.getTaskId());
}
/**
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
index fcf0ac7..8240874 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
@@ -184,6 +184,11 @@
return allActivities;
}
+ /** Whether this TaskFragment is visible. */
+ boolean isVisible() {
+ return mInfo != null && mInfo.isVisible();
+ }
+
/** Whether the TaskFragment is in an intermediate state waiting for the server update.*/
boolean isInIntermediateState() {
if (mInfo == null) {
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java
index bc03e4e..2f92a57 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java
@@ -163,11 +163,17 @@
/** Creates a mock TaskFragmentInfo for the given TaskFragment. */
static TaskFragmentInfo createMockTaskFragmentInfo(@NonNull TaskFragmentContainer container,
@NonNull Activity activity) {
+ return createMockTaskFragmentInfo(container, activity, true /* isVisible */);
+ }
+
+ /** Creates a mock TaskFragmentInfo for the given TaskFragment. */
+ static TaskFragmentInfo createMockTaskFragmentInfo(@NonNull TaskFragmentContainer container,
+ @NonNull Activity activity, boolean isVisible) {
return new TaskFragmentInfo(container.getTaskFragmentToken(),
mock(WindowContainerToken.class),
new Configuration(),
1,
- true /* isVisible */,
+ isVisible,
Collections.singletonList(activity.getActivityToken()),
new Point(),
false /* isTaskClearedForReuse */,
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
index 6725dfd..3cc31f9 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
@@ -1254,6 +1254,68 @@
verify(mEmbeddingCallback).accept(any());
}
+ @Test
+ public void testLaunchPlaceholderIfNecessary_nonEmbeddedActivity() {
+ // Launch placeholder for non embedded activity.
+ setupPlaceholderRule(mActivity);
+ mTransactionManager.startNewTransaction();
+ mSplitController.launchPlaceholderIfNecessary(mTransaction, mActivity,
+ true /* isOnCreated */);
+
+ verify(mTransaction).startActivityInTaskFragment(any(), any(), eq(PLACEHOLDER_INTENT),
+ any());
+ }
+
+ @Test
+ public void testLaunchPlaceholderIfNecessary_embeddedInTopTaskFragment() {
+ // Launch placeholder for activity in top TaskFragment.
+ setupPlaceholderRule(mActivity);
+ mTransactionManager.startNewTransaction();
+ final TaskFragmentContainer container = mSplitController.newContainer(mActivity, TASK_ID);
+ mSplitController.launchPlaceholderIfNecessary(mTransaction, mActivity,
+ true /* isOnCreated */);
+
+ assertTrue(container.hasActivity(mActivity.getActivityToken()));
+ verify(mTransaction).startActivityInTaskFragment(any(), any(), eq(PLACEHOLDER_INTENT),
+ any());
+ }
+
+ @Test
+ public void testLaunchPlaceholderIfNecessary_embeddedBelowTaskFragment() {
+ // Do not launch placeholder for invisible activity below the top TaskFragment.
+ setupPlaceholderRule(mActivity);
+ mTransactionManager.startNewTransaction();
+ final TaskFragmentContainer bottomTf = mSplitController.newContainer(mActivity, TASK_ID);
+ final TaskFragmentContainer topTf = mSplitController.newContainer(new Intent(), mActivity,
+ TASK_ID);
+ bottomTf.setInfo(mTransaction, createMockTaskFragmentInfo(bottomTf, mActivity,
+ false /* isVisible */));
+ topTf.setInfo(mTransaction, createMockTaskFragmentInfo(topTf, createMockActivity()));
+ assertFalse(bottomTf.isVisible());
+ mSplitController.launchPlaceholderIfNecessary(mTransaction, mActivity,
+ true /* isOnCreated */);
+
+ verify(mTransaction, never()).startActivityInTaskFragment(any(), any(), any(), any());
+ }
+
+ @Test
+ public void testLaunchPlaceholderIfNecessary_embeddedBelowTransparentTaskFragment() {
+ // Launch placeholder for visible activity below the top TaskFragment.
+ setupPlaceholderRule(mActivity);
+ mTransactionManager.startNewTransaction();
+ final TaskFragmentContainer bottomTf = mSplitController.newContainer(mActivity, TASK_ID);
+ final TaskFragmentContainer topTf = mSplitController.newContainer(new Intent(), mActivity,
+ TASK_ID);
+ bottomTf.setInfo(mTransaction, createMockTaskFragmentInfo(bottomTf, mActivity,
+ true /* isVisible */));
+ topTf.setInfo(mTransaction, createMockTaskFragmentInfo(topTf, createMockActivity()));
+ assertTrue(bottomTf.isVisible());
+ mSplitController.launchPlaceholderIfNecessary(mTransaction, mActivity,
+ true /* isOnCreated */);
+
+ verify(mTransaction).startActivityInTaskFragment(any(), any(), any(), any());
+ }
+
/** Creates a mock activity in the organizer process. */
private Activity createMockActivity() {
return createMockActivity(TASK_ID);
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
index 5c3ba72..9877236 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
@@ -500,6 +500,29 @@
assertEquals(2, taskContainer.indexOf(tf1));
}
+ @Test
+ public void testIsVisible() {
+ final TaskContainer taskContainer = createTestTaskContainer();
+ final TaskFragmentContainer container = new TaskFragmentContainer(
+ null /* pendingAppearedActivity */, new Intent(), taskContainer, mController,
+ null /* pairedPrimaryTaskFragment */);
+
+ // Not visible when there is not appeared.
+ assertFalse(container.isVisible());
+
+ // Respect info.isVisible.
+ TaskFragmentInfo info = createMockTaskFragmentInfo(container, mActivity,
+ true /* isVisible */);
+ container.setInfo(mTransaction, info);
+
+ assertTrue(container.isVisible());
+
+ info = createMockTaskFragmentInfo(container, mActivity, false /* isVisible */);
+ container.setInfo(mTransaction, info);
+
+ assertFalse(container.isVisible());
+ }
+
/** Creates a mock activity in the organizer process. */
private Activity createMockActivity() {
final Activity activity = mock(Activity.class);
diff --git a/libs/WindowManager/Shell/AndroidManifest.xml b/libs/WindowManager/Shell/AndroidManifest.xml
index 8881be7..36d3313 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 70755e6..dcce4698 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 c4dbd39..b2ac85b 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-hi/strings.xml b/libs/WindowManager/Shell/res/values-hi/strings.xml
index 1a7cf3e..9a926d8 100644
--- a/libs/WindowManager/Shell/res/values-hi/strings.xml
+++ b/libs/WindowManager/Shell/res/values-hi/strings.xml
@@ -34,8 +34,7 @@
<string name="accessibility_action_pip_unstash" msgid="7467499339610437646">"दिखाएं"</string>
<string name="dock_forced_resizable" msgid="1749750436092293116">"ऐप्लिकेशन शायद स्प्लिट स्क्रीन मोड में काम न करे."</string>
<string name="dock_non_resizeble_failed_to_dock_text" msgid="7408396418008948957">"ऐप विभाजित स्क्रीन का समर्थन नहीं करता है."</string>
- <!-- no translation found for dock_multi_instances_not_supported_text (5242868470666346929) -->
- <skip />
+ <string name="dock_multi_instances_not_supported_text" msgid="5242868470666346929">"इस ऐप्लिकेशन को सिर्फ़ एक विंडो में खोला जा सकता है."</string>
<string name="forced_resizable_secondary_display" msgid="1768046938673582671">"हो सकता है कि ऐप प्राइमरी (मुख्य) डिस्प्ले के अलावा बाकी दूसरे डिस्प्ले पर काम न करे."</string>
<string name="activity_launch_on_secondary_display_failed_text" msgid="4226485344988071769">"प्राइमरी (मुख्य) डिस्प्ले के अलावा बाकी दूसरे डिस्प्ले पर ऐप लॉन्च नहीं किया जा सकता."</string>
<string name="accessibility_divider" msgid="703810061635792791">"विभाजित स्क्रीन विभाजक"</string>
diff --git a/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml b/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml
index 9833a88..0b61d7a 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/TvWindowMenuActionButton.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TvWindowMenuActionButton.java
index 39b0b55..8ba785a 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 @@
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 @@
/**
* 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 @@
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 @@
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 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 @@
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 @@
@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 8022e9b..b144d22 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 @@
PipParamsChangedForwarder pipParamsChangedForwarder,
DisplayController displayController,
WindowManagerShellWrapper windowManagerShellWrapper,
+ @ShellMainThread Handler mainHandler, // needed for registerReceiverForAllUsers()
@ShellMainThread ShellExecutor mainExecutor) {
return Optional.of(
TvPipController.create(
@@ -100,6 +101,7 @@
pipParamsChangedForwarder,
displayController,
windowManagerShellWrapper,
+ mainHandler,
mainExecutor));
}
@@ -157,22 +159,17 @@
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/draganddrop/DragAndDropPolicy.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java
index 62bf517..d93a901 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropPolicy.java
@@ -240,7 +240,7 @@
// Update launch options for the split side we are targeting.
position = leftOrTop ? SPLIT_POSITION_TOP_OR_LEFT : SPLIT_POSITION_BOTTOM_OR_RIGHT;
// Add some data for logging splitscreen once it is invoked
- mSplitScreen.logOnDroppedToSplit(position, mLoggerSessionId);
+ mSplitScreen.onDroppedToSplit(position, mLoggerSessionId);
}
final ClipDescription description = data.getDescription();
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 0000000..222307f
--- /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 0000000..fa62a73
--- /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 3e8de45..7671081 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 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 @@
*/
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 @@
*/
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 @@
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 @@
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 @@
PipParamsChangedForwarder pipParamsChangedForwarder,
DisplayController displayController,
WindowManagerShellWrapper wmShell,
+ Handler mainHandler,
ShellExecutor mainExecutor) {
return new TvPipController(
context,
@@ -164,6 +182,7 @@
pipParamsChangedForwarder,
displayController,
wmShell,
+ mainHandler,
mainExecutor).mImpl;
}
@@ -184,8 +203,10 @@
PipParamsChangedForwarder pipParamsChangedForwarder,
DisplayController displayController,
WindowManagerShellWrapper wmShellWrapper,
+ Handler mainHandler,
ShellExecutor mainExecutor) {
mContext = context;
+ mMainHandler = mainHandler;
mMainExecutor = mainExecutor;
mShellController = shellController;
mDisplayController = displayController;
@@ -198,12 +219,17 @@
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 @@
"%s: onConfigurationChanged(), state=%s", TAG, stateToName(mState));
loadConfigurations();
- mPipNotificationController.onConfigurationChanged(mContext);
+ mPipNotificationController.onConfigurationChanged();
mTvPipBoundsAlgorithm.onConfigurationChanged(mContext);
}
@@ -256,9 +282,10 @@
* 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 @@
}
setState(STATE_PIP_MENU);
- mTvPipMenuController.showMenu();
+ if (moveMenu) {
+ mTvPipMenuController.showMovementMenu();
+ } else {
+ mTvPipMenuController.showMenu();
+ }
updatePinnedStackBounds();
}
@@ -289,8 +320,7 @@
/**
* 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 @@
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 @@
}
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 @@
@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);
+ closeCurrentPiP(mPinnedTaskId);
+ }
- 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);
- }
+ /**
+ * 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 @@
mPinnedTaskId = pinnedTask.taskId;
mPipMediaController.onActivityPinned();
+ mActionBroadcastReceiver.register();
mPipNotificationController.show(pinnedTask.topActivity.getPackageName());
}
@@ -445,6 +461,8 @@
"%s: onPipDisappeared() state=%s", TAG, stateToName(mState));
mPipNotificationController.dismiss();
+ mActionBroadcastReceiver.unregister();
+
mTvPipMenuController.closeMenu();
mTvPipBoundsState.resetTvPipState();
mTvPipBoundsController.onPipDismissed();
@@ -454,6 +472,11 @@
@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 @@
"%s: onPipTransition_Canceled(), state=%s", TAG, stateToName(mState));
mTvPipMenuController.onPipTransitionFinished(
PipAnimationController.isInPipDirection(direction));
+ mTvPipActionsProvider.onPipExpansionToggled(mTvPipBoundsState.isTvPipExpanded());
}
@Override
@@ -477,6 +501,12 @@
"%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 @@
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 @@
"%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 @@
}
}
+ 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 0000000..449a2bf
--- /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 ab7edbf..00e4f47 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 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 @@
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 @@
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 @@
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 @@
mDelegate = delegate;
}
+ void setTvPipActionsProvider(TvPipActionsProvider tvPipActionsProvider) {
+ mTvPipActionsProvider = tvPipActionsProvider;
+ }
+
@Override
public void attach(SurfaceControl leash) {
if (mDelegate == null) {
@@ -146,15 +141,19 @@
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 @@
// 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 @@
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 @@
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 @@
if (mPipMenuView == null) {
return;
}
+
+ mMenuIsOpen = false;
mPipMenuView.hideAllUserControls();
grantPipMenuFocus(false);
mDelegate.onMenuClosed();
@@ -272,29 +272,19 @@
}
@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 @@
@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 @@
}
@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 57e95c4..56c602a 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_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 LinearLayout mActionButtonsContainer;
- private final View mMenuFrameView;
- private final List<TvWindowMenuActionButton> mAdditionalButtons = new ArrayList<>();
+ private final RecyclerView mActionButtonsRecyclerView;
+ private final LinearLayoutManager mButtonLayoutManager;
+ private final RecyclerViewAdapter mRecyclerViewAdapter;
+
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);
+ mActionButtonsRecyclerView = findViewById(R.id.tv_pip_menu_action_buttons);
+ mButtonLayoutManager = new LinearLayoutManager(mContext);
+ mActionButtonsRecyclerView.setLayoutManager(mButtonLayoutManager);
+ mActionButtonsRecyclerView.setPreserveFocusAfterLayout(true);
- mCloseButton = mActionButtonsContainer.findViewById(R.id.tv_pip_menu_close_button);
- mCloseButton.setOnClickListener(this);
- mCloseButton.setIsCustomCloseAction(true);
+ mTvPipActionsProvider = tvPipActionsProvider;
+ mRecyclerViewAdapter = new RecyclerViewAdapter(tvPipActionsProvider.getActionsList());
+ mActionButtonsRecyclerView.setAdapter(mRecyclerViewAdapter);
- mActionButtonsContainer.findViewById(R.id.tv_pip_menu_move_button)
- .setOnClickListener(this);
- mExpandButton = findViewById(R.id.tv_pip_menu_expand_button);
- mExpandButton.setOnClickListener(this);
-
- 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 @@
}
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 @@
// 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 @@
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 @@
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 @@
"%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 @@
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 @@
*/
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);
}
- refocusPreviousButton();
+ }
+
+ 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));
+ }
}
/**
- * 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 @@
});
}
- /**
- * 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 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 @@
animateAlphaTo(a11yEnabled ? 1f : 0f, mA11yDoneButton);
if (a11yEnabled) {
+ mA11yDoneButton.setVisibility(VISIBLE);
mA11yDoneButton.setOnClickListener(v -> {
mListener.onExitMoveMode();
});
+ mA11yDoneButton.requestFocus();
+ mA11yDoneButton.requestAccessibilityFocus();
}
}
@@ -684,33 +461,67 @@
/**
* 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 @@
* @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 e3308f0..f22ee59 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.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.PipUtils;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
-import java.util.ArrayList;
import java.util.List;
/**
@@ -55,39 +48,18 @@
* <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 @@
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 @@
.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)));
+ .setContentIntent(
+ createPendingIntent(context, TvPipController.ACTION_TO_FULLSCREEN));
+ // TvExtender and DeleteIntent set later since they might change.
- mActionBroadcastReceiver = new ActionBroadcastReceiver();
-
- 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 @@
}
});
- onConfigurationChanged(context);
+ onConfigurationChanged();
}
- void setDelegate(Delegate delegate) {
- ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: setDelegate(), delegate=%s",
- TAG, delegate);
+ /**
+ * Call before showing any notification.
+ */
+ void setTvPipActionsProvider(@NonNull TvPipActionsProvider tvPipActionsProvider) {
+ mTvPipActionsProvider = tvPipActionsProvider;
+ mTvPipActionsProvider.addListener(this);
+ }
- 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.");
- }
-
- 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 @@
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 @@
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);
+ @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);
}
- 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();
- }
- }
+ 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 0000000..93b6a90
--- /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/MainStage.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java
index e7ec15e..89538cb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/MainStage.java
@@ -16,9 +16,6 @@
package com.android.wm.shell.splitscreen;
-import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_ACTIVITY_TYPES;
-import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_WINDOWING_MODES;
-
import android.content.Context;
import android.view.SurfaceSession;
import android.window.WindowContainerToken;
@@ -34,8 +31,6 @@
* @see StageCoordinator
*/
class MainStage extends StageTaskListener {
- private static final String TAG = MainStage.class.getSimpleName();
-
private boolean mIsActive = false;
MainStage(Context context, ShellTaskOrganizer taskOrganizer, int displayId,
@@ -52,15 +47,8 @@
void activate(WindowContainerTransaction wct, boolean includingTopTask) {
if (mIsActive) return;
- final WindowContainerToken rootToken = mRootTaskInfo.token;
if (includingTopTask) {
- wct.reparentTasks(
- null /* currentParent */,
- rootToken,
- CONTROLLED_WINDOWING_MODES,
- CONTROLLED_ACTIVITY_TYPES,
- true /* onTop */,
- true /* reparentTopOnly */);
+ reparentTopTask(wct);
}
mIsActive = true;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
index b26bc9c..ef70d9b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
@@ -121,7 +121,8 @@
public static final int EXIT_REASON_SCREEN_LOCKED = 7;
public static final int EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP = 8;
public static final int EXIT_REASON_CHILD_TASK_ENTER_PIP = 9;
- public static final int EXIT_REASON_FULLSCREEN_SHORTCUT = 10;
+ public static final int EXIT_REASON_RECREATE_SPLIT = 10;
+ public static final int EXIT_REASON_FULLSCREEN_SHORTCUT = 11;
@IntDef(value = {
EXIT_REASON_UNKNOWN,
EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW,
@@ -133,6 +134,7 @@
EXIT_REASON_SCREEN_LOCKED,
EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP,
EXIT_REASON_CHILD_TASK_ENTER_PIP,
+ EXIT_REASON_RECREATE_SPLIT,
EXIT_REASON_FULLSCREEN_SHORTCUT,
})
@Retention(RetentionPolicy.SOURCE)
@@ -470,7 +472,7 @@
*/
public void startShortcut(String packageName, String shortcutId, @SplitPosition int position,
@Nullable Bundle options, UserHandle user, @NonNull InstanceId instanceId) {
- mStageCoordinator.getLogger().enterRequested(instanceId, ENTER_REASON_LAUNCHER);
+ mStageCoordinator.onRequestToSplit(instanceId, ENTER_REASON_LAUNCHER);
startShortcut(packageName, shortcutId, position, options, user);
}
@@ -518,7 +520,7 @@
*/
public void startIntent(PendingIntent intent, @Nullable Intent fillInIntent,
@SplitPosition int position, @Nullable Bundle options, @NonNull InstanceId instanceId) {
- mStageCoordinator.getLogger().enterRequested(instanceId, ENTER_REASON_LAUNCHER);
+ mStageCoordinator.onRequestToSplit(instanceId, ENTER_REASON_LAUNCHER);
startIntent(intent, fillInIntent, position, options);
}
@@ -784,10 +786,10 @@
return splitTasksLayer;
}
/**
- * Sets drag info to be logged when splitscreen is entered.
+ * Drop callback when splitscreen is entered.
*/
- public void logOnDroppedToSplit(@SplitPosition int position, InstanceId dragSessionId) {
- mStageCoordinator.logOnDroppedToSplit(position, dragSessionId);
+ public void onDroppedToSplit(@SplitPosition int position, InstanceId dragSessionId) {
+ mStageCoordinator.onDroppedToSplit(position, dragSessionId);
}
/**
@@ -815,6 +817,8 @@
return "APP_DOES_NOT_SUPPORT_MULTIWINDOW";
case EXIT_REASON_CHILD_TASK_ENTER_PIP:
return "CHILD_TASK_ENTER_PIP";
+ case EXIT_REASON_RECREATE_SPLIT:
+ return "RECREATE_SPLIT";
default:
return "unknown reason, reason int = " + exitReason;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitscreenEventLogger.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitscreenEventLogger.java
index 1016e1b..5483fa5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitscreenEventLogger.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitscreenEventLogger.java
@@ -21,9 +21,11 @@
import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__ENTER_REASON__UNKNOWN_ENTER;
import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__APP_DOES_NOT_SUPPORT_MULTIWINDOW;
import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__APP_FINISHED;
+import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__CHILD_TASK_ENTER_PIP;
import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__DEVICE_FOLDED;
import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__DRAG_DIVIDER;
import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__FULLSCREEN_SHORTCUT;
+import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__RECREATE_SPLIT;
import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__RETURN_HOME;
import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__ROOT_TASK_VANISHED;
import static com.android.internal.util.FrameworkStatsLog.SPLITSCREEN_UICHANGED__EXIT_REASON__SCREEN_LOCKED;
@@ -37,9 +39,11 @@
import static com.android.wm.shell.splitscreen.SplitScreenController.ENTER_REASON_UNKNOWN;
import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW;
import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_FINISHED;
+import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_CHILD_TASK_ENTER_PIP;
import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DEVICE_FOLDED;
import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DRAG_DIVIDER;
import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_FULLSCREEN_SHORTCUT;
+import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_RECREATE_SPLIT;
import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_RETURN_HOME;
import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_ROOT_TASK_VANISHED;
import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_SCREEN_LOCKED;
@@ -182,6 +186,10 @@
return SPLITSCREEN_UICHANGED__EXIT_REASON__SCREEN_LOCKED;
case EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP:
return SPLITSCREEN_UICHANGED__EXIT_REASON__SCREEN_LOCKED_SHOW_ON_TOP;
+ case EXIT_REASON_CHILD_TASK_ENTER_PIP:
+ return SPLITSCREEN_UICHANGED__EXIT_REASON__CHILD_TASK_ENTER_PIP;
+ case EXIT_REASON_RECREATE_SPLIT:
+ return SPLITSCREEN_UICHANGED__EXIT_REASON__RECREATE_SPLIT;
case EXIT_REASON_FULLSCREEN_SHORTCUT:
return SPLITSCREEN_UICHANGED__EXIT_REASON__FULLSCREEN_SHORTCUT;
case EXIT_REASON_UNKNOWN:
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 da8dc87..5be29db 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
@@ -49,10 +49,11 @@
import static com.android.wm.shell.splitscreen.SplitScreenController.ENTER_REASON_MULTI_INSTANCE;
import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW;
import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_FINISHED;
-import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_FULLSCREEN_SHORTCUT;
import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_CHILD_TASK_ENTER_PIP;
import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DEVICE_FOLDED;
import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DRAG_DIVIDER;
+import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_FULLSCREEN_SHORTCUT;
+import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_RECREATE_SPLIT;
import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_RETURN_HOME;
import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_ROOT_TASK_VANISHED;
import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP;
@@ -199,7 +200,8 @@
// and exit, since exit itself can trigger a number of changes that update the stages.
private boolean mShouldUpdateRecents;
private boolean mExitSplitScreenOnHide;
- private boolean mIsDividerRemoteAnimating;
+ private boolean mIsSplitEntering;
+ private boolean mIsDropEntering;
private boolean mIsExiting;
/** The target stage to dismiss to when unlock after folded. */
@@ -347,10 +349,14 @@
return mSplitTransitions;
}
- boolean isSplitScreenVisible() {
+ public boolean isSplitScreenVisible() {
return mSideStageListener.mVisible && mMainStageListener.mVisible;
}
+ public boolean isSplitActive() {
+ return mMainStage.isActive();
+ }
+
@StageType
int getStageOfTask(int taskId) {
if (mMainStage.containsTask(taskId)) {
@@ -373,11 +379,14 @@
targetStage = mSideStage;
sideStagePosition = stagePosition;
} else {
- if (mMainStage.isActive()) {
+ if (isSplitScreenVisible()) {
// If the split screen is activated, retrieves target stage based on position.
targetStage = stagePosition == mSideStagePosition ? mSideStage : mMainStage;
sideStagePosition = mSideStagePosition;
} else {
+ // Exit split if it running background.
+ exitSplitScreen(null /* childrenToTop */, EXIT_REASON_RECREATE_SPLIT);
+
targetStage = mSideStage;
sideStagePosition = stagePosition;
}
@@ -673,6 +682,10 @@
@Nullable PendingIntent mainPendingIntent, @Nullable Intent mainFillInIntent,
@Nullable Bundle mainOptions, @SplitPosition int sidePosition, float splitRatio,
RemoteAnimationAdapter adapter, InstanceId instanceId) {
+ 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);
@@ -685,11 +698,13 @@
// Set false to avoid record new bounds with old task still on top;
mShouldUpdateRecents = false;
- mIsDividerRemoteAnimating = true;
+ mIsSplitEntering = true;
final WindowContainerTransaction evictWct = new WindowContainerTransaction();
- prepareEvictChildTasks(SPLIT_POSITION_TOP_OR_LEFT, evictWct);
- prepareEvictChildTasks(SPLIT_POSITION_BOTTOM_OR_RIGHT, evictWct);
+ if (isSplitScreenVisible()) {
+ mMainStage.evictAllChildren(evictWct);
+ mSideStage.evictAllChildren(evictWct);
+ }
IRemoteAnimationRunner wrapper = new IRemoteAnimationRunner.Stub() {
@Override
@@ -769,7 +784,7 @@
private void onRemoteAnimationFinishedOrCancelled(boolean cancel,
WindowContainerTransaction evictWct) {
- mIsDividerRemoteAnimating = false;
+ mIsSplitEntering = false;
mShouldUpdateRecents = true;
// If any stage has no child after animation finished, it means that split will display
// nothing, such status will happen if task and intent is same app but not support
@@ -781,6 +796,9 @@
mSplitUnsupportedToast.show();
} else {
mSyncQueue.queue(evictWct);
+ mSyncQueue.runInSync(t -> {
+ updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */);
+ });
}
}
@@ -815,7 +833,7 @@
switch (stage) {
case STAGE_TYPE_UNDEFINED: {
if (position != SPLIT_POSITION_UNDEFINED) {
- if (mMainStage.isActive()) {
+ if (isSplitScreenVisible()) {
// Use the stage of the specified position
options = resolveStartStage(
position == mSideStagePosition ? STAGE_TYPE_SIDE : STAGE_TYPE_MAIN,
@@ -1045,14 +1063,13 @@
}
});
mShouldUpdateRecents = false;
- mIsDividerRemoteAnimating = false;
+ mIsSplitEntering = false;
mSplitLayout.getInvisibleBounds(mTempRect1);
if (childrenToTop == null || childrenToTop.getTopVisibleChildTaskId() == INVALID_TASK_ID) {
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 {
@@ -1064,6 +1081,8 @@
wct.setSmallestScreenWidthDp(childrenToTop.mRootTaskInfo.token,
SMALLEST_SCREEN_WIDTH_DP_UNDEFINED);
}
+ wct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token,
+ false /* reparentLeafTaskIfRelaunch */);
mSyncQueue.queue(wct);
mSyncQueue.runInSync(t -> {
t.setWindowCrop(mMainStage.mRootLeash, null)
@@ -1082,7 +1101,6 @@
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 -> {
@@ -1374,7 +1392,7 @@
&& !ENABLE_SHELL_TRANSITIONS) {
// Clear the divider remote animating flag as the divider will be re-rendered to apply
// the new rotation config.
- mIsDividerRemoteAnimating = false;
+ mIsSplitEntering = false;
mSplitLayout.update(null /* t */);
onLayoutSizeChanged(mSplitLayout);
}
@@ -1423,6 +1441,36 @@
});
}
+ void onChildTaskAppeared(StageListenerImpl stageListener, int taskId) {
+ if (stageListener == mSideStageListener && !isSplitScreenVisible() && isSplitActive()
+ && !mIsSplitEntering) {
+ // Handle entring split case here if split already running background.
+ if (mIsDropEntering) {
+ mSplitLayout.resetDividerPosition();
+ } else {
+ mSplitLayout.setDividerAtBorder(mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT);
+ }
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
+ mMainStage.reparentTopTask(wct);
+ mMainStage.evictAllChildren(wct);
+ mSideStage.evictOtherChildren(wct, taskId);
+ updateWindowBounds(mSplitLayout, wct);
+ wct.reorder(mRootTaskInfo.token, true);
+ wct.setForceTranslucent(mRootTaskInfo.token, false);
+
+ mSyncQueue.queue(wct);
+ mSyncQueue.runInSync(t -> {
+ if (mIsDropEntering) {
+ updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */);
+ mIsDropEntering = false;
+ } else {
+ mShowDecorImmediately = true;
+ mSplitLayout.flingDividerToCenter();
+ }
+ });
+ }
+ }
+
private void onRootTaskVanished() {
final WindowContainerTransaction wct = new WindowContainerTransaction();
if (mRootTaskInfo != null) {
@@ -1441,20 +1489,22 @@
return;
}
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
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
- // Don't dismiss split screen when both stages are not visible due to sleeping
- // display.
- || (!mMainStage.mRootTaskInfo.isSleeping
- && !mSideStage.mRootTaskInfo.isSleeping)) {
+ if (mExitSplitScreenOnHide) {
exitSplitScreen(null /* childrenToTop */, EXIT_REASON_RETURN_HOME);
}
+ } else {
+ wct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token,
+ false /* setReparentLeafTaskIfRelaunch */);
+ wct.setForceTranslucent(mRootTaskInfo.token, false);
}
-
+ mSyncQueue.queue(wct);
mSyncQueue.runInSync(t -> {
- t.setVisibility(mSideStage.mRootLeash, sideStageVisible)
- .setVisibility(mMainStage.mRootLeash, mainStageVisible);
setDividerVisibility(mainStageVisible, t);
});
}
@@ -1479,7 +1529,7 @@
mDividerVisible = visible;
sendSplitVisibilityChanged();
- if (mIsDividerRemoteAnimating) {
+ if (mIsSplitEntering) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
" Skip animating divider bar due to it's remote animating.");
return;
@@ -1499,7 +1549,7 @@
" Skip animating divider bar due to divider leash not ready.");
return;
}
- if (mIsDividerRemoteAnimating) {
+ if (mIsSplitEntering) {
ProtoLog.d(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN,
" Skip animating divider bar due to it's remote animating.");
return;
@@ -1555,26 +1605,21 @@
if (!hasChildren && !mIsExiting && mMainStage.isActive()) {
if (isSideStage && mMainStageListener.mVisible) {
// Exit to main stage if side stage no longer has children.
- if (ENABLE_SHELL_TRANSITIONS) {
- exitSplitScreen(mMainStage, EXIT_REASON_APP_FINISHED);
- } else {
- mSplitLayout.flingDividerToDismiss(
- mSideStagePosition == SPLIT_POSITION_BOTTOM_OR_RIGHT,
- EXIT_REASON_APP_FINISHED);
- }
+ mSplitLayout.flingDividerToDismiss(
+ mSideStagePosition == SPLIT_POSITION_BOTTOM_OR_RIGHT,
+ EXIT_REASON_APP_FINISHED);
} else if (!isSideStage && mSideStageListener.mVisible) {
// Exit to side stage if main stage no longer has children.
- if (ENABLE_SHELL_TRANSITIONS) {
- exitSplitScreen(mSideStage, EXIT_REASON_APP_FINISHED);
- } else {
- mSplitLayout.flingDividerToDismiss(
- mSideStagePosition != SPLIT_POSITION_BOTTOM_OR_RIGHT,
- EXIT_REASON_APP_FINISHED);
- }
+ mSplitLayout.flingDividerToDismiss(
+ mSideStagePosition != SPLIT_POSITION_BOTTOM_OR_RIGHT,
+ EXIT_REASON_APP_FINISHED);
+ } else if (!isSplitScreenVisible()) {
+ exitSplitScreen(null /* childrenToTop */, EXIT_REASON_APP_FINISHED);
}
} else if (isSideStage && hasChildren && !mMainStage.isActive()) {
- final WindowContainerTransaction wct = new WindowContainerTransaction();
mSplitLayout.init();
+
+ final WindowContainerTransaction wct = new WindowContainerTransaction();
if (mLogger.isEnterRequestedByDrag()) {
prepareEnterSplitScreen(wct);
} else {
@@ -1589,8 +1634,9 @@
mSyncQueue.queue(wct);
mSyncQueue.runInSync(t -> {
- if (mLogger.isEnterRequestedByDrag()) {
+ if (mIsDropEntering) {
updateSurfaceBounds(mSplitLayout, t, false /* applyResizingOffset */);
+ mIsDropEntering = false;
} else {
mShowDecorImmediately = true;
mSplitLayout.flingDividerToCenter();
@@ -1945,10 +1991,6 @@
}
}
- public boolean isSplitActive() {
- return mMainStage.isActive();
- }
-
@Override
public void mergeAnimation(IBinder transition, TransitionInfo info,
SurfaceControl.Transaction t, IBinder mergeTarget,
@@ -2304,11 +2346,29 @@
/**
* Sets drag info to be logged when splitscreen is next entered.
*/
- public void logOnDroppedToSplit(@SplitPosition int position, InstanceId dragSessionId) {
+ public void onDroppedToSplit(@SplitPosition int position, InstanceId dragSessionId) {
+ if (!isSplitScreenVisible()) {
+ mIsDropEntering = true;
+ }
+ if (!isSplitScreenVisible()) {
+ // If split running background, exit split first.
+ exitSplitScreen(null /* childrenToTop */, EXIT_REASON_RECREATE_SPLIT);
+ }
mLogger.enterRequestedByDrag(position, dragSessionId);
}
/**
+ * Sets info to be logged when splitscreen is next entered.
+ */
+ public void onRequestToSplit(InstanceId sessionId, int enterReason) {
+ if (!isSplitScreenVisible()) {
+ // If split running background, exit split first.
+ exitSplitScreen(null /* childrenToTop */, EXIT_REASON_RECREATE_SPLIT);
+ }
+ mLogger.enterRequested(sessionId, enterReason);
+ }
+
+ /**
* Logs the exit of splitscreen.
*/
private void logExit(@ExitReason int exitReason) {
@@ -2343,6 +2403,11 @@
}
@Override
+ public void onChildTaskAppeared(int taskId) {
+ StageCoordinator.this.onChildTaskAppeared(this, taskId);
+ }
+
+ @Override
public void onStatusChanged(boolean visible, boolean hasChildren) {
if (!mHasRootTask) return;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
index 8a52c87..a841b7f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
@@ -22,6 +22,7 @@
import static android.view.RemoteAnimationTarget.MODE_OPENING;
import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_ACTIVITY_TYPES;
+import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_WINDOWING_MODES;
import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE;
import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS;
@@ -69,6 +70,8 @@
public interface StageListenerCallbacks {
void onRootTaskAppeared();
+ void onChildTaskAppeared(int taskId);
+
void onStatusChanged(boolean visible, boolean hasChildren);
void onChildTaskStatusChanged(int taskId, boolean present, boolean visible);
@@ -185,6 +188,7 @@
// Status is managed/synchronized by the transition lifecycle.
return;
}
+ mCallbacks.onChildTaskAppeared(taskId);
sendStatusChanged();
} else {
throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo
@@ -338,6 +342,14 @@
}
}
+ void evictOtherChildren(WindowContainerTransaction wct, int taskId) {
+ for (int i = mChildrenTaskInfo.size() - 1; i >= 0; i--) {
+ final ActivityManager.RunningTaskInfo taskInfo = mChildrenTaskInfo.valueAt(i);
+ if (taskId == taskInfo.taskId) continue;
+ wct.reparent(taskInfo.token, null /* parent */, false /* onTop */);
+ }
+ }
+
void evictNonOpeningChildren(RemoteAnimationTarget[] apps, WindowContainerTransaction wct) {
final SparseArray<ActivityManager.RunningTaskInfo> toBeEvict = mChildrenTaskInfo.clone();
for (int i = 0; i < apps.length; i++) {
@@ -360,6 +372,12 @@
}
}
+ void reparentTopTask(WindowContainerTransaction wct) {
+ wct.reparentTasks(null /* currentParent */, mRootTaskInfo.token,
+ CONTROLLED_WINDOWING_MODES, CONTROLLED_ACTIVITY_TYPES,
+ true /* onTop */, true /* reparentTopOnly */);
+ }
+
void resetBounds(WindowContainerTransaction wct) {
wct.setBounds(mRootTaskInfo.token, null);
wct.setAppBounds(mRootTaskInfo.token, null);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
index 3cba929..a2d7bc4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
@@ -111,7 +111,7 @@
@Override
public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
@NonNull TransitionRequestInfo request) {
- if (mPipHandler.requestHasPipEnter(request) && mSplitHandler.isSplitActive()) {
+ if (mPipHandler.requestHasPipEnter(request) && mSplitHandler.isSplitScreenVisible()) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Got a PiP-enter request while "
+ "Split-Screen is active, so treat it as Mixed.");
if (request.getRemoteTransition() != null) {
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 afefd5d..42e2b3f 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 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.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 @@
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 @@
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 @@
syncQueue,
desktopModeController,
new CaptionWindowDecoration.Factory(),
- InputManager::getInstance);
+ new InputMonitorFactory());
}
- public CaptionWindowDecorViewModel(
+ @VisibleForTesting
+ CaptionWindowDecorViewModel(
Context context,
Handler mainHandler,
Choreographer mainChoreographer,
@@ -113,8 +113,7 @@
SyncTransactionQueue syncQueue,
Optional<DesktopModeController> desktopModeController,
CaptionWindowDecoration.Factory captionWindowDecorFactory,
- Supplier<InputManager> inputManagerSupplier) {
-
+ InputMonitorFactory inputMonitorFactory) {
mContext = context;
mMainHandler = mainHandler;
mMainChoreographer = mainChoreographer;
@@ -125,11 +124,7 @@
mDesktopModeController = desktopModeController;
mCaptionWindowDecorFactory = captionWindowDecorFactory;
- mInputManagerSupplier = inputManagerSupplier;
- }
-
- void setEventReceiverFactory(EventReceiverFactory eventReceiverFactory) {
- mEventReceiverFactory = eventReceiverFactory;
+ mInputMonitorFactory = inputMonitorFactory;
}
@Override
@@ -205,7 +200,6 @@
decoration.close();
int displayId = taskInfo.displayId;
if (mEventReceiversByDisplay.contains(displayId)) {
- EventReceiver eventReceiver = mEventReceiversByDisplay.get(displayId);
removeTaskFromEventReceiver(displayId);
}
}
@@ -408,12 +402,6 @@
}
}
- 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 @@
}
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 @@
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 9215496..7f85988 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.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 @@
.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 @@
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 @@
.setEffectLayer()
.setParent(mTaskSurface)
.build();
+
+ startT.setLayer(mTaskBackgroundSurface, TaskConstants.TASK_CHILD_LAYER_TASK_BACKGROUND);
}
float shadowRadius = loadDimension(resources, params.mShadowRadiusId);
@@ -236,8 +239,6 @@
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/AndroidManifest.xml b/libs/WindowManager/Shell/tests/unittest/AndroidManifest.xml
index 59d9104..fac0461 100644
--- a/libs/WindowManager/Shell/tests/unittest/AndroidManifest.xml
+++ b/libs/WindowManager/Shell/tests/unittest/AndroidManifest.xml
@@ -19,6 +19,8 @@
xmlns:tools="http://schemas.android.com/tools"
package="com.android.wm.shell.tests">
+ <uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
+
<application android:debuggable="true" android:largeHeap="true">
<uses-library android:name="android.test.mock" />
<uses-library android:name="android.test.runner" />
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 0000000..91040e9
--- /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 ad6fced..0dbf30d 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_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.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 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 @@
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 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);
- verify(mCaptionWindowDecorFactory, never())
+ taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
+ taskInfo.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_STANDARD);
+
+ 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);
- taskInfo.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_STANDARD);
-
- mCaptionWindowDecorViewModel.onTaskChanging(taskInfo, surfaceControl, startT, finishT);
-
- verify(mCaptionWindowDecorFactory)
- .create(
- mContext,
- mDisplayController,
- mTaskOrganizer,
- taskInfo,
- surfaceControl,
- mMainHandler,
- mMainChoreographer,
- mSyncQueue);
+ mContext,
+ mDisplayController,
+ mTaskOrganizer,
+ taskInfo,
+ surfaceControl,
+ mMainHandler,
+ mMainChoreographer,
+ mSyncQueue);
}
- private static ActivityManager.RunningTaskInfo createTaskInfo(int taskId, int windowingMode) {
+ @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.onTaskOpening(taskInfo, surfaceControl, startT, finishT);
+
+ mCaptionWindowDecorViewModel.destroyWindowDecoration(taskInfo);
+ });
+ verify(mMockInputMonitorFactory).create(any(), any());
+ verify(mInputMonitor).dispose();
+ }
+
+ @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 ccd4ed0..4d3f05b 100644
--- a/media/java/android/media/AudioFormat.java
+++ b/media/java/android/media/AudioFormat.java
@@ -373,6 +373,8 @@
* 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 @@
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 @@
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 @@
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 @@
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 @@
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 5c5f837..356b765 100644
--- a/media/java/android/media/AudioProfile.java
+++ b/media/java/android/media/AudioProfile.java
@@ -46,11 +46,17 @@
* 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 @@
*
* @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 4cf3b3e..490809c 100644
--- a/media/java/android/media/audio/common/AidlConversion.java
+++ b/media/java/android/media/audio/common/AidlConversion.java
@@ -614,6 +614,8 @@
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 @@
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 f3743d1..2c8de2e 100644
--- a/media/java/android/media/projection/IMediaProjectionCallback.aidl
+++ b/media/java/android/media/projection/IMediaProjectionCallback.aidl
@@ -19,4 +19,5 @@
/** {@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 1d58a40..a63d02b 100644
--- a/media/java/android/media/projection/IMediaProjectionManager.aidl
+++ b/media/java/android/media/projection/IMediaProjectionManager.aidl
@@ -27,12 +27,32 @@
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 ae44fc5..3dfff1f 100644
--- a/media/java/android/media/projection/MediaProjection.java
+++ b/media/java/android/media/projection/MediaProjection.java
@@ -234,7 +234,7 @@
/**
* 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 @@
* </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 @@
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 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 0000000..f78385f
--- /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 0000000..29afaa6
--- /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 b3bd980..a4215e68 100644
--- a/media/java/android/media/projection/MediaProjectionManager.java
+++ b/media/java/android/media/projection/MediaProjectionManager.java
@@ -38,6 +38,13 @@
@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 @@
}
/**
- * 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 @@
}
/**
+ * 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/java/android/media/tv/interactive/TvInteractiveAppService.java b/media/java/android/media/tv/interactive/TvInteractiveAppService.java
index 90eed9e..00150d5 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppService.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppService.java
@@ -457,7 +457,9 @@
/**
* Receives started recording's ID.
*
- * @param recordingId The ID of the recording started
+ * @param recordingId The ID of the recording started. The TV app should provide and
+ * maintain this ID to identify the recording in the future.
+ * @see #onRecordingStopped(String)
*/
public void onRecordingStarted(@NonNull String recordingId) {
}
@@ -465,13 +467,13 @@
/**
* Receives stopped recording's ID.
*
- * @param recordingId The ID of the recording stopped
- * @hide
+ * @param recordingId The ID of the recording stopped. This ID is created and maintained by
+ * the TV app when the recording was started.
+ * @see #onRecordingStarted(String)
*/
public void onRecordingStopped(@NonNull String recordingId) {
}
-
/**
* Receives signing result.
* @param signingId the ID to identify the request. It's the same as the corresponding ID in
@@ -952,13 +954,14 @@
}
/**
- * Requests starting of recording
+ * Requests the recording associated with the recordingId to stop.
*
- * <p> This is used to request the active {@link android.media.tv.TvRecordingClient} to
+ * <p> This is used to request the associated {@link android.media.tv.TvRecordingClient} to
* call {@link android.media.tv.TvRecordingClient#stopRecording()}.
- * @see android.media.tv.TvRecordingClient#stopRecording()
*
- * @hide
+ * @param recordingId The ID of the recording to stop. This is provided by the TV app in
+ * {@link TvInteractiveAppView#notifyRecordingStarted(String)}
+ * @see android.media.tv.TvRecordingClient#stopRecording()
*/
@CallSuper
public void requestStopRecording(@NonNull String recordingId) {
@@ -976,8 +979,6 @@
});
}
-
-
/**
* Requests signing of the given data.
*
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppView.java b/media/java/android/media/tv/interactive/TvInteractiveAppView.java
index fcd781b..1177688 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppView.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppView.java
@@ -585,6 +585,7 @@
*
* @param recordingId The ID of the recording started. This ID is created and maintained by the
* TV app and is used to identify the recording in the future.
+ * @see TvInteractiveAppView#notifyRecordingStopped(String)
*/
public void notifyRecordingStarted(@NonNull String recordingId) {
if (DEBUG) {
@@ -601,7 +602,6 @@
* @param recordingId The ID of the recording stopped. This ID is created and maintained
* by the TV app when a recording is started.
* @see TvInteractiveAppView#notifyRecordingStarted(String)
- * @hide
*/
public void notifyRecordingStopped(@NonNull String recordingId) {
if (DEBUG) {
@@ -877,7 +877,8 @@
* is called.
*
* @param iAppServiceId The ID of the TV interactive app service bound to this view.
- * @param programUri The program URI to record
+ * @param programUri The URI of the program to record
+ *
*/
public void onRequestStartRecording(
@NonNull String iAppServiceId,
@@ -885,12 +886,14 @@
}
/**
- * This is called when {@link TvInteractiveAppService.Session#requestStopRecording()}
+ * This is called when {@link TvInteractiveAppService.Session#requestStopRecording(String)}
* is called.
*
* @param iAppServiceId The ID of the TV interactive app service bound to this view.
- * @param recordingId The ID of the recording to stop.
- * @hide
+ * @param recordingId The ID of the recording to stop. This is provided by the TV app in
+ * {@link #notifyRecordingStarted(String)}
+ * @see #notifyRecordingStarted(String)
+ * @see #notifyRecordingStopped(String)
*/
public void onRequestStopRecording(
@NonNull String iAppServiceId,
diff --git a/media/tests/projection/Android.bp b/media/tests/projection/Android.bp
new file mode 100644
index 0000000..08d9501
--- /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 0000000..62f148c
--- /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 0000000..f64930a
--- /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 0000000..ddb68af
--- /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 0000000..a30f2e3
--- /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 0000000..a3e4908
--- /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/CompanionDeviceManager/res/drawable-night/ic_permission_microphone.xml b/packages/CompanionDeviceManager/res/drawable-night/ic_permission_microphone.xml
new file mode 100644
index 0000000..161e4e6e
--- /dev/null
+++ b/packages/CompanionDeviceManager/res/drawable-night/ic_permission_microphone.xml
@@ -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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:tint="@android:color/system_accent1_200">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M12,14Q10.75,14 9.875,13.125Q9,12.25 9,11V5Q9,3.75 9.875,2.875Q10.75,2 12,2Q13.25,2 14.125,2.875Q15,3.75 15,5V11Q15,12.25 14.125,13.125Q13.25,14 12,14ZM12,8Q12,8 12,8Q12,8 12,8Q12,8 12,8Q12,8 12,8Q12,8 12,8Q12,8 12,8Q12,8 12,8Q12,8 12,8ZM11,21V17.925Q8.4,17.575 6.7,15.6Q5,13.625 5,11H7Q7,13.075 8.463,14.537Q9.925,16 12,16Q14.075,16 15.538,14.537Q17,13.075 17,11H19Q19,13.625 17.3,15.6Q15.6,17.575 13,17.925V21ZM12,12Q12.425,12 12.713,11.712Q13,11.425 13,11V5Q13,4.575 12.713,4.287Q12.425,4 12,4Q11.575,4 11.288,4.287Q11,4.575 11,5V11Q11,11.425 11.288,11.712Q11.575,12 12,12Z"/>
+</vector>
diff --git a/packages/CompanionDeviceManager/res/drawable/ic_permission_microphone.xml b/packages/CompanionDeviceManager/res/drawable/ic_permission_microphone.xml
new file mode 100644
index 0000000..eca625d
--- /dev/null
+++ b/packages/CompanionDeviceManager/res/drawable/ic_permission_microphone.xml
@@ -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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24"
+ android:tint="@android:color/system_accent1_600">
+ <path android:fillColor="@android:color/white" android:pathData="M12,14Q10.75,14 9.875,13.125Q9,12.25 9,11V5Q9,3.75 9.875,2.875Q10.75,2 12,2Q13.25,2 14.125,2.875Q15,3.75 15,5V11Q15,12.25 14.125,13.125Q13.25,14 12,14ZM12,8Q12,8 12,8Q12,8 12,8Q12,8 12,8Q12,8 12,8Q12,8 12,8Q12,8 12,8Q12,8 12,8Q12,8 12,8ZM11,21V17.925Q8.4,17.575 6.7,15.6Q5,13.625 5,11H7Q7,13.075 8.463,14.537Q9.925,16 12,16Q14.075,16 15.538,14.537Q17,13.075 17,11H19Q19,13.625 17.3,15.6Q15.6,17.575 13,17.925V21ZM12,12Q12.425,12 12.713,11.712Q13,11.425 13,11V5Q13,4.575 12.713,4.287Q12.425,4 12,4Q11.575,4 11.288,4.287Q11,4.575 11,5V11Q11,11.425 11.288,11.712Q11.575,12 12,12Z"/>
+</vector>
diff --git a/packages/CompanionDeviceManager/res/values/strings.xml b/packages/CompanionDeviceManager/res/values/strings.xml
index ecc5f81..3b21541 100644
--- a/packages/CompanionDeviceManager/res/values/strings.xml
+++ b/packages/CompanionDeviceManager/res/values/strings.xml
@@ -140,6 +140,9 @@
<!-- Calendar permission will be granted of corresponding profile [CHAR LIMIT=30] -->
<string name="permission_calendar">Calendar</string>
+ <!-- Microphone permission will be granted to corresponding profile [CHAR LIMIT=30] -->
+ <string name="permission_microphone">Microphone</string>
+
<!-- Nearby devices permission will be granted of corresponding profile [CHAR LIMIT=30] -->
<string name="permission_nearby_devices">Nearby devices</string>
@@ -169,6 +172,10 @@
<!-- TODO(b/253644212) Need the description for calendar permission -->
<string name="permission_calendar_summary"></string>
+ <!-- Description of microphone permission of corresponding profile [CHAR LIMIT=NONE] -->
+ <!-- TODO(b/256140614) Need the description for microphone permission -->
+ <string name="permission_microphone_summary">Can record audio using the microphone</string>
+
<!-- Description of nearby devices' permission of corresponding profile [CHAR LIMIT=NONE] -->
<!-- TODO(b/253644212) Need the description for nearby devices' permission -->
<string name="permission_nearby_devices_summary"></string>
diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java
index c249f55..49a63345 100644
--- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java
+++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/CompanionDeviceActivity.java
@@ -36,6 +36,7 @@
import static com.android.companiondevicemanager.PermissionListAdapter.PERMISSION_APP_STREAMING;
import static com.android.companiondevicemanager.PermissionListAdapter.PERMISSION_CALENDAR;
import static com.android.companiondevicemanager.PermissionListAdapter.PERMISSION_CONTACTS;
+import static com.android.companiondevicemanager.PermissionListAdapter.PERMISSION_MICROPHONE;
import static com.android.companiondevicemanager.PermissionListAdapter.PERMISSION_NEARBY_DEVICES;
import static com.android.companiondevicemanager.PermissionListAdapter.PERMISSION_NEARBY_DEVICE_STREAMING;
import static com.android.companiondevicemanager.PermissionListAdapter.PERMISSION_NOTIFICATION;
@@ -596,10 +597,9 @@
this, R.string.summary_glasses_single_device, profileName, appLabel);
profileIcon = getIcon(this, R.drawable.ic_glasses);
- // TODO (b/256140614): add PERMISSION_MICROPHONE
mPermissionTypes.addAll(Arrays.asList(
PERMISSION_PHONE, PERMISSION_SMS, PERMISSION_CONTACTS,
- PERMISSION_NEARBY_DEVICES));
+ PERMISSION_MICROPHONE, PERMISSION_NEARBY_DEVICES));
setupPermissionList();
} else {
diff --git a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/PermissionListAdapter.java b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/PermissionListAdapter.java
index 90b94fb..00c44d6 100644
--- a/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/PermissionListAdapter.java
+++ b/packages/CompanionDeviceManager/src/com/android/companiondevicemanager/PermissionListAdapter.java
@@ -51,6 +51,7 @@
static final int PERMISSION_CALENDAR = 6;
static final int PERMISSION_NEARBY_DEVICES = 7;
static final int PERMISSION_NEARBY_DEVICE_STREAMING = 8;
+ static final int PERMISSION_MICROPHONE = 9;
private static final Map<Integer, Integer> sTitleMap;
static {
@@ -64,6 +65,7 @@
map.put(PERMISSION_CALENDAR, R.string.permission_calendar);
map.put(PERMISSION_NEARBY_DEVICES, R.string.permission_nearby_devices);
map.put(PERMISSION_NEARBY_DEVICE_STREAMING, R.string.permission_nearby_device_streaming);
+ map.put(PERMISSION_MICROPHONE, R.string.permission_microphone);
sTitleMap = unmodifiableMap(map);
}
@@ -80,6 +82,7 @@
map.put(PERMISSION_NEARBY_DEVICES, R.string.permission_nearby_devices_summary);
map.put(PERMISSION_NEARBY_DEVICE_STREAMING,
R.string.permission_nearby_device_streaming_summary);
+ map.put(PERMISSION_MICROPHONE, R.string.permission_microphone_summary);
sSummaryMap = unmodifiableMap(map);
}
@@ -96,6 +99,7 @@
map.put(PERMISSION_NEARBY_DEVICES, R.drawable.ic_permission_nearby_devices);
map.put(PERMISSION_NEARBY_DEVICE_STREAMING,
R.drawable.ic_permission_nearby_device_streaming);
+ map.put(PERMISSION_MICROPHONE, R.drawable.ic_permission_microphone);
sIconMap = unmodifiableMap(map);
}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
index 4faf00c..d2b2924 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt
@@ -41,8 +41,6 @@
import android.os.ResultReceiver
import android.service.credentials.CredentialProviderService
import com.android.credentialmanager.createflow.CreateCredentialUiState
-import com.android.credentialmanager.createflow.EnabledProviderInfo
-import com.android.credentialmanager.createflow.RemoteInfo
import com.android.credentialmanager.getflow.GetCredentialUiState
import com.android.credentialmanager.jetpack.developer.CreatePasswordRequest.Companion.toBundle
import com.android.credentialmanager.jetpack.developer.CreatePublicKeyCredentialRequest
@@ -63,7 +61,7 @@
requestInfo = intent.extras?.getParcelable(
RequestInfo.EXTRA_REQUEST_INFO,
RequestInfo::class.java
- ) ?: testGetRequestInfo()
+ ) ?: testCreatePasskeyRequestInfo()
providerEnabledList = when (requestInfo.type) {
RequestInfo.TYPE_CREATE ->
@@ -101,7 +99,7 @@
}
fun onOptionSelected(
- providerPackageName: String,
+ providerId: String,
entryKey: String,
entrySubkey: String,
resultCode: Int? = null,
@@ -109,7 +107,7 @@
) {
val userSelectionDialogResult = UserSelectionDialogResult(
requestInfo.token,
- providerPackageName,
+ providerId,
entryKey,
entrySubkey,
if (resultCode != null) ProviderPendingIntentResponse(resultCode, resultData) else null
@@ -138,36 +136,15 @@
val providerDisabledList = CreateFlowUtils.toDisabledProviderList(
// Handle runtime cast error
providerDisabledList, context)
- var defaultProvider: EnabledProviderInfo? = null
- var remoteEntry: RemoteInfo? = null
- var createOptionSize = 0
- var lastSeenProviderWithNonEmptyCreateOptions: EnabledProviderInfo? = null
providerEnabledList.forEach{providerInfo -> providerInfo.createOptions =
providerInfo.createOptions.sortedWith(compareBy { it.lastUsedTimeMillis }).reversed()
- if (providerInfo.isDefault) {defaultProvider = providerInfo}
- if (providerInfo.remoteEntry != null) {
- remoteEntry = providerInfo.remoteEntry!!
- }
- if (providerInfo.createOptions.isNotEmpty()) {
- createOptionSize += providerInfo.createOptions.size
- lastSeenProviderWithNonEmptyCreateOptions = providerInfo
- }
}
- return CreateCredentialUiState(
- enabledProviders = providerEnabledList,
- disabledProviders = providerDisabledList,
- CreateFlowUtils.toCreateScreenState(
- createOptionSize, false,
- requestDisplayInfo, defaultProvider, remoteEntry),
- requestDisplayInfo,
- false,
- CreateFlowUtils.toActiveEntry(
- /*defaultProvider=*/defaultProvider, createOptionSize,
- lastSeenProviderWithNonEmptyCreateOptions, remoteEntry),
- )
+ return CreateFlowUtils.toCreateCredentialUiState(
+ providerEnabledList, providerDisabledList, requestDisplayInfo, false)
}
companion object {
+ // TODO: find a way to resolve this static field leak problem
lateinit var repo: CredentialManagerRepo
fun setup(
@@ -198,7 +175,6 @@
.setRemoteEntry(
newRemoteEntry("key2", "subkey-1")
)
- .setIsDefaultProvider(true)
.build(),
CreateCredentialProviderData
.Builder("com.dashlane")
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt
index 6a4c599..cdff2d4 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt
@@ -43,6 +43,7 @@
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
CredentialManagerRepo.setup(this, intent)
+ UserConfigRepo.setup(this)
val requestInfo = CredentialManagerRepo.getInstance().requestInfo
setContent {
CredentialSelectorTheme {
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
index 56fbf66..db676b2 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
@@ -31,6 +31,8 @@
import com.android.credentialmanager.createflow.EnabledProviderInfo
import com.android.credentialmanager.createflow.CreateScreenState
import com.android.credentialmanager.createflow.ActiveEntry
+import com.android.credentialmanager.createflow.DisabledProviderInfo
+import com.android.credentialmanager.createflow.CreateCredentialUiState
import com.android.credentialmanager.getflow.ActionEntryInfo
import com.android.credentialmanager.getflow.AuthenticationEntryInfo
import com.android.credentialmanager.getflow.CredentialEntryInfo
@@ -208,14 +210,13 @@
val pkgInfo = packageManager
.getPackageInfo(packageName!!,
PackageManager.PackageInfoFlags.of(0))
- com.android.credentialmanager.createflow.EnabledProviderInfo(
+ EnabledProviderInfo(
// TODO: decide what to do when failed to load a provider icon
icon = pkgInfo.applicationInfo.loadIcon(packageManager)!!,
name = it.providerFlattenedComponentName,
displayName = pkgInfo.applicationInfo.loadLabel(packageManager).toString(),
createOptions = toCreationOptionInfoList(
it.providerFlattenedComponentName, it.saveEntries, requestDisplayInfo, context),
- isDefault = it.isDefaultProvider,
remoteEntry = toRemoteInfo(it.providerFlattenedComponentName, it.remoteEntry),
)
}
@@ -256,8 +257,7 @@
createCredentialRequestJetpack.password,
createCredentialRequestJetpack.type,
requestInfo.appPackageName,
- context.getDrawable(R.drawable.ic_password)!!,
- requestInfo.isFirstUsage
+ context.getDrawable(R.drawable.ic_password)!!
)
}
is CreatePublicKeyCredentialRequest -> {
@@ -275,8 +275,7 @@
displayName,
createCredentialRequestJetpack.type,
requestInfo.appPackageName,
- context.getDrawable(R.drawable.ic_passkey)!!,
- requestInfo.isFirstUsage)
+ context.getDrawable(R.drawable.ic_passkey)!!)
}
// TODO: correctly parsing for other sign-ins
else -> {
@@ -285,20 +284,60 @@
"Elisa Beckett",
"other-sign-ins",
requestInfo.appPackageName,
- context.getDrawable(R.drawable.ic_other_sign_in)!!,
- requestInfo.isFirstUsage)
+ context.getDrawable(R.drawable.ic_other_sign_in)!!)
}
}
}
- fun toCreateScreenState(
+ fun toCreateCredentialUiState(
+ enabledProviders: List<EnabledProviderInfo>,
+ disabledProviders: List<DisabledProviderInfo>?,
+ requestDisplayInfo: RequestDisplayInfo,
+ isOnPasskeyIntroStateAlready: Boolean,
+ ): CreateCredentialUiState {
+ var createOptionSize = 0
+ var lastSeenProviderWithNonEmptyCreateOptions: EnabledProviderInfo? = null
+ var remoteEntry: RemoteInfo? = null
+ var defaultProvider: EnabledProviderInfo? = null
+ val defaultProviderId = UserConfigRepo.getInstance().getDefaultProviderId()
+ enabledProviders.forEach {
+ enabledProvider ->
+ if (defaultProviderId != null) {
+ if (enabledProvider.name == defaultProviderId) {
+ defaultProvider = enabledProvider
+ }
+ }
+ if (enabledProvider.createOptions.isNotEmpty()) {
+ createOptionSize += enabledProvider.createOptions.size
+ lastSeenProviderWithNonEmptyCreateOptions = enabledProvider
+ }
+ if (enabledProvider.remoteEntry != null) {
+ remoteEntry = enabledProvider.remoteEntry!!
+ }
+ }
+ return CreateCredentialUiState(
+ enabledProviders = enabledProviders,
+ disabledProviders = disabledProviders,
+ toCreateScreenState(
+ createOptionSize, isOnPasskeyIntroStateAlready,
+ requestDisplayInfo, defaultProvider, remoteEntry),
+ requestDisplayInfo,
+ isOnPasskeyIntroStateAlready,
+ toActiveEntry(
+ /*defaultProvider=*/defaultProvider, createOptionSize,
+ lastSeenProviderWithNonEmptyCreateOptions, remoteEntry),
+ )
+ }
+
+ private fun toCreateScreenState(
createOptionSize: Int,
isOnPasskeyIntroStateAlready: Boolean,
requestDisplayInfo: RequestDisplayInfo,
defaultProvider: EnabledProviderInfo?,
remoteEntry: RemoteInfo?,
): CreateScreenState {
- return if (requestDisplayInfo.isFirstUsage && requestDisplayInfo
+ return if (
+ UserConfigRepo.getInstance().getIsFirstUse() && requestDisplayInfo
.type == TYPE_PUBLIC_KEY_CREDENTIAL && !isOnPasskeyIntroStateAlready) {
CreateScreenState.PASSKEY_INTRO
} else if (
@@ -313,12 +352,12 @@
} else if (createOptionSize == 0 && remoteEntry != null) {
CreateScreenState.EXTERNAL_ONLY_SELECTION
} else {
- // TODO: properly handle error and gracefully finish itself
- throw java.lang.IllegalStateException("Empty provider list.")
+ // TODO: properly handle error and gracefully finish itself
+ throw java.lang.IllegalStateException("Empty provider list.")
}
}
- fun toActiveEntry(
+ private fun toActiveEntry(
defaultProvider: EnabledProviderInfo?,
createOptionSize: Int,
lastSeenProviderWithNonEmptyCreateOptions: EnabledProviderInfo?,
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/UserConfigRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/UserConfigRepo.kt
new file mode 100644
index 0000000..5e77663
--- /dev/null
+++ b/packages/CredentialManager/src/com/android/credentialmanager/UserConfigRepo.kt
@@ -0,0 +1,70 @@
+/*
+ * 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.credentialmanager
+
+import android.content.Context
+import android.content.SharedPreferences
+
+class UserConfigRepo(context: Context) {
+ val sharedPreferences: SharedPreferences = context.getSharedPreferences(
+ context.packageName, Context.MODE_PRIVATE)
+
+ fun setDefaultProvider(
+ providerId: String
+ ) {
+ sharedPreferences.edit().apply {
+ putString(DEFAULT_PROVIDER, providerId)
+ apply()
+ }
+ }
+
+ fun setIsFirstUse(
+ isFirstUse: Boolean
+ ) {
+ sharedPreferences.edit().apply {
+ putBoolean(IS_PASSKEY_FIRST_USE, isFirstUse)
+ apply()
+ }
+ }
+
+ fun getDefaultProviderId(): String? {
+ return sharedPreferences.getString(DEFAULT_PROVIDER, null)
+ }
+
+ fun getIsFirstUse(): Boolean {
+ return sharedPreferences.getBoolean(IS_PASSKEY_FIRST_USE, true)
+ }
+
+ companion object {
+ lateinit var repo: UserConfigRepo
+
+ const val DEFAULT_PROVIDER = "default_provider"
+ // This first use value only applies to passkeys, not related with if generally
+ // credential manager is first use or not
+ const val IS_PASSKEY_FIRST_USE = "is_passkey_first_use"
+
+ fun setup(
+ context: Context,
+ ) {
+ repo = UserConfigRepo(context)
+ }
+
+ fun getInstance(): UserConfigRepo {
+ return repo
+ }
+ }
+}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt
index c26fac8..38e2caa 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt
@@ -108,7 +108,8 @@
)
CreateScreenState.MORE_OPTIONS_ROW_INTRO -> MoreOptionsRowIntroCard(
providerInfo = uiState.activeEntry?.activeProvider!!,
- onDefaultOrNotSelected = viewModel::onDefaultOrNotSelected
+ onChangeDefaultSelected = viewModel::onChangeDefaultSelected,
+ onUseOnceSelected = viewModel::onUseOnceSelected,
)
CreateScreenState.EXTERNAL_ONLY_SELECTION -> ExternalOnlySelectionCard(
requestDisplayInfo = uiState.requestDisplayInfo,
@@ -464,7 +465,8 @@
@Composable
fun MoreOptionsRowIntroCard(
providerInfo: EnabledProviderInfo,
- onDefaultOrNotSelected: () -> Unit,
+ onChangeDefaultSelected: () -> Unit,
+ onUseOnceSelected: () -> Unit,
) {
ContainerCard() {
Column() {
@@ -496,11 +498,11 @@
) {
CancelButton(
stringResource(R.string.use_once),
- onClick = onDefaultOrNotSelected
+ onClick = onUseOnceSelected
)
ConfirmButton(
stringResource(R.string.set_as_default),
- onClick = onDefaultOrNotSelected
+ onClick = onChangeDefaultSelected
)
}
Divider(
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt
index 518aaee..9d029dff 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialViewModel.kt
@@ -29,6 +29,7 @@
import androidx.lifecycle.ViewModel
import com.android.credentialmanager.CreateFlowUtils
import com.android.credentialmanager.CredentialManagerRepo
+import com.android.credentialmanager.UserConfigRepo
import com.android.credentialmanager.common.DialogResult
import com.android.credentialmanager.common.ProviderActivityResult
import com.android.credentialmanager.common.ResultState
@@ -61,32 +62,15 @@
}
fun onConfirmIntro() {
- var createOptionSize = 0
- var lastSeenProviderWithNonEmptyCreateOptions: EnabledProviderInfo? = null
- var remoteEntry: RemoteInfo? = null
- uiState.enabledProviders.forEach {
- enabledProvider ->
- if (enabledProvider.createOptions.isNotEmpty()) {
- createOptionSize += enabledProvider.createOptions.size
- lastSeenProviderWithNonEmptyCreateOptions = enabledProvider
- }
- if (enabledProvider.remoteEntry != null) {
- remoteEntry = enabledProvider.remoteEntry!!
- }
- }
- uiState = uiState.copy(
- currentScreenState = CreateFlowUtils.toCreateScreenState(
- createOptionSize, true,
- uiState.requestDisplayInfo, null, remoteEntry),
- showActiveEntryOnly = createOptionSize > 1,
- activeEntry = CreateFlowUtils.toActiveEntry(
- null, createOptionSize, lastSeenProviderWithNonEmptyCreateOptions, remoteEntry),
- )
+ uiState = CreateFlowUtils.toCreateCredentialUiState(
+ uiState.enabledProviders, uiState.disabledProviders,
+ uiState.requestDisplayInfo, true)
+ UserConfigRepo.getInstance().setIsFirstUse(false)
}
fun getProviderInfoByName(providerName: String): EnabledProviderInfo {
return uiState.enabledProviders.single {
- it.name.equals(providerName)
+ it.name == providerName
}
}
@@ -116,6 +100,8 @@
showActiveEntryOnly = true,
activeEntry = activeEntry
)
+ val providerId = uiState.activeEntry?.activeProvider?.name
+ onDefaultChanged(providerId)
}
fun onDisabledPasswordManagerSelected() {
@@ -127,11 +113,29 @@
dialogResult.value = DialogResult(ResultState.CANCELED)
}
- fun onDefaultOrNotSelected() {
+ fun onChangeDefaultSelected() {
uiState = uiState.copy(
currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION,
)
- // TODO: implement the if choose as default or not logic later
+ val providerId = uiState.activeEntry?.activeProvider?.name
+ onDefaultChanged(providerId)
+ }
+
+ fun onUseOnceSelected() {
+ uiState = uiState.copy(
+ currentScreenState = CreateScreenState.CREATION_OPTION_SELECTION,
+ )
+ }
+
+ fun onDefaultChanged(providerId: String?) {
+ if (providerId != null) {
+ Log.d(
+ "Account Selector", "Default provider changed to: " +
+ " {provider=$providerId")
+ UserConfigRepo.getInstance().setDefaultProvider(providerId)
+ } else {
+ Log.w("Account Selector", "Null provider is being changed")
+ }
}
fun onEntrySelected(selectedEntry: EntryInfo) {
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt
index 21abe08..fda0b97 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateModel.kt
@@ -31,7 +31,6 @@
name: String,
displayName: String,
var createOptions: List<CreateOptionInfo>,
- val isDefault: Boolean,
var remoteEntry: RemoteInfo?,
) : ProviderInfo(icon, name, displayName)
@@ -77,7 +76,6 @@
val type: String,
val appDomainName: String,
val typeIcon: Drawable,
- val isFirstUsage: Boolean,
)
/**
diff --git a/packages/PackageInstaller/res/values/strings.xml b/packages/PackageInstaller/res/values/strings.xml
index 6a3b239..b713c14 100644
--- a/packages/PackageInstaller/res/values/strings.xml
+++ b/packages/PackageInstaller/res/values/strings.xml
@@ -119,6 +119,10 @@
<string name="uninstall_update_text_multiuser">Replace this app with the factory version? All data will be removed. This affects all users of this device, including those with work profiles.</string>
<!-- Label of a checkbox that allows to keep the data (e.g. files, settings) of the app on uninstall [CHAR LIMIT=none] -->
<string name="uninstall_keep_data">Keep <xliff:g id="size" example="1.5MB">%1$s</xliff:g> of app data.</string>
+ <!-- [CHAR LIMIT=none] -->
+ <string name="uninstall_application_text_current_user_clone_profile">Do you want to delete this app?</string>
+ <!-- [CHAR LIMIT=none] -->
+ <string name="uninstall_application_text_with_clone_instance">Do you want to uninstall this app? <xliff:g id="package_label">%1$s</xliff:g> clone will also be deleted.</string>
<!-- Label for the notification channel containing notifications for current uninstall operations [CHAR LIMIT=40] -->
<string name="uninstalling_notification_channel">Running uninstalls</string>
@@ -137,6 +141,8 @@
<string name="uninstall_failed">Uninstall unsuccessful.</string>
<!-- [CHAR LIMIT=100] -->
<string name="uninstall_failed_app">Uninstalling <xliff:g id="package_label">%1$s</xliff:g> unsuccessful.</string>
+ <!-- [CHAR LIMIT=100] -->
+ <string name="uninstalling_cloned_app">Deleting <xliff:g id="package_label">%1$s</xliff:g> clone\u2026</string>
<!-- String presented to the user when uninstalling a package failed because the target package
is a current device administrator [CHAR LIMIT=80] -->
<string name="uninstall_failed_device_policy_manager">Can\'t uninstall active device admin
@@ -219,6 +225,9 @@
TV or loss of data that may result from its use.
</string>
+ <!-- Label for cloned app in uninstall dialogue [CHAR LIMIT=40] -->
+ <string name="cloned_app_label"><xliff:g id="package_label">%1$s</xliff:g> Clone</string>
+
<!-- Label for button to continue install of an app whose source cannot be identified [CHAR LIMIT=40] -->
<string name="anonymous_source_continue">Continue</string>
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/UninstallUninstalling.java b/packages/PackageInstaller/src/com/android/packageinstaller/UninstallUninstalling.java
index 7bf27df..1485352 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/UninstallUninstalling.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/UninstallUninstalling.java
@@ -33,8 +33,10 @@
import android.content.pm.VersionedPackage;
import android.os.Bundle;
import android.os.IBinder;
+import android.os.Process;
import android.os.RemoteException;
import android.os.UserHandle;
+import android.os.UserManager;
import android.util.Log;
import android.widget.Toast;
@@ -51,6 +53,7 @@
static final String EXTRA_APP_LABEL = "com.android.packageinstaller.extra.APP_LABEL";
static final String EXTRA_KEEP_DATA = "com.android.packageinstaller.extra.KEEP_DATA";
+ public static final String EXTRA_IS_CLONE_USER = "isCloneUser";
private int mUninstallId;
private ApplicationInfo mAppInfo;
@@ -76,6 +79,18 @@
boolean keepData = getIntent().getBooleanExtra(EXTRA_KEEP_DATA, false);
UserHandle user = getIntent().getParcelableExtra(Intent.EXTRA_USER);
+ boolean isCloneUser = false;
+ if (user == null) {
+ user = Process.myUserHandle();
+ }
+
+ UserManager customUserManager = UninstallUninstalling.this
+ .createContextAsUser(UserHandle.of(user.getIdentifier()), 0)
+ .getSystemService(UserManager.class);
+ if (customUserManager.isUserOfType(UserManager.USER_TYPE_PROFILE_CLONE)) {
+ isCloneUser = true;
+ }
+
// Show dialog, which is the whole UI
FragmentTransaction transaction = getFragmentManager().beginTransaction();
Fragment prev = getFragmentManager().findFragmentByTag("dialog");
@@ -83,6 +98,9 @@
transaction.remove(prev);
}
DialogFragment dialog = new UninstallUninstallingFragment();
+ Bundle args = new Bundle();
+ args.putBoolean(EXTRA_IS_CLONE_USER, isCloneUser);
+ dialog.setArguments(args);
dialog.setCancelable(false);
dialog.show(transaction, "dialog");
@@ -176,9 +194,20 @@
public Dialog onCreateDialog(Bundle savedInstanceState) {
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getActivity());
+ Bundle bundle = getArguments();
+ boolean isCloneUser = false;
+ if (bundle != null) {
+ isCloneUser = bundle.getBoolean(EXTRA_IS_CLONE_USER);
+ }
+
dialogBuilder.setCancelable(false);
- dialogBuilder.setMessage(getActivity().getString(R.string.uninstalling_app,
- ((UninstallUninstalling) getActivity()).mLabel));
+ if (isCloneUser) {
+ dialogBuilder.setMessage(getActivity().getString(R.string.uninstalling_cloned_app,
+ ((UninstallUninstalling) getActivity()).mLabel));
+ } else {
+ dialogBuilder.setMessage(getActivity().getString(R.string.uninstalling_app,
+ ((UninstallUninstalling) getActivity()).mLabel));
+ }
Dialog dialog = dialogBuilder.create();
dialog.setCanceledOnTouchOutside(false);
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/handheld/UninstallAlertDialogFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/handheld/UninstallAlertDialogFragment.java
index c9230b4..a1bc992 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/handheld/UninstallAlertDialogFragment.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/handheld/UninstallAlertDialogFragment.java
@@ -123,6 +123,7 @@
messageBuilder.append(" ").append(appLabel).append(".\n\n");
}
}
+ boolean isClonedApp = false;
final boolean isUpdate =
((dialogInfo.appInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0);
@@ -144,16 +145,36 @@
messageBuilder.append(
getString(R.string.uninstall_application_text_current_user_work_profile,
userInfo.name));
+ } else if (userInfo.isCloneProfile()
+ && userInfo.profileGroupId == myUserHandle.getIdentifier()) {
+ isClonedApp = true;
+ messageBuilder.append(getString(
+ R.string.uninstall_application_text_current_user_clone_profile));
} else {
messageBuilder.append(
getString(R.string.uninstall_application_text_user, userInfo.name));
}
+ } else if (isCloneProfile(myUserHandle)) {
+ isClonedApp = true;
+ messageBuilder.append(getString(
+ R.string.uninstall_application_text_current_user_clone_profile));
} else {
- messageBuilder.append(getString(R.string.uninstall_application_text));
+ if (Process.myUserHandle().equals(UserHandle.SYSTEM)
+ && hasClonedInstance(dialogInfo.appInfo.packageName)) {
+ messageBuilder.append(getString(
+ R.string.uninstall_application_text_with_clone_instance,
+ appLabel));
+ } else {
+ messageBuilder.append(getString(R.string.uninstall_application_text));
+ }
}
}
- dialogBuilder.setTitle(appLabel);
+ if (isClonedApp) {
+ dialogBuilder.setTitle(getString(R.string.cloned_app_label, appLabel));
+ } else {
+ dialogBuilder.setTitle(appLabel);
+ }
dialogBuilder.setPositiveButton(android.R.string.ok, this);
dialogBuilder.setNegativeButton(android.R.string.cancel, this);
@@ -192,6 +213,42 @@
return dialogBuilder.create();
}
+ private boolean isCloneProfile(UserHandle userHandle) {
+ UserManager customUserManager = getContext()
+ .createContextAsUser(UserHandle.of(userHandle.getIdentifier()), 0)
+ .getSystemService(UserManager.class);
+ if (customUserManager.isUserOfType(UserManager.USER_TYPE_PROFILE_CLONE)) {
+ return true;
+ }
+ return false;
+ }
+
+ private boolean hasClonedInstance(String packageName) {
+ // Check if clone user is present on the device.
+ UserHandle cloneUser = null;
+ UserManager userManager = getContext().getSystemService(UserManager.class);
+ List<UserHandle> profiles = userManager.getUserProfiles();
+ for (UserHandle userHandle : profiles) {
+ if (!Process.myUserHandle().equals(UserHandle.SYSTEM) && isCloneProfile(userHandle)) {
+ cloneUser = userHandle;
+ break;
+ }
+ }
+
+ // Check if another instance of given package exists in clone user profile.
+ if (cloneUser != null) {
+ try {
+ if (getContext().getPackageManager()
+ .getPackageUidAsUser(packageName, cloneUser.getIdentifier()) > 0) {
+ return true;
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ return false;
+ }
+ }
+ return false;
+ }
+
@Override
public void onClick(DialogInterface dialog, int which) {
if (which == Dialog.BUTTON_POSITIVE) {
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt
index 5ae5ada..62db7bd 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt
@@ -93,7 +93,7 @@
) {
val context = LocalContext.current
val internalListModel = remember {
- TogglePermissionInternalAppListModel(context, listModel)
+ TogglePermissionInternalAppListModel(context, listModel, ::RestrictionsProviderImpl)
}
val record = remember { listModel.transformItem(app) }
if (!remember { listModel.isChangeable(record) }) return
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPage.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPage.kt
index 00eb607..f65e310 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPage.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPage.kt
@@ -42,6 +42,7 @@
import com.android.settingslib.spaprivileged.model.app.AppRecord
import com.android.settingslib.spaprivileged.model.app.userId
import com.android.settingslib.spaprivileged.model.enterprise.Restrictions
+import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderFactory
import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderImpl
import com.android.settingslib.spaprivileged.template.preference.RestrictedSwitchPreference
import kotlinx.coroutines.flow.Flow
@@ -64,41 +65,18 @@
val permissionType = parameter.getStringArg(PERMISSION, arguments)!!
val appListPage = SettingsPage.create(name, parameter = parameter, arguments = arguments)
val appInfoPage = TogglePermissionAppInfoPageProvider.buildPageData(permissionType)
- val entryList = mutableListOf<SettingsEntry>()
// TODO: add more categories, such as personal, work, cloned, etc.
- for (category in listOf("personal")) {
- entryList.add(
- SettingsEntryBuilder.createLinkFrom("${ENTRY_NAME}_$category", appListPage)
- .setLink(toPage = appInfoPage)
- .build()
- )
+ return listOf("personal").map { category ->
+ SettingsEntryBuilder.createLinkFrom("${ENTRY_NAME}_$category", appListPage)
+ .setLink(toPage = appInfoPage)
+ .build()
}
- return entryList
}
@Composable
override fun Page(arguments: Bundle?) {
- TogglePermissionAppList(arguments?.getString(PERMISSION)!!)
- }
-
- @Composable
- private fun TogglePermissionAppList(permissionType: String) {
- val listModel = appListTemplate.rememberModel(permissionType)
- val context = LocalContext.current
- val internalListModel = remember {
- TogglePermissionInternalAppListModel(context, listModel)
- }
- AppListPage(
- title = stringResource(listModel.pageTitleResId),
- listModel = internalListModel,
- ) {
- AppListItem(
- onClick = TogglePermissionAppInfoPageProvider.navigator(
- permissionType = permissionType,
- app = record.app,
- ),
- )
- }
+ val permissionType = arguments?.getString(PERMISSION)!!
+ appListTemplate.rememberModel(permissionType).TogglePermissionAppList(permissionType)
}
companion object {
@@ -132,9 +110,34 @@
}
}
+@Composable
+internal fun <T : AppRecord> TogglePermissionAppListModel<T>.TogglePermissionAppList(
+ permissionType: String,
+ restrictionsProviderFactory: RestrictionsProviderFactory = ::RestrictionsProviderImpl,
+ appList: @Composable AppListInput<T>.() -> Unit = { AppList() },
+) {
+ val context = LocalContext.current
+ val internalListModel = remember {
+ TogglePermissionInternalAppListModel(context, this, restrictionsProviderFactory)
+ }
+ AppListPage(
+ title = stringResource(pageTitleResId),
+ listModel = internalListModel,
+ appList = appList,
+ ) {
+ AppListItem(
+ onClick = TogglePermissionAppInfoPageProvider.navigator(
+ permissionType = permissionType,
+ app = record.app,
+ ),
+ )
+ }
+}
+
internal class TogglePermissionInternalAppListModel<T : AppRecord>(
private val context: Context,
private val listModel: TogglePermissionAppListModel<T>,
+ private val restrictionsProviderFactory: RestrictionsProviderFactory,
) : AppListModel<T> {
override fun transform(userIdFlow: Flow<Int>, appListFlow: Flow<List<ApplicationInfo>>) =
listModel.transform(userIdFlow, appListFlow)
@@ -147,12 +150,12 @@
@Composable
fun getSummary(record: T): State<String> {
- val restrictionsProvider = remember {
+ val restrictionsProvider = remember(record.app.userId) {
val restrictions = Restrictions(
userId = record.app.userId,
keys = listModel.switchRestrictionKeys,
)
- RestrictionsProviderImpl(context, restrictions)
+ restrictionsProviderFactory(context, restrictions)
}
val restrictedMode = restrictionsProvider.restrictedModeState()
val allowed = listModel.isAllowed(record)
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/scaffold/RestrictedMenuItem.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/scaffold/RestrictedMenuItem.kt
index 8c1421a..86b6f02 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/scaffold/RestrictedMenuItem.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/scaffold/RestrictedMenuItem.kt
@@ -16,7 +16,6 @@
package com.android.settingslib.spaprivileged.template.scaffold
-import android.content.Context
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
@@ -24,7 +23,7 @@
import com.android.settingslib.spaprivileged.model.enterprise.BaseUserRestricted
import com.android.settingslib.spaprivileged.model.enterprise.BlockedByAdmin
import com.android.settingslib.spaprivileged.model.enterprise.Restrictions
-import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProvider
+import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderFactory
import com.android.settingslib.spaprivileged.model.enterprise.RestrictionsProviderImpl
@Composable
@@ -41,7 +40,7 @@
text: String,
restrictions: Restrictions,
onClick: () -> Unit,
- restrictionsProviderFactory: (Context, Restrictions) -> RestrictionsProvider,
+ restrictionsProviderFactory: RestrictionsProviderFactory,
) {
val context = LocalContext.current
val restrictionsProvider = remember(restrictions) {
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPageTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPageTest.kt
index 355dfb6..75b884c 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPageTest.kt
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppListPageTest.kt
@@ -17,14 +17,22 @@
package com.android.settingslib.spaprivileged.template.app
import android.content.Context
+import android.content.pm.ApplicationInfo
import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.State
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.android.settingslib.spaprivileged.test.R
+import com.android.settingslib.spa.framework.compose.stateOf
+import com.android.settingslib.spa.testutils.FakeNavControllerWrapper
+import com.android.settingslib.spaprivileged.R
+import com.android.settingslib.spaprivileged.model.enterprise.NoRestricted
+import com.android.settingslib.spaprivileged.tests.testutils.FakeRestrictionsProvider
+import com.android.settingslib.spaprivileged.tests.testutils.TestAppRecord
import com.android.settingslib.spaprivileged.tests.testutils.TestTogglePermissionAppListModel
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
@@ -38,10 +46,97 @@
private val context: Context = ApplicationProvider.getApplicationContext()
+ private val fakeNavControllerWrapper = FakeNavControllerWrapper()
+
+ private val fakeRestrictionsProvider = FakeRestrictionsProvider()
+
@Test
- fun appListInjectEntry_titleDisplayed() {
+ fun internalAppListModel_whenAllowed() {
+ fakeRestrictionsProvider.restrictedMode = NoRestricted
+ val listModel = TestTogglePermissionAppListModel(isAllowed = true)
+ val internalAppListModel = TogglePermissionInternalAppListModel(
+ context = context,
+ listModel = listModel,
+ restrictionsProviderFactory = { _, _ -> fakeRestrictionsProvider },
+ )
+
+ val summaryState = getSummary(internalAppListModel)
+
+ assertThat(summaryState.value).isEqualTo(
+ context.getString(R.string.app_permission_summary_allowed)
+ )
+ }
+
+ @Test
+ fun internalAppListModel_whenNotAllowed() {
+ fakeRestrictionsProvider.restrictedMode = NoRestricted
+ val listModel = TestTogglePermissionAppListModel(isAllowed = false)
+ val internalAppListModel = TogglePermissionInternalAppListModel(
+ context = context,
+ listModel = listModel,
+ restrictionsProviderFactory = { _, _ -> fakeRestrictionsProvider },
+ )
+
+ val summaryState = getSummary(internalAppListModel)
+
+ assertThat(summaryState.value).isEqualTo(
+ context.getString(R.string.app_permission_summary_not_allowed)
+ )
+ }
+
+ @Test
+ fun internalAppListModel_whenComputingAllowed() {
+ fakeRestrictionsProvider.restrictedMode = NoRestricted
+ val listModel = TestTogglePermissionAppListModel(isAllowed = null)
+ val internalAppListModel = TogglePermissionInternalAppListModel(
+ context = context,
+ listModel = listModel,
+ restrictionsProviderFactory = { _, _ -> fakeRestrictionsProvider },
+ )
+
+ val summaryState = getSummary(internalAppListModel)
+
+ assertThat(summaryState.value).isEqualTo(
+ context.getString(R.string.summary_placeholder)
+ )
+ }
+
+ @Test
+ fun appListItem_onClick_navigate() {
+ val listModel = TestTogglePermissionAppListModel()
+ composeTestRule.setContent {
+ listModel.TogglePermissionAppList(
+ permissionType = PERMISSION_TYPE,
+ restrictionsProviderFactory = { _, _ -> fakeRestrictionsProvider },
+ ) {
+ fakeNavControllerWrapper.Wrapper {
+ AppListItemModel(
+ record = listModel.transformItem(APP),
+ label = LABEL,
+ summary = stateOf(SUMMARY),
+ ).appItem()
+ }
+ }
+ }
+
+ composeTestRule.onNodeWithText(LABEL).performClick()
+
+ assertThat(fakeNavControllerWrapper.navigateCalledWith)
+ .isEqualTo("TogglePermissionAppInfoPage/test.PERMISSION/package.name/0")
+ }
+
+ @Test
+ fun getRoute() {
+ val route = TogglePermissionAppListPageProvider.getRoute(PERMISSION_TYPE)
+
+ assertThat(route).isEqualTo("TogglePermissionAppList/test.PERMISSION")
+ }
+
+ @Test
+ fun buildInjectEntry_titleDisplayed() {
+ val listModel = TestTogglePermissionAppListModel()
val entry = TogglePermissionAppListPageProvider.buildInjectEntry(PERMISSION_TYPE) {
- TestTogglePermissionAppListModel()
+ listModel
}.build()
composeTestRule.setContent {
@@ -50,18 +145,27 @@
}
}
- composeTestRule.onNodeWithText(context.getString(R.string.test_permission_title))
+ composeTestRule.onNodeWithText(context.getString(listModel.pageTitleResId))
.assertIsDisplayed()
}
- @Test
- fun appListRoute() {
- val route = TogglePermissionAppListPageProvider.getRoute(PERMISSION_TYPE)
-
- assertThat(route).isEqualTo("TogglePermissionAppList/test.PERMISSION")
+ private fun getSummary(
+ internalAppListModel: TogglePermissionInternalAppListModel<TestAppRecord>,
+ ): State<String> {
+ lateinit var summary: State<String>
+ composeTestRule.setContent {
+ summary = internalAppListModel.getSummary(record = TestAppRecord(APP))
+ }
+ return summary
}
private companion object {
const val PERMISSION_TYPE = "test.PERMISSION"
+ const val PACKAGE_NAME = "package.name"
+ const val LABEL = "Label"
+ const val SUMMARY = "Summary"
+ val APP = ApplicationInfo().apply {
+ packageName = PACKAGE_NAME
+ }
}
}
diff --git a/packages/SettingsLib/res/values-af/strings.xml b/packages/SettingsLib/res/values-af/strings.xml
index 0234330..fb46dfb 100644
--- a/packages/SettingsLib/res/values-af/strings.xml
+++ b/packages/SettingsLib/res/values-af/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> oor tot vol"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="TIME">%2$s</xliff:g> oor tot vol"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> – Laaiproses is onderbreek"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - Laaiproses is onderbreek"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – Laai tot <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Onbekend"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Laai"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Laai tans vinnig"</string>
diff --git a/packages/SettingsLib/res/values-am/strings.xml b/packages/SettingsLib/res/values-am/strings.xml
index 29b188c..fa53f0d 100644
--- a/packages/SettingsLib/res/values-am/strings.xml
+++ b/packages/SettingsLib/res/values-am/strings.xml
@@ -233,7 +233,7 @@
<string name="adb_pair_method_qrcode_title" msgid="6982904096137468634">"የQR ኮድን በመጠቀም መሣሪያን ያጣምሩ"</string>
<string name="adb_pair_method_qrcode_summary" msgid="7130694277228970888">"የQR ኮድ መቃኛን በመጠቀም አዲስ መሣሪያዎችን ያጣምሩ"</string>
<string name="adb_pair_method_code_title" msgid="1122590300445142904">"የማጣመሪያ ኮድን በመጠቀም መሣሪያን ያጣምሩ"</string>
- <string name="adb_pair_method_code_summary" msgid="6370414511333685185">"የስድስት አኃዝ ኮድ በመጠቀም አዲስ መሣሪያዎችን ያጣምሩ"</string>
+ <string name="adb_pair_method_code_summary" msgid="6370414511333685185">"የስድስት አሃዝ ኮድ በመጠቀም አዲስ መሣሪያዎችን ያጣምሩ"</string>
<string name="adb_paired_devices_title" msgid="5268997341526217362">"የተጣመሩ መሣሪያዎች"</string>
<string name="adb_wireless_device_connected_summary" msgid="3039660790249148713">"አሁን ላይ ተገናኝቷል"</string>
<string name="adb_wireless_device_details_title" msgid="7129369670526565786">"የመሣሪያ ዝርዝሮች"</string>
@@ -344,7 +344,7 @@
<string name="wait_for_debugger" msgid="7461199843335409809">"ስህተት ማስወገጃውን ጠብቅ"</string>
<string name="wait_for_debugger_summary" msgid="6846330006113363286">"ስህተቱ የተወገደለት መተግበሪያ ከመፈጸሙ በፊት የስህተት ማስወገጃው እስኪያያዝ ድረስ እየጠበቀው ነው"</string>
<string name="debug_input_category" msgid="7349460906970849771">"ግብዓት"</string>
- <string name="debug_drawing_category" msgid="5066171112313666619">"ስዕል"</string>
+ <string name="debug_drawing_category" msgid="5066171112313666619">"ሥዕል"</string>
<string name="debug_hw_drawing_category" msgid="5830815169336975162">"የተፋጠነ የሃርድዌር አሰጣጥ"</string>
<string name="media_category" msgid="8122076702526144053">"ማህደረመረጃ"</string>
<string name="debug_monitoring_category" msgid="1597387133765424994">"ቁጥጥር"</string>
@@ -467,7 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"እስኪሞላ ድረስ <xliff:g id="TIME">%1$s</xliff:g> ይቀራል"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - እስኪሞላ ድረስ <xliff:g id="TIME">%2$s</xliff:g> ይቀራል"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - ኃይል መሙላት ባለበት ቆሟል"</string>
+ <!-- no translation found for power_charging_limited (6732738149313642521) -->
+ <skip />
<!-- no translation found for power_charging_future_paused (6829683663982987290) -->
<skip />
<string name="battery_info_status_unknown" msgid="268625384868401114">"ያልታወቀ"</string>
@@ -548,10 +549,10 @@
<string name="shared_data_title" msgid="1017034836800864953">"የተጋራ ውሂብ"</string>
<string name="shared_data_summary" msgid="5516326713822885652">"የተጋራ ውሂብን ይመልከቱ እና ያሻሽሉ"</string>
<string name="shared_data_no_blobs_text" msgid="3108114670341737434">"ለዚህ ተጠቃሚ ምንም የተጋራ ውሂብ የለም።"</string>
- <string name="shared_data_query_failure_text" msgid="3489828881998773687">"የተጋራውን ውሂብ በማግኘት ላይ ስሕተት ነበረ። እንደገና ይሞክሩ።"</string>
+ <string name="shared_data_query_failure_text" msgid="3489828881998773687">"የተጋራውን ውሂብ በማግኘት ላይ ስህተት ነበረ። እንደገና ይሞክሩ።"</string>
<string name="blob_id_text" msgid="8680078988996308061">"የተጋራ ውሂብ መታወቂያ፦ <xliff:g id="BLOB_ID">%d</xliff:g>"</string>
<string name="blob_expires_text" msgid="7882727111491739331">"በ<xliff:g id="DATE">%s</xliff:g> ላይ የአገልግሎት ጊዜው ያበቃል"</string>
- <string name="shared_data_delete_failure_text" msgid="3842701391009628947">"የተጋራውን ውሂብ በመሰረዝ ላይ ስሕተት ነበረ።"</string>
+ <string name="shared_data_delete_failure_text" msgid="3842701391009628947">"የተጋራውን ውሂብ በመሰረዝ ላይ ስህተት ነበረ።"</string>
<string name="shared_data_no_accessors_dialog_text" msgid="8903738462570715315">"ለዚህ የተጋራ ውሂብ ምንም የሚያስፈልጉ ኪራዮች የሉም። ሊሰርዙት ይፈልጋሉ?"</string>
<string name="accessor_info_title" msgid="8289823651512477787">"ውሂብ የሚጋሩ መተግበሪያዎች"</string>
<string name="accessor_no_description_text" msgid="7510967452505591456">"በመተግበሪያው ምንም ዝርዝር መረጃ አልተሰጠም።"</string>
diff --git a/packages/SettingsLib/res/values-ar/strings.xml b/packages/SettingsLib/res/values-ar/strings.xml
index 28a3f18..88db7be 100644
--- a/packages/SettingsLib/res/values-ar/strings.xml
+++ b/packages/SettingsLib/res/values-ar/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"يتبقّى <xliff:g id="TIME">%1$s</xliff:g> حتى اكتمال شحن البطارية."</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - يتبقّى <xliff:g id="TIME">%2$s</xliff:g> حتى اكتمال شحن البطارية."</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - تم إيقاف الشحن مؤقتًا"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - الشحن متوقّف مؤقتًا"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - الشحن حتى <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"غير معروف"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"جارٍ الشحن"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"جارٍ الشحن سريعًا"</string>
diff --git a/packages/SettingsLib/res/values-as/strings.xml b/packages/SettingsLib/res/values-as/strings.xml
index fc54e04..3f068b4 100644
--- a/packages/SettingsLib/res/values-as/strings.xml
+++ b/packages/SettingsLib/res/values-as/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"সম্পূৰ্ণ হ’বলৈ <xliff:g id="TIME">%1$s</xliff:g> বাকী আছে"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"সম্পূৰ্ণ হ’বলৈ <xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="TIME">%2$s</xliff:g> বাকী আছে"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - চাৰ্জিং পজ কৰা হৈছে"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - চাৰ্জিং পজ কৰা হৈছে"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>লৈ চাৰ্জিং"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"অজ্ঞাত"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"চাৰ্জ কৰি থকা হৈছে"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"দ্ৰুততাৰে চাৰ্জ হৈছে"</string>
diff --git a/packages/SettingsLib/res/values-az/strings.xml b/packages/SettingsLib/res/values-az/strings.xml
index e2e294f..8eb8d5b 100644
--- a/packages/SettingsLib/res/values-az/strings.xml
+++ b/packages/SettingsLib/res/values-az/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"Tam şarj edilənədək <xliff:g id="TIME">%1$s</xliff:g> qalıb"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - tam şarj edilənədək <xliff:g id="TIME">%2$s</xliff:g> qalıb"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Şarj durdurulub"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - Şarj durdurulub"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g> olana qədər şarj edilir"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Naməlum"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Enerji doldurma"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Sürətlə doldurulur"</string>
diff --git a/packages/SettingsLib/res/values-b+sr+Latn/strings.xml b/packages/SettingsLib/res/values-b+sr+Latn/strings.xml
index 479c9ca..bbdd8bf 100644
--- a/packages/SettingsLib/res/values-b+sr+Latn/strings.xml
+++ b/packages/SettingsLib/res/values-b+sr+Latn/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> do kraja punjenja"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="TIME">%2$s</xliff:g> do kraja punjenja"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> – Punjenje je zaustavljeno"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – Punjenje je pauzirano"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – punjenje do <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Nepoznato"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Puni se"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Brzo se puni"</string>
diff --git a/packages/SettingsLib/res/values-be/strings.xml b/packages/SettingsLib/res/values-be/strings.xml
index b45996c..28c1e86 100644
--- a/packages/SettingsLib/res/values-be/strings.xml
+++ b/packages/SettingsLib/res/values-be/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"Да поўнай зарадкі засталося <xliff:g id="TIME">%1$s</xliff:g>"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – да поўнай зарадкі засталося: <xliff:g id="TIME">%2$s</xliff:g>"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Зарадка прыпынена"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - Зарадка прыпынена"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – зарадка да <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Невядома"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Зарадка"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Хуткая зарадка"</string>
diff --git a/packages/SettingsLib/res/values-bg/strings.xml b/packages/SettingsLib/res/values-bg/strings.xml
index ecb5208..61e441d 100644
--- a/packages/SettingsLib/res/values-bg/strings.xml
+++ b/packages/SettingsLib/res/values-bg/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"Оставащо време до пълно зареждане: <xliff:g id="TIME">%1$s</xliff:g>"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – Оставащо време до пълно зареждане: <xliff:g id="TIME">%2$s</xliff:g>"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g>: Зареждането е на пауза"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – зареждането е на пауза"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – зарежда се до <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Неизвестно"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Зарежда се"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Зарежда се бързо"</string>
diff --git a/packages/SettingsLib/res/values-bn/strings.xml b/packages/SettingsLib/res/values-bn/strings.xml
index fda27ed..a9d32a9 100644
--- a/packages/SettingsLib/res/values-bn/strings.xml
+++ b/packages/SettingsLib/res/values-bn/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g>-এ ব্যাটারি পুরো চার্জ হয়ে যাবে"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="TIME">%2$s</xliff:g>-এ ব্যাটারি পুরো চার্জ হয়ে যাবে"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - চার্জিং পজ করা হয়েছে"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - চার্জিং পজ করা হয়েছে"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g> পর্যন্ত চার্জ হচ্ছে"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"অজানা"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"চার্জ হচ্ছে"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"দ্রুত চার্জ হচ্ছে"</string>
diff --git a/packages/SettingsLib/res/values-bs/strings.xml b/packages/SettingsLib/res/values-bs/strings.xml
index cae96e9..b5c9177 100644
--- a/packages/SettingsLib/res/values-bs/strings.xml
+++ b/packages/SettingsLib/res/values-bs/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> do potpune napunjenosti"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="TIME">%2$s</xliff:g> do potpune napunjenosti"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Punjenje je pauzirano"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - Punjenje je pauzirano"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – Punjenje do <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Nepoznato"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Punjenje"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Brzo punjenje"</string>
diff --git a/packages/SettingsLib/res/values-ca/strings.xml b/packages/SettingsLib/res/values-ca/strings.xml
index 51a4a02..8add8c8 100644
--- a/packages/SettingsLib/res/values-ca/strings.xml
+++ b/packages/SettingsLib/res/values-ca/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g>: <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> per completar la càrrega"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g>: <xliff:g id="TIME">%2$s</xliff:g> per completar la càrrega"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g>: la càrrega s\'ha posat en pausa"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g>: la càrrega s\'ha posat en pausa"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g>: s\'està carregant fins al <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Desconegut"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"S\'està carregant"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Carregant ràpidament"</string>
diff --git a/packages/SettingsLib/res/values-cs/strings.xml b/packages/SettingsLib/res/values-cs/strings.xml
index a50e527..4dc3c66 100644
--- a/packages/SettingsLib/res/values-cs/strings.xml
+++ b/packages/SettingsLib/res/values-cs/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> do úplného nabití"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="TIME">%2$s</xliff:g> do úplného nabití"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> – Nabíjení je pozastaveno"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – Nabíjení pozastaveno"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – nabíjení do <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Neznámé"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Nabíjí se"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Rychlé nabíjení"</string>
diff --git a/packages/SettingsLib/res/values-da/strings.xml b/packages/SettingsLib/res/values-da/strings.xml
index 95e0130..9ab499a 100644
--- a/packages/SettingsLib/res/values-da/strings.xml
+++ b/packages/SettingsLib/res/values-da/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"Fuldt opladet om <xliff:g id="TIME">%1$s</xliff:g>"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – fuldt opladet om <xliff:g id="TIME">%2$s</xliff:g>"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Opladningen er sat på pause"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – Opladning er sat på pause"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – Oplader til <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Ukendt"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Oplader"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Oplader hurtigt"</string>
diff --git a/packages/SettingsLib/res/values-de/strings.xml b/packages/SettingsLib/res/values-de/strings.xml
index 12fce37..68d61c0 100644
--- a/packages/SettingsLib/res/values-de/strings.xml
+++ b/packages/SettingsLib/res/values-de/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"Voll in <xliff:g id="TIME">%1$s</xliff:g>"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – voll in <xliff:g id="TIME">%2$s</xliff:g>"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> – Ladevorgang angehalten"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – Laden pausiert"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – Aufladung auf <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Unbekannt"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Wird aufgeladen"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Schnelles Aufladen"</string>
diff --git a/packages/SettingsLib/res/values-el/strings.xml b/packages/SettingsLib/res/values-el/strings.xml
index f54d5d2..59ae638 100644
--- a/packages/SettingsLib/res/values-el/strings.xml
+++ b/packages/SettingsLib/res/values-el/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"Απομένουν <xliff:g id="TIME">%1$s</xliff:g> για πλήρη φόρτιση"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - Απομένουν <xliff:g id="TIME">%2$s</xliff:g> για πλήρη φόρτιση"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Η φόρτιση τέθηκε σε παύση"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - Φόρτιση σε παύση"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - Φόρτιση έως το <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Άγνωστο"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Φόρτιση"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Ταχεία φόρτιση"</string>
diff --git a/packages/SettingsLib/res/values-en-rAU/strings.xml b/packages/SettingsLib/res/values-en-rAU/strings.xml
index 208dbfa..3c98c25 100644
--- a/packages/SettingsLib/res/values-en-rAU/strings.xml
+++ b/packages/SettingsLib/res/values-en-rAU/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> left until full"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="TIME">%2$s</xliff:g> left until full"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Charging is paused"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – charging paused"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – charging to <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Unknown"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Charging"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Charging rapidly"</string>
diff --git a/packages/SettingsLib/res/values-en-rCA/strings.xml b/packages/SettingsLib/res/values-en-rCA/strings.xml
index aac80ab..833a9e4 100644
--- a/packages/SettingsLib/res/values-en-rCA/strings.xml
+++ b/packages/SettingsLib/res/values-en-rCA/strings.xml
@@ -467,7 +467,7 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> left until full"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="TIME">%2$s</xliff:g> left until full"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Charging is paused"</string>
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - Charging paused"</string>
<string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - Charging to <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Unknown"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Charging"</string>
diff --git a/packages/SettingsLib/res/values-en-rGB/strings.xml b/packages/SettingsLib/res/values-en-rGB/strings.xml
index 208dbfa..3c98c25 100644
--- a/packages/SettingsLib/res/values-en-rGB/strings.xml
+++ b/packages/SettingsLib/res/values-en-rGB/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> left until full"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="TIME">%2$s</xliff:g> left until full"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Charging is paused"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – charging paused"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – charging to <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Unknown"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Charging"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Charging rapidly"</string>
diff --git a/packages/SettingsLib/res/values-en-rIN/strings.xml b/packages/SettingsLib/res/values-en-rIN/strings.xml
index 208dbfa..3c98c25 100644
--- a/packages/SettingsLib/res/values-en-rIN/strings.xml
+++ b/packages/SettingsLib/res/values-en-rIN/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> left until full"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="TIME">%2$s</xliff:g> left until full"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Charging is paused"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – charging paused"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – charging to <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Unknown"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Charging"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Charging rapidly"</string>
diff --git a/packages/SettingsLib/res/values-en-rXC/strings.xml b/packages/SettingsLib/res/values-en-rXC/strings.xml
index bfd7e83..b856eb1 100644
--- a/packages/SettingsLib/res/values-en-rXC/strings.xml
+++ b/packages/SettingsLib/res/values-en-rXC/strings.xml
@@ -467,7 +467,7 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> left until full"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="TIME">%2$s</xliff:g> left until full"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Charging is paused"</string>
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - Charging paused"</string>
<string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - Charging to <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Unknown"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Charging"</string>
diff --git a/packages/SettingsLib/res/values-es-rUS/strings.xml b/packages/SettingsLib/res/values-es-rUS/strings.xml
index c11d6eb0..78af70a 100644
--- a/packages/SettingsLib/res/values-es-rUS/strings.xml
+++ b/packages/SettingsLib/res/values-es-rUS/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g>: <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> para completar"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="TIME">%2$s</xliff:g> para completar"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Se pausó la carga"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - Se pausó la carga"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - Cargando hasta <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Desconocido"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Cargando"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Cargando rápidamente"</string>
diff --git a/packages/SettingsLib/res/values-es/strings.xml b/packages/SettingsLib/res/values-es/strings.xml
index 1ef351a..ea3047b 100644
--- a/packages/SettingsLib/res/values-es/strings.xml
+++ b/packages/SettingsLib/res/values-es/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> hasta la carga completa"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g>: <xliff:g id="TIME">%2$s</xliff:g> hasta la carga completa"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Carga en pausa"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - Carga pausada"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - Cargando hasta <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Desconocido"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Cargando"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Carga rápida"</string>
diff --git a/packages/SettingsLib/res/values-et/strings.xml b/packages/SettingsLib/res/values-et/strings.xml
index e156011..78ade67 100644
--- a/packages/SettingsLib/res/values-et/strings.xml
+++ b/packages/SettingsLib/res/values-et/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"Täislaadimiseks kulub <xliff:g id="TIME">%1$s</xliff:g>"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – täislaadimiseks kulub <xliff:g id="TIME">%2$s</xliff:g>"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> – laadimine on peatatud"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – laadimine peatati"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – laadimine tasemeni <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Tundmatu"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Laadimine"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Kiirlaadimine"</string>
diff --git a/packages/SettingsLib/res/values-eu/strings.xml b/packages/SettingsLib/res/values-eu/strings.xml
index e5f399c..748f991 100644
--- a/packages/SettingsLib/res/values-eu/strings.xml
+++ b/packages/SettingsLib/res/values-eu/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> guztiz kargatu arte"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="TIME">%2$s</xliff:g> guztiz kargatu arte"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Kargatze-prozesua etenda dago"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - Kargatze-prozesua pausatuta dago"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g> arte kargatzen"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Ezezaguna"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Kargatzen"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Bizkor kargatzen"</string>
diff --git a/packages/SettingsLib/res/values-fa/strings.xml b/packages/SettingsLib/res/values-fa/strings.xml
index ccacfde..44e61ef 100644
--- a/packages/SettingsLib/res/values-fa/strings.xml
+++ b/packages/SettingsLib/res/values-fa/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> تا شارژ کامل باقی مانده است"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="TIME">%2$s</xliff:g> تا شارژ کامل باقی مانده است"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - شارژ موقتاً متوقف شده است"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - شارژ موقتاً متوقف شد"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - درحال شارژ تا <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"ناشناس"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"در حال شارژ شدن"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"درحال شارژ شدن سریع"</string>
diff --git a/packages/SettingsLib/res/values-fi/strings.xml b/packages/SettingsLib/res/values-fi/strings.xml
index 101dc67..2599eb3 100644
--- a/packages/SettingsLib/res/values-fi/strings.xml
+++ b/packages/SettingsLib/res/values-fi/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> kunnes täynnä"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="TIME">%2$s</xliff:g> kunnes täynnä"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> – Lataus on keskeytetty"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – Lataaminen keskeytetty"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – Tavoite: <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Tuntematon"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Ladataan"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Nopea lataus"</string>
diff --git a/packages/SettingsLib/res/values-fr-rCA/strings.xml b/packages/SettingsLib/res/values-fr-rCA/strings.xml
index ff58a51..314e8d1 100644
--- a/packages/SettingsLib/res/values-fr-rCA/strings.xml
+++ b/packages/SettingsLib/res/values-fr-rCA/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> jusqu\'à la recharge complète"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> (<xliff:g id="TIME">%2$s</xliff:g> jusqu\'à la recharge complète)"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - La recharge est interrompue"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – Recharge interrompue"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - En charge jusqu\'à <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Inconnu"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Charge en cours…"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Recharge rapide"</string>
diff --git a/packages/SettingsLib/res/values-fr/strings.xml b/packages/SettingsLib/res/values-fr/strings.xml
index f9725f6..5cab1de 100644
--- a/packages/SettingsLib/res/values-fr/strings.xml
+++ b/packages/SettingsLib/res/values-fr/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"Chargée à 100 %% dans <xliff:g id="TIME">%1$s</xliff:g>"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - chargée à 100 %% dans <xliff:g id="TIME">%2$s</xliff:g>"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> – La recharge est en pause"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – Recharge interrompue"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - Recharge jusqu\'à <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Inconnu"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Batterie en charge"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Charge rapide"</string>
diff --git a/packages/SettingsLib/res/values-gl/strings.xml b/packages/SettingsLib/res/values-gl/strings.xml
index 3ceb99c..52bdaf6 100644
--- a/packages/SettingsLib/res/values-gl/strings.xml
+++ b/packages/SettingsLib/res/values-gl/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> para completar a carga"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> (<xliff:g id="TIME">%2$s</xliff:g> para completar a carga)"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g>: a carga está en pausa"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g>: Carga en pausa"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g>: Cargando ata o <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Descoñecido"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Cargando"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Cargando rapidamente"</string>
diff --git a/packages/SettingsLib/res/values-gu/strings.xml b/packages/SettingsLib/res/values-gu/strings.xml
index 1821546..5a875a7 100644
--- a/packages/SettingsLib/res/values-gu/strings.xml
+++ b/packages/SettingsLib/res/values-gu/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"પૂર્ણ ચાર્જ થવામાં <xliff:g id="TIME">%1$s</xliff:g> બાકી છે"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - પૂર્ણ ચાર્જ થવામાં <xliff:g id="TIME">%2$s</xliff:g> બાકી છે"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - ચાર્જિંગ થોભાવવામાં આવ્યું છે"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - ચાર્જિંગ થોભાવેલું છે"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g> સુધી ચાર્જિંગ"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"અજાણ્યું"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"ચાર્જ થઈ રહ્યું છે"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"ઝડપથી ચાર્જ થાય છે"</string>
diff --git a/packages/SettingsLib/res/values-hi/strings.xml b/packages/SettingsLib/res/values-hi/strings.xml
index 4b899bb..0a38d16 100644
--- a/packages/SettingsLib/res/values-hi/strings.xml
+++ b/packages/SettingsLib/res/values-hi/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> में बैटरी पूरी चार्ज हो जाएगी"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="TIME">%2$s</xliff:g> में बैटरी पूरी चार्ज हो जाएगी"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - चार्जिंग को रोका गया है"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - चार्जिंग रोकी गई है"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g> तक चार्ज किया जा रहा है"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"अज्ञात"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"चार्ज हो रही है"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"तेज़ चार्ज हो रही है"</string>
@@ -602,7 +601,7 @@
<string name="guest_exit_clear_data_button" msgid="3425812652180679014">"मिटाएं"</string>
<string name="guest_exit_save_data_button" msgid="3690974510644963547">"सेव करें"</string>
<string name="guest_exit_button" msgid="5774985819191803960">"मेहमान मोड से बाहर निकलें"</string>
- <string name="guest_reset_button" msgid="2515069346223503479">"मेहमान मोड के सेशन को रीसेट करें?"</string>
+ <string name="guest_reset_button" msgid="2515069346223503479">"मेहमान मोड के सेशन को रीसेट करें"</string>
<string name="guest_exit_quick_settings_button" msgid="1912362095913765471">"मेहमान मोड से बाहर निकलें"</string>
<string name="guest_notification_ephemeral" msgid="7263252466950923871">"बाहर निकलने पर, सारी गतिविधि मिट जाएगी"</string>
<string name="guest_notification_non_ephemeral" msgid="6843799963012259330">"बाहर निकलने पर, गतिविधि को मिटाया या सेव किया जा सकता है"</string>
diff --git a/packages/SettingsLib/res/values-hr/strings.xml b/packages/SettingsLib/res/values-hr/strings.xml
index 46c8b90..03eba0f 100644
--- a/packages/SettingsLib/res/values-hr/strings.xml
+++ b/packages/SettingsLib/res/values-hr/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> do napunjenosti"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="TIME">%2$s</xliff:g> do napunjenosti"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> – punjenje je pauzirano"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – punjenje pauzirano"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – punjenje do <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Nepoznato"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Punjenje"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Brzo punjenje"</string>
diff --git a/packages/SettingsLib/res/values-hu/strings.xml b/packages/SettingsLib/res/values-hu/strings.xml
index d74490f..04a3ac6 100644
--- a/packages/SettingsLib/res/values-hu/strings.xml
+++ b/packages/SettingsLib/res/values-hu/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> a teljes töltöttségig"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="TIME">%2$s</xliff:g> a teljes töltöttségig"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> – A töltés szünetel"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – Töltés szüneteltetve"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – Töltés eddig: <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Ismeretlen"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Töltés"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Gyorstöltés"</string>
diff --git a/packages/SettingsLib/res/values-hy/strings.xml b/packages/SettingsLib/res/values-hy/strings.xml
index c26f94f..13f969c 100644
--- a/packages/SettingsLib/res/values-hy/strings.xml
+++ b/packages/SettingsLib/res/values-hy/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> մինչև լրիվ լիցքավորումը"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="TIME">%2$s</xliff:g> մինչև լրիվ լիցքավորումը"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> – Լիցքավորումը դադարեցված է"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – Լիցքավորումը դադարեցվել է"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – Լիցքավորում մինչև <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Անհայտ"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Լիցքավորում"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Արագ լիցքավորում"</string>
diff --git a/packages/SettingsLib/res/values-in/strings.xml b/packages/SettingsLib/res/values-in/strings.xml
index 203485f..650d50b 100644
--- a/packages/SettingsLib/res/values-in/strings.xml
+++ b/packages/SettingsLib/res/values-in/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> lagi sampai penuh"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="TIME">%2$s</xliff:g> lagi sampai penuh"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Pengisian daya dijeda"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - Pengisian daya dijeda"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - Mengisi daya sampai <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Tidak diketahui"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Mengisi daya"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Mengisi daya cepat"</string>
diff --git a/packages/SettingsLib/res/values-is/strings.xml b/packages/SettingsLib/res/values-is/strings.xml
index 9185877..da576f5 100644
--- a/packages/SettingsLib/res/values-is/strings.xml
+++ b/packages/SettingsLib/res/values-is/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> fram að fullri hleðslu"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="TIME">%2$s</xliff:g> fram að fullri hleðslu"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Hlé var gert á hleðslu"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - Hlé gert á hleðslu"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - hleður upp að <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Óþekkt"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Í hleðslu"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Hröð hleðsla"</string>
diff --git a/packages/SettingsLib/res/values-it/strings.xml b/packages/SettingsLib/res/values-it/strings.xml
index 0cd9e1e..7042506 100644
--- a/packages/SettingsLib/res/values-it/strings.xml
+++ b/packages/SettingsLib/res/values-it/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> alla ricarica completa"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="TIME">%2$s</xliff:g> alla ricarica completa"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Ricarica in pausa"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - Ricarica in pausa"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - Ricarica fino a <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Sconosciuta"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"In carica"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Ricarica veloce"</string>
diff --git a/packages/SettingsLib/res/values-iw/strings.xml b/packages/SettingsLib/res/values-iw/strings.xml
index 6293105..143c99d 100644
--- a/packages/SettingsLib/res/values-iw/strings.xml
+++ b/packages/SettingsLib/res/values-iw/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"הזמן הנותר לטעינה מלאה: <xliff:g id="TIME">%1$s</xliff:g>"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – הזמן הנותר לטעינה מלאה: <xliff:g id="TIME">%2$s</xliff:g>"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> – הטעינה הושהתה"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – הטעינה מושהית"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – טעינה עד מצב של <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"לא ידוע"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"בטעינה"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"הסוללה נטענת מהר"</string>
@@ -514,7 +513,7 @@
<string name="ims_reg_status_not_registered" msgid="2989287366045704694">"לא רשום"</string>
<string name="status_unavailable" msgid="5279036186589861608">"לא זמין"</string>
<string name="wifi_status_mac_randomized" msgid="466382542497832189">"כתובת ה-MAC אקראית"</string>
- <string name="wifi_tether_connected_summary" msgid="5282919920463340158">"{count,plural, =0{אין מכשירים מחוברים}=1{מכשיר אחד מחובר}two{# מכשירים מחוברים}many{# מכשירים מחוברים}other{# מכשירים מחוברים}}"</string>
+ <string name="wifi_tether_connected_summary" msgid="5282919920463340158">"{count,plural, =0{אין מכשירים מחוברים}=1{מכשיר אחד מחובר}one{# מכשירים מחוברים}two{# מכשירים מחוברים}other{# מכשירים מחוברים}}"</string>
<string name="accessibility_manual_zen_more_time" msgid="5141801092071134235">"יותר זמן."</string>
<string name="accessibility_manual_zen_less_time" msgid="6828877595848229965">"פחות זמן."</string>
<string name="cancel" msgid="5665114069455378395">"ביטול"</string>
diff --git a/packages/SettingsLib/res/values-ja/strings.xml b/packages/SettingsLib/res/values-ja/strings.xml
index 9a9d6ea..11a5258 100644
--- a/packages/SettingsLib/res/values-ja/strings.xml
+++ b/packages/SettingsLib/res/values-ja/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"完了まであと <xliff:g id="TIME">%1$s</xliff:g>"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - 完了まであと <xliff:g id="TIME">%2$s</xliff:g>"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - 充電は一時停止中"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - 充電を一時停止しています"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g> まで充電"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"不明"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"充電中"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"急速充電中"</string>
diff --git a/packages/SettingsLib/res/values-ka/strings.xml b/packages/SettingsLib/res/values-ka/strings.xml
index 2fa2d80..d6c0f38 100644
--- a/packages/SettingsLib/res/values-ka/strings.xml
+++ b/packages/SettingsLib/res/values-ka/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"სრულ დატენვამდე დარჩენილია <xliff:g id="TIME">%1$s</xliff:g>"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> — სრულ დატენვამდე დარჩენილია <xliff:g id="TIME">%2$s</xliff:g>"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - დატენვა ᲨეᲩერებულია"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - დატენვა დაპაუზებულია"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – დატენვა შემდეგ ნიშნულამდე: <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"უცნობი"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"იტენება"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"სწრაფად იტენება"</string>
diff --git a/packages/SettingsLib/res/values-kk/strings.xml b/packages/SettingsLib/res/values-kk/strings.xml
index a332004..96f2595 100644
--- a/packages/SettingsLib/res/values-kk/strings.xml
+++ b/packages/SettingsLib/res/values-kk/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"Толық зарядталғанға дейін <xliff:g id="TIME">%1$s</xliff:g> қалды."</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – толық зарядталғанға дейін <xliff:g id="TIME">%2$s</xliff:g> қалды."</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> – зарядтау кідіртілді."</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – Зарядтау кідіртілді"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g> деңгейіне дейін зарядталады"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Белгісіз"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Зарядталуда"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Жылдам зарядталуда"</string>
diff --git a/packages/SettingsLib/res/values-km/strings.xml b/packages/SettingsLib/res/values-km/strings.xml
index 2d78295..db4afdf 100644
--- a/packages/SettingsLib/res/values-km/strings.xml
+++ b/packages/SettingsLib/res/values-km/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> ទៀតទើបពេញ"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - នៅសល់ <xliff:g id="TIME">%2$s</xliff:g> ទៀតទើបពេញ"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - ការសាកថ្មត្រូវបានផ្អាក"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - បានផ្អាកការសាកថ្ម"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - កំពុងសាកថ្មឱ្យដល់កម្រិត <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"មិនស្គាល់"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"កំពុងសាកថ្ម"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"កំពុងសាកថ្មយ៉ាងឆាប់រហ័ស"</string>
diff --git a/packages/SettingsLib/res/values-kn/strings.xml b/packages/SettingsLib/res/values-kn/strings.xml
index 5db4b16..74c2c74 100644
--- a/packages/SettingsLib/res/values-kn/strings.xml
+++ b/packages/SettingsLib/res/values-kn/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> - ಸಮಯದಲ್ಲಿ ಪೂರ್ತಿ ಚಾರ್ಜ್ ಆಗುತ್ತದೆ"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="TIME">%2$s</xliff:g> ಸಮಯದಲ್ಲಿ ಪೂರ್ತಿ ಚಾರ್ಜ್ ಆಗುತ್ತದೆ"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - ಚಾರ್ಜಿಂಗ್ ಅನ್ನು ವಿರಾಮಗೊಳಿಸಲಾಗಿದೆ"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - ಚಾರ್ಜಿಂಗ್ ವಿರಾಮಗೊಂಡಿದೆ"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g> ವರೆಗೆ ಚಾರ್ಜ್ ಮಾಡಲಾಗುತ್ತಿದೆ"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"ಅಪರಿಚಿತ"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"ಚಾರ್ಜ್ ಆಗುತ್ತಿದೆ"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"ವೇಗದ ಚಾರ್ಜಿಂಗ್"</string>
diff --git a/packages/SettingsLib/res/values-ko/strings.xml b/packages/SettingsLib/res/values-ko/strings.xml
index 30ab26c..7a8ce1a 100644
--- a/packages/SettingsLib/res/values-ko/strings.xml
+++ b/packages/SettingsLib/res/values-ko/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> 후 충전 완료"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g>: <xliff:g id="TIME">%2$s</xliff:g> 후 충전 완료"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - 충전 일시중지됨"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - 충전 일시중지됨"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>까지 충전"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"알 수 없음"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"충전 중"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"고속 충전 중"</string>
diff --git a/packages/SettingsLib/res/values-ky/strings.xml b/packages/SettingsLib/res/values-ky/strings.xml
index baacfd7..534c94f 100644
--- a/packages/SettingsLib/res/values-ky/strings.xml
+++ b/packages/SettingsLib/res/values-ky/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> кийин толук кубатталат"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="TIME">%2$s</xliff:g> кийин толук кубатталат"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Кубаттоо тындырылды"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - Кубаттоо тындырылды"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g> чейин кубаттоо"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Белгисиз"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Кубатталууда"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Ыкчам кубатталууда"</string>
diff --git a/packages/SettingsLib/res/values-lo/strings.xml b/packages/SettingsLib/res/values-lo/strings.xml
index 36755f7..f37a4ea 100644
--- a/packages/SettingsLib/res/values-lo/strings.xml
+++ b/packages/SettingsLib/res/values-lo/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"ຍັງເຫຼືອອີກ <xliff:g id="TIME">%1$s</xliff:g> ຈຶ່ງຈະສາກເຕັມ"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"ຍັງເຫຼືອອີກ <xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="TIME">%2$s</xliff:g> ຈຶ່ງຈະສາກເຕັມ"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - ການສາກໄຟຖືກຢຸດໄວ້ຊົ່ວຄາວ"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - ຢຸດການສາກໄວ້ຊົ່ວຄາວແລ້ວ"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - ກຳລັງສາກຈົນເຖິງ <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"ບໍ່ຮູ້ຈັກ"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"ກຳລັງສາກໄຟ"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"ກຳລັງສາກໄຟດ່ວນ"</string>
diff --git a/packages/SettingsLib/res/values-lt/strings.xml b/packages/SettingsLib/res/values-lt/strings.xml
index 50652ea..cdfa115 100644
--- a/packages/SettingsLib/res/values-lt/strings.xml
+++ b/packages/SettingsLib/res/values-lt/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"Liko <xliff:g id="TIME">%1$s</xliff:g>, kol bus visiškai įkrauta"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – liko <xliff:g id="TIME">%2$s</xliff:g>, kol bus visiškai įkrauta"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> – įkrovimas pristabdytas"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – įkrovimas pristabdytas"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – įkraunama iki <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Nežinomas"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Kraunasi..."</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Greitai įkraunama"</string>
diff --git a/packages/SettingsLib/res/values-lv/strings.xml b/packages/SettingsLib/res/values-lv/strings.xml
index c199fea..a5ecfff 100644
--- a/packages/SettingsLib/res/values-lv/strings.xml
+++ b/packages/SettingsLib/res/values-lv/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> — <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> līdz pilnai uzlādei"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> — <xliff:g id="TIME">%2$s</xliff:g> līdz pilnai uzlādei"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> — uzlāde ir pārtraukta"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - Uzlāde ir apturēta"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g>. Tiks uzlādēts līdz <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Nezināms"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Uzlāde"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Notiek ātrā uzlāde"</string>
diff --git a/packages/SettingsLib/res/values-mk/strings.xml b/packages/SettingsLib/res/values-mk/strings.xml
index 957f68b..a41d953 100644
--- a/packages/SettingsLib/res/values-mk/strings.xml
+++ b/packages/SettingsLib/res/values-mk/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> до полна батерија"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="TIME">%2$s</xliff:g> до полна батерија"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Полнењето е паузирано"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - Полнењето е паузирано"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - Се полни на <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Непознато"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Се полни"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Брзо полнење"</string>
diff --git a/packages/SettingsLib/res/values-ml/strings.xml b/packages/SettingsLib/res/values-ml/strings.xml
index 1cc92ca..83b87f8 100644
--- a/packages/SettingsLib/res/values-ml/strings.xml
+++ b/packages/SettingsLib/res/values-ml/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"പൂർണ്ണമാകാൻ <xliff:g id="TIME">%1$s</xliff:g> ശേഷിക്കുന്നു"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - പൂർണ്ണമാകാൻ <xliff:g id="TIME">%2$s</xliff:g> ശേഷിക്കുന്നു"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - ചാർജിംഗ് താൽക്കാലികമായി നിർത്തി"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - ചാർജ് ചെയ്യൽ താൽക്കാലികമായി നിർത്തി"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g> വരെ ചാർജ് ചെയ്യുന്നു"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"അജ്ഞാതം"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"ചാർജ് ചെയ്യുന്നു"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"അതിവേഗ ചാർജിംഗ്"</string>
diff --git a/packages/SettingsLib/res/values-mn/strings.xml b/packages/SettingsLib/res/values-mn/strings.xml
index a107c70..b77d8fb 100644
--- a/packages/SettingsLib/res/values-mn/strings.xml
+++ b/packages/SettingsLib/res/values-mn/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"Дүүрэх хүртэл <xliff:g id="TIME">%1$s</xliff:g> үлдсэн"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - дүүрэх хүртэл <xliff:g id="TIME">%2$s</xliff:g> үлдсэн"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Цэнэглэхийг түр зогсоосон"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - Цэнэглэлтийг түр зогсоосон"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g> руу цэнэглэж байна"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Тодорхойгүй"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Цэнэглэж байна"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Хурдан цэнэглэж байна"</string>
diff --git a/packages/SettingsLib/res/values-mr/strings.xml b/packages/SettingsLib/res/values-mr/strings.xml
index d5c83a8..9d91684 100644
--- a/packages/SettingsLib/res/values-mr/strings.xml
+++ b/packages/SettingsLib/res/values-mr/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"पूर्ण चार्ज होण्यासाठी <xliff:g id="TIME">%1$s</xliff:g> शिल्लक आहेत"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - पूर्ण चार्ज होण्यासाठी <xliff:g id="TIME">%2$s</xliff:g> शिल्लक आहे"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - चार्ज करणे थांबवले आहे"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - चार्ज करणे थांबवले"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g> वर चार्ज करत आहे"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"अज्ञात"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"चार्ज होत आहे"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"वेगाने चार्ज होत आहे"</string>
diff --git a/packages/SettingsLib/res/values-ms/strings.xml b/packages/SettingsLib/res/values-ms/strings.xml
index ae8be8e..d302214 100644
--- a/packages/SettingsLib/res/values-ms/strings.xml
+++ b/packages/SettingsLib/res/values-ms/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> lagi sebelum penuh"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="TIME">%2$s</xliff:g> lagi sebelum penuh"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Pengecasan dijeda"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - Pengecasan dijeda"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - Mengecas kepada <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Tidak diketahui"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Mengecas"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Mengecas dgn cepat"</string>
diff --git a/packages/SettingsLib/res/values-my/strings.xml b/packages/SettingsLib/res/values-my/strings.xml
index f16cdca..1e26b08 100644
--- a/packages/SettingsLib/res/values-my/strings.xml
+++ b/packages/SettingsLib/res/values-my/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"အားပြည့်ရန် <xliff:g id="TIME">%1$s</xliff:g> လိုသည်"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"အားပြည့်ရန် <xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="TIME">%2$s</xliff:g> လိုသည်"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - အားသွင်းခြင်းကို ခဏရပ်ထားသည်"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - အားသွင်းမှု ခဏရပ်ထားသည်"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g> အထိ အားသွင်းရန်"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"မသိ"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"အားသွင်းနေပါသည်"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"အမြန် အားသွင်းနေသည်"</string>
diff --git a/packages/SettingsLib/res/values-nb/strings.xml b/packages/SettingsLib/res/values-nb/strings.xml
index f7a10a6..be52597 100644
--- a/packages/SettingsLib/res/values-nb/strings.xml
+++ b/packages/SettingsLib/res/values-nb/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"Fulladet om <xliff:g id="TIME">%1$s</xliff:g>"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – Fulladet om <xliff:g id="TIME">%2$s</xliff:g>"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Ladingen er satt på pause"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - Ladingen er satt på pause"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – lader til <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Ukjent"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Lader"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Lader raskt"</string>
diff --git a/packages/SettingsLib/res/values-ne/strings.xml b/packages/SettingsLib/res/values-ne/strings.xml
index a966f15..4d2eb2e 100644
--- a/packages/SettingsLib/res/values-ne/strings.xml
+++ b/packages/SettingsLib/res/values-ne/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"पूरा चार्ज हुन <xliff:g id="TIME">%1$s</xliff:g> लाग्ने छ"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - पूरा चार्ज हुन <xliff:g id="TIME">%2$s</xliff:g> लाग्ने छ"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - चार्ज गर्ने प्रक्रिया रोकिएको छ"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - चार्ज गर्ने प्रक्रिया पज गरिएको छ"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g> सम्म चार्ज हुने छ"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"अज्ञात"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"चार्ज हुँदै छ"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"द्रुत गतिमा चार्ज गरिँदै छ"</string>
diff --git a/packages/SettingsLib/res/values-nl/strings.xml b/packages/SettingsLib/res/values-nl/strings.xml
index c5406ee..898da11 100644
--- a/packages/SettingsLib/res/values-nl/strings.xml
+++ b/packages/SettingsLib/res/values-nl/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"Vol over <xliff:g id="TIME">%1$s</xliff:g>"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - vol over <xliff:g id="TIME">%2$s</xliff:g>"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Opladen is onderbroken"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - Opladen onderbroken"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - Opladen tot <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Onbekend"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Opladen"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Snel opladen"</string>
diff --git a/packages/SettingsLib/res/values-or/strings.xml b/packages/SettingsLib/res/values-or/strings.xml
index 37cc25e..6b6752f 100644
--- a/packages/SettingsLib/res/values-or/strings.xml
+++ b/packages/SettingsLib/res/values-or/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"ପୂର୍ଣ୍ଣ ହେବାକୁ ଆଉ <xliff:g id="TIME">%1$s</xliff:g> ବାକି ଅଛି"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - ପୂର୍ଣ୍ଣ ହେବାକୁ ଆଉ <xliff:g id="TIME">%2$s</xliff:g> ବାକି ଅଛି"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - ଚାର୍ଜିଂକୁ ବିରତ କରାଯାଇଛି"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - ଚାର୍ଜିଂ ବିରତ କରାଯାଇଛି"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g> ପର୍ଯ୍ୟନ୍ତ ଚାର୍ଜ ହେଉଛି"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"ଅଜ୍ଞାତ"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"ଚାର୍ଜ ହେଉଛି"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"ଶୀଘ୍ର ଚାର୍ଜ ହେଉଛି"</string>
diff --git a/packages/SettingsLib/res/values-pa/strings.xml b/packages/SettingsLib/res/values-pa/strings.xml
index 767cbc5..896b5ec 100644
--- a/packages/SettingsLib/res/values-pa/strings.xml
+++ b/packages/SettingsLib/res/values-pa/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"ਬੈਟਰੀ ਪੂਰੀ ਚਾਰਜ ਹੋਣ ਵਿੱਚ <xliff:g id="TIME">%1$s</xliff:g> ਬਾਕੀ"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - ਬੈਟਰੀ ਪੂਰੀ ਚਾਰਜ ਹੋਣ ਵਿੱਚ <xliff:g id="TIME">%2$s</xliff:g> ਬਾਕੀ"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - ਚਾਰਜਿੰਗ ਨੂੰ ਰੋਕਿਆ ਗਿਆ ਹੈ"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - ਬੈਟਰੀ ਦੀ ਚਾਰਜਿੰਗ ਨੂੰ ਰੋਕਿਆ ਗਿਆ"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g> ਤੱਕ ਚਾਰਜ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"ਅਗਿਆਤ"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"ਚਾਰਜ ਹੋ ਰਹੀ ਹੈ"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"ਤੇਜ਼ ਚਾਰਜ ਹੋ ਰਹੀ ਹੈ"</string>
diff --git a/packages/SettingsLib/res/values-pl/strings.xml b/packages/SettingsLib/res/values-pl/strings.xml
index 48bc82e..263e0d7 100644
--- a/packages/SettingsLib/res/values-pl/strings.xml
+++ b/packages/SettingsLib/res/values-pl/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> do pełnego naładowania"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="TIME">%2$s</xliff:g> do pełnego naładowania"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> – ładowanie zostało wstrzymane"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – wstrzymano ładowanie"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – Ładuję do <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Nieznane"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Ładowanie"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Szybkie ładowanie"</string>
diff --git a/packages/SettingsLib/res/values-pt-rBR/strings.xml b/packages/SettingsLib/res/values-pt-rBR/strings.xml
index 68a1c49..e107064 100644
--- a/packages/SettingsLib/res/values-pt-rBR/strings.xml
+++ b/packages/SettingsLib/res/values-pt-rBR/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> até a conclusão"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g>: <xliff:g id="TIME">%2$s</xliff:g> até a conclusão"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g>: o carregamento está pausado"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g>: carregamento pausado"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g>: carregando até <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Desconhecido"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Carregando"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Carregando rápido"</string>
diff --git a/packages/SettingsLib/res/values-pt-rPT/strings.xml b/packages/SettingsLib/res/values-pt-rPT/strings.xml
index 87bab50..165ecc4 100644
--- a/packages/SettingsLib/res/values-pt-rPT/strings.xml
+++ b/packages/SettingsLib/res/values-pt-rPT/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> até à carga máxima"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="TIME">%2$s</xliff:g> até à carga máxima"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> – O carregamento está pausado"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – Carregamento em pausa"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – A carregar até <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Desconhecido"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"A carregar"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Carregamento rápido"</string>
diff --git a/packages/SettingsLib/res/values-pt/strings.xml b/packages/SettingsLib/res/values-pt/strings.xml
index 68a1c49..e107064 100644
--- a/packages/SettingsLib/res/values-pt/strings.xml
+++ b/packages/SettingsLib/res/values-pt/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> até a conclusão"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g>: <xliff:g id="TIME">%2$s</xliff:g> até a conclusão"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g>: o carregamento está pausado"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g>: carregamento pausado"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g>: carregando até <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Desconhecido"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Carregando"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Carregando rápido"</string>
diff --git a/packages/SettingsLib/res/values-ro/strings.xml b/packages/SettingsLib/res/values-ro/strings.xml
index f81ef4c..44ec043 100644
--- a/packages/SettingsLib/res/values-ro/strings.xml
+++ b/packages/SettingsLib/res/values-ro/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> până la finalizare"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="TIME">%2$s</xliff:g> până la finalizare"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> – Încărcarea este întreruptă"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – Încărcarea s-a întrerupt"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – Se încarcă până la <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Necunoscut"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Se încarcă"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Se încarcă rapid"</string>
diff --git a/packages/SettingsLib/res/values-ru/strings.xml b/packages/SettingsLib/res/values-ru/strings.xml
index 4dcee13..102cf1b 100644
--- a/packages/SettingsLib/res/values-ru/strings.xml
+++ b/packages/SettingsLib/res/values-ru/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> до полной зарядки"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="TIME">%2$s</xliff:g> до полной зарядки"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g>: зарядка приостановлена"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – зарядка приостановлена"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – заряжается до <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Неизвестно"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Идет зарядка"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Быстрая зарядка"</string>
diff --git a/packages/SettingsLib/res/values-si/strings.xml b/packages/SettingsLib/res/values-si/strings.xml
index 17c59ea..a8963af 100644
--- a/packages/SettingsLib/res/values-si/strings.xml
+++ b/packages/SettingsLib/res/values-si/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"සම්පූර්ණ වීමට <xliff:g id="TIME">%1$s</xliff:g>ක් ඉතිරියි"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - සම්පූර්ණ වීමට <xliff:g id="TIME">%2$s</xliff:g>ක් ඉතිරියි"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - ආරෝපණය විරාම කර ඇත"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - ආරෝපණය විරාම කළා"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g> වෙත ආරෝපණය"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"නොදනී"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"ආරෝපණය වෙමින්"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"ශීඝ්ර ආරෝපණය"</string>
diff --git a/packages/SettingsLib/res/values-sk/strings.xml b/packages/SettingsLib/res/values-sk/strings.xml
index d9ce6cf..d7bdd89 100644
--- a/packages/SettingsLib/res/values-sk/strings.xml
+++ b/packages/SettingsLib/res/values-sk/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> do úplného nabitia"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="TIME">%2$s</xliff:g> do úplného nabitia"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Nabíjanie je pozastavené"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – nabíjanie bolo pozastavené"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – nabíja sa na úroveň <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Neznáme"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Nabíja sa"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Rýchle nabíjanie"</string>
diff --git a/packages/SettingsLib/res/values-sl/strings.xml b/packages/SettingsLib/res/values-sl/strings.xml
index 8f9b059..6cb5b3c 100644
--- a/packages/SettingsLib/res/values-sl/strings.xml
+++ b/packages/SettingsLib/res/values-sl/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"Še <xliff:g id="TIME">%1$s</xliff:g> do napolnjenosti"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – še <xliff:g id="TIME">%2$s</xliff:g> do napolnjenosti"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> – Polnjenje je začasno zaustavljeno"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – polnjenje je začasno zaustavljeno"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – polnjenje do <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Neznano"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Polnjenje"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Hitro polnjenje"</string>
diff --git a/packages/SettingsLib/res/values-sq/strings.xml b/packages/SettingsLib/res/values-sq/strings.xml
index d467eea..54a8361 100644
--- a/packages/SettingsLib/res/values-sq/strings.xml
+++ b/packages/SettingsLib/res/values-sq/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> derisa të mbushet"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="TIME">%2$s</xliff:g> derisa të mbushet"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Karikimi është vendosur në pauzë"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - Karikimi në pauzë"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - Po karikohet deri në <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"I panjohur"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Po karikohet"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Karikim i shpejtë"</string>
diff --git a/packages/SettingsLib/res/values-sr/strings.xml b/packages/SettingsLib/res/values-sr/strings.xml
index 9cc43d9..d61938b 100644
--- a/packages/SettingsLib/res/values-sr/strings.xml
+++ b/packages/SettingsLib/res/values-sr/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> до краја пуњења"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="TIME">%2$s</xliff:g> до краја пуњења"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> – Пуњење је заустављено"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – Пуњење је паузирано"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – пуњење до <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Непознато"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Пуни се"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Брзо се пуни"</string>
diff --git a/packages/SettingsLib/res/values-sv/strings.xml b/packages/SettingsLib/res/values-sv/strings.xml
index 89deafa..3ba4e9b 100644
--- a/packages/SettingsLib/res/values-sv/strings.xml
+++ b/packages/SettingsLib/res/values-sv/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> kvar tills fulladdat"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="TIME">%2$s</xliff:g> kvar tills fulladdat"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Laddningen har pausats"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – Laddningen har pausats"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – Laddar till <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Okänd"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Laddar"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Laddas snabbt"</string>
diff --git a/packages/SettingsLib/res/values-sw/strings.xml b/packages/SettingsLib/res/values-sw/strings.xml
index 1fa5edd..c167697 100644
--- a/packages/SettingsLib/res/values-sw/strings.xml
+++ b/packages/SettingsLib/res/values-sw/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"Zimesalia <xliff:g id="TIME">%1$s</xliff:g> ijae chaji"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="TIME">%2$s</xliff:g> zimesalia ijae chaji"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Imesitisha kuchaji"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - Imesitisha kuchaji"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - Itachaji hadi <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Haijulikani"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Inachaji"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Inachaji kwa kasi"</string>
diff --git a/packages/SettingsLib/res/values-ta/strings.xml b/packages/SettingsLib/res/values-ta/strings.xml
index 1b7b643..57661ba 100644
--- a/packages/SettingsLib/res/values-ta/strings.xml
+++ b/packages/SettingsLib/res/values-ta/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"முழுவதும் சார்ஜாக <xliff:g id="TIME">%1$s</xliff:g> ஆகும்"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - முழுவதும் சார்ஜாக <xliff:g id="TIME">%2$s</xliff:g> ஆகும்"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - சார்ஜ் ஏறுவது இடைநிறுத்தப்பட்டுள்ளது"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - சார்ஜிங் இடைநிறுத்தப்பட்டது"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g> வரை சார்ஜ் செய்யப்படும்"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"அறியப்படாத"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"சார்ஜ் ஆகிறது"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"வேகமாக சார்ஜாகிறது"</string>
diff --git a/packages/SettingsLib/res/values-te/strings.xml b/packages/SettingsLib/res/values-te/strings.xml
index 27e9dbd..365a8f2 100644
--- a/packages/SettingsLib/res/values-te/strings.xml
+++ b/packages/SettingsLib/res/values-te/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g>లో పూర్తిగా ఛార్జ్ అవుతుంది"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="TIME">%2$s</xliff:g>లో పూర్తిగా ఛార్జ్ అవుతుంది"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - ఛార్జింగ్ పాజ్ చేయబడింది"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - ఛార్జింగ్ పాజ్ చేయబడింది"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g> ఛార్జింగ్"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"తెలియదు"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"ఛార్జ్ అవుతోంది"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"వేగవంతమైన ఛార్జింగ్"</string>
diff --git a/packages/SettingsLib/res/values-th/strings.xml b/packages/SettingsLib/res/values-th/strings.xml
index 6b9c077..cea4dd2 100644
--- a/packages/SettingsLib/res/values-th/strings.xml
+++ b/packages/SettingsLib/res/values-th/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"อีก <xliff:g id="TIME">%1$s</xliff:g>จึงจะเต็ม"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - อีก <xliff:g id="TIME">%2$s</xliff:g> จึงจะเต็ม"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - การชาร์จหยุดชั่วคราว"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - หยุดชาร์จชั่วคราว"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - กำลังชาร์จจนถึง <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"ไม่ทราบ"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"กำลังชาร์จ"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"กำลังชาร์จอย่างเร็ว"</string>
diff --git a/packages/SettingsLib/res/values-tl/strings.xml b/packages/SettingsLib/res/values-tl/strings.xml
index fddc1d0..e4503a0 100644
--- a/packages/SettingsLib/res/values-tl/strings.xml
+++ b/packages/SettingsLib/res/values-tl/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> na lang bago mapuno"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="TIME">%2$s</xliff:g> na lang bago mapuno"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Naka-pause ang pag-charge"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - Na-pause ang pag-charge"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - China-charge hanggang <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Hindi Kilala"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Nagcha-charge"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Mabilis na charge"</string>
diff --git a/packages/SettingsLib/res/values-tr/strings.xml b/packages/SettingsLib/res/values-tr/strings.xml
index 31dcedb8..eada480 100644
--- a/packages/SettingsLib/res/values-tr/strings.xml
+++ b/packages/SettingsLib/res/values-tr/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"Tamamen şarj olmasına <xliff:g id="TIME">%1$s</xliff:g> kaldı"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - Tamamen şarj olmasına <xliff:g id="TIME">%2$s</xliff:g> kaldı"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g>: Şarj işlemi duraklatıldı"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - Şarj işlemi duraklatıldı"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g> seviyesine kadar şarj ediliyor"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Bilinmiyor"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Şarj oluyor"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Hızlı şarj oluyor"</string>
diff --git a/packages/SettingsLib/res/values-uk/strings.xml b/packages/SettingsLib/res/values-uk/strings.xml
index 2fdc227..8ab8db7 100644
--- a/packages/SettingsLib/res/values-uk/strings.xml
+++ b/packages/SettingsLib/res/values-uk/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> до повного заряду"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="TIME">%2$s</xliff:g> до повного заряду"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Заряджання призупинено"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – заряджання призупинено"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - Заряджання до <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Невідомо"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Заряджається"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Швидке заряджання"</string>
diff --git a/packages/SettingsLib/res/values-ur/strings.xml b/packages/SettingsLib/res/values-ur/strings.xml
index 82dbdb3..ebc6edc 100644
--- a/packages/SettingsLib/res/values-ur/strings.xml
+++ b/packages/SettingsLib/res/values-ur/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"مکمل چارج ہونے میں <xliff:g id="TIME">%1$s</xliff:g> باقی ہے"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"مکمل چارج ہونے میں <xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="TIME">%2$s</xliff:g> باقی ہے"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - چارجنگ موقوف ہے"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - چارجنگ موقوف کی گئی"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g> چارج کیا جائے گا"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"نامعلوم"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"چارج ہو رہا ہے"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"تیزی سے چارج ہو رہا ہے"</string>
diff --git a/packages/SettingsLib/res/values-uz/strings.xml b/packages/SettingsLib/res/values-uz/strings.xml
index ab997b6..19601dd 100644
--- a/packages/SettingsLib/res/values-uz/strings.xml
+++ b/packages/SettingsLib/res/values-uz/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"Toʻlishiga <xliff:g id="TIME">%1$s</xliff:g> qoldi"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – Toʻlishiga <xliff:g id="TIME">%2$s</xliff:g> qoldi"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Quvvatlash pauza qilindi"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – Quvvatlash pauzada"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g>, <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g> gacha quvvat oladi"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Noma’lum"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Quvvat olmoqda"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Tezkor quvvat olmoqda"</string>
diff --git a/packages/SettingsLib/res/values-vi/strings.xml b/packages/SettingsLib/res/values-vi/strings.xml
index 44c820a5..f900480 100644
--- a/packages/SettingsLib/res/values-vi/strings.xml
+++ b/packages/SettingsLib/res/values-vi/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> nữa là pin đầy"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> – <xliff:g id="TIME">%2$s</xliff:g> nữa là pin đầy"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> – Đã tạm dừng sạc"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> – Đã tạm dừng sạc"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> – Sạc đến <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Không xác định"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Đang sạc"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Đang sạc nhanh"</string>
diff --git a/packages/SettingsLib/res/values-zh-rCN/strings.xml b/packages/SettingsLib/res/values-zh-rCN/strings.xml
index eae1c39..6909f22 100644
--- a/packages/SettingsLib/res/values-zh-rCN/strings.xml
+++ b/packages/SettingsLib/res/values-zh-rCN/strings.xml
@@ -141,7 +141,7 @@
<string name="bluetooth_pairing_decline" msgid="6483118841204885890">"取消"</string>
<string name="bluetooth_pairing_will_share_phonebook" msgid="3064334458659165176">"配对之后,所配对的设备将可以在建立连接后访问您的通讯录和通话记录。"</string>
<string name="bluetooth_pairing_error_message" msgid="6626399020672335565">"无法与“<xliff:g id="DEVICE_NAME">%1$s</xliff:g>”进行配对。"</string>
- <string name="bluetooth_pairing_pin_error_message" msgid="264422127613704940">"PIN 码或密钥不正确,因此无法与“<xliff:g id="DEVICE_NAME">%1$s</xliff:g>”配对。"</string>
+ <string name="bluetooth_pairing_pin_error_message" msgid="264422127613704940">"PIN 码或通行密钥不正确,因此无法与“<xliff:g id="DEVICE_NAME">%1$s</xliff:g>”配对。"</string>
<string name="bluetooth_pairing_device_down_error_message" msgid="2554424863101358857">"无法与“<xliff:g id="DEVICE_NAME">%1$s</xliff:g>”进行通信。"</string>
<string name="bluetooth_pairing_rejected_error_message" msgid="5943444352777314442">"<xliff:g id="DEVICE_NAME">%1$s</xliff:g> 已拒绝配对。"</string>
<string name="bluetooth_talkback_computer" msgid="3736623135703893773">"计算机"</string>
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"还需<xliff:g id="TIME">%1$s</xliff:g>充满"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - 还需<xliff:g id="TIME">%2$s</xliff:g>充满"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - 充电已暂停"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - 已暂停充电"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - 正在充到 <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"未知"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"正在充电"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"正在快速充电"</string>
diff --git a/packages/SettingsLib/res/values-zh-rHK/strings.xml b/packages/SettingsLib/res/values-zh-rHK/strings.xml
index 4371bb3..2d59d2d 100644
--- a/packages/SettingsLib/res/values-zh-rHK/strings.xml
+++ b/packages/SettingsLib/res/values-zh-rHK/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g>後充滿電"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="TIME">%2$s</xliff:g>後充滿電"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - 已暫停充電"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - 已暫停充電"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - 正在充電至 <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"未知"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"充電中"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"快速充電中"</string>
diff --git a/packages/SettingsLib/res/values-zh-rTW/strings.xml b/packages/SettingsLib/res/values-zh-rTW/strings.xml
index d76a4a4..40b42fc 100644
--- a/packages/SettingsLib/res/values-zh-rTW/strings.xml
+++ b/packages/SettingsLib/res/values-zh-rTW/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g>後充飽"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="TIME">%2$s</xliff:g>後充飽"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - 已暫停充電"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - 已暫停充電"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - 正在充電至 <xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"不明"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"充電中"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"快速充電中"</string>
diff --git a/packages/SettingsLib/res/values-zu/strings.xml b/packages/SettingsLib/res/values-zu/strings.xml
index 4dc7651..a73f067 100644
--- a/packages/SettingsLib/res/values-zu/strings.xml
+++ b/packages/SettingsLib/res/values-zu/strings.xml
@@ -467,9 +467,8 @@
<string name="power_charging" msgid="6727132649743436802">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="STATE">%2$s</xliff:g>"</string>
<string name="power_remaining_charging_duration_only" msgid="8085099012811384899">"<xliff:g id="TIME">%1$s</xliff:g> okusele kuze kugcwale"</string>
<string name="power_charging_duration" msgid="6127154952524919719">"<xliff:g id="LEVEL">%1$s</xliff:g> - <xliff:g id="TIME">%2$s</xliff:g> okusele kuze kugcwale"</string>
- <string name="power_charging_limited" msgid="6971664137170239141">"<xliff:g id="LEVEL">%1$s</xliff:g> - Ukushaja kumisiwe okwesikhashana"</string>
- <!-- no translation found for power_charging_future_paused (6829683663982987290) -->
- <skip />
+ <string name="power_charging_limited" msgid="6732738149313642521">"<xliff:g id="LEVEL">%1$s</xliff:g> - Ukushaja kumiswe isikhashana"</string>
+ <string name="power_charging_future_paused" msgid="6829683663982987290">"<xliff:g id="LEVEL">%1$s</xliff:g> - Ishaja ku-<xliff:g id="DOCK_DEFENDER_THRESHOLD">%2$s</xliff:g>"</string>
<string name="battery_info_status_unknown" msgid="268625384868401114">"Akwaziwa"</string>
<string name="battery_info_status_charging" msgid="4279958015430387405">"Iyashaja"</string>
<string name="battery_info_status_charging_fast" msgid="8027559755902954885">"Ishaja ngokushesha"</string>
diff --git a/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java b/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java
index 1573edb..5610ac4 100644
--- a/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java
+++ b/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java
@@ -17,6 +17,7 @@
package com.android.settingslib;
import static android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_FEATURES_NONE;
+import static android.app.admin.DevicePolicyManager.MTE_NOT_CONTROLLED_BY_POLICY;
import static android.app.admin.DevicePolicyManager.PROFILE_KEYGUARD_FEATURES_AFFECT_OWNER;
import android.annotation.NonNull;
@@ -733,6 +734,26 @@
}
/**
+ * Checks whether MTE (Advanced memory protection) controls are disabled by the enterprise
+ * policy.
+ */
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ public static EnforcedAdmin checkIfMteIsDisabled(Context context) {
+ final DevicePolicyManager dpm = context.getSystemService(DevicePolicyManager.class);
+ if (dpm.getMtePolicy() == MTE_NOT_CONTROLLED_BY_POLICY) {
+ return null;
+ }
+ EnforcedAdmin admin =
+ RestrictedLockUtils.getProfileOrDeviceOwner(
+ context, UserHandle.of(UserHandle.USER_SYSTEM));
+ if (admin != null) {
+ return admin;
+ }
+ int profileId = getManagedProfileId(context, UserHandle.USER_SYSTEM);
+ return RestrictedLockUtils.getProfileOrDeviceOwner(context, UserHandle.of(profileId));
+ }
+
+ /**
* Show restricted setting dialog.
*/
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
index f4355c3..3e63052 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java
@@ -161,10 +161,7 @@
* @return {@code true} if successfully call, otherwise return {@code false}
*/
public boolean connectDevice(MediaDevice connectDevice) {
- MediaDevice device = null;
- synchronized (mMediaDevicesLock) {
- device = getMediaDeviceById(mMediaDevices, connectDevice.getId());
- }
+ MediaDevice device = getMediaDeviceById(connectDevice.getId());
if (device == null) {
Log.w(TAG, "connectDevice() connectDevice not in the list!");
return false;
@@ -277,23 +274,6 @@
/**
* Find the MediaDevice through id.
*
- * @param devices the list of MediaDevice
- * @param id the unique id of MediaDevice
- * @return MediaDevice
- */
- public MediaDevice getMediaDeviceById(List<MediaDevice> devices, String id) {
- for (MediaDevice mediaDevice : devices) {
- if (TextUtils.equals(mediaDevice.getId(), id)) {
- return mediaDevice;
- }
- }
- Log.i(TAG, "getMediaDeviceById() can't found device");
- return null;
- }
-
- /**
- * Find the MediaDevice from all media devices by id.
- *
* @param id the unique id of MediaDevice
* @return MediaDevice
*/
@@ -305,7 +285,7 @@
}
}
}
- Log.i(TAG, "Unable to find device " + id);
+ Log.i(TAG, "getMediaDeviceById() failed to find device with id: " + id);
return null;
}
@@ -672,10 +652,7 @@
@Override
public void onConnectedDeviceChanged(String id) {
- MediaDevice connectDevice = null;
- synchronized (mMediaDevicesLock) {
- connectDevice = getMediaDeviceById(mMediaDevices, id);
- }
+ MediaDevice connectDevice = getMediaDeviceById(id);
connectDevice = connectDevice != null
? connectDevice : updateCurrentConnectedDevice();
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java
index 24bb1bc..3ec0ba6 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/LocalMediaManagerTest.java
@@ -206,8 +206,7 @@
when(device1.getId()).thenReturn(TEST_DEVICE_ID_1);
when(device2.getId()).thenReturn(TEST_DEVICE_ID_2);
- final MediaDevice device = mLocalMediaManager
- .getMediaDeviceById(mLocalMediaManager.mMediaDevices, TEST_DEVICE_ID_2);
+ MediaDevice device = mLocalMediaManager.getMediaDeviceById(TEST_DEVICE_ID_2);
assertThat(device.getId()).isEqualTo(TEST_DEVICE_ID_2);
}
@@ -222,8 +221,7 @@
when(device1.getId()).thenReturn(TEST_DEVICE_ID_1);
when(device2.getId()).thenReturn(TEST_DEVICE_ID_2);
- final MediaDevice device = mLocalMediaManager
- .getMediaDeviceById(mLocalMediaManager.mMediaDevices, TEST_CURRENT_DEVICE_ID);
+ MediaDevice device = mLocalMediaManager.getMediaDeviceById(TEST_CURRENT_DEVICE_ID);
assertThat(device).isNull();
}
@@ -238,12 +236,7 @@
when(device1.getId()).thenReturn(null);
when(device2.getId()).thenReturn(null);
- MediaDevice device = mLocalMediaManager
- .getMediaDeviceById(mLocalMediaManager.mMediaDevices, TEST_CURRENT_DEVICE_ID);
-
- assertThat(device).isNull();
-
- device = mLocalMediaManager.getMediaDeviceById(TEST_CURRENT_DEVICE_ID);
+ MediaDevice device = mLocalMediaManager.getMediaDeviceById(TEST_CURRENT_DEVICE_ID);
assertThat(device).isNull();
}
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
index 267c196..e59ba5a 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 @@
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 def9c19..58ec349 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 @@
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/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index 48114ba1e..4365a9b 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -425,6 +425,7 @@
Settings.Global.RESTRICTED_NETWORKING_MODE,
Settings.Global.REQUIRE_PASSWORD_TO_DECRYPT,
Settings.Global.SAFE_BOOT_DISALLOWED,
+ Settings.Global.SECURE_FRP_MODE,
Settings.Global.SELINUX_STATUS,
Settings.Global.SELINUX_UPDATE_CONTENT_URL,
Settings.Global.SELINUX_UPDATE_METADATA_URL,
diff --git a/packages/Shell/src/com/android/shell/BugreportProgressService.java b/packages/Shell/src/com/android/shell/BugreportProgressService.java
index 6f7d20a..68679c79 100644
--- a/packages/Shell/src/com/android/shell/BugreportProgressService.java
+++ b/packages/Shell/src/com/android/shell/BugreportProgressService.java
@@ -455,7 +455,8 @@
intent.putExtra(DevicePolicyManager.EXTRA_REMOTE_BUGREPORT_HASH, bugreportHash);
intent.putExtra(DevicePolicyManager.EXTRA_REMOTE_BUGREPORT_NONCE, nonce);
intent.putExtra(EXTRA_BUGREPORT, bugreportFileName);
- context.sendBroadcast(intent, android.Manifest.permission.DUMP);
+ context.sendBroadcastAsUser(intent, UserHandle.SYSTEM,
+ android.Manifest.permission.DUMP);
}
/**
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 4538179..6984b5a 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -411,6 +411,7 @@
<service android:name=".screenshot.ScreenshotCrossProfileService"
android:permission="com.android.systemui.permission.SELF"
+ android:process=":screenshot_cross_profile"
android:exported="false" />
<service android:name=".screenrecord.RecordingService" />
diff --git a/packages/SystemUI/TEST_MAPPING b/packages/SystemUI/TEST_MAPPING
index aaee42f..f92625b 100644
--- a/packages/SystemUI/TEST_MAPPING
+++ b/packages/SystemUI/TEST_MAPPING
@@ -33,6 +33,18 @@
]
},
{
+ // TODO(b/251476085): Consider merging with SystemUIGoogleScreenshotTests (in U+)
+ "name": "SystemUIGoogleBiometricsScreenshotTests",
+ "options": [
+ {
+ "exclude-annotation": "org.junit.Ignore"
+ },
+ {
+ "exclude-annotation": "androidx.test.filters.FlakyTest"
+ }
+ ]
+ },
+ {
// Permission indicators
"name": "CtsPermission4TestCases",
"options": [
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt
index c540f0f..e138ef8 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt
@@ -20,6 +20,7 @@
import android.icu.text.NumberFormat
import android.util.TypedValue
import android.view.LayoutInflater
+import android.view.View
import android.widget.FrameLayout
import androidx.annotation.VisibleForTesting
import com.android.systemui.customization.R
@@ -151,9 +152,15 @@
view: AnimatableClockView,
) : DefaultClockFaceController(view) {
override fun recomputePadding(targetRegion: Rect?) {
- // Ignore Target Region until top padding fixed in aod
+ // We center the view within the targetRegion instead of within the parent
+ // view by computing the difference and adding that to the padding.
+ val parent = view.parent
+ val yDiff =
+ if (targetRegion != null && parent is View && parent.isLaidOut())
+ targetRegion.centerY() - parent.height / 2f
+ else 0f
val lp = view.getLayoutParams() as FrameLayout.LayoutParams
- lp.topMargin = (-0.5f * view.bottom).toInt()
+ lp.topMargin = (-0.5f * view.bottom + yDiff).toInt()
view.setLayoutParams(lp)
}
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 0000000..18e8a96
--- /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_clock_switch.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml
index 218c5cc..b49afee 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml
@@ -35,7 +35,6 @@
android:visibility="invisible" />
<FrameLayout
android:id="@+id/lockscreen_clock_view_large"
- android:layout_marginTop="@dimen/keyguard_large_clock_top_margin"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_host_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_host_view.xml
index e64b586..8497ff0 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/dream_aqi_badge_bg.xml b/packages/SystemUI/res/drawable/controls_panel_background.xml
similarity index 82%
rename from packages/SystemUI/res/drawable/dream_aqi_badge_bg.xml
rename to packages/SystemUI/res/drawable/controls_panel_background.xml
index 1992c77..9092877 100644
--- a/packages/SystemUI/res/drawable/dream_aqi_badge_bg.xml
+++ b/packages/SystemUI/res/drawable/controls_panel_background.xml
@@ -1,3 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2022 The Android Open Source Project
~
@@ -12,9 +13,10 @@
~ 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="@color/dream_overlay_aqi_unknown" />
- <corners android:radius="@dimen/dream_aqi_badge_corner_radius" />
+ <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 41123c8..18fcebb 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 9efad22..ee3adba 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/layout/dream_overlay_complication_aqi.xml b/packages/SystemUI/res/layout/dream_overlay_complication_aqi.xml
deleted file mode 100644
index fcebb8d..0000000
--- a/packages/SystemUI/res/layout/dream_overlay_complication_aqi.xml
+++ /dev/null
@@ -1,26 +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.
- -->
-
-<TextView
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/aqi_view"
- style="@style/clock_subtitle"
- android:visibility="gone"
- android:background="@drawable/dream_aqi_badge_bg"
- android:paddingHorizontal="@dimen/dream_aqi_badge_padding_horizontal"
- android:paddingVertical="@dimen/dream_aqi_badge_padding_vertical"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"/>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/dream_overlay_complication_clock_date.xml b/packages/SystemUI/res/layout/dream_overlay_complication_clock_date.xml
deleted file mode 100644
index efbdd1a..0000000
--- a/packages/SystemUI/res/layout/dream_overlay_complication_clock_date.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<?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.
--->
-<TextClock
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/date_view"
- style="@style/clock_subtitle"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:format12Hour="@string/dream_date_complication_date_format"
- android:format24Hour="@string/dream_date_complication_date_format"/>
diff --git a/packages/SystemUI/res/layout/dream_overlay_complication_weather.xml b/packages/SystemUI/res/layout/dream_overlay_complication_weather.xml
deleted file mode 100644
index f05922f..0000000
--- a/packages/SystemUI/res/layout/dream_overlay_complication_weather.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<?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.
--->
-<TextView
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/weather_view"
- style="@style/clock_subtitle"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content" />
diff --git a/packages/SystemUI/res/drawable/dream_aqi_badge_bg.xml b/packages/SystemUI/res/values-h411dp/dimens.xml
similarity index 74%
copy from packages/SystemUI/res/drawable/dream_aqi_badge_bg.xml
copy to packages/SystemUI/res/values-h411dp/dimens.xml
index 1992c77..6b21353 100644
--- a/packages/SystemUI/res/drawable/dream_aqi_badge_bg.xml
+++ b/packages/SystemUI/res/values-h411dp/dimens.xml
@@ -1,3 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2022 The Android Open Source Project
~
@@ -13,8 +14,6 @@
~ 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="@color/dream_overlay_aqi_unknown" />
- <corners android:radius="@dimen/dream_aqi_badge_corner_radius" />
-</shape>
\ No newline at end of file
+<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 055308f..39777ab 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/drawable/dream_aqi_badge_bg.xml b/packages/SystemUI/res/values-h841dp/dimens.xml
similarity index 74%
copy from packages/SystemUI/res/drawable/dream_aqi_badge_bg.xml
copy to packages/SystemUI/res/values-h841dp/dimens.xml
index 1992c77..412da19 100644
--- a/packages/SystemUI/res/drawable/dream_aqi_badge_bg.xml
+++ b/packages/SystemUI/res/values-h841dp/dimens.xml
@@ -1,3 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2022 The Android Open Source Project
~
@@ -13,8 +14,6 @@
~ 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="@color/dream_overlay_aqi_unknown" />
- <corners android:radius="@dimen/dream_aqi_badge_corner_radius" />
-</shape>
\ No newline at end of file
+<resources>
+ <dimen name="volume_row_slider_height">237dp</dimen>
+</resources>
diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml
index 70d53c7..ba6977a 100644
--- a/packages/SystemUI/res/values/colors.xml
+++ b/packages/SystemUI/res/values/colors.xml
@@ -246,15 +246,6 @@
<color name="dream_overlay_camera_mic_off_dot_color">#FCBE03</color>
- <!-- Air Quality -->
- <color name="dream_overlay_aqi_good">#689F38</color>
- <color name="dream_overlay_aqi_moderate">#FBC02D</color>
- <color name="dream_overlay_aqi_unhealthy_sensitive">#F57C00</color>
- <color name="dream_overlay_aqi_unhealthy">#C53929</color>
- <color name="dream_overlay_aqi_very_unhealthy">#AD1457</color>
- <color name="dream_overlay_aqi_hazardous">#880E4F</color>
- <color name="dream_overlay_aqi_unknown">#BDC1C6</color>
-
<!-- Dream overlay text shadows -->
<color name="dream_overlay_clock_key_text_shadow_color">#4D000000</color>
<color name="dream_overlay_clock_ambient_text_shadow_color">#4D000000</color>
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 247e44d..6a87630 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 73baeac..227c0dd 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,12 +1585,9 @@
<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="dream_aqi_badge_corner_radius">28dp</dimen>
- <dimen name="dream_aqi_badge_padding_vertical">6dp</dimen>
- <dimen name="dream_aqi_badge_padding_horizontal">16dp</dimen>
-
<dimen name="status_view_margin_horizontal">0dp</dimen>
<!-- Media output broadcast dialog QR code picture size -->
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 5d4fa58..a2e03c3 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2792,9 +2792,6 @@
<!-- [CHAR LIMIT=NONE] Le audio broadcast dialog, media app is unknown -->
<string name="bt_le_audio_broadcast_dialog_unknown_name">Unknown</string>
- <!-- Date format for the Dream Date Complication [CHAR LIMIT=NONE] -->
- <string name="dream_date_complication_date_format">EEE, MMM d</string>
-
<!-- Time format for the Dream Time Complication for 12-hour time format [CHAR LIMIT=NONE] -->
<string name="dream_time_complication_12_hr_time_format">h:mm</string>
@@ -2862,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/res/xml/combined_qs_header_scene.xml b/packages/SystemUI/res/xml/combined_qs_header_scene.xml
index c32de70..38c1640 100644
--- a/packages/SystemUI/res/xml/combined_qs_header_scene.xml
+++ b/packages/SystemUI/res/xml/combined_qs_header_scene.xml
@@ -124,6 +124,11 @@
</KeyFrameSet>
</Transition>
+ <Transition
+ android:id="@+id/large_screen_header_transition"
+ app:constraintSetStart="@id/large_screen_header_constraint"
+ app:constraintSetEnd="@id/large_screen_header_constraint"/>
+
<Include app:constraintSet="@xml/large_screen_shade_header"/>
<Include app:constraintSet="@xml/qs_header"/>
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginActionManager.java b/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginActionManager.java
index e226d58..b057fe4 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginActionManager.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/plugins/PluginActionManager.java
@@ -362,8 +362,7 @@
nb.addAction(new Action.Builder(null, "Disable plugin", pi).build());
mNotificationManager.notify(SystemMessage.NOTE_PLUGIN, nb.build());
// TODO: Warn user.
- Log.w(TAG, "Plugin has invalid interface version " + e.getActualVersion()
- + ", expected " + e.getExpectedVersion());
+ Log.w(TAG, "Error loading plugin; " + e.getMessage());
}
/**
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java
index 5883b6c..b927155 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/PreviewPositionHelper.java
@@ -39,7 +39,6 @@
private boolean mIsOrientationChanged;
private SplitBounds mSplitBounds;
private int mDesiredStagePosition;
- private boolean mTaskbarInApp;
public Matrix getMatrix() {
return mMatrix;
@@ -58,10 +57,6 @@
mDesiredStagePosition = desiredStagePosition;
}
- public void setTaskbarInApp(boolean taskbarInApp) {
- mTaskbarInApp = taskbarInApp;
- }
-
/**
* Updates the matrix based on the provided parameters
*/
@@ -79,34 +74,21 @@
float scaledTaskbarSize;
float canvasScreenRatio;
if (mSplitBounds != null) {
- float fullscreenTaskWidth;
- float fullscreenTaskHeight;
-
- float taskPercent;
if (mSplitBounds.appsStackedVertically) {
- taskPercent = mDesiredStagePosition != STAGE_POSITION_TOP_OR_LEFT
- ? mSplitBounds.topTaskPercent
- : (1 - (mSplitBounds.topTaskPercent + mSplitBounds.dividerHeightPercent));
- // Scale portrait height to that of the actual screen
- fullscreenTaskHeight = screenHeightPx * taskPercent;
- if (mTaskbarInApp) {
- canvasScreenRatio = canvasHeight / fullscreenTaskHeight;
+ if (mDesiredStagePosition == STAGE_POSITION_TOP_OR_LEFT) {
+ // Top app isn't cropped at all by taskbar
+ canvasScreenRatio = 0;
} else {
- if (mDesiredStagePosition == STAGE_POSITION_TOP_OR_LEFT) {
- // Top app isn't cropped at all by taskbar
- canvasScreenRatio = 0;
- } else {
- // Same as fullscreen ratio
- canvasScreenRatio = (float) canvasWidth / screenWidthPx;
- }
+ // Same as fullscreen ratio
+ canvasScreenRatio = (float) canvasWidth / screenWidthPx;
}
} else {
// For landscape, scale the width
- taskPercent = mDesiredStagePosition == STAGE_POSITION_TOP_OR_LEFT
+ float taskPercent = mDesiredStagePosition == STAGE_POSITION_TOP_OR_LEFT
? mSplitBounds.leftTaskPercent
: (1 - (mSplitBounds.leftTaskPercent + mSplitBounds.dividerWidthPercent));
// Scale landscape width to that of actual screen
- fullscreenTaskWidth = screenWidthPx * taskPercent;
+ float fullscreenTaskWidth = screenWidthPx * taskPercent;
canvasScreenRatio = canvasWidth / fullscreenTaskWidth;
}
} else {
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java
index 860c8e3..7da27b1 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java
@@ -260,7 +260,8 @@
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 40423cd..62babad 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.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 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 @@
}
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/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
index e6aae9b..6ce84a9 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
@@ -78,6 +78,7 @@
private int mCurrentClockSize = SMALL;
private int mKeyguardSmallClockTopMargin = 0;
+ private int mKeyguardLargeClockTopMargin = 0;
private final ClockRegistry.ClockChangeListener mClockChangedListener;
private ViewGroup mStatusArea;
@@ -164,6 +165,8 @@
mClockEventController.registerListeners(mView);
mKeyguardSmallClockTopMargin =
mView.getResources().getDimensionPixelSize(R.dimen.keyguard_clock_top_margin);
+ mKeyguardLargeClockTopMargin =
+ mView.getResources().getDimensionPixelSize(R.dimen.keyguard_large_clock_top_margin);
if (mOnlyClock) {
View ksv = mView.findViewById(R.id.keyguard_slice_view);
@@ -246,6 +249,8 @@
mView.onDensityOrFontScaleChanged();
mKeyguardSmallClockTopMargin =
mView.getResources().getDimensionPixelSize(R.dimen.keyguard_clock_top_margin);
+ mKeyguardLargeClockTopMargin =
+ mView.getResources().getDimensionPixelSize(R.dimen.keyguard_large_clock_top_margin);
mView.updateClockTargetRegions();
}
@@ -324,10 +329,18 @@
}
if (mLargeClockFrame.getVisibility() == View.VISIBLE) {
+ // This gets the expected clock bottom if mLargeClockFrame had a top margin, but it's
+ // top margin only contributed to height and didn't move the top of the view (as this
+ // was the computation previously). As we no longer have a margin, we add this back
+ // into the computation manually.
int frameHeight = mLargeClockFrame.getHeight();
int clockHeight = clock.getLargeClock().getView().getHeight();
- return frameHeight / 2 + clockHeight / 2;
+ return frameHeight / 2 + clockHeight / 2 + mKeyguardLargeClockTopMargin / -2;
} else {
+ // This is only called if we've never shown the large clock as the frame is inflated
+ // with 'gone', but then the visibility is never set when it is animated away by
+ // KeyguardClockSwitch, instead it is removed from the view hierarchy.
+ // TODO(b/261755021): Cleanup Large Frame Visibility
int clockHeight = clock.getSmallClock().getView().getHeight();
return clockHeight + statusBarHeaderHeight + mKeyguardSmallClockTopMargin;
}
@@ -345,11 +358,15 @@
if (mLargeClockFrame.getVisibility() == View.VISIBLE) {
return clock.getLargeClock().getView().getHeight();
} else {
+ // Is not called except in certain edge cases, see comment in getClockBottom
+ // TODO(b/261755021): Cleanup Large Frame Visibility
return clock.getSmallClock().getView().getHeight();
}
}
boolean isClockTopAligned() {
+ // Returns false except certain edge cases, see comment in getClockBottom
+ // TODO(b/261755021): Cleanup Large Frame Visibility
return mLargeClockFrame.getVisibility() != View.VISIBLE;
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
index 2e9ad58..d1c9a30 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java
@@ -142,8 +142,11 @@
}
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 @@
}
/** 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 5d86ccd..67e3400 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPINView.java
@@ -52,6 +52,7 @@
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 @@
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 c985fd7..c1fae9e 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_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 @@
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 571d274..0c17489 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternViewController.java
@@ -313,6 +313,9 @@
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 c46e33d..0a91150 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_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 @@
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 f7423ed..8011efd 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java
@@ -139,4 +139,9 @@
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 f51ac32..35b2db2 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java
@@ -74,9 +74,4 @@
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 8f3484a..5d7a6f1 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 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 @@
}
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 ac00e94..67d77e5 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityView.java
@@ -61,6 +61,12 @@
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 a5c8c78..39b567f 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewFlipperController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityViewFlipperController.java
@@ -156,5 +156,10 @@
@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 f5aaecf..746616a 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -1651,7 +1651,7 @@
@Override
public void onAuthenticationFailed() {
- requestActiveUnlock(
+ requestActiveUnlockDismissKeyguard(
ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN.BIOMETRIC_FAIL,
"fingerprintFailure");
handleFingerprintAuthFailed();
@@ -2583,6 +2583,18 @@
}
/**
+ * 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/SystemUIInitializer.java b/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java
index 632fcdc..0fc9ef9 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java
@@ -22,6 +22,8 @@
import android.os.HandlerThread;
import android.util.Log;
+import androidx.annotation.Nullable;
+
import com.android.systemui.dagger.GlobalRootComponent;
import com.android.systemui.dagger.SysUIComponent;
import com.android.systemui.dagger.WMComponent;
@@ -53,6 +55,7 @@
mContext = context;
}
+ @Nullable
protected abstract GlobalRootComponent.Builder getGlobalRootComponentBuilder();
/**
@@ -69,6 +72,11 @@
* Starts the initialization process. This stands up the Dagger graph.
*/
public void init(boolean fromTest) throws ExecutionException, InterruptedException {
+ GlobalRootComponent.Builder globalBuilder = getGlobalRootComponentBuilder();
+ if (globalBuilder == null) {
+ return;
+ }
+
mRootComponent = getGlobalRootComponentBuilder()
.context(mContext)
.instrumentationTest(fromTest)
@@ -119,6 +127,7 @@
.setBackAnimation(Optional.ofNullable(null))
.setDesktopMode(Optional.ofNullable(null));
}
+
mSysUIComponent = builder.build();
if (initializeComponents) {
mSysUIComponent.init();
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIInitializerImpl.kt b/packages/SystemUI/src/com/android/systemui/SystemUIInitializerImpl.kt
index 8aa3040..55c095b 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIInitializerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIInitializerImpl.kt
@@ -16,6 +16,7 @@
package com.android.systemui
+import android.app.Application
import android.content.Context
import com.android.systemui.dagger.DaggerReferenceGlobalRootComponent
import com.android.systemui.dagger.GlobalRootComponent
@@ -24,7 +25,17 @@
* {@link SystemUIInitializer} that stands up AOSP SystemUI.
*/
class SystemUIInitializerImpl(context: Context) : SystemUIInitializer(context) {
- override fun getGlobalRootComponentBuilder(): GlobalRootComponent.Builder {
- return DaggerReferenceGlobalRootComponent.builder()
+
+ override fun getGlobalRootComponentBuilder(): GlobalRootComponent.Builder? {
+ return when (Application.getProcessName()) {
+ SCREENSHOT_CROSS_PROFILE_PROCESS -> null
+ else -> DaggerReferenceGlobalRootComponent.builder()
+ }
+ }
+
+ companion object {
+ private const val SYSTEMUI_PROCESS = "com.android.systemui"
+ private const val SCREENSHOT_CROSS_PROFILE_PROCESS =
+ "$SYSTEMUI_PROCESS:screenshot_cross_profile"
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/TEST_MAPPING b/packages/SystemUI/src/com/android/systemui/biometrics/TEST_MAPPING
deleted file mode 100644
index 794eba4..0000000
--- a/packages/SystemUI/src/com/android/systemui/biometrics/TEST_MAPPING
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "presubmit": [
- {
- // TODO(b/251476085): Consider merging with SystemUIGoogleScreenshotTests (in U+)
- "name": "SystemUIGoogleBiometricsScreenshotTests",
- "options": [
- {
- "exclude-annotation": "org.junit.Ignore"
- },
- {
- "exclude-annotation": "androidx.test.filters.FlakyTest"
- }
- ]
- }
- ]
-}
diff --git a/packages/SystemUI/src/com/android/systemui/camera/CameraGestureHelper.kt b/packages/SystemUI/src/com/android/systemui/camera/CameraGestureHelper.kt
index e2ef247..58d40d3 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.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 @@
*/
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 @@
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 f8a2002..867faf9 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 @@
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 4aa597e..8d0edf8 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 @@
* 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 @@
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 5d611c4..d8d8c0e 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 @@
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 fb678aa..1e3e5cd 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 @@
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 7143be2..f5764c2 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.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 @@
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 0087c84..9b8ef71 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.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 @@
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 @@
*/
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 @@
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
),
- alphaAnimator(
- from = 0f,
- to = 1f,
- durationMs = mDreamInComplicationsAnimDurationMs,
- delayMs = mDreamInBottomComplicationsAnimDelayMs,
- position = ComplicationLayoutParams.POSITION_BOTTOM
- )
+ translationYAnimator(
+ from = mDreamInTranslationYDistance.toFloat(),
+ to = 0f,
+ durationMs = mDreamInTranslationYDurationMs,
+ interpolator = Interpolators.EMPHASIZED_DECELERATE
+ ),
)
doOnEnd {
mAnimator = null
@@ -130,47 +128,48 @@
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 @@
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 @@
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 @@
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 @@
CrossFadeHelper.fadeIn(view, alpha, /* remap= */ false)
}
}
- if (position == ComplicationLayoutParams.POSITION_TOP) {
+ if (position == POSITION_TOP) {
mStatusBarViewController.setFadeAmount(alpha, fadingOut)
}
}
@@ -273,7 +284,7 @@
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/DreamOverlayLifecycleOwner.kt b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayLifecycleOwner.kt
new file mode 100644
index 0000000..8325356
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayLifecycleOwner.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.dreams
+
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.LifecycleRegistry
+import javax.inject.Inject
+
+/**
+ * {@link DreamOverlayLifecycleOwner} is a concrete implementation of {@link LifecycleOwner}, which
+ * provides access to an associated {@link LifecycleRegistry}.
+ */
+class DreamOverlayLifecycleOwner @Inject constructor() : LifecycleOwner {
+ val registry: LifecycleRegistry = LifecycleRegistry(this)
+
+ override fun getLifecycle(): Lifecycle {
+ return registry
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayRegistrant.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayRegistrant.java
index d145f5c..87c5f51 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayRegistrant.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayRegistrant.java
@@ -16,6 +16,8 @@
package com.android.systemui.dreams;
+import static com.android.systemui.dreams.dagger.DreamModule.DREAM_OVERLAY_SERVICE_COMPONENT;
+
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
@@ -35,6 +37,7 @@
import com.android.systemui.dagger.qualifiers.Main;
import javax.inject.Inject;
+import javax.inject.Named;
/**
* {@link DreamOverlayRegistrant} is responsible for telling system server that SystemUI should be
@@ -98,12 +101,13 @@
}
@Inject
- public DreamOverlayRegistrant(Context context, @Main Resources resources) {
+ public DreamOverlayRegistrant(Context context, @Main Resources resources,
+ @Named(DREAM_OVERLAY_SERVICE_COMPONENT) ComponentName dreamOverlayServiceComponent) {
mContext = context;
mResources = resources;
mDreamManager = IDreamManager.Stub.asInterface(
ServiceManager.getService(DreamService.DREAM_SERVICE));
- mOverlayServiceComponent = new ComponentName(mContext, DreamOverlayService.class);
+ mOverlayServiceComponent = dreamOverlayServiceComponent;
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
index b927ae7..16b4f99 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
@@ -30,6 +30,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LifecycleRegistry;
import androidx.lifecycle.ViewModelStore;
@@ -44,7 +45,10 @@
import com.android.systemui.dreams.complication.dagger.ComplicationComponent;
import com.android.systemui.dreams.dagger.DreamOverlayComponent;
import com.android.systemui.dreams.touch.DreamOverlayTouchMonitor;
+import com.android.systemui.touch.TouchInsetManager;
+import java.util.Arrays;
+import java.util.HashSet;
import java.util.concurrent.Executor;
import javax.inject.Inject;
@@ -83,8 +87,12 @@
private final ComplicationComponent mComplicationComponent;
+ private final com.android.systemui.dreams.dreamcomplication.dagger.ComplicationComponent
+ mDreamComplicationComponent;
+
private final DreamOverlayComponent mDreamOverlayComponent;
+ private final DreamOverlayLifecycleOwner mLifecycleOwner;
private final LifecycleRegistry mLifecycleRegistry;
private DreamOverlayTouchMonitor mDreamOverlayTouchMonitor;
@@ -130,12 +138,16 @@
public DreamOverlayService(
Context context,
@Main Executor executor,
+ DreamOverlayLifecycleOwner lifecycleOwner,
WindowManager windowManager,
ComplicationComponent.Factory complicationComponentFactory,
+ com.android.systemui.dreams.dreamcomplication.dagger.ComplicationComponent.Factory
+ dreamComplicationComponentFactory,
DreamOverlayComponent.Factory dreamOverlayComponentFactory,
DreamOverlayStateController stateController,
KeyguardUpdateMonitor keyguardUpdateMonitor,
UiEventLogger uiEventLogger,
+ TouchInsetManager touchInsetManager,
@Nullable @Named(LowLightDreamModule.LOW_LIGHT_DREAM_COMPONENT)
ComponentName lowLightDreamComponent) {
mContext = context;
@@ -151,9 +163,16 @@
final Complication.Host host =
() -> mExecutor.execute(DreamOverlayService.this::requestExit);
- mComplicationComponent = complicationComponentFactory.create();
- mDreamOverlayComponent = dreamOverlayComponentFactory.create(viewModelStore, host, null);
- mLifecycleRegistry = mDreamOverlayComponent.getLifecycleRegistry();
+ mComplicationComponent = complicationComponentFactory.create(lifecycleOwner, host,
+ viewModelStore, touchInsetManager);
+ mDreamComplicationComponent = dreamComplicationComponentFactory.create(
+ mComplicationComponent.getVisibilityController(), touchInsetManager);
+ mDreamOverlayComponent = dreamOverlayComponentFactory.create(lifecycleOwner,
+ mComplicationComponent.getComplicationHostViewController(), touchInsetManager,
+ new HashSet<>(Arrays.asList(
+ mDreamComplicationComponent.getHideComplicationTouchHandler())));
+ mLifecycleOwner = lifecycleOwner;
+ mLifecycleRegistry = mLifecycleOwner.getRegistry();
mExecutor.execute(() -> setCurrentStateLocked(Lifecycle.State.CREATED));
}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java
index 5f942b6..ccfdd096 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java
@@ -16,6 +16,8 @@
package com.android.systemui.dreams;
+import static com.android.systemui.dreams.dagger.DreamModule.DREAM_OVERLAY_ENABLED;
+
import android.service.dreams.DreamService;
import android.util.Log;
@@ -37,6 +39,7 @@
import java.util.stream.Collectors;
import javax.inject.Inject;
+import javax.inject.Named;
/**
* {@link DreamOverlayStateController} is the source of truth for Dream overlay configurations and
@@ -83,6 +86,7 @@
}
private final Executor mExecutor;
+ private final boolean mOverlayEnabled;
private final ArrayList<Callback> mCallbacks = new ArrayList<>();
@Complication.ComplicationType
@@ -94,14 +98,27 @@
@VisibleForTesting
@Inject
- public DreamOverlayStateController(@Main Executor executor) {
+ public DreamOverlayStateController(@Main Executor executor,
+ @Named(DREAM_OVERLAY_ENABLED) boolean overlayEnabled) {
mExecutor = executor;
+ mOverlayEnabled = overlayEnabled;
+ if (DEBUG) {
+ Log.d(TAG, "Dream overlay enabled:" + mOverlayEnabled);
+ }
}
/**
* Adds a complication to be included on the dream overlay.
*/
public void addComplication(Complication complication) {
+ if (!mOverlayEnabled) {
+ if (DEBUG) {
+ Log.d(TAG,
+ "Ignoring adding complication due to overlay disabled:" + complication);
+ }
+ return;
+ }
+
mExecutor.execute(() -> {
if (mComplications.add(complication)) {
if (DEBUG) {
@@ -116,6 +133,14 @@
* Removes a complication from inclusion on the dream overlay.
*/
public void removeComplication(Complication complication) {
+ if (!mOverlayEnabled) {
+ if (DEBUG) {
+ Log.d(TAG,
+ "Ignoring removing complication due to overlay disabled:" + complication);
+ }
+ return;
+ }
+
mExecutor.execute(() -> {
if (mComplications.remove(complication)) {
if (DEBUG) {
@@ -193,7 +218,7 @@
* @return {@code true} if overlay is active, {@code false} otherwise.
*/
public boolean isOverlayActive() {
- return containsState(STATE_DREAM_OVERLAY_ACTIVE);
+ return mOverlayEnabled && containsState(STATE_DREAM_OVERLAY_ACTIVE);
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationHostViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationHostViewController.java
index 100ccc3..a2e11b2 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationHostViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationHostViewController.java
@@ -138,19 +138,27 @@
final ComplicationId id = complication.getId();
final Complication.ViewHolder viewHolder = complication.getComplication()
.createView(complication);
+
+ final View view = viewHolder.getView();
+
+ if (view == null) {
+ Log.e(TAG, "invalid complication view. null view supplied by ViewHolder");
+ return;
+ }
+
// Complications to be added before dream entry animations are finished are set
// to invisible and are animated in.
if (!mEntryAnimationsFinished) {
- viewHolder.getView().setVisibility(View.INVISIBLE);
+ view.setVisibility(View.INVISIBLE);
}
mComplications.put(id, viewHolder);
- if (viewHolder.getView().getParent() != null) {
+ if (view.getParent() != null) {
Log.e(TAG, "View for complication "
+ complication.getComplication().getClass()
+ " already has a parent. Make sure not to reuse complication "
+ "views!");
}
- mLayoutEngine.addComplication(id, viewHolder.getView(),
+ mLayoutEngine.addComplication(id, view,
viewHolder.getLayoutParams(), viewHolder.getCategory());
});
}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutEngine.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutEngine.java
index 46ce7a9..3e9b010 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutEngine.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationLayoutEngine.java
@@ -30,7 +30,7 @@
import com.android.systemui.R;
import com.android.systemui.dreams.complication.ComplicationLayoutParams.Position;
-import com.android.systemui.dreams.dagger.DreamOverlayComponent;
+import com.android.systemui.dreams.complication.dagger.ComplicationModule;
import com.android.systemui.statusbar.CrossFadeHelper;
import com.android.systemui.touch.TouchInsetManager;
@@ -50,7 +50,7 @@
* their layout parameters and attributes. The management of this set is done by
* {@link ComplicationHostViewController}.
*/
-@DreamOverlayComponent.DreamOverlayScope
+@ComplicationModule.ComplicationScope
public class ComplicationLayoutEngine implements Complication.VisibilityController {
public static final String TAG = "ComplicationLayoutEng";
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 1755cb92..99e19fc 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 @@
* 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 ee00512..1065b94 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 @@
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/complication/dagger/ComplicationComponent.kt b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/ComplicationComponent.kt
index 8949709..8d133bd 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/ComplicationComponent.kt
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/ComplicationComponent.kt
@@ -1,12 +1,29 @@
package com.android.systemui.dreams.complication.dagger
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.ViewModelStore
+import com.android.systemui.dreams.complication.Complication
+import com.android.systemui.dreams.complication.ComplicationHostViewController
+import com.android.systemui.dreams.complication.ComplicationLayoutEngine
+import com.android.systemui.touch.TouchInsetManager
+import dagger.BindsInstance
import dagger.Subcomponent
-@Subcomponent
+@Subcomponent(modules = [ComplicationModule::class])
+@ComplicationModule.ComplicationScope
interface ComplicationComponent {
/** Factory for generating [ComplicationComponent]. */
@Subcomponent.Factory
interface Factory {
- fun create(): ComplicationComponent
+ fun create(
+ @BindsInstance lifecycleOwner: LifecycleOwner,
+ @BindsInstance host: Complication.Host,
+ @BindsInstance viewModelStore: ViewModelStore,
+ @BindsInstance touchInsetManager: TouchInsetManager
+ ): ComplicationComponent
}
+
+ fun getComplicationHostViewController(): ComplicationHostViewController
+
+ fun getVisibilityController(): ComplicationLayoutEngine
}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/ComplicationHostViewModule.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/ComplicationHostViewModule.java
index 09cc7c5..797906f 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/ComplicationHostViewModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/ComplicationHostViewModule.java
@@ -24,13 +24,12 @@
import com.android.internal.util.Preconditions;
import com.android.systemui.R;
import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.dreams.dagger.DreamOverlayComponent;
-
-import javax.inject.Named;
import dagger.Module;
import dagger.Provides;
+import javax.inject.Named;
+
/**
* Module for providing a scoped host view.
*/
@@ -49,7 +48,7 @@
*/
@Provides
@Named(SCOPED_COMPLICATIONS_LAYOUT)
- @DreamOverlayComponent.DreamOverlayScope
+ @ComplicationModule.ComplicationScope
static ConstraintLayout providesComplicationHostView(
LayoutInflater layoutInflater) {
return Preconditions.checkNotNull((ConstraintLayout)
@@ -60,7 +59,6 @@
@Provides
@Named(COMPLICATION_MARGIN_DEFAULT)
- @DreamOverlayComponent.DreamOverlayScope
static int providesComplicationPadding(@Main Resources resources) {
return resources.getDimensionPixelSize(R.dimen.dream_overlay_complication_margin);
}
@@ -70,7 +68,6 @@
*/
@Provides
@Named(COMPLICATIONS_FADE_OUT_DURATION)
- @DreamOverlayComponent.DreamOverlayScope
static int providesComplicationsFadeOutDuration(@Main Resources resources) {
return resources.getInteger(R.integer.complicationFadeOutMs);
}
@@ -80,7 +77,6 @@
*/
@Provides
@Named(COMPLICATIONS_FADE_OUT_DELAY)
- @DreamOverlayComponent.DreamOverlayScope
static int providesComplicationsFadeOutDelay(@Main Resources resources) {
return resources.getInteger(R.integer.complicationFadeOutDelayMs);
}
@@ -90,7 +86,6 @@
*/
@Provides
@Named(COMPLICATIONS_FADE_IN_DURATION)
- @DreamOverlayComponent.DreamOverlayScope
static int providesComplicationsFadeInDuration(@Main Resources resources) {
return resources.getInteger(R.integer.complicationFadeInMs);
}
@@ -100,7 +95,6 @@
*/
@Provides
@Named(COMPLICATIONS_RESTORE_TIMEOUT)
- @DreamOverlayComponent.DreamOverlayScope
static int providesComplicationsRestoreTimeout(@Main Resources resources) {
return resources.getInteger(R.integer.complicationRestoreMs);
}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/ComplicationModule.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/ComplicationModule.java
index 5c2fdf5..dbf5ab0 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/ComplicationModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/ComplicationModule.java
@@ -24,16 +24,16 @@
import com.android.systemui.dreams.complication.Complication;
import com.android.systemui.dreams.complication.ComplicationCollectionViewModel;
import com.android.systemui.dreams.complication.ComplicationLayoutEngine;
+import com.android.systemui.touch.TouchInsetManager;
+
+import dagger.Module;
+import dagger.Provides;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import javax.inject.Named;
import javax.inject.Scope;
-
-import dagger.Module;
-import dagger.Provides;
-
/**
* Module for housing components related to rendering complications.
*/
@@ -73,4 +73,13 @@
ComplicationLayoutEngine engine) {
return engine;
}
+
+ /**
+ * Provides a new touch inset session instance for complication logic.
+ */
+ @Provides
+ static TouchInsetManager.TouchInsetSession providesTouchInsetSession(
+ TouchInsetManager manager) {
+ return manager.createSession();
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
index 101f4a4..8770cd1 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
@@ -16,7 +16,9 @@
package com.android.systemui.dreams.dagger;
+import android.content.ComponentName;
import android.content.Context;
+import android.content.pm.PackageManager;
import android.content.res.Resources;
import com.android.dream.lowlight.dagger.LowLightDreamModule;
@@ -24,14 +26,17 @@
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dreams.DreamOverlayNotificationCountProvider;
+import com.android.systemui.dreams.DreamOverlayService;
import com.android.systemui.dreams.complication.dagger.RegisteredComplicationsModule;
+import com.android.systemui.dreams.dreamcomplication.dagger.ComplicationComponent;
+
+import dagger.Module;
+import dagger.Provides;
import java.util.Optional;
import javax.inject.Named;
-import dagger.Module;
-import dagger.Provides;
/**
* Dagger Module providing Dream-related functionality.
@@ -41,14 +46,40 @@
LowLightDreamModule.class,
},
subcomponents = {
+ ComplicationComponent.class,
DreamOverlayComponent.class,
})
public interface DreamModule {
String DREAM_ONLY_ENABLED_FOR_DOCK_USER = "dream_only_enabled_for_dock_user";
+ String DREAM_OVERLAY_SERVICE_COMPONENT = "dream_overlay_service_component";
+ String DREAM_OVERLAY_ENABLED = "dream_overlay_enabled";
String DREAM_SUPPORTED = "dream_supported";
/**
+ * Provides the dream component
+ */
+ @Provides
+ @Named(DREAM_OVERLAY_SERVICE_COMPONENT)
+ static ComponentName providesDreamOverlayService(Context context) {
+ return new ComponentName(context, DreamOverlayService.class);
+ }
+
+ /**
+ * Provides whether dream overlay is enabled.
+ */
+ @Provides
+ @Named(DREAM_OVERLAY_ENABLED)
+ static Boolean providesDreamOverlayEnabled(PackageManager packageManager,
+ @Named(DREAM_OVERLAY_SERVICE_COMPONENT) ComponentName component) {
+ try {
+ return packageManager.getServiceInfo(component, PackageManager.GET_META_DATA).enabled;
+ } catch (PackageManager.NameNotFoundException e) {
+ return false;
+ }
+ }
+
+ /**
* Provides an instance of the dream backend.
*/
@Provides
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayComponent.java b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayComponent.java
index fab4b86..0332f88 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayComponent.java
@@ -23,15 +23,13 @@
import android.annotation.Nullable;
import androidx.lifecycle.LifecycleOwner;
-import androidx.lifecycle.LifecycleRegistry;
-import androidx.lifecycle.ViewModelStore;
import com.android.systemui.dreams.DreamOverlayContainerViewController;
-import com.android.systemui.dreams.complication.Complication;
-import com.android.systemui.dreams.complication.dagger.ComplicationModule;
+import com.android.systemui.dreams.complication.ComplicationHostViewController;
import com.android.systemui.dreams.touch.DreamOverlayTouchMonitor;
import com.android.systemui.dreams.touch.DreamTouchHandler;
import com.android.systemui.dreams.touch.dagger.DreamTouchModule;
+import com.android.systemui.touch.TouchInsetManager;
import dagger.BindsInstance;
import dagger.Subcomponent;
@@ -49,15 +47,16 @@
@Subcomponent(modules = {
DreamTouchModule.class,
DreamOverlayModule.class,
- ComplicationModule.class,
})
@DreamOverlayComponent.DreamOverlayScope
public interface DreamOverlayComponent {
/** Simple factory for {@link DreamOverlayComponent}. */
@Subcomponent.Factory
interface Factory {
- DreamOverlayComponent create(@BindsInstance ViewModelStore store,
- @BindsInstance Complication.Host host,
+ DreamOverlayComponent create(
+ @BindsInstance LifecycleOwner lifecycleOwner,
+ @BindsInstance ComplicationHostViewController complicationHostViewController,
+ @BindsInstance TouchInsetManager touchInsetManager,
@BindsInstance @Named(DREAM_TOUCH_HANDLERS) @Nullable
Set<DreamTouchHandler> dreamTouchHandlers);
}
@@ -71,12 +70,6 @@
/** Builds a {@link DreamOverlayContainerViewController}. */
DreamOverlayContainerViewController getDreamOverlayContainerViewController();
- /** Builds a {@link androidx.lifecycle.LifecycleRegistry} */
- LifecycleRegistry getLifecycleRegistry();
-
- /** Builds a {@link androidx.lifecycle.LifecycleOwner} */
- LifecycleOwner getLifecycleOwner();
-
/** Builds a {@link DreamOverlayTouchMonitor} */
DreamOverlayTouchMonitor getDreamOverlayTouchMonitor();
}
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 fc448a3..4aa46d4 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java
@@ -23,7 +23,6 @@
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleOwner;
-import androidx.lifecycle.LifecycleRegistry;
import com.android.internal.util.Preconditions;
import com.android.systemui.R;
@@ -33,14 +32,12 @@
import com.android.systemui.dreams.touch.DreamTouchHandler;
import com.android.systemui.touch.TouchInsetManager;
-import dagger.Lazy;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.ElementsIntoSet;
import java.util.HashSet;
import java.util.Set;
-import java.util.concurrent.Executor;
import javax.inject.Named;
@@ -53,14 +50,14 @@
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 =
@@ -107,14 +104,6 @@
/** */
@Provides
@DreamOverlayComponent.DreamOverlayScope
- public static TouchInsetManager providesTouchInsetManager(@Main Executor executor,
- DreamOverlayContainerView view) {
- return new TouchInsetManager(executor, view);
- }
-
- /** */
- @Provides
- @DreamOverlayComponent.DreamOverlayScope
public static DreamOverlayStatusBarView providesDreamOverlayStatusBarView(
DreamOverlayContainerView view) {
return Preconditions.checkNotNull(view.findViewById(R.id.dream_overlay_status_bar),
@@ -145,6 +134,15 @@
}
/**
+ * The blur radius applied to the dream overlay at dream entry and exit.
+ */
+ @Provides
+ @Named(DREAM_BLUR_RADIUS)
+ static int providesDreamBlurRadius(@Main Resources resources) {
+ return resources.getDimensionPixelSize(R.dimen.dream_overlay_anim_blur_radius);
+ }
+
+ /**
* Duration in milliseconds of the dream in un-blur animation.
*/
@Provides
@@ -154,15 +152,6 @@
}
/**
- * Delay 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);
- }
-
- /**
* Duration in milliseconds of the dream in complications fade-in animation.
*/
@Provides
@@ -172,22 +161,23 @@
}
/**
- * 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);
}
/**
@@ -252,18 +242,6 @@
@Provides
@DreamOverlayComponent.DreamOverlayScope
- static LifecycleOwner providesLifecycleOwner(Lazy<LifecycleRegistry> lifecycleRegistryLazy) {
- return () -> lifecycleRegistryLazy.get();
- }
-
- @Provides
- @DreamOverlayComponent.DreamOverlayScope
- static LifecycleRegistry providesLifecycleRegistry(LifecycleOwner lifecycleOwner) {
- return new LifecycleRegistry(lifecycleOwner);
- }
-
- @Provides
- @DreamOverlayComponent.DreamOverlayScope
static Lifecycle providesLifecycle(LifecycleOwner lifecycleOwner) {
return lifecycleOwner.getLifecycle();
}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/HideComplicationTouchHandler.java b/packages/SystemUI/src/com/android/systemui/dreams/dreamcomplication/HideComplicationTouchHandler.java
similarity index 94%
rename from packages/SystemUI/src/com/android/systemui/dreams/touch/HideComplicationTouchHandler.java
rename to packages/SystemUI/src/com/android/systemui/dreams/dreamcomplication/HideComplicationTouchHandler.java
index e276e0c..3a4578b 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/touch/HideComplicationTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/dreamcomplication/HideComplicationTouchHandler.java
@@ -14,10 +14,10 @@
* limitations under the License.
*/
-package com.android.systemui.dreams.touch;
+package com.android.systemui.dreams.dreamcomplication;
-import static com.android.systemui.dreams.complication.dagger.ComplicationHostViewModule.COMPLICATIONS_FADE_OUT_DELAY;
-import static com.android.systemui.dreams.complication.dagger.ComplicationHostViewModule.COMPLICATIONS_RESTORE_TIMEOUT;
+import static com.android.systemui.dreams.dreamcomplication.dagger.ComplicationModule.COMPLICATIONS_FADE_OUT_DELAY;
+import static com.android.systemui.dreams.dreamcomplication.dagger.ComplicationModule.COMPLICATIONS_RESTORE_TIMEOUT;
import android.util.Log;
import android.view.MotionEvent;
@@ -28,6 +28,8 @@
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dreams.DreamOverlayStateController;
import com.android.systemui.dreams.complication.Complication;
+import com.android.systemui.dreams.touch.DreamOverlayTouchMonitor;
+import com.android.systemui.dreams.touch.DreamTouchHandler;
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
import com.android.systemui.touch.TouchInsetManager;
import com.android.systemui.util.concurrency.DelayableExecutor;
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dreamcomplication/dagger/ComplicationComponent.kt b/packages/SystemUI/src/com/android/systemui/dreams/dreamcomplication/dagger/ComplicationComponent.kt
new file mode 100644
index 0000000..f2fb48d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/dreamcomplication/dagger/ComplicationComponent.kt
@@ -0,0 +1,21 @@
+package com.android.systemui.dreams.dreamcomplication.dagger
+
+import com.android.systemui.dreams.complication.Complication
+import com.android.systemui.dreams.dreamcomplication.HideComplicationTouchHandler
+import com.android.systemui.touch.TouchInsetManager
+import dagger.BindsInstance
+import dagger.Subcomponent
+
+@Subcomponent(modules = [ComplicationModule::class])
+interface ComplicationComponent {
+ /** Factory for generating [ComplicationComponent]. */
+ @Subcomponent.Factory
+ interface Factory {
+ fun create(
+ @BindsInstance visibilityController: Complication.VisibilityController,
+ @BindsInstance touchInsetManager: TouchInsetManager
+ ): ComplicationComponent
+ }
+
+ fun getHideComplicationTouchHandler(): HideComplicationTouchHandler
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dreamcomplication/dagger/ComplicationModule.kt b/packages/SystemUI/src/com/android/systemui/dreams/dreamcomplication/dagger/ComplicationModule.kt
new file mode 100644
index 0000000..ef75ce1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/dreamcomplication/dagger/ComplicationModule.kt
@@ -0,0 +1,28 @@
+package com.android.systemui.dreams.dreamcomplication.dagger
+
+import android.content.res.Resources
+import com.android.systemui.R
+import com.android.systemui.dagger.qualifiers.Main
+import dagger.Module
+import dagger.Provides
+import javax.inject.Named
+
+@Module
+object ComplicationModule {
+ const val COMPLICATIONS_RESTORE_TIMEOUT = "complication_restore_timeout"
+ const val COMPLICATIONS_FADE_OUT_DELAY = "complication_fade_out_delay"
+
+ /** Provides the delay to wait for before fading out complications. */
+ @Provides
+ @Named(COMPLICATIONS_FADE_OUT_DELAY)
+ fun providesComplicationsFadeOutDelay(@Main resources: Resources): Int {
+ return resources.getInteger(R.integer.complicationFadeOutDelayMs)
+ }
+
+ /** Provides the timeout for restoring complication visibility. */
+ @Provides
+ @Named(COMPLICATIONS_RESTORE_TIMEOUT)
+ fun providesComplicationsRestoreTimeout(@Main resources: Resources): Int {
+ return resources.getInteger(R.integer.complicationRestoreMs)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/DreamTouchModule.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/DreamTouchModule.java
index 7338ecb..dad0004 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/DreamTouchModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/DreamTouchModule.java
@@ -23,7 +23,6 @@
*/
@Module(includes = {
BouncerSwipeModule.class,
- HideComplicationModule.class,
}, subcomponents = {
InputSessionComponent.class,
})
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/HideComplicationModule.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/HideComplicationModule.java
deleted file mode 100644
index 3800ff7..0000000
--- a/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/HideComplicationModule.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.dreams.touch.dagger;
-
-import com.android.systemui.dreams.touch.DreamTouchHandler;
-import com.android.systemui.dreams.touch.HideComplicationTouchHandler;
-
-import dagger.Module;
-import dagger.Provides;
-import dagger.multibindings.IntoSet;
-
-/**
- * Module for {@link HideComplicationTouchHandler}.
- */
-@Module
-public class HideComplicationModule {
- /**
- * Provides {@link HideComplicationTouchHandler} for inclusion in touch handling over the dream.
- */
- @Provides
- @IntoSet
- public static DreamTouchHandler providesHideComplicationTouchHandler(
- HideComplicationTouchHandler touchHandler) {
- return touchHandler;
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index 0f52a2b..1a244cc5 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -83,7 +83,7 @@
val STABILITY_INDEX_FIX = releasedFlag(114, "stability_index_fix")
// TODO(b/259559750): Tracking Bug
- val SEMI_STABLE_SORT = unreleasedFlag(115, "semi_stable_sort", teamfood = true)
+ val SEMI_STABLE_SORT = releasedFlag(115, "semi_stable_sort")
@JvmField
val USE_ROUNDNESS_SOURCETYPES = unreleasedFlag(116, "use_roundness_sourcetype", teamfood = true)
@@ -290,7 +290,8 @@
// TODO(b/254513168): Tracking Bug
@JvmField val UMO_SURFACE_RIPPLE = unreleasedFlag(907, "umo_surface_ripple")
- @JvmField val MEDIA_FALSING_PENALTY = unreleasedFlag(908, "media_falsing_media")
+ @JvmField
+ val MEDIA_FALSING_PENALTY = unreleasedFlag(908, "media_falsing_media", teamfood = true)
// TODO(b/261734857): Tracking Bug
@JvmField val UMO_TURBULENCE_NOISE = unreleasedFlag(909, "umo_turbulence_noise")
@@ -405,9 +406,10 @@
val CHOOSER_UNBUNDLED = unreleasedFlag(1500, "chooser_unbundled", teamfood = true)
// 1600 - accessibility
+ // TODO(b/262224538): Tracking Bug
@JvmField
val A11Y_FLOATING_MENU_FLING_SPRING_ANIMATIONS =
- unreleasedFlag(1600, "a11y_floating_menu_fling_spring_animations", teamfood = true)
+ releasedFlag(1600, "a11y_floating_menu_fling_spring_animations")
// 1700 - clipboard
@JvmField val CLIPBOARD_OVERLAY_REFACTOR = releasedFlag(1700, "clipboard_overlay_refactor")
@@ -440,6 +442,8 @@
// 2300 - stylus
@JvmField val TRACK_STYLUS_EVER_USED = unreleasedFlag(2300, "track_stylus_ever_used")
@JvmField val ENABLE_STYLUS_CHARGING_UI = unreleasedFlag(2301, "enable_stylus_charging_ui")
+ @JvmField
+ val ENABLE_USI_BATTERY_NOTIFICATIONS = unreleasedFlag(2302, "enable_usi_battery_notifications")
// 2400 - performance tools and debugging info
// TODO(b/238923086): Tracking Bug
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt
index 4ae37c5..cbcede0 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.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 @@
ContentProvider(), SystemUIAppComponentFactoryBase.ContextInitializer {
@Inject lateinit var interactor: KeyguardQuickAffordanceInteractor
+ @Inject lateinit var previewManager: KeyguardRemotePreviewManager
private lateinit var contextAvailableCallback: ContextAvailableCallback
@@ -149,6 +154,21 @@
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 36c939d..9234136 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.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.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 @@
} 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;
@@ -1614,7 +1618,7 @@
// TODO: Rename all screen off/on references to interactive/sleeping
synchronized (this) {
mDeviceInteractive = true;
- if (mPendingLock && !cameraGestureTriggered) {
+ if (mPendingLock && !cameraGestureTriggered && !mWakeAndUnlocking) {
doKeyguardLocked(null);
}
mAnimatingScreenOff = false;
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 2558fab..394426d 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 @@
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 @@
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 748c6e8..57668c7 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.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 @@
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 @@
}
/**
+ * 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 @@
}
}
- 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 a18b036..2581b59 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 cbe512f..ae8edfe 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.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 @@
* 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 @@
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 @@
view = startButton,
viewModel = buttonModel,
falsingManager = falsingManager,
+ messageDisplayer = messageDisplayer,
)
}
}
@@ -118,6 +128,7 @@
view = endButton,
viewModel = buttonModel,
falsingManager = falsingManager,
+ messageDisplayer = messageDisplayer,
)
}
}
@@ -222,10 +233,12 @@
}
}
+ @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 @@
},
)
)
+
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 0000000..a5ae8ba5
--- /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 0000000..be1d3a1
--- /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 0000000..50722d5
--- /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 227796f..5d85680 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.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 @@
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 @@
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 @@
* 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 @@
},
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 44f48f9..cf3a6da 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 @@
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 3e5d337..bb833df 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.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 @@
@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 b55bedd..8aef938 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 @@
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 009595a..066c185 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.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 @@
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 40ea1e6..11348ad 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 @@
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 1c3a53c..7b9d0b4 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.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 @@
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 @@
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 @@
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 @@
}
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 @@
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 39a2763..6e515f2 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 @@
@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 ec1984d..9f44d98 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.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 @@
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 @@
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 @@
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 f97385b..6c99b67 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 @@
}
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 @@
// 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/FgsManagerController.kt b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt
index 930de13..d1cf46c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt
@@ -157,7 +157,7 @@
private const val DEFAULT_TASK_MANAGER_ENABLED = true
private const val DEFAULT_TASK_MANAGER_SHOW_FOOTER_DOT = false
private const val DEFAULT_TASK_MANAGER_SHOW_STOP_BUTTON_FOR_USER_ALLOWLISTED_APPS = true
- private const val DEFAULT_TASK_MANAGER_SHOW_USER_VISIBLE_JOBS = false
+ private const val DEFAULT_TASK_MANAGER_SHOW_USER_VISIBLE_JOBS = true
}
override var newChangesSinceDialogWasDismissed = false
diff --git a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
index f92bbf7..8ceee1a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
@@ -741,6 +741,14 @@
}
};
+ /**
+ * Force all tiles to be redistributed across pages.
+ * Should be called when one of the following changes: rows, columns, number of tiles.
+ */
+ public void forceTilesRedistribution() {
+ mDistributeTiles = true;
+ }
+
public interface PageListener {
int INVALID_PAGE = -1;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
index 1827eaf..b2ca6b7 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
@@ -148,6 +148,11 @@
}
}
+ @Override
+ protected void onSplitShadeChanged() {
+ ((PagedTileLayout) mView.getOrCreateTileLayout()).forceTilesRedistribution();
+ }
+
/** */
public void setVisibility(int visibility) {
mView.setVisibility(visibility);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
index dd88c83..60d2c17 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
@@ -96,17 +96,23 @@
/* newOrientation= */ newConfig.orientation,
/* containerName= */ mView.getDumpableTag());
+ boolean previousSplitShadeState = mShouldUseSplitNotificationShade;
mShouldUseSplitNotificationShade =
LargeScreenUtils.shouldUseSplitNotificationShade(getResources());
mLastOrientation = newConfig.orientation;
switchTileLayoutIfNeeded();
onConfigurationChanged();
+ if (previousSplitShadeState != mShouldUseSplitNotificationShade) {
+ onSplitShadeChanged();
+ }
}
};
protected void onConfigurationChanged() { }
+ protected void onSplitShadeChanged() { }
+
private final Function1<Boolean, Unit> mMediaHostVisibilityListener = (visible) -> {
if (mMediaVisibilityChangedListener != null) {
mMediaVisibilityChangedListener.accept(visible);
@@ -264,14 +270,6 @@
}
}
}
- protected QSTile getTile(String subPanel) {
- for (int i = 0; i < mRecords.size(); i++) {
- if (subPanel.equals(mRecords.get(i).tile.getTileSpec())) {
- return mRecords.get(i).tile;
- }
- }
- return mHost.createTile(subPanel);
- }
boolean areThereTiles() {
return !mRecords.isEmpty();
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 9743c3e..206a620 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 @@
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/LargeScreenShadeHeaderController.kt b/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt
index b511b54..7fc0a5f 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt
@@ -101,6 +101,8 @@
@VisibleForTesting
internal val HEADER_TRANSITION_ID = R.id.header_transition
@VisibleForTesting
+ internal val LARGE_SCREEN_HEADER_TRANSITION_ID = R.id.large_screen_header_transition
+ @VisibleForTesting
internal val QQS_HEADER_CONSTRAINT = R.id.qqs_header_constraint
@VisibleForTesting
internal val QS_HEADER_CONSTRAINT = R.id.qs_header_constraint
@@ -429,8 +431,11 @@
}
header as MotionLayout
if (largeScreenActive) {
- header.getConstraintSet(LARGE_SCREEN_HEADER_CONSTRAINT).applyTo(header)
+ logInstantEvent("Large screen constraints set")
+ header.setTransition(HEADER_TRANSITION_ID)
+ header.transitionToStart()
} else {
+ logInstantEvent("Small screen constraints set")
header.setTransition(HEADER_TRANSITION_ID)
header.transitionToStart()
updatePosition()
@@ -440,15 +445,19 @@
private fun updatePosition() {
if (header is MotionLayout && !largeScreenActive && visible) {
- Trace.instantForTrack(
- TRACE_TAG_APP,
- "LargeScreenHeaderController - updatePosition",
- "position: $qsExpandedFraction"
- )
+ logInstantEvent("updatePosition: $qsExpandedFraction")
header.progress = qsExpandedFraction
}
}
+ private fun logInstantEvent(message: String) {
+ Trace.instantForTrack(
+ TRACE_TAG_APP,
+ "LargeScreenHeaderController",
+ message
+ )
+ }
+
private fun updateListeners() {
qsCarrierGroupController.setListening(visible)
if (visible) {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 568f6d3..61e53da 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_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;
@@ -1331,7 +1332,9 @@
mKeyguardBottomArea.init(
mKeyguardBottomAreaViewModel,
mFalsingManager,
- mLockIconViewController
+ mLockIconViewController,
+ stringResourceId ->
+ mKeyguardIndicationController.showTransientIndication(stringResourceId)
);
}
@@ -2329,7 +2332,7 @@
private boolean handleQsTouch(MotionEvent event) {
- if (mSplitShadeEnabled && touchXOutsideOfQs(event.getX())) {
+ if (isSplitShadeAndTouchXOutsideQs(event.getX())) {
return false;
}
final int action = event.getActionMasked();
@@ -2386,12 +2389,14 @@
return false;
}
- private boolean touchXOutsideOfQs(float touchX) {
- return touchX < mQsFrame.getX() || touchX > mQsFrame.getX() + mQsFrame.getWidth();
+ /** Returns whether split shade is enabled and an x coordinate is outside of the QS frame. */
+ private boolean isSplitShadeAndTouchXOutsideQs(float touchX) {
+ return mSplitShadeEnabled && (touchX < mQsFrame.getX()
+ || touchX > mQsFrame.getX() + mQsFrame.getWidth());
}
private boolean isInQsArea(float x, float y) {
- if (touchXOutsideOfQs(x)) {
+ if (isSplitShadeAndTouchXOutsideQs(x)) {
return false;
}
// Let's reject anything at the very bottom around the home handle in gesture nav
@@ -4724,6 +4729,7 @@
if (!openingWithTouch || !mHasVibratedOnOpen) {
mVibratorHelper.vibrate(VibrationEffect.EFFECT_TICK);
mHasVibratedOnOpen = true;
+ mShadeLog.v("Vibrating on opening, mHasVibratedOnOpen=true");
}
}
}
@@ -5320,7 +5326,7 @@
@Override
public void flingTopOverscroll(float velocity, boolean open) {
// in split shade mode we want to expand/collapse QS only when touch happens within QS
- if (mSplitShadeEnabled && touchXOutsideOfQs(mInitialTouchX)) {
+ if (isSplitShadeAndTouchXOutsideQs(mInitialTouchX)) {
return;
}
mLastOverscroll = 0f;
@@ -5481,6 +5487,15 @@
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)) {
@@ -5500,27 +5515,12 @@
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();
@@ -6124,6 +6124,7 @@
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 0b59af3..5fedbeb 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 @@
})
}
+ 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/NotifPipelineFlags.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt
index 39daa13..3072c81 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt
@@ -33,6 +33,8 @@
fun fullScreenIntentRequiresKeyguard(): Boolean =
featureFlags.isEnabled(Flags.FSI_REQUIRES_KEYGUARD)
+ fun fsiOnDNDUpdate(): Boolean = featureFlags.isEnabled(Flags.FSI_ON_DND_UPDATE)
+
val isStabilityIndexFixEnabled: Boolean by lazy {
featureFlags.isEnabled(Flags.STABILITY_INDEX_FIX)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
index 3e2dd05..aeae89c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt
@@ -244,7 +244,7 @@
}
override fun onDozeAmountChanged(linear: Float, eased: Float) {
- logger.logOnDozeAmountChanged(linear, eased)
+ logger.logOnDozeAmountChanged(linear = linear, eased = eased)
if (overrideDozeAmountIfAnimatingScreenOff(linear)) {
return
}
@@ -263,6 +263,7 @@
fun setDozeAmount(linear: Float, eased: Float, source: String) {
val changed = linear != mLinearDozeAmount
+ logger.logSetDozeAmount(linear, eased, source, statusBarStateController.state, changed)
mLinearDozeAmount = linear
mDozeAmount = eased
mDozeAmountSource = source
@@ -276,7 +277,7 @@
}
override fun onStateChanged(newState: Int) {
- logger.logOnStateChanged(newState)
+ logger.logOnStateChanged(newState = newState, storedState = state)
if (state == StatusBarState.SHADE && newState == StatusBarState.SHADE) {
// The SHADE -> SHADE transition is only possible as part of cancelling the screen-off
// animation (e.g. by fingerprint unlock). This is done because the system is in an
@@ -324,12 +325,8 @@
private fun overrideDozeAmountIfBypass(): Boolean {
if (bypassController.bypassEnabled) {
if (statusBarStateController.state == StatusBarState.KEYGUARD) {
- logger.logSetDozeAmount("1.0", "1.0",
- "Override: bypass (keyguard)", StatusBarState.KEYGUARD)
setDozeAmount(1f, 1f, source = "Override: bypass (keyguard)")
} else {
- logger.logSetDozeAmount("0.0", "0.0",
- "Override: bypass (shade)", statusBarStateController.state)
setDozeAmount(0f, 0f, source = "Override: bypass (shade)")
}
return true
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorLogger.kt
index b40ce25..de18b0c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorLogger.kt
@@ -16,22 +16,33 @@
import com.android.systemui.log.dagger.NotificationLog
import com.android.systemui.plugins.log.LogBuffer
import com.android.systemui.plugins.log.LogLevel.DEBUG
+import com.android.systemui.statusbar.StatusBarState
import javax.inject.Inject
class NotificationWakeUpCoordinatorLogger
@Inject
constructor(@NotificationLog private val buffer: LogBuffer) {
- fun logSetDozeAmount(linear: String, eased: String, source: String, state: Int) {
+ fun logSetDozeAmount(
+ linear: Float,
+ eased: Float,
+ source: String,
+ state: Int,
+ changed: Boolean,
+ ) {
buffer.log(
TAG,
DEBUG,
{
- str1 = linear
- str2 = eased
+ double1 = linear.toDouble()
+ str2 = eased.toString()
str3 = source
int1 = state
+ bool1 = changed
},
- { "setDozeAmount: linear: $str1, eased: $str2, source: $str3, state: $int1" }
+ {
+ "setDozeAmount(linear=$double1, eased=$str2, source=$str3)" +
+ " state=${StatusBarState.toString(int1)} changed=$bool1"
+ }
)
}
@@ -43,12 +54,23 @@
double1 = linear.toDouble()
str2 = eased.toString()
},
- { "onDozeAmountChanged($double1, $str2)" }
+ { "onDozeAmountChanged(linear=$double1, eased=$str2)" }
)
}
- fun logOnStateChanged(newState: Int) {
- buffer.log(TAG, DEBUG, { int1 = newState }, { "onStateChanged($int1)" })
+ fun logOnStateChanged(newState: Int, storedState: Int) {
+ buffer.log(
+ TAG,
+ DEBUG,
+ {
+ int1 = newState
+ int2 = storedState
+ },
+ {
+ "onStateChanged(newState=${StatusBarState.toString(int1)})" +
+ " stored=${StatusBarState.toString(int2)}"
+ }
+ )
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
index 5dbb4f9..1004ec1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
@@ -22,6 +22,7 @@
import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.statusbar.NotificationRemoteInputManager
+import com.android.systemui.statusbar.notification.NotifPipelineFlags
import com.android.systemui.statusbar.notification.collection.GroupEntry
import com.android.systemui.statusbar.notification.collection.ListEntry
import com.android.systemui.statusbar.notification.collection.NotifPipeline
@@ -38,6 +39,7 @@
import com.android.systemui.statusbar.notification.dagger.IncomingHeader
import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder
import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider
+import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider.FullScreenIntentDecision
import com.android.systemui.statusbar.notification.logKey
import com.android.systemui.statusbar.notification.stack.BUCKET_HEADS_UP
import com.android.systemui.statusbar.policy.HeadsUpManager
@@ -70,11 +72,13 @@
private val mNotificationInterruptStateProvider: NotificationInterruptStateProvider,
private val mRemoteInputManager: NotificationRemoteInputManager,
private val mLaunchFullScreenIntentProvider: LaunchFullScreenIntentProvider,
+ private val mFlags: NotifPipelineFlags,
@IncomingHeader private val mIncomingHeaderController: NodeController,
@Main private val mExecutor: DelayableExecutor,
) : Coordinator {
private val mEntriesBindingUntil = ArrayMap<String, Long>()
private val mEntriesUpdateTimes = ArrayMap<String, Long>()
+ private val mFSIUpdateCandidates = ArrayMap<String, Long>()
private var mEndLifetimeExtension: OnEndLifetimeExtensionCallback? = null
private lateinit var mNotifPipeline: NotifPipeline
private var mNow: Long = -1
@@ -278,7 +282,7 @@
mPostedEntries.clear()
// Also take this opportunity to clean up any stale entry update times
- cleanUpEntryUpdateTimes()
+ cleanUpEntryTimes()
}
/**
@@ -384,8 +388,15 @@
override fun onEntryAdded(entry: NotificationEntry) {
// First check whether this notification should launch a full screen intent, and
// launch it if needed.
- if (mNotificationInterruptStateProvider.shouldLaunchFullScreenIntentWhenAdded(entry)) {
+ val fsiDecision = mNotificationInterruptStateProvider.getFullScreenIntentDecision(entry)
+ if (fsiDecision != null && fsiDecision.shouldLaunch) {
+ mNotificationInterruptStateProvider.logFullScreenIntentDecision(entry, fsiDecision)
mLaunchFullScreenIntentProvider.launchFullScreenIntent(entry)
+ } else if (mFlags.fsiOnDNDUpdate() &&
+ fsiDecision.equals(FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND)) {
+ // If DND was the only reason this entry was suppressed, note it for potential
+ // reconsideration on later ranking updates.
+ addForFSIReconsideration(entry, mSystemClock.currentTimeMillis())
}
// shouldHeadsUp includes check for whether this notification should be filtered
@@ -488,11 +499,32 @@
if (!isNewEnoughForRankingUpdate(entry)) continue
// The only entries we consider alerting for here are entries that have never
- // interrupted and that now say they should heads up; if they've alerted in the
- // past, we don't want to incorrectly alert a second time if there wasn't an
+ // interrupted and that now say they should heads up or FSI; if they've alerted in
+ // the past, we don't want to incorrectly alert a second time if there wasn't an
// explicit notification update.
if (entry.hasInterrupted()) continue
+ // Before potentially allowing heads-up, check for any candidates for a FSI launch.
+ // Any entry that is a candidate meets two criteria:
+ // - was suppressed from FSI launch only by a DND suppression
+ // - is within the recency window for reconsideration
+ // If any of these entries are no longer suppressed, launch the FSI now.
+ if (mFlags.fsiOnDNDUpdate() && isCandidateForFSIReconsideration(entry)) {
+ val decision =
+ mNotificationInterruptStateProvider.getFullScreenIntentDecision(entry)
+ if (decision.shouldLaunch) {
+ // Log both the launch of the full screen and also that this was via a
+ // ranking update.
+ mLogger.logEntryUpdatedToFullScreen(entry.key)
+ mNotificationInterruptStateProvider.logFullScreenIntentDecision(
+ entry, decision)
+ mLaunchFullScreenIntentProvider.launchFullScreenIntent(entry)
+
+ // if we launch the FSI then this is no longer a candidate for HUN
+ continue
+ }
+ }
+
// The cases where we should consider this notification to be updated:
// - if this entry is not present in PostedEntries, and is now in a shouldHeadsUp
// state
@@ -528,6 +560,15 @@
}
/**
+ * Add the entry to the list of entries potentially considerable for FSI ranking update, where
+ * the provided time is the time the entry was added.
+ */
+ @VisibleForTesting
+ fun addForFSIReconsideration(entry: NotificationEntry, time: Long) {
+ mFSIUpdateCandidates[entry.key] = time
+ }
+
+ /**
* Checks whether the entry is new enough to be updated via ranking update.
* We want to avoid updating an entry too long after it was originally posted/updated when we're
* only reacting to a ranking change, as relevant ranking updates are expected to come in
@@ -541,17 +582,38 @@
return (mSystemClock.currentTimeMillis() - updateTime) <= MAX_RANKING_UPDATE_DELAY_MS
}
- private fun cleanUpEntryUpdateTimes() {
+ /**
+ * Checks whether the entry is present new enough for reconsideration for full screen launch.
+ * The time window is the same as for ranking update, but this doesn't allow a potential update
+ * to an entry with full screen intent to count for timing purposes.
+ */
+ private fun isCandidateForFSIReconsideration(entry: NotificationEntry): Boolean {
+ val addedTime = mFSIUpdateCandidates[entry.key] ?: return false
+ return (mSystemClock.currentTimeMillis() - addedTime) <= MAX_RANKING_UPDATE_DELAY_MS
+ }
+
+ private fun cleanUpEntryTimes() {
// Because we won't update entries that are older than this amount of time anyway, clean
- // up any entries that are too old to notify.
+ // up any entries that are too old to notify from both the general and FSI specific lists.
+
+ // Anything newer than this time is still within the window.
+ val timeThreshold = mSystemClock.currentTimeMillis() - MAX_RANKING_UPDATE_DELAY_MS
+
val toRemove = ArraySet<String>()
for ((key, updateTime) in mEntriesUpdateTimes) {
- if (updateTime == null ||
- (mSystemClock.currentTimeMillis() - updateTime) > MAX_RANKING_UPDATE_DELAY_MS) {
+ if (updateTime == null || timeThreshold > updateTime) {
toRemove.add(key)
}
}
mEntriesUpdateTimes.removeAll(toRemove)
+
+ val toRemoveForFSI = ArraySet<String>()
+ for ((key, addedTime) in mFSIUpdateCandidates) {
+ if (addedTime == null || timeThreshold > addedTime) {
+ toRemoveForFSI.add(key)
+ }
+ }
+ mFSIUpdateCandidates.removeAll(toRemoveForFSI)
}
/** When an action is pressed on a notification, end HeadsUp lifetime extension. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt
index 473c35d..2c6bf6b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorLogger.kt
@@ -70,6 +70,14 @@
})
}
+ fun logEntryUpdatedToFullScreen(key: String) {
+ buffer.log(TAG, LogLevel.DEBUG, {
+ str1 = key
+ }, {
+ "updating entry to launch full screen intent: $str1"
+ })
+ }
+
fun logSummaryMarkedInterrupted(summaryKey: String, childKey: String) {
buffer.log(TAG, LogLevel.DEBUG, {
str1 = summaryKey
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 0380fff..1fcf17f 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 @@
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 0000000..ffd931c
--- /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 0000000..ec8501a
--- /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 7d39e18..41fb91e 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 @@
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 @@
.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 @@
): 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 @@
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 @@
smallIcon = smallIconUse,
largeIcon = largeIconUse,
extras = extrasSize,
- style = style?.simpleName,
+ style = style,
styleIcon =
bigPictureIconUse +
peopleUse +
@@ -144,6 +164,25 @@
}
/**
+ * 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 @@
*
* @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 c09cc43..f38c1e5 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 @@
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 @@
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 a0bee15..2d04211 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 @@
/**
* 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 @@
}
// 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 aff7b4c..b6cf948 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 @@
}
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 @@
*
* @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 @@
// 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 @@
}
// 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 6b72e96..936589c 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 @@
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 d2be8f3..65946c5 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.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;
@@ -1035,8 +1037,21 @@
// 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 78b28d2..2ce1163 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.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 @@
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 @@
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/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt
index f5b5950..cc67c84 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt
@@ -46,6 +46,7 @@
* view-model to be reused for multiple view/view-binder bindings.
*/
@OptIn(InternalCoroutinesApi::class)
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
object WifiViewBinder {
/**
@@ -59,6 +60,12 @@
/** Notifies that the visibility state has changed. */
fun onVisibilityStateChanged(@StatusBarIconView.VisibleState state: Int)
+
+ /** Notifies that the icon tint has been updated. */
+ fun onIconTintChanged(newTint: Int)
+
+ /** Notifies that the decor tint has been updated (used only for the dot). */
+ fun onDecorTintChanged(newTint: Int)
}
/** Binds the view to the view-model, continuing to update the former based on the latter. */
@@ -82,6 +89,9 @@
@StatusBarIconView.VisibleState
val visibilityState: MutableStateFlow<Int> = MutableStateFlow(STATE_HIDDEN)
+ val iconTint: MutableStateFlow<Int> = MutableStateFlow(viewModel.defaultColor)
+ val decorTint: MutableStateFlow<Int> = MutableStateFlow(viewModel.defaultColor)
+
view.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
@@ -101,7 +111,7 @@
}
launch {
- viewModel.tint.collect { tint ->
+ iconTint.collect { tint ->
val tintList = ColorStateList.valueOf(tint)
iconView.imageTintList = tintList
activityInView.imageTintList = tintList
@@ -110,6 +120,8 @@
}
}
+ launch { decorTint.collect { tint -> dotView.setDecorColor(tint) } }
+
launch {
viewModel.isActivityInViewVisible.distinctUntilChanged().collect { visible ->
activityInView.isVisible = visible
@@ -144,6 +156,20 @@
override fun onVisibilityStateChanged(@StatusBarIconView.VisibleState state: Int) {
visibilityState.value = state
}
+
+ override fun onIconTintChanged(newTint: Int) {
+ if (viewModel.useDebugColoring) {
+ return
+ }
+ iconTint.value = newTint
+ }
+
+ override fun onDecorTintChanged(newTint: Int) {
+ if (viewModel.useDebugColoring) {
+ return
+ }
+ decorTint.value = newTint
+ }
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt
index a45076b..be7782c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt
@@ -22,6 +22,7 @@
import android.view.Gravity
import android.view.LayoutInflater
import com.android.systemui.R
+import com.android.systemui.plugins.DarkIconDispatcher
import com.android.systemui.statusbar.BaseStatusBarFrameLayout
import com.android.systemui.statusbar.StatusBarIconView
import com.android.systemui.statusbar.StatusBarIconView.STATE_DOT
@@ -51,18 +52,20 @@
binding.onVisibilityStateChanged(value)
}
- override fun onDarkChanged(areas: ArrayList<Rect>?, darkIntensity: Float, tint: Int) {
- // TODO(b/238425913)
- }
-
override fun getSlot() = slot
+ override fun onDarkChanged(areas: ArrayList<Rect>?, darkIntensity: Float, tint: Int) {
+ val newTint = DarkIconDispatcher.getTint(areas, this, tint)
+ binding.onIconTintChanged(newTint)
+ binding.onDecorTintChanged(newTint)
+ }
+
override fun setStaticDrawableColor(color: Int) {
- // TODO(b/238425913)
+ binding.onIconTintChanged(color)
}
override fun setDecorColor(color: Int) {
- // TODO(b/238425913)
+ binding.onDecorTintChanged(color)
}
override fun setVisibleState(@StatusBarIconView.VisibleState state: Int, animate: Boolean) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt
index e35a8fe..a4615cc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt
@@ -21,7 +21,6 @@
import com.android.systemui.statusbar.pipeline.wifi.ui.model.WifiIcon
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.flowOf
/**
* A view model for a wifi icon in a specific location. This allows us to control parameters that
@@ -48,24 +47,12 @@
/** True if the airplane spacer view should be visible. */
val isAirplaneSpacerVisible: Flow<Boolean>,
) {
- /** The color that should be used to tint the icon. */
- val tint: Flow<Int> =
- flowOf(
- if (statusBarPipelineFlags.useWifiDebugColoring()) {
- debugTint
- } else {
- DEFAULT_TINT
- }
- )
+ val useDebugColoring: Boolean = statusBarPipelineFlags.useWifiDebugColoring()
- companion object {
- /**
- * A default icon tint.
- *
- * TODO(b/238425913): The tint is actually controlled by
- * [com.android.systemui.statusbar.phone.StatusBarIconController.TintedIconManager]. We
- * should use that logic instead of white as a default.
- */
- private const val DEFAULT_TINT = Color.WHITE
- }
+ val defaultColor: Int =
+ if (useDebugColoring) {
+ debugTint
+ } else {
+ Color.WHITE
+ }
}
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 0000000..7ccb316
--- /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 ea40208..db7315f 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.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 @@
*
* 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 @@
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 @@
*/
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
+ internal val activeViews: MutableList<DisplayInfo> = mutableListOf()
- /** A string that keeps track of wakelock reason once it is acquired till it gets released */
- private var wakeReasonAcquired: String? = null
-
- /**
- * 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 @@
* 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
- // Only cancel timeout of the most recent view displayed, as it will be reset.
- if (position == 0) {
- cancelViewTimeout?.run()
+ 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
}
- cancelViewTimeout = mainExecutor.executeDelayed(
+
+ 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)
+ }
+
+ 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()
+ }
+ 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 @@
}
/** 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 @@
}
}
+ 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
-
- val removalPosition = findAndRemoveFromActiveViewsList(id)
- if (removalPosition == null) {
- logger.logViewRemovalIgnored(id, "view not found in the list")
- return
- }
- if (removalPosition != 0) {
- logger.logViewRemovalIgnored(id, "most recent view is being displayed.")
- return
- }
logger.logViewRemoval(id, removalReason)
- val newViewToDisplay = if (activeViews.isEmpty()) {
- null
- } else {
- activeViews[0].second
+ val displayInfo = activeViews.firstOrNull { it.info.id == id }
+ if (displayInfo == null) {
+ logger.logViewRemovalIgnored(id, "View not found in list")
+ return
}
- val currentView = currentDisplayInfo.view
- animateViewOut(currentView) {
- windowManager.removeView(currentView)
- wakeLock?.release(wakeReasonAcquired)
- }
+ 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)
- 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()
+ displayInfo.cancelViewTimeout?.run()
+
+ if (displayInfo.view == null) {
+ logger.logViewRemovalIgnored(id, "No view to remove")
+ return
+ }
+
+ if (currentlyDisplayedView.info.id != id) {
+ logger.logViewRemovalIgnored(id, "View isn't the currently displayed view")
+ return
+ }
+
+ removeViewFromWindow(displayInfo)
+
+ // 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
}
- return null
+ 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)
+ }
}
/**
@@ -311,17 +410,47 @@
}
/** A container for all the display-related state objects. */
- private inner class DisplayInfo(
- /** The view currently being displayed. */
- val view: ViewGroup,
+ inner class DisplayInfo(
+ /**
+ * The view currently being displayed.
+ *
+ * Null if this info isn't currently being displayed.
+ */
+ var view: ViewGroup?,
- /** The info currently being displayed. */
+ /** 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 df83960..5596cf6 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 @@
* 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 133a384..ec6965a 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.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 added. window=$str1 id=$str2" }
+ { "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
+ },
+ { "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 4d91e35..14ba63a 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.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 @@
private val viewUtil: ViewUtil,
private val vibratorHelper: VibratorHelper,
wakeLockBuilder: WakeLock.Builder,
+ systemClock: SystemClock,
) : TemporaryViewDisplayController<ChipbarInfo, ChipbarLogger>(
context,
logger,
@@ -87,6 +89,7 @@
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 a3eef80..dd4bd26 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 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 @@
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 e477cd6..fcfbe0a 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 @@
@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/src/com/android/systemui/touch/TouchInsetManager.java b/packages/SystemUI/src/com/android/systemui/touch/TouchInsetManager.java
index 3d07491..166ac9e 100644
--- a/packages/SystemUI/src/com/android/systemui/touch/TouchInsetManager.java
+++ b/packages/SystemUI/src/com/android/systemui/touch/TouchInsetManager.java
@@ -18,41 +18,58 @@
import android.graphics.Rect;
import android.graphics.Region;
+import android.util.Log;
+import android.view.AttachedSurfaceControl;
import android.view.View;
-import android.view.ViewRootImpl;
import androidx.concurrent.futures.CallbackToFutureAdapter;
+import com.android.systemui.dagger.qualifiers.Main;
+
import com.google.common.util.concurrent.ListenableFuture;
import java.util.HashMap;
import java.util.HashSet;
import java.util.concurrent.Executor;
+import javax.inject.Inject;
+
/**
* {@link TouchInsetManager} handles setting the touchable inset regions for a given View. This
* is useful for passing through touch events for all but select areas.
*/
public class TouchInsetManager {
+ private static final String TAG = "TouchInsetManager";
/**
* {@link TouchInsetSession} provides an individualized session with the
* {@link TouchInsetManager}, linking any action to the client.
*/
public static class TouchInsetSession {
private final TouchInsetManager mManager;
-
private final HashSet<View> mTrackedViews;
private final Executor mExecutor;
private final View.OnLayoutChangeListener mOnLayoutChangeListener =
(v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom)
- -> updateTouchRegion();
+ -> updateTouchRegions();
+
+ private final View.OnAttachStateChangeListener mAttachListener =
+ new View.OnAttachStateChangeListener() {
+ @Override
+ public void onViewAttachedToWindow(View v) {
+ updateTouchRegions();
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(View v) {
+ updateTouchRegions();
+ }
+ };
/**
* Default constructor
* @param manager The parent {@link TouchInsetManager} which will be affected by actions on
* this session.
- * @param rootView The parent of views that will be tracked.
* @param executor An executor for marshalling operations.
*/
TouchInsetSession(TouchInsetManager manager, Executor executor) {
@@ -68,8 +85,9 @@
public void addViewToTracking(View view) {
mExecutor.execute(() -> {
mTrackedViews.add(view);
+ view.addOnAttachStateChangeListener(mAttachListener);
view.addOnLayoutChangeListener(mOnLayoutChangeListener);
- updateTouchRegion();
+ updateTouchRegions();
});
}
@@ -81,22 +99,30 @@
mExecutor.execute(() -> {
mTrackedViews.remove(view);
view.removeOnLayoutChangeListener(mOnLayoutChangeListener);
- updateTouchRegion();
+ view.removeOnAttachStateChangeListener(mAttachListener);
+ updateTouchRegions();
});
}
- private void updateTouchRegion() {
- final Region cumulativeRegion = Region.obtain();
+ private void updateTouchRegions() {
+ mExecutor.execute(() -> {
+ final HashMap<AttachedSurfaceControl, Region> affectedSurfaces = new HashMap<>();
+ mTrackedViews.stream().forEach(view -> {
+ if (!view.isAttachedToWindow()) {
+ return;
+ }
- mTrackedViews.stream().forEach(view -> {
- final Rect boundaries = new Rect();
- view.getBoundsOnScreen(boundaries);
- cumulativeRegion.op(boundaries, Region.Op.UNION);
+ final AttachedSurfaceControl surface = view.getRootSurfaceControl();
+
+ if (!affectedSurfaces.containsKey(surface)) {
+ affectedSurfaces.put(surface, Region.obtain());
+ }
+ final Rect boundaries = new Rect();
+ view.getBoundsOnScreen(boundaries);
+ affectedSurfaces.get(surface).op(boundaries, Region.Op.UNION);
+ });
+ mManager.setTouchRegions(this, affectedSurfaces);
});
-
- mManager.setTouchRegion(this, cumulativeRegion);
-
- cumulativeRegion.recycle();
}
/**
@@ -110,32 +136,18 @@
}
}
- private final HashMap<TouchInsetSession, Region> mDefinedRegions = new HashMap<>();
+ private final HashMap<TouchInsetSession, HashMap<AttachedSurfaceControl, Region>>
+ mSessionRegions = new HashMap<>();
+ private final HashMap<AttachedSurfaceControl, Region> mLastAffectedSurfaces = new HashMap();
private final Executor mExecutor;
- private final View mRootView;
-
- private final View.OnAttachStateChangeListener mAttachListener =
- new View.OnAttachStateChangeListener() {
- @Override
- public void onViewAttachedToWindow(View v) {
- updateTouchInset();
- }
-
- @Override
- public void onViewDetachedFromWindow(View v) {
- }
- };
/**
* Default constructor.
* @param executor An {@link Executor} to marshal all operations on.
- * @param rootView The root {@link View} for all views in sessions.
*/
- public TouchInsetManager(Executor executor, View rootView) {
+ @Inject
+ public TouchInsetManager(@Main Executor executor) {
mExecutor = executor;
- mRootView = rootView;
- mRootView.addOnAttachStateChangeListener(mAttachListener);
-
}
/**
@@ -151,47 +163,68 @@
public ListenableFuture<Boolean> checkWithinTouchRegion(int x, int y) {
return CallbackToFutureAdapter.getFuture(completer -> {
mExecutor.execute(() -> completer.set(
- mDefinedRegions.values().stream().anyMatch(region -> region.contains(x, y))));
+ mLastAffectedSurfaces.values().stream().anyMatch(
+ region -> region.contains(x, y))));
return "DreamOverlayTouchMonitor::checkWithinTouchRegion";
});
}
- private void updateTouchInset() {
- final ViewRootImpl viewRootImpl = mRootView.getViewRootImpl();
+ private void updateTouchInsets() {
+ // Get affected
+ final HashMap<AttachedSurfaceControl, Region> affectedSurfaces = new HashMap<>();
+ mSessionRegions.values().stream().forEach(regionMapping -> {
+ regionMapping.entrySet().stream().forEach(entry -> {
+ final AttachedSurfaceControl surface = entry.getKey();
+ if (!affectedSurfaces.containsKey(surface)) {
+ affectedSurfaces.put(surface, Region.obtain());
+ }
- if (viewRootImpl == null) {
+ affectedSurfaces.get(surface).op(entry.getValue(), Region.Op.UNION);
+ });
+ });
+
+ affectedSurfaces.entrySet().stream().forEach(entry -> {
+ entry.getKey().setTouchableRegion(entry.getValue());
+ });
+
+ mLastAffectedSurfaces.entrySet().forEach(entry -> {
+ final AttachedSurfaceControl surface = entry.getKey();
+ if (!affectedSurfaces.containsKey(surface)) {
+ surface.setTouchableRegion(null);
+ }
+ entry.getValue().recycle();
+ });
+
+ mLastAffectedSurfaces.clear();
+ mLastAffectedSurfaces.putAll(affectedSurfaces);
+ }
+
+ protected void setTouchRegions(TouchInsetSession session,
+ HashMap<AttachedSurfaceControl, Region> regions) {
+ mExecutor.execute(() -> {
+ recycleRegions(session);
+ mSessionRegions.put(session, regions);
+ updateTouchInsets();
+ });
+ }
+
+ private void recycleRegions(TouchInsetSession session) {
+ if (!mSessionRegions.containsKey(session)) {
+ Log.w(TAG, "Removing a session with no regions:" + session);
return;
}
- final Region aggregateRegion = Region.obtain();
-
- for (Region region : mDefinedRegions.values()) {
- aggregateRegion.op(region, Region.Op.UNION);
+ for (Region region : mSessionRegions.get(session).values()) {
+ region.recycle();
}
-
- viewRootImpl.setTouchableRegion(aggregateRegion);
-
- aggregateRegion.recycle();
- }
-
- protected void setTouchRegion(TouchInsetSession session, Region region) {
- final Region introducedRegion = Region.obtain(region);
- mExecutor.execute(() -> {
- mDefinedRegions.put(session, introducedRegion);
- updateTouchInset();
- });
}
private void clearRegion(TouchInsetSession session) {
mExecutor.execute(() -> {
- final Region storedRegion = mDefinedRegions.remove(session);
-
- if (storedRegion != null) {
- storedRegion.recycle();
- }
-
- updateTouchInset();
+ recycleRegions(session);
+ mSessionRegions.remove(session);
+ updateTouchInsets();
});
}
}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardAbsKeyInputViewControllerTest.java
index 8bbaf3d..1059543 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 @@
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 @@
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 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 @@
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/KeyguardClockSwitchControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
index 61c7bb5..c8e7538 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
@@ -29,7 +29,6 @@
import android.content.res.Resources;
import android.database.ContentObserver;
-import android.graphics.Rect;
import android.net.Uri;
import android.os.UserHandle;
import android.provider.Settings;
@@ -47,6 +46,8 @@
import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
import com.android.systemui.plugins.ClockAnimations;
import com.android.systemui.plugins.ClockController;
+import com.android.systemui.plugins.ClockEvents;
+import com.android.systemui.plugins.ClockFaceController;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.shared.clocks.AnimatableClockView;
import com.android.systemui.shared.clocks.ClockRegistry;
@@ -88,7 +89,15 @@
@Mock
KeyguardUnlockAnimationController mKeyguardUnlockAnimationController;
@Mock
- private ClockController mClock;
+ private ClockController mClockController;
+ @Mock
+ private ClockFaceController mLargeClockController;
+ @Mock
+ private ClockFaceController mSmallClockController;
+ @Mock
+ private ClockAnimations mClockAnimations;
+ @Mock
+ private ClockEvents mClockEvents;
@Mock
DumpManager mDumpManager;
@Mock
@@ -97,10 +106,12 @@
@Mock
private NotificationIconContainer mNotificationIcons;
@Mock
- private AnimatableClockView mClockView;
+ private AnimatableClockView mSmallClockView;
@Mock
private AnimatableClockView mLargeClockView;
@Mock
+ private FrameLayout mSmallClockFrame;
+ @Mock
private FrameLayout mLargeClockFrame;
@Mock
private SecureSettings mSecureSettings;
@@ -121,9 +132,14 @@
mock(RelativeLayout.LayoutParams.class));
when(mView.getContext()).thenReturn(getContext());
when(mView.getResources()).thenReturn(mResources);
+ when(mResources.getDimensionPixelSize(R.dimen.keyguard_clock_top_margin))
+ .thenReturn(100);
+ when(mResources.getDimensionPixelSize(R.dimen.keyguard_large_clock_top_margin))
+ .thenReturn(-200);
when(mView.findViewById(R.id.lockscreen_clock_view_large)).thenReturn(mLargeClockFrame);
- when(mClockView.getContext()).thenReturn(getContext());
+ when(mView.findViewById(R.id.lockscreen_clock_view)).thenReturn(mSmallClockFrame);
+ when(mSmallClockView.getContext()).thenReturn(getContext());
when(mLargeClockView.getContext()).thenReturn(getContext());
when(mView.isAttachedToWindow()).thenReturn(true);
@@ -144,7 +160,14 @@
);
when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE);
- when(mClockRegistry.createCurrentClock()).thenReturn(mClock);
+ when(mLargeClockController.getView()).thenReturn(mLargeClockView);
+ when(mSmallClockController.getView()).thenReturn(mSmallClockView);
+ when(mClockController.getLargeClock()).thenReturn(mLargeClockController);
+ when(mClockController.getSmallClock()).thenReturn(mSmallClockController);
+ when(mClockController.getEvents()).thenReturn(mClockEvents);
+ when(mClockController.getAnimations()).thenReturn(mClockAnimations);
+ when(mClockRegistry.createCurrentClock()).thenReturn(mClockController);
+ when(mClockEventController.getClock()).thenReturn(mClockController);
mSliceView = new View(getContext());
when(mView.findViewById(R.id.keyguard_slice_view)).thenReturn(mSliceView);
@@ -203,8 +226,8 @@
verify(mClockRegistry).registerClockChangeListener(listenerArgumentCaptor.capture());
listenerArgumentCaptor.getValue().onClockChanged();
- verify(mView, times(2)).setClock(mClock, StatusBarState.SHADE);
- verify(mClockEventController, times(2)).setClock(mClock);
+ verify(mView, times(2)).setClock(mClockController, StatusBarState.SHADE);
+ verify(mClockEventController, times(2)).setClock(mClockController);
}
@Test
@@ -262,17 +285,40 @@
@Test
public void testGetClockAnimationsForwardsToClock() {
- ClockController mockClockController = mock(ClockController.class);
- ClockAnimations mockClockAnimations = mock(ClockAnimations.class);
- when(mClockEventController.getClock()).thenReturn(mockClockController);
- when(mockClockController.getAnimations()).thenReturn(mockClockAnimations);
-
- Rect r1 = new Rect(1, 2, 3, 4);
- Rect r2 = new Rect(5, 6, 7, 8);
- mController.getClockAnimations().onPositionUpdated(r1, r2, 0.2f);
- verify(mockClockAnimations).onPositionUpdated(r1, r2, 0.2f);
+ assertEquals(mClockAnimations, mController.getClockAnimations());
}
+ @Test
+ public void testGetLargeClockBottom_returnsExpectedValue() {
+ when(mLargeClockFrame.getVisibility()).thenReturn(View.VISIBLE);
+ when(mLargeClockFrame.getHeight()).thenReturn(100);
+ when(mSmallClockFrame.getHeight()).thenReturn(50);
+ when(mLargeClockView.getHeight()).thenReturn(40);
+ when(mSmallClockView.getHeight()).thenReturn(20);
+ mController.init();
+
+ assertEquals(170, mController.getClockBottom(1000));
+ }
+
+ @Test
+ public void testGetSmallLargeClockBottom_returnsExpectedValue() {
+ when(mLargeClockFrame.getVisibility()).thenReturn(View.GONE);
+ when(mLargeClockFrame.getHeight()).thenReturn(100);
+ when(mSmallClockFrame.getHeight()).thenReturn(50);
+ when(mLargeClockView.getHeight()).thenReturn(40);
+ when(mSmallClockView.getHeight()).thenReturn(20);
+ mController.init();
+
+ assertEquals(1120, mController.getClockBottom(1000));
+ }
+
+ @Test
+ public void testGetClockBottom_nullClock_returnsZero() {
+ when(mClockEventController.getClock()).thenReturn(null);
+ assertEquals(0, mController.getClockBottom(10));
+ }
+
+
private void verifyAttachment(VerificationMode times) {
verify(mClockRegistry, times).registerClockChangeListener(
any(ClockRegistry.ClockChangeListener.class));
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPasswordViewControllerTest.kt
index d20be56..d912793 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 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 @@
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 b3d1c8f..85dbdb8 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 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
- )
- }
+ 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 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() {
- 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)
- }
+ @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 ce1101f..b742100 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 @@
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 8bcfe6f..cdb7bbb 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 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 @@
keyguardMessageAreaControllerFactory.create(any(KeyguardMessageArea::class.java))
)
.thenReturn(keyguardMessageAreaController)
+ `when`(keyguardPinView.resources).thenReturn(context.resources)
pinViewController =
KeyguardPinViewController(
keyguardPinView,
@@ -98,14 +102,14 @@
@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 bdd29aa..0f4cf41 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 @@
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 @@
}
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 ca94ea8..262b4b8 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 @@
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 779788a..d172c9a 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.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.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 @@
.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 5cd2ace..de04ef8 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 @@
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 99406ed..8e689cf 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 @@
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 @@
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/DreamOverlayServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
index 99ca9a6..430575c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
@@ -19,7 +19,6 @@
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -39,16 +38,18 @@
import android.view.WindowManagerImpl;
import androidx.lifecycle.Lifecycle;
-import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LifecycleRegistry;
import androidx.test.filters.SmallTest;
import com.android.internal.logging.UiEventLogger;
import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.dreams.complication.ComplicationLayoutEngine;
import com.android.systemui.dreams.complication.dagger.ComplicationComponent;
import com.android.systemui.dreams.dagger.DreamOverlayComponent;
+import com.android.systemui.dreams.dreamcomplication.HideComplicationTouchHandler;
import com.android.systemui.dreams.touch.DreamOverlayTouchMonitor;
+import com.android.systemui.touch.TouchInsetManager;
import com.android.systemui.util.concurrency.FakeExecutor;
import com.android.systemui.util.time.FakeSystemClock;
import com.android.systemui.utils.leaks.LeakCheckedTest;
@@ -72,7 +73,7 @@
private final FakeExecutor mMainExecutor = new FakeExecutor(mFakeSystemClock);
@Mock
- LifecycleOwner mLifecycleOwner;
+ DreamOverlayLifecycleOwner mLifecycleOwner;
@Mock
LifecycleRegistry mLifecycleRegistry;
@@ -95,6 +96,20 @@
ComplicationComponent mComplicationComponent;
@Mock
+ ComplicationLayoutEngine mComplicationVisibilityController;
+
+ @Mock
+ com.android.systemui.dreams.dreamcomplication.dagger.ComplicationComponent.Factory
+ mDreamComplicationComponentFactory;
+
+ @Mock
+ com.android.systemui.dreams.dreamcomplication.dagger.ComplicationComponent
+ mDreamComplicationComponent;
+
+ @Mock
+ HideComplicationTouchHandler mHideComplicationTouchHandler;
+
+ @Mock
DreamOverlayComponent.Factory mDreamOverlayComponentFactory;
@Mock
@@ -119,6 +134,9 @@
ViewGroup mDreamOverlayContainerViewParent;
@Mock
+ TouchInsetManager mTouchInsetManager;
+
+ @Mock
UiEventLogger mUiEventLogger;
@Captor
@@ -132,29 +150,38 @@
when(mDreamOverlayComponent.getDreamOverlayContainerViewController())
.thenReturn(mDreamOverlayContainerViewController);
- when(mDreamOverlayComponent.getLifecycleOwner())
- .thenReturn(mLifecycleOwner);
- when(mDreamOverlayComponent.getLifecycleRegistry())
+ when(mLifecycleOwner.getRegistry())
.thenReturn(mLifecycleRegistry);
when(mDreamOverlayComponent.getDreamOverlayTouchMonitor())
.thenReturn(mDreamOverlayTouchMonitor);
when(mComplicationComponentFactory
- .create())
+ .create(any(), any(), any(), any()))
.thenReturn(mComplicationComponent);
- // TODO(b/261781069): A touch handler should be passed in from the complication component
- // when the complication component is introduced.
+ when(mComplicationComponent.getVisibilityController())
+ .thenReturn(mComplicationVisibilityController);
+ when(mDreamComplicationComponent.getHideComplicationTouchHandler())
+ .thenReturn(mHideComplicationTouchHandler);
+ when(mDreamComplicationComponentFactory
+ .create(any(), any()))
+ .thenReturn(mDreamComplicationComponent);
when(mDreamOverlayComponentFactory
- .create(any(), any(), isNull()))
+ .create(any(), any(), any(), any()))
.thenReturn(mDreamOverlayComponent);
when(mDreamOverlayContainerViewController.getContainerView())
.thenReturn(mDreamOverlayContainerView);
- mService = new DreamOverlayService(mContext, mMainExecutor, mWindowManager,
+ mService = new DreamOverlayService(
+ mContext,
+ mMainExecutor,
+ mLifecycleOwner,
+ mWindowManager,
mComplicationComponentFactory,
+ mDreamComplicationComponentFactory,
mDreamOverlayComponentFactory,
mStateController,
mKeyguardUpdateMonitor,
mUiEventLogger,
+ mTouchInsetManager,
LOW_LIGHT_COMPONENT);
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java
index c21c7a2..ee989d1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java
@@ -63,7 +63,7 @@
@Test
public void testStateChange_overlayActive() {
final DreamOverlayStateController stateController = new DreamOverlayStateController(
- mExecutor);
+ mExecutor, true);
stateController.addCallback(mCallback);
stateController.setOverlayActive(true);
mExecutor.runAllReady();
@@ -85,7 +85,7 @@
@Test
public void testCallback() {
final DreamOverlayStateController stateController = new DreamOverlayStateController(
- mExecutor);
+ mExecutor, true);
stateController.addCallback(mCallback);
// Add complication and verify callback is notified.
@@ -111,7 +111,7 @@
@Test
public void testNotifyOnCallbackAdd() {
final DreamOverlayStateController stateController =
- new DreamOverlayStateController(mExecutor);
+ new DreamOverlayStateController(mExecutor, true);
stateController.addComplication(mComplication);
mExecutor.runAllReady();
@@ -123,9 +123,24 @@
}
@Test
+ public void testNotifyOnCallbackAddOverlayDisabled() {
+ final DreamOverlayStateController stateController =
+ new DreamOverlayStateController(mExecutor, false);
+
+ stateController.addComplication(mComplication);
+ mExecutor.runAllReady();
+
+ // Verify callback occurs on add when an overlay is already present.
+ stateController.addCallback(mCallback);
+ mExecutor.runAllReady();
+ verify(mCallback, never()).onComplicationsChanged();
+ }
+
+
+ @Test
public void testComplicationFilteringWhenShouldShowComplications() {
final DreamOverlayStateController stateController =
- new DreamOverlayStateController(mExecutor);
+ new DreamOverlayStateController(mExecutor, true);
stateController.setShouldShowComplications(true);
final Complication alwaysAvailableComplication = Mockito.mock(Complication.class);
@@ -165,7 +180,7 @@
@Test
public void testComplicationFilteringWhenShouldHideComplications() {
final DreamOverlayStateController stateController =
- new DreamOverlayStateController(mExecutor);
+ new DreamOverlayStateController(mExecutor, true);
stateController.setShouldShowComplications(true);
final Complication alwaysAvailableComplication = Mockito.mock(Complication.class);
@@ -212,7 +227,7 @@
public void testComplicationWithNoTypeNotFiltered() {
final Complication complication = Mockito.mock(Complication.class);
final DreamOverlayStateController stateController =
- new DreamOverlayStateController(mExecutor);
+ new DreamOverlayStateController(mExecutor, true);
stateController.addComplication(complication);
mExecutor.runAllReady();
assertThat(stateController.getComplications(true).contains(complication))
@@ -222,7 +237,7 @@
@Test
public void testNotifyLowLightChanged() {
final DreamOverlayStateController stateController =
- new DreamOverlayStateController(mExecutor);
+ new DreamOverlayStateController(mExecutor, true);
stateController.addCallback(mCallback);
mExecutor.runAllReady();
@@ -238,7 +253,7 @@
@Test
public void testNotifyEntryAnimationsFinishedChanged() {
final DreamOverlayStateController stateController =
- new DreamOverlayStateController(mExecutor);
+ new DreamOverlayStateController(mExecutor, true);
stateController.addCallback(mCallback);
mExecutor.runAllReady();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationHostViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationHostViewControllerTest.java
index b477592..dcd8736 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationHostViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationHostViewControllerTest.java
@@ -15,6 +15,8 @@
*/
package com.android.systemui.dreams.complication;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
@@ -139,6 +141,21 @@
}
@Test
+ public void testMalformedComplicationAddition() {
+ final Observer<Collection<ComplicationViewModel>> observer =
+ captureComplicationViewModelsObserver();
+
+ // Add a complication and ensure it is added to the view.
+ final HashSet<ComplicationViewModel> complications = new HashSet<>(
+ Collections.singletonList(mComplicationViewModel));
+ when(mViewHolder.getView()).thenReturn(null);
+ observer.onChanged(complications);
+
+ verify(mLayoutEngine, never()).addComplication(any(), any(), any(), anyInt());
+
+ }
+
+ @Test
public void testNewComplicationsBeforeEntryAnimationsFinishSetToInvisible() {
final Observer<Collection<ComplicationViewModel>> observer =
captureComplicationViewModelsObserver();
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 fdb4cc4..e414942 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 @@
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 java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.function.Consumer;
@SmallTest
@RunWith(AndroidTestingRunner.class)
@@ -197,4 +202,19 @@
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 e6d3a69..89c7280 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.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.Mock;
import org.mockito.MockitoAnnotations;
+import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@@ -147,6 +149,19 @@
}
@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 @@
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/dreams/touch/HideComplicationTouchHandlerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/dreamcomplication/HideComplicationTouchHandlerTest.java
similarity index 98%
rename from packages/SystemUI/tests/src/com/android/systemui/dreams/touch/HideComplicationTouchHandlerTest.java
rename to packages/SystemUI/tests/src/com/android/systemui/dreams/dreamcomplication/HideComplicationTouchHandlerTest.java
index 4e3aca7..d68f032 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/HideComplicationTouchHandlerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/dreamcomplication/HideComplicationTouchHandlerTest.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.dreams.touch;
+package com.android.systemui.dreams.dreamcomplication;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
@@ -33,6 +33,7 @@
import com.android.systemui.SysuiTestCase;
import com.android.systemui.dreams.DreamOverlayStateController;
import com.android.systemui.dreams.complication.Complication;
+import com.android.systemui.dreams.touch.DreamTouchHandler;
import com.android.systemui.shared.system.InputChannelCompat;
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
import com.android.systemui.touch.TouchInsetManager;
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 cef452b..09c8e6a 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 @@
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.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.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 @@
)
val remoteUserSelectionManager =
KeyguardQuickAffordanceRemoteUserSelectionManager(
- scope = scope,
+ scope = testScope.backgroundScope,
userTracker = userTracker,
clientFactory = FakeKeyguardQuickAffordanceProviderClientFactory(userTracker),
userHandle = UserHandle.SYSTEM,
@@ -104,7 +126,7 @@
val quickAffordanceRepository =
KeyguardQuickAffordanceRepository(
appContext = context,
- scope = scope,
+ scope = testScope.backgroundScope,
localUserSelectionManager = localUserSelectionManager,
remoteUserSelectionManager = remoteUserSelectionManager,
userTracker = userTracker,
@@ -123,8 +145,8 @@
),
legacySettingSyncer =
KeyguardQuickAffordanceLegacySettingSyncer(
- scope = scope,
- backgroundDispatcher = IMMEDIATE,
+ scope = testScope.backgroundScope,
+ backgroundDispatcher = testDispatcher,
secureSettings = FakeSettings(),
selectionsManager = localUserSelectionManager,
),
@@ -148,6 +170,12 @@
},
repository = { quickAffordanceRepository },
)
+ underTest.previewManager =
+ KeyguardRemotePreviewManager(
+ previewRendererFactory = previewRendererFactory,
+ mainDispatcher = testDispatcher,
+ backgroundHandler = backgroundHandler,
+ )
underTest.attachInfoForTesting(
context,
@@ -190,7 +218,7 @@
@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 @@
@Test
fun `query slots`() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
assertThat(querySlots())
.isEqualTo(
listOf(
@@ -232,7 +260,7 @@
@Test
fun `query affordances`() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
assertThat(queryAffordances())
.isEqualTo(
listOf(
@@ -252,7 +280,7 @@
@Test
fun `delete and query selection`() =
- runBlocking(IMMEDIATE) {
+ testScope.runTest {
insertSelection(
slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
affordanceId = AFFORDANCE_1,
@@ -286,7 +314,7 @@
@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 @@
)
}
+ @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 @@
)
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/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
index 798839d..804960d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
@@ -168,6 +168,45 @@
}
@Test
+ @TestableLooper.RunWithLooper(setAsMainLooper = true)
+ public void testOnStartedWakingUp_whileSleeping_ifWakeAndUnlocking_doesNotShowKeyguard() {
+ when(mLockPatternUtils.isLockScreenDisabled(anyInt())).thenReturn(false);
+ when(mLockPatternUtils.getPowerButtonInstantlyLocks(anyInt())).thenReturn(true);
+ mViewMediator.onSystemReady();
+ TestableLooper.get(this).processAllMessages();
+
+ mViewMediator.setShowingLocked(false);
+ TestableLooper.get(this).processAllMessages();
+
+ mViewMediator.onStartedGoingToSleep(OFF_BECAUSE_OF_USER);
+ mViewMediator.onWakeAndUnlocking();
+ mViewMediator.onStartedWakingUp(OFF_BECAUSE_OF_USER, false);
+ TestableLooper.get(this).processAllMessages();
+
+ assertFalse(mViewMediator.isShowingAndNotOccluded());
+ verify(mKeyguardStateController, never()).notifyKeyguardState(eq(true), anyBoolean());
+ }
+
+ @Test
+ @TestableLooper.RunWithLooper(setAsMainLooper = true)
+ public void testOnStartedWakingUp_whileSleeping_ifNotWakeAndUnlocking_showsKeyguard() {
+ when(mLockPatternUtils.isLockScreenDisabled(anyInt())).thenReturn(false);
+ when(mLockPatternUtils.getPowerButtonInstantlyLocks(anyInt())).thenReturn(true);
+ mViewMediator.onSystemReady();
+ TestableLooper.get(this).processAllMessages();
+
+ mViewMediator.setShowingLocked(false);
+ TestableLooper.get(this).processAllMessages();
+
+ mViewMediator.onStartedGoingToSleep(OFF_BECAUSE_OF_USER);
+ mViewMediator.onStartedWakingUp(OFF_BECAUSE_OF_USER, false);
+
+ TestableLooper.get(this).processAllMessages();
+
+ assertTrue(mViewMediator.isShowingAndNotOccluded());
+ }
+
+ @Test
public void testRegisterDumpable() {
verify(mDumpManager).registerDumpable(KeyguardViewMediator.class.getName(), mViewMediator);
verify(mStatusBarKeyguardViewManager, never()).setKeyguardGoingAwayState(anyBoolean());
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 322014a..f8cb408 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 @@
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 @@
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 @@
@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 @@
@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 @@
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 @@
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 11fe905..d97571bc 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.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.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 @@
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 @@
)
qrCodeScanner =
FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER)
- val scope = CoroutineScope(IMMEDIATE)
+ val testDispatcher = StandardTestDispatcher()
+ testScope = TestScope(testDispatcher)
val localUserSelectionManager =
KeyguardQuickAffordanceLocalUserSelectionManager(
@@ -120,7 +119,7 @@
)
val remoteUserSelectionManager =
KeyguardQuickAffordanceRemoteUserSelectionManager(
- scope = scope,
+ scope = testScope.backgroundScope,
userTracker = userTracker,
clientFactory = FakeKeyguardQuickAffordanceProviderClientFactory(userTracker),
userHandle = UserHandle.SYSTEM,
@@ -128,14 +127,14 @@
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 @@
}
@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()
+ val collectedValue =
+ collectLastValue(
+ underTest.quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_START)
+ )
- 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()
- }
+ 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()
+ val collectedValue =
+ collectLastValue(
+ underTest.quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_END)
+ )
- 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()
- }
+ 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 @@
)
)
- 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 @@
)
)
- 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 @@
activationState = ActivationState.NotSupported,
)
)
- assertThat(endConfig)
+ assertThat(endConfig())
.isEqualTo(
KeyguardQuickAffordanceModel.Hidden,
)
@@ -345,9 +353,8 @@
KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
quickAccessWallet.key
)
- yield()
- yield()
- assertThat(startConfig)
+
+ assertThat(startConfig())
.isEqualTo(
KeyguardQuickAffordanceModel.Visible(
configKey =
@@ -357,7 +364,7 @@
activationState = ActivationState.NotSupported,
)
)
- assertThat(endConfig)
+ assertThat(endConfig())
.isEqualTo(
KeyguardQuickAffordanceModel.Hidden,
)
@@ -377,9 +384,8 @@
)
underTest.select(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, qrCodeScanner.key)
- yield()
- yield()
- assertThat(startConfig)
+
+ assertThat(startConfig())
.isEqualTo(
KeyguardQuickAffordanceModel.Visible(
configKey =
@@ -389,7 +395,7 @@
activationState = ActivationState.NotSupported,
)
)
- assertThat(endConfig)
+ assertThat(endConfig())
.isEqualTo(
KeyguardQuickAffordanceModel.Visible(
configKey =
@@ -420,14 +426,11 @@
),
)
)
-
- 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 @@
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 @@
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 @@
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 @@
)
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 @@
KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
null,
)
- yield()
- yield()
assertThat(underTest.getSelections())
.isEqualTo(
@@ -584,6 +562,5 @@
)
}
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 83a5d0e..0abff88 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.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.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.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 @@
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 @@
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 @@
)
val remoteUserSelectionManager =
KeyguardQuickAffordanceRemoteUserSelectionManager(
- scope = scope,
+ scope = testScope.backgroundScope,
userTracker = userTracker,
clientFactory = FakeKeyguardQuickAffordanceProviderClientFactory(userTracker),
userHandle = UserHandle.SYSTEM,
@@ -151,14 +155,14 @@
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 @@
}
@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)
+ 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,
- )
- 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)
-
- 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,
- )
- job.cancel()
- }
-
- @Test
- fun `startButton - not present - model is hidden`() = runBlockingTest {
- var latest: KeyguardQuickAffordanceViewModel? = null
- val job = underTest.startButton.onEach { latest = it }.launchIn(this)
-
- val config =
- TestConfig(
- isVisible = false,
- )
- val configKey =
- setUpQuickAffordanceModel(
- position = KeyguardQuickAffordancePosition.BOTTOM_START,
- testConfig = config,
- )
-
- 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"),
- )
-
- setUpQuickAffordanceModel(
- position = KeyguardQuickAffordancePosition.BOTTOM_START,
- testConfig = testConfig,
- )
-
- val values = mutableListOf<Boolean>()
- val job = underTest.startButton.onEach { values.add(it.animateReveal) }.launchIn(this)
-
- repository.setAnimateDozingTransitions(true)
- yield()
- repository.setAnimateDozingTransitions(false)
- yield()
-
- // 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 isOverlayContainerVisible() = runBlockingTest {
- val values = mutableListOf<Boolean>()
- val job = underTest.isOverlayContainerVisible.onEach(values::add).launchIn(this)
-
- repository.setDozing(true)
- repository.setDozing(false)
-
- assertThat(values).isEqualTo(listOf(true, false, true))
- job.cancel()
- }
-
- @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)
-
- assertThat(values).isEqualTo(listOf(1f, 0.1f, 0.5f, 0.2f, 0f))
- job.cancel()
- }
-
- @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 =
+ val testConfig =
TestConfig(
isVisible = true,
isClickable = true,
+ isActivated = true,
icon = mock(),
- canShowWhileLocked = true,
+ canShowWhileLocked = false,
+ intent = Intent("action"),
)
- )
- setUpQuickAffordanceModel(
- position = KeyguardQuickAffordancePosition.BOTTOM_END,
- testConfig =
+ val configKey =
+ setUpQuickAffordanceModel(
+ position = KeyguardQuickAffordancePosition.BOTTOM_START,
+ testConfig = testConfig,
+ )
+
+ assertQuickAffordanceViewModel(
+ viewModel = latest(),
+ testConfig = testConfig,
+ configKey = configKey,
+ )
+ }
+
+ @Test
+ 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"),
+ ),
+ )
+
+ assertQuickAffordanceViewModel(
+ viewModel = latest(),
+ testConfig =
+ TestConfig(
+ isVisible = true,
+ isClickable = false,
+ isActivated = true,
+ icon = icon,
+ canShowWhileLocked = false,
+ intent = Intent("action"),
+ ),
+ configKey = configKey,
+ )
+ 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.
)
- )
- setUpQuickAffordanceModel(
- position = KeyguardQuickAffordancePosition.BOTTOM_START,
- testConfig =
+ val configKey =
+ setUpQuickAffordanceModel(
+ position = KeyguardQuickAffordancePosition.BOTTOM_END,
+ testConfig = config,
+ )
+
+ assertQuickAffordanceViewModel(
+ viewModel = latest(),
+ testConfig = config,
+ configKey = configKey,
+ )
+ }
+
+ @Test
+ fun `startButton - not present - model is hidden`() =
+ testScope.runTest {
+ val latest = collectLastValue(underTest.startButton)
+
+ val config =
TestConfig(
isVisible = false,
)
- )
- setUpQuickAffordanceModel(
- position = KeyguardQuickAffordancePosition.BOTTOM_END,
- testConfig =
+ val configKey =
+ setUpQuickAffordanceModel(
+ position = KeyguardQuickAffordancePosition.BOTTOM_START,
+ testConfig = config,
+ )
+
+ assertQuickAffordanceViewModel(
+ viewModel = latest(),
+ testConfig = config,
+ configKey = configKey,
+ )
+ }
+
+ @Test
+ fun animateButtonReveal() =
+ testScope.runTest {
+ repository.setKeyguardShowing(true)
+ val testConfig =
TestConfig(
- isVisible = false,
+ isVisible = true,
+ isClickable = true,
+ icon = mock(),
+ canShowWhileLocked = false,
+ intent = Intent("action"),
)
- )
- 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,
+ setUpQuickAffordanceModel(
+ position = KeyguardQuickAffordancePosition.BOTTOM_START,
+ testConfig = testConfig,
+ )
+
+ val value = collectLastValue(underTest.startButton.map { it.animateReveal })
+
+ assertThat(value()).isFalse()
+ repository.setAnimateDozingTransitions(true)
+ assertThat(value()).isTrue()
+ repository.setAnimateDozingTransitions(false)
+ assertThat(value()).isFalse()
+ }
+
+ @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()
+ }
+
+ @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 `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)
+ }
+
+ @Test
+ fun isIndicationAreaPadded() =
+ testScope.runTest {
+ repository.setKeyguardShowing(true)
+ val value = collectLastValue(underTest.isIndicationAreaPadded)
+
+ 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 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)
+ }
+
+ @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 `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"),
)
- )
- job.cancel()
- }
+ val configKey =
+ setUpQuickAffordanceModel(
+ position = KeyguardQuickAffordancePosition.BOTTOM_START,
+ testConfig = testConfig,
+ )
- @Test
- fun indicationAreaTranslationX() = runBlockingTest {
- val values = mutableListOf<Float>()
- val job = underTest.indicationAreaTranslationX.onEach(values::add).launchIn(this)
+ val latest = collectLastValue(underTest.startButton)
- repository.setClockPosition(100, 100)
- repository.setClockPosition(200, 100)
- repository.setClockPosition(200, 200)
- repository.setClockPosition(300, 100)
-
- assertThat(values).isEqualTo(listOf(0f, 100f, 200f, 300f))
- job.cancel()
- }
-
- @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),
- )
-
- assertThat(values).isEqualTo(expectedTranslationValues)
- job.cancel()
- }
-
- @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"),
+ 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),
)
- val configKey =
- setUpQuickAffordanceModel(
- position = KeyguardQuickAffordancePosition.BOTTOM_START,
+
+ val testConfig =
+ TestConfig(
+ isVisible = true,
+ isClickable = 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 `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"),
+ 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),
)
- val configKey =
- setUpQuickAffordanceModel(
- position = KeyguardQuickAffordancePosition.BOTTOM_START,
+
+ 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()
- }
+ }
@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)
+ 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,
+ 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 @@
when (testConfig.isActivated) {
true -> ActivationState.Active
false -> ActivationState.Inactive
- null -> ActivationState.NotSupported
}
)
} else {
@@ -636,6 +667,5 @@
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 e009e86..0e7bf8d 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.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 @@
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 cce3e36..561867f 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.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 @@
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 4aa982e..bad3f03 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.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 @@
uiEventLogger: MediaTttReceiverUiEventLogger,
viewUtil: ViewUtil,
wakeLockBuilder: WakeLock.Builder,
+ systemClock: SystemClock,
) :
MediaTttChipControllerReceiver(
commandQueue,
@@ -59,6 +61,7 @@
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 23f7cdb..ef0bfb7 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 @@
@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 @@
receiverUiEventLogger,
viewUtil,
fakeWakeLockBuilder,
+ fakeClock,
)
controllerReceiver.start()
@@ -155,6 +156,7 @@
receiverUiEventLogger,
viewUtil,
fakeWakeLockBuilder,
+ fakeClock,
)
controllerReceiver.start()
@@ -193,6 +195,36 @@
}
@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 @@
}
@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 311740e..b03a545 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.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 @@
@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 @@
viewUtil,
vibratorHelper,
fakeWakeLockBuilder,
+ fakeClock,
)
chipbarCoordinator.start()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt
index 9f28708..5e082f6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerTest.kt
@@ -1,9 +1,12 @@
package com.android.systemui.qs
+import android.content.res.Configuration
import android.test.suitebuilder.annotation.SmallTest
import android.testing.AndroidTestingRunner
+import android.testing.TestableResources
import com.android.internal.logging.MetricsLogger
import com.android.internal.logging.UiEventLogger
+import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.dump.DumpManager
import com.android.systemui.flags.FeatureFlags
@@ -26,10 +29,11 @@
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.Mockito.any
+import org.mockito.Mockito.never
import org.mockito.Mockito.reset
import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when` as whenever
import org.mockito.MockitoAnnotations
+import org.mockito.Mockito.`when` as whenever
@SmallTest
@RunWith(AndroidTestingRunner::class)
@@ -54,8 +58,11 @@
@Mock private lateinit var otherTile: QSTile
@Mock private lateinit var statusBarKeyguardViewManager: StatusBarKeyguardViewManager
@Mock private lateinit var featureFlags: FeatureFlags
+ @Mock private lateinit var configuration: Configuration
+ @Mock private lateinit var pagedTileLayout: PagedTileLayout
private lateinit var controller: QSPanelController
+ private val testableResources: TestableResources = mContext.orCreateTestableResources
@Before
fun setUp() {
@@ -63,7 +70,9 @@
whenever(brightnessSliderFactory.create(any(), any())).thenReturn(brightnessSlider)
whenever(brightnessControllerFactory.create(any())).thenReturn(brightnessController)
- whenever(qsPanel.resources).thenReturn(mContext.orCreateTestableResources.resources)
+ testableResources.addOverride(R.bool.config_use_split_notification_shade, false)
+ whenever(qsPanel.resources).thenReturn(testableResources.resources)
+ whenever(qsPanel.getOrCreateTileLayout()).thenReturn(pagedTileLayout)
whenever(statusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(false)
whenever(qsPanel.setListening(anyBoolean())).then {
whenever(qsPanel.isListening).thenReturn(it.getArgument(0))
@@ -121,4 +130,15 @@
whenever(statusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(false)
assertThat(controller.isBouncerInTransit()).isEqualTo(false)
}
+
+ @Test
+ fun configurationChange_onlySplitShadeConfigChanges_tileAreRedistributed() {
+ testableResources.addOverride(R.bool.config_use_split_notification_shade, false)
+ controller.mOnConfigurationChangedListener.onConfigurationChange(configuration)
+ verify(pagedTileLayout, never()).forceTilesRedistribution()
+
+ testableResources.addOverride(R.bool.config_use_split_notification_shade, true)
+ controller.mOnConfigurationChangedListener.onConfigurationChange(configuration)
+ verify(pagedTileLayout).forceTilesRedistribution()
+ }
}
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 3ae8428..b59005a 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 @@
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 @@
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 @@
mInternetDialog.updateDialog(true);
- assertThat(mMobileDataToggle.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mMobileDataLayout.getVisibility()).isEqualTo(View.GONE);
}
@Test
@@ -248,7 +250,7 @@
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 @@
mInternetDialog.updateDialog(true);
- assertThat(mMobileDataToggle.getVisibility()).isEqualTo(View.VISIBLE);
+ assertThat(mMobileDataLayout.getVisibility()).isEqualTo(View.VISIBLE);
}
@Test
@@ -267,7 +269,7 @@
mInternetDialog.updateDialog(true);
- assertThat(mMobileDataToggle.getVisibility()).isEqualTo(View.GONE);
+ assertThat(mMobileDataLayout.getVisibility()).isEqualTo(View.GONE);
}
@Test
@@ -279,7 +281,7 @@
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 @@
}
@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 @@
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 d928f6f..6a7308c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
@@ -1105,6 +1105,17 @@
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/collection/coordinator/HeadsUpCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
index aa1114b..cb4f119 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinatorTest.kt
@@ -23,6 +23,7 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.dump.logcatLogBuffer
import com.android.systemui.statusbar.NotificationRemoteInputManager
+import com.android.systemui.statusbar.notification.NotifPipelineFlags
import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder
import com.android.systemui.statusbar.notification.collection.NotifPipeline
import com.android.systemui.statusbar.notification.collection.NotificationEntry
@@ -38,6 +39,7 @@
import com.android.systemui.statusbar.notification.collection.render.NodeController
import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder
import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider
+import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider.FullScreenIntentDecision
import com.android.systemui.statusbar.notification.row.NotifBindPipeline.BindCallback
import com.android.systemui.statusbar.phone.NotificationGroupTestHelper
import com.android.systemui.statusbar.policy.HeadsUpManager
@@ -88,6 +90,7 @@
private val mEndLifetimeExtension: OnEndLifetimeExtensionCallback = mock()
private val mHeaderController: NodeController = mock()
private val mLaunchFullScreenIntentProvider: LaunchFullScreenIntentProvider = mock()
+ private val mFlags: NotifPipelineFlags = mock()
private lateinit var mEntry: NotificationEntry
private lateinit var mGroupSummary: NotificationEntry
@@ -113,6 +116,7 @@
mNotificationInterruptStateProvider,
mRemoteInputManager,
mLaunchFullScreenIntentProvider,
+ mFlags,
mHeaderController,
mExecutor)
mCoordinator.attach(mNotifPipeline)
@@ -246,14 +250,14 @@
@Test
fun testOnEntryAdded_shouldFullScreen() {
- setShouldFullScreen(mEntry)
+ setShouldFullScreen(mEntry, FullScreenIntentDecision.FSI_EXPECTED_NOT_TO_HUN)
mCollectionListener.onEntryAdded(mEntry)
verify(mLaunchFullScreenIntentProvider).launchFullScreenIntent(mEntry)
}
@Test
fun testOnEntryAdded_shouldNotFullScreen() {
- setShouldFullScreen(mEntry, should = false)
+ setShouldFullScreen(mEntry, FullScreenIntentDecision.NO_FULL_SCREEN_INTENT)
mCollectionListener.onEntryAdded(mEntry)
verify(mLaunchFullScreenIntentProvider, never()).launchFullScreenIntent(any())
}
@@ -805,15 +809,96 @@
verify(mHeadsUpManager, never()).showNotification(any())
}
+ @Test
+ fun testOnRankingApplied_noFSIOnUpdateWhenFlagOff() {
+ // Ensure the feature flag is off
+ whenever(mFlags.fsiOnDNDUpdate()).thenReturn(false)
+
+ // GIVEN that mEntry was previously suppressed from full-screen only by DND
+ setShouldFullScreen(mEntry, FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND)
+ mCollectionListener.onEntryAdded(mEntry)
+
+ // and it is then updated to allow full screen
+ setShouldFullScreen(mEntry, FullScreenIntentDecision.FSI_DEVICE_NOT_INTERACTIVE)
+ whenever(mNotifPipeline.allNotifs).thenReturn(listOf(mEntry))
+ mCollectionListener.onRankingApplied()
+
+ // THEN it should not full screen because the feature is off
+ verify(mLaunchFullScreenIntentProvider, never()).launchFullScreenIntent(mEntry)
+ }
+
+ @Test
+ fun testOnRankingApplied_updateToFullScreen() {
+ // Turn on the feature
+ whenever(mFlags.fsiOnDNDUpdate()).thenReturn(true)
+
+ // GIVEN that mEntry was previously suppressed from full-screen only by DND
+ setShouldFullScreen(mEntry, FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND)
+ mCollectionListener.onEntryAdded(mEntry)
+
+ // at this point, it should not have full screened
+ verify(mLaunchFullScreenIntentProvider, never()).launchFullScreenIntent(mEntry)
+
+ // and it is then updated to allow full screen AND HUN
+ setShouldFullScreen(mEntry, FullScreenIntentDecision.FSI_DEVICE_NOT_INTERACTIVE)
+ setShouldHeadsUp(mEntry)
+ whenever(mNotifPipeline.allNotifs).thenReturn(listOf(mEntry))
+ mCollectionListener.onRankingApplied()
+ mBeforeTransformGroupsListener.onBeforeTransformGroups(listOf(mEntry))
+ mBeforeFinalizeFilterListener.onBeforeFinalizeFilter(listOf(mEntry))
+
+ // THEN it should full screen but it should NOT HUN
+ verify(mLaunchFullScreenIntentProvider).launchFullScreenIntent(mEntry)
+ verify(mHeadsUpViewBinder, never()).bindHeadsUpView(any(), any())
+ verify(mHeadsUpManager, never()).showNotification(any())
+ }
+
+ @Test
+ fun testOnRankingApplied_noFSIWhenAlsoSuppressedForOtherReasons() {
+ // Feature on
+ whenever(mFlags.fsiOnDNDUpdate()).thenReturn(true)
+
+ // GIVEN that mEntry is suppressed by DND (functionally), but not *only* DND
+ setShouldFullScreen(mEntry, FullScreenIntentDecision.NO_FSI_SUPPRESSED_BY_DND)
+ mCollectionListener.onEntryAdded(mEntry)
+
+ // and it is updated to full screen later
+ setShouldFullScreen(mEntry, FullScreenIntentDecision.FSI_DEVICE_NOT_INTERACTIVE)
+ mCollectionListener.onRankingApplied()
+
+ // THEN it should still not full screen because something else was blocking it before
+ verify(mLaunchFullScreenIntentProvider, never()).launchFullScreenIntent(mEntry)
+ }
+
+ @Test
+ fun testOnRankingApplied_noFSIWhenTooOld() {
+ // Feature on
+ whenever(mFlags.fsiOnDNDUpdate()).thenReturn(true)
+
+ // GIVEN that mEntry is suppressed only by DND
+ setShouldFullScreen(mEntry, FullScreenIntentDecision.NO_FSI_SUPPRESSED_ONLY_BY_DND)
+ mCollectionListener.onEntryAdded(mEntry)
+
+ // but it's >10s old
+ mCoordinator.addForFSIReconsideration(mEntry, mSystemClock.currentTimeMillis() - 10000)
+
+ // and it is updated to full screen later
+ setShouldFullScreen(mEntry, FullScreenIntentDecision.FSI_EXPECTED_NOT_TO_HUN)
+ mCollectionListener.onRankingApplied()
+
+ // THEN it should still not full screen because it's too old
+ verify(mLaunchFullScreenIntentProvider, never()).launchFullScreenIntent(mEntry)
+ }
+
private fun setShouldHeadsUp(entry: NotificationEntry, should: Boolean = true) {
whenever(mNotificationInterruptStateProvider.shouldHeadsUp(entry)).thenReturn(should)
whenever(mNotificationInterruptStateProvider.checkHeadsUp(eq(entry), any()))
.thenReturn(should)
}
- private fun setShouldFullScreen(entry: NotificationEntry, should: Boolean = true) {
- whenever(mNotificationInterruptStateProvider.shouldLaunchFullScreenIntentWhenAdded(entry))
- .thenReturn(should)
+ private fun setShouldFullScreen(entry: NotificationEntry, decision: FullScreenIntentDecision) {
+ whenever(mNotificationInterruptStateProvider.getFullScreenIntentDecision(entry))
+ .thenReturn(decision)
}
private fun finishBind(entry: NotificationEntry) {
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 0000000..33b94e3
--- /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 f69839b..072a497 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.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 @@
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 @@
extras = 3316,
bigPicture = 0,
extender = 0,
- style = null,
+ style = NotificationEnums.STYLE_NONE,
styleIcon = 0,
hasCustomView = false,
)
@@ -92,7 +113,7 @@
extras = 3384,
bigPicture = 0,
extender = 0,
- style = null,
+ style = NotificationEnums.STYLE_NONE,
styleIcon = 0,
hasCustomView = true,
)
@@ -112,7 +133,7 @@
extras = 3212,
bigPicture = 0,
extender = 0,
- style = null,
+ style = NotificationEnums.STYLE_NONE,
styleIcon = 0,
hasCustomView = false,
)
@@ -141,7 +162,7 @@
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 @@
extras = 4084,
bigPicture = 0,
extender = 0,
- style = "CallStyle",
+ style = NotificationEnums.STYLE_CALL,
styleIcon = personIcon.bitmap.allocationByteCount,
hasCustomView = false,
)
@@ -203,7 +224,7 @@
extras = 5024,
bigPicture = 0,
extender = 0,
- style = "MessagingStyle",
+ style = NotificationEnums.STYLE_MESSAGING,
styleIcon =
personIcon.bitmap.allocationByteCount +
historicPersonIcon.bitmap.allocationByteCount,
@@ -225,7 +246,7 @@
extras = 3612,
bigPicture = 0,
extender = 556656,
- style = null,
+ style = NotificationEnums.STYLE_NONE,
styleIcon = 0,
hasCustomView = false,
)
@@ -246,7 +267,7 @@
extras = 3820,
bigPicture = 0,
extender = 388 + wearBackground.allocationByteCount,
- style = null,
+ style = NotificationEnums.STYLE_NONE,
styleIcon = 0,
hasCustomView = false,
)
@@ -272,7 +293,7 @@
extras: Int,
bigPicture: Int,
extender: Int,
- style: String?,
+ style: Int,
styleIcon: Int,
hasCustomView: Boolean,
) {
@@ -282,11 +303,7 @@
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 3a16fb3..a0f5048 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.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 @@
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 @@
.build()
)
val result = NotificationMemoryViewWalker.getViewUsage(row)
- assertThat(result).hasSize(5)
+ assertThat(result).hasSize(3)
assertThat(result)
.contains(
NotificationViewUsage(
@@ -95,8 +164,20 @@
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 @@
.build()
)
val result = NotificationMemoryViewWalker.getViewUsage(row)
- assertThat(result).hasSize(5)
+ assertThat(result).hasSize(3)
assertThat(result)
.contains(
NotificationViewUsage(
@@ -142,7 +223,17 @@
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 4d9db8c..5832569 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 @@
val childHunView = createHunViewMock(
isShadeOpen = true,
fullyVisible = false,
- headerVisibleAmount = 1f
+ headerVisibleAmount = 1f,
)
val algorithmState = StackScrollAlgorithm.StackScrollAlgorithmState()
algorithmState.visibleChildren.add(childHunView)
@@ -526,7 +526,6 @@
// 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 @@
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 @@
// 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 @@
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 @@
// 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 @@
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 @@
// 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 @@
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 @@
// 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 @@
private fun createHunViewMock(
isShadeOpen: Boolean,
fullyVisible: Boolean,
- headerVisibleAmount: Float
+ headerVisibleAmount: Float,
) =
mock<ExpandableNotificationRow>().apply {
val childViewStateMock = createHunChildViewState(isShadeOpen, fullyVisible)
@@ -680,7 +675,10 @@
}
- 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/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt
index 3d9fd96..22c0ea1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt
@@ -16,11 +16,14 @@
package com.android.systemui.statusbar.pipeline.wifi.ui.view
+import android.content.res.ColorStateList
+import android.graphics.Rect
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.testing.TestableLooper.RunWithLooper
import android.testing.ViewUtils
import android.view.View
+import android.widget.ImageView
import androidx.test.filters.SmallTest
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
@@ -44,6 +47,7 @@
import com.android.systemui.statusbar.pipeline.wifi.shared.WifiConstants
import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel
import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel
+import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -229,10 +233,43 @@
ViewUtils.detachView(view)
}
+ @Test
+ fun onDarkChanged_iconHasNewColor() {
+ whenever(statusBarPipelineFlags.useWifiDebugColoring()).thenReturn(false)
+ val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel)
+ ViewUtils.attachView(view)
+ testableLooper.processAllMessages()
+
+ val areas = ArrayList(listOf(Rect(0, 0, 1000, 1000)))
+ val color = 0x12345678
+ view.onDarkChanged(areas, 1.0f, color)
+ testableLooper.processAllMessages()
+
+ assertThat(view.getIconView().imageTintList).isEqualTo(ColorStateList.valueOf(color))
+ }
+
+ @Test
+ fun setStaticDrawableColor_iconHasNewColor() {
+ whenever(statusBarPipelineFlags.useWifiDebugColoring()).thenReturn(false)
+ val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel)
+ ViewUtils.attachView(view)
+ testableLooper.processAllMessages()
+
+ val color = 0x23456789
+ view.setStaticDrawableColor(color)
+ testableLooper.processAllMessages()
+
+ assertThat(view.getIconView().imageTintList).isEqualTo(ColorStateList.valueOf(color))
+ }
+
private fun View.getIconGroupView(): View {
return this.requireViewById(R.id.wifi_group)
}
+ private fun View.getIconView(): ImageView {
+ return this.requireViewById(R.id.wifi_signal)
+ }
+
private fun View.getDotView(): View {
return this.requireViewById(R.id.status_bar_dot)
}
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 09f0d4a..82153d5 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.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 @@
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 @@
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 @@
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 @@
@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 @@
}
@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 @@
underTest.displayView(
ViewInfo(
name = "name",
+ id = "Second",
windowTitle = "Second Fake Window Title",
)
)
@@ -263,19 +267,69 @@
}
@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"))
+
+ verify(windowManager).removeView(any())
+ verify(windowManager).addView(any(), any())
+ reset(windowManager)
+
underTest.removeView("id2", "test reason")
verify(windowManager).removeView(any())
-
- fakeClock.advanceTime(DISPLAY_VIEW_DELAY + 1)
-
+ verify(windowManager).addView(any(), any())
assertThat(underTest.mostRecentViewInfo?.id).isEqualTo("id1")
assertThat(underTest.mostRecentViewInfo?.name).isEqualTo("First name")
@@ -284,6 +338,7 @@
verify(windowManager).removeView(any())
assertThat(underTest.activeViews.size).isEqualTo(0)
+ verify(configurationController).removeCallback(any())
}
@Test
@@ -291,19 +346,28 @@
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 @@
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 @@
underTest.displayView(ViewInfo("New 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)
+
underTest.removeView("id2", "test reason")
verify(windowManager).removeView(any())
-
- fakeClock.advanceTime(DISPLAY_VIEW_DELAY + 1)
-
+ 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 @@
}
@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 @@
verify(windowManager).removeView(any())
verify(logger).logViewRemoval(deviceId, reason)
+ verify(configurationController).removeCallback(any())
}
@Test
@@ -414,14 +984,15 @@
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 @@
powerManager,
R.layout.chipbar,
wakeLockBuilder,
+ systemClock,
) {
var mostRecentViewInfo: ViewInfo? = null
@@ -447,12 +1019,13 @@
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 116b8fe..2e66b20 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 @@
@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 @@
@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 7014f93..2e4d8e7 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.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 @@
viewUtil,
vibratorHelper,
fakeWakeLockBuilder,
+ fakeClock,
)
underTest.start()
}
@@ -408,6 +410,7 @@
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 beedf9f..d5167b3 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.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 @@
viewUtil: ViewUtil,
vibratorHelper: VibratorHelper,
wakeLockBuilder: WakeLock.Builder,
+ systemClock: SystemClock,
) :
ChipbarCoordinator(
context,
@@ -57,6 +59,7 @@
viewUtil,
vibratorHelper,
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/touch/TouchInsetManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/touch/TouchInsetManagerTest.java
index 14b9bfb..a707222 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/touch/TouchInsetManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/touch/TouchInsetManagerTest.java
@@ -26,8 +26,8 @@
import android.graphics.Rect;
import android.graphics.Region;
import android.testing.AndroidTestingRunner;
+import android.view.AttachedSurfaceControl;
import android.view.View;
-import android.view.ViewRootImpl;
import androidx.test.filters.SmallTest;
@@ -47,41 +47,78 @@
@RunWith(AndroidTestingRunner.class)
public class TouchInsetManagerTest extends SysuiTestCase {
@Mock
- private View mRootView;
-
- @Mock
- private ViewRootImpl mRootViewImpl;
+ private AttachedSurfaceControl mAttachedSurfaceControl;
private FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock());
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
- when(mRootView.getViewRootImpl()).thenReturn(mRootViewImpl);
}
@Test
- public void testRootViewOnAttachedHandling() {
+ public void testViewOnAttachedHandling() {
// Create inset manager
- final TouchInsetManager insetManager = new TouchInsetManager(mFakeExecutor,
- mRootView);
+ final TouchInsetManager insetManager = new TouchInsetManager(mFakeExecutor);
final ArgumentCaptor<View.OnAttachStateChangeListener> listener =
ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class);
+ final View view = createView(new Rect(0, 0, 0, 0));
+ when(view.isAttachedToWindow()).thenReturn(false);
+
+ // Create session
+ final TouchInsetManager.TouchInsetSession session = insetManager.createSession();
+ session.addViewToTracking(view);
+
+ mFakeExecutor.runAllReady();
// Ensure manager has registered to listen to attached state of root view.
- verify(mRootView).addOnAttachStateChangeListener(listener.capture());
+ verify(view).addOnAttachStateChangeListener(listener.capture());
+
+ clearInvocations(mAttachedSurfaceControl);
+ when(view.isAttachedToWindow()).thenReturn(true);
// Trigger attachment and verify touchable region is set.
- listener.getValue().onViewAttachedToWindow(mRootView);
- verify(mRootViewImpl).setTouchableRegion(any());
+ listener.getValue().onViewAttachedToWindow(view);
+
+ mFakeExecutor.runAllReady();
+
+ verify(mAttachedSurfaceControl).setTouchableRegion(any());
+ }
+
+ @Test
+ public void testViewOnDetachedHandling() {
+ // Create inset manager
+ final TouchInsetManager insetManager = new TouchInsetManager(mFakeExecutor);
+
+ final ArgumentCaptor<View.OnAttachStateChangeListener> listener =
+ ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class);
+ final View view = createView(new Rect(0, 0, 0, 0));
+ when(view.isAttachedToWindow()).thenReturn(true);
+
+ // Create session
+ final TouchInsetManager.TouchInsetSession session = insetManager.createSession();
+ session.addViewToTracking(view);
+
+ mFakeExecutor.runAllReady();
+ // Ensure manager has registered to listen to attached state of root view.
+ verify(view).addOnAttachStateChangeListener(listener.capture());
+
+ clearInvocations(mAttachedSurfaceControl);
+ when(view.isAttachedToWindow()).thenReturn(false);
+
+ // Trigger detachment and verify touchable region is set.
+ listener.getValue().onViewDetachedFromWindow(view);
+
+ mFakeExecutor.runAllReady();
+
+ verify(mAttachedSurfaceControl).setTouchableRegion(any());
}
@Test
public void testInsetRegionPropagation() {
// Create inset manager
- final TouchInsetManager insetManager = new TouchInsetManager(mFakeExecutor,
- mRootView);
+ final TouchInsetManager insetManager = new TouchInsetManager(mFakeExecutor);
// Create session
final TouchInsetManager.TouchInsetSession session = insetManager.createSession();
@@ -95,14 +132,13 @@
// Check to see if view was properly accounted for.
final Region expectedRegion = Region.obtain();
expectedRegion.op(rect, Region.Op.UNION);
- verify(mRootViewImpl).setTouchableRegion(eq(expectedRegion));
+ verify(mAttachedSurfaceControl).setTouchableRegion(eq(expectedRegion));
}
@Test
public void testMultipleRegions() {
// Create inset manager
- final TouchInsetManager insetManager = new TouchInsetManager(mFakeExecutor,
- mRootView);
+ final TouchInsetManager insetManager = new TouchInsetManager(mFakeExecutor);
// Create session
final TouchInsetManager.TouchInsetSession session = insetManager.createSession();
@@ -112,7 +148,7 @@
session.addViewToTracking(createView(firstBounds));
mFakeExecutor.runAllReady();
- clearInvocations(mRootViewImpl);
+ clearInvocations(mAttachedSurfaceControl);
// Create second session
final TouchInsetManager.TouchInsetSession secondSession = insetManager.createSession();
@@ -128,27 +164,26 @@
final Region expectedRegion = Region.obtain();
expectedRegion.op(firstBounds, Region.Op.UNION);
expectedRegion.op(secondBounds, Region.Op.UNION);
- verify(mRootViewImpl).setTouchableRegion(eq(expectedRegion));
+ verify(mAttachedSurfaceControl).setTouchableRegion(eq(expectedRegion));
}
- clearInvocations(mRootViewImpl);
+ clearInvocations(mAttachedSurfaceControl);
// clear first session, ensure second session is still reflected.
session.clear();
mFakeExecutor.runAllReady();
{
final Region expectedRegion = Region.obtain();
- expectedRegion.op(firstBounds, Region.Op.UNION);
- verify(mRootViewImpl).setTouchableRegion(eq(expectedRegion));
+ expectedRegion.op(secondBounds, Region.Op.UNION);
+ verify(mAttachedSurfaceControl).setTouchableRegion(eq(expectedRegion));
}
}
@Test
public void testMultipleViews() {
// Create inset manager
- final TouchInsetManager insetManager = new TouchInsetManager(mFakeExecutor,
- mRootView);
+ final TouchInsetManager insetManager = new TouchInsetManager(mFakeExecutor);
// Create session
final TouchInsetManager.TouchInsetSession session = insetManager.createSession();
@@ -159,7 +194,7 @@
// only capture second invocation.
mFakeExecutor.runAllReady();
- clearInvocations(mRootViewImpl);
+ clearInvocations(mAttachedSurfaceControl);
// Add a second view to the session
final Rect secondViewBounds = new Rect(4, 4, 9, 10);
@@ -173,20 +208,20 @@
final Region expectedRegion = Region.obtain();
expectedRegion.op(firstViewBounds, Region.Op.UNION);
expectedRegion.op(secondViewBounds, Region.Op.UNION);
- verify(mRootViewImpl).setTouchableRegion(eq(expectedRegion));
+ verify(mAttachedSurfaceControl).setTouchableRegion(eq(expectedRegion));
}
// Remove second view.
session.removeViewFromTracking(secondView);
- clearInvocations(mRootViewImpl);
+ clearInvocations(mAttachedSurfaceControl);
mFakeExecutor.runAllReady();
// Ensure first view still reflected in touch region.
{
final Region expectedRegion = Region.obtain();
expectedRegion.op(firstViewBounds, Region.Op.UNION);
- verify(mRootViewImpl).setTouchableRegion(eq(expectedRegion));
+ verify(mAttachedSurfaceControl).setTouchableRegion(eq(expectedRegion));
}
}
@@ -197,6 +232,8 @@
((Rect) invocation.getArgument(0)).set(rect);
return null;
}).when(view).getBoundsOnScreen(any());
+ when(view.isAttachedToWindow()).thenReturn(true);
+ when(view.getRootSurfaceControl()).thenReturn(mAttachedSurfaceControl);
return view;
}
diff --git a/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java b/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java
index 2ad2119..dd9f1d8 100644
--- a/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java
+++ b/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java
@@ -48,6 +48,7 @@
import android.hardware.camera2.extension.IRequestProcessorImpl;
import android.hardware.camera2.extension.IRequestUpdateProcessorImpl;
import android.hardware.camera2.extension.ISessionProcessorImpl;
+import android.hardware.camera2.extension.LatencyPair;
import android.hardware.camera2.extension.LatencyRange;
import android.hardware.camera2.extension.OutputConfigId;
import android.hardware.camera2.extension.OutputSurface;
@@ -1266,6 +1267,21 @@
public int startCapture(ICaptureCallback callback) {
return mSessionProcessor.startCapture(new CaptureCallbackStub(callback, mCameraId));
}
+
+ @Override
+ public LatencyPair getRealtimeCaptureLatency() {
+ if (LATENCY_IMPROVEMENTS_SUPPORTED) {
+ Pair<Long, Long> latency = mSessionProcessor.getRealtimeCaptureLatency();
+ if (latency != null) {
+ LatencyPair ret = new LatencyPair();
+ ret.first = latency.first;
+ ret.second = latency.second;
+ return ret;
+ }
+ }
+
+ return null;
+ }
}
private class OutputSurfaceConfigurationImplStub implements OutputSurfaceConfigurationImpl {
@@ -1578,6 +1594,21 @@
}
@Override
+ public LatencyPair getRealtimeCaptureLatency() {
+ if (LATENCY_IMPROVEMENTS_SUPPORTED) {
+ Pair<Long, Long> latency = mImageExtender.getRealtimeCaptureLatency();
+ if (latency != null) {
+ LatencyPair ret = new LatencyPair();
+ ret.first = latency.first;
+ ret.second = latency.second;
+ return ret;
+ }
+ }
+
+ return null;
+ }
+
+ @Override
public CameraMetadataNative getAvailableCaptureRequestKeys() {
if (RESULT_API_SUPPORTED) {
List<CaptureRequest.Key> supportedCaptureKeys =
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 3145139..59c1c54 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_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;
@@ -1018,10 +1021,13 @@
}
private void dispatchAccessibilityEventLocked(AccessibilityEvent event) {
- notifyAccessibilityServicesDelayedLocked(event, false);
- notifyAccessibilityServicesDelayedLocked(event, true);
+ if (mProxyManager.isProxyed(event.getDisplayId())) {
+ mProxyManager.sendAccessibilityEventLocked(event);
+ } else {
+ notifyAccessibilityServicesDelayedLocked(event, false);
+ notifyAccessibilityServicesDelayedLocked(event, true);
+ }
mUiAutomationManager.sendAccessibilityEventLocked(event);
- mProxyManager.sendAccessibilityEvent(event);
}
private void sendAccessibilityEventToInputFilter(AccessibilityEvent event) {
@@ -1162,7 +1168,7 @@
}
List<AccessibilityServiceConnection> services =
getUserStateLocked(resolvedUserId).mBoundServices;
- int numServices = services.size() + mProxyManager.getNumProxys();
+ int numServices = services.size() + mProxyManager.getNumProxysLocked();
interfacesToInterrupt = new ArrayList<>(numServices);
for (int i = 0; i < services.size(); i++) {
AccessibilityServiceConnection service = services.get(i);
@@ -1172,7 +1178,7 @@
interfacesToInterrupt.add(a11yServiceInterface);
}
}
- mProxyManager.addServiceInterfaces(interfacesToInterrupt);
+ mProxyManager.addServiceInterfacesLocked(interfacesToInterrupt);
}
for (int i = 0, count = interfacesToInterrupt.size(); i < count; i++) {
try {
@@ -1899,6 +1905,16 @@
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.
@@ -1963,7 +1979,7 @@
mUiAutomationManager.getServiceInfo(), client)
? mUiAutomationManager.getRelevantEventTypes()
: 0;
- relevantEventTypes |= mProxyManager.getRelevantEventTypes();
+ relevantEventTypes |= mProxyManager.getRelevantEventTypesLocked();
return relevantEventTypes;
}
@@ -2565,6 +2581,7 @@
somethingChanged |= readMagnificationModeForDefaultDisplayLocked(userState);
somethingChanged |= readMagnificationCapabilitiesLocked(userState);
somethingChanged |= readMagnificationFollowTypingLocked(userState);
+ somethingChanged |= readUiContrastLocked(userState);
return somethingChanged;
}
@@ -3706,6 +3723,19 @@
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;
@@ -4153,6 +4183,9 @@
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);
}
@@ -4193,6 +4226,8 @@
mMagnificationCapabilityUri, false, this, UserHandle.USER_ALL);
contentResolver.registerContentObserver(
mMagnificationFollowTypingUri, false, this, UserHandle.USER_ALL);
+ contentResolver.registerContentObserver(
+ mUiContrastUri, false, this, UserHandle.USER_ALL);
}
@Override
@@ -4262,6 +4297,10 @@
}
} else if (mMagnificationFollowTypingUri.equals(uri)) {
readMagnificationFollowTypingLocked(userState);
+ } else if (mUiContrastUri.equals(uri)) {
+ if (readUiContrastLocked(userState)) {
+ updateUiContrastLocked(userState);
+ }
}
}
}
@@ -4551,7 +4590,22 @@
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 0db169f..43730fc 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_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 @@
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 @@
mFocusStrokeWidth = mFocusStrokeWidthDefaultValue;
mFocusColor = mFocusColorDefaultValue;
mMagnificationFollowTypingEnabled = true;
+ mUiContrast = CONTRAST_NOT_SET;
}
void addServiceLocked(AccessibilityServiceConnection serviceConnection) {
@@ -983,6 +988,7 @@
return mFocusColor;
}
+
/**
* Sets the stroke width and color of the focus rectangle.
*
@@ -1008,4 +1014,20 @@
}
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/accessibility/java/com/android/server/accessibility/ProxyManager.java b/services/accessibility/java/com/android/server/accessibility/ProxyManager.java
index 2184878..f28191f 100644
--- a/services/accessibility/java/com/android/server/accessibility/ProxyManager.java
+++ b/services/accessibility/java/com/android/server/accessibility/ProxyManager.java
@@ -37,6 +37,7 @@
* proxy connection will belong to a separate user state.
*
* TODO(241117292): Remove or cut down during simultaneous user refactoring.
+ * TODO(262244375): Add unit tests.
*/
public class ProxyManager {
// Names used to populate ComponentName and ResolveInfo in connection.mA11yServiceInfo and in
@@ -84,7 +85,9 @@
windowManagerInternal,
awm, displayId);
- mProxyA11yServiceConnections.put(displayId, connection);
+ synchronized (mLock) {
+ mProxyA11yServiceConnections.put(displayId, connection);
+ }
// If the client dies, make sure to remove the connection.
IBinder.DeathRecipient deathRecipient =
@@ -98,7 +101,9 @@
client.asBinder().linkToDeath(deathRecipient, 0);
// Notify apps that the service state has changed.
// A11yManager#A11yServicesStateChangeListener
- connection.mSystemSupport.onClientChangeLocked(true);
+ synchronized (mLock) {
+ connection.mSystemSupport.onClientChangeLocked(true);
+ }
connection.initializeServiceInterface(client);
}
@@ -111,9 +116,11 @@
}
private boolean clearConnection(int displayId) {
- if (mProxyA11yServiceConnections.contains(displayId)) {
- mProxyA11yServiceConnections.remove(displayId);
- return true;
+ synchronized (mLock) {
+ if (mProxyA11yServiceConnections.contains(displayId)) {
+ mProxyA11yServiceConnections.remove(displayId);
+ return true;
+ }
}
return false;
}
@@ -122,7 +129,9 @@
* Checks if a display id is being proxy-ed.
*/
public boolean isProxyed(int displayId) {
- return mProxyA11yServiceConnections.contains(displayId);
+ synchronized (mLock) {
+ return mProxyA11yServiceConnections.contains(displayId);
+ }
}
/**
@@ -130,10 +139,10 @@
* {@link android.view.accessibility.AccessibilityDisplayProxy} will filter based on display.
* TODO(b/250929565): Filtering should happen in the system, not in the proxy.
*/
- public void sendAccessibilityEvent(AccessibilityEvent event) {
- for (int i = 0; i < mProxyA11yServiceConnections.size(); i++) {
- ProxyAccessibilityServiceConnection proxy =
- mProxyA11yServiceConnections.valueAt(i);
+ public void sendAccessibilityEventLocked(AccessibilityEvent event) {
+ final ProxyAccessibilityServiceConnection proxy =
+ mProxyA11yServiceConnections.get(event.getDisplayId());
+ if (proxy != null) {
proxy.notifyAccessibilityEvent(event);
}
}
@@ -187,7 +196,7 @@
* Returns the relevant event types of every proxy.
* TODO(254545943): When A11yManager is separated, return based on the A11yManager display.
*/
- public int getRelevantEventTypes() {
+ public int getRelevantEventTypesLocked() {
int relevantEventTypes = 0;
for (int i = 0; i < mProxyA11yServiceConnections.size(); i++) {
ProxyAccessibilityServiceConnection proxy =
@@ -201,7 +210,7 @@
* Gets the number of current proxy connections.
* @return
*/
- public int getNumProxys() {
+ public int getNumProxysLocked() {
return mProxyA11yServiceConnections.size();
}
@@ -209,7 +218,7 @@
* Adds the service interfaces to a list.
* @param interfaces
*/
- public void addServiceInterfaces(List<IAccessibilityServiceClient> interfaces) {
+ public void addServiceInterfacesLocked(List<IAccessibilityServiceClient> interfaces) {
for (int i = 0; i < mProxyA11yServiceConnections.size(); i++) {
final ProxyAccessibilityServiceConnection proxy =
mProxyA11yServiceConnections.valueAt(i);
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 0cea3d0..97b5d6d 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 @@
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 @@
}
}
+ 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 @@
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 @@
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 5819861..12ad9f1 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.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 @@
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 @@
IBinder token,
int ownerUid,
int deviceId,
- OnDeviceCloseListener listener,
+ OnDeviceCloseListener onDeviceCloseListener,
PendingTrampolineCallback pendingTrampolineCallback,
IVirtualDeviceActivityListener activityListener,
Consumer<ArraySet<Integer>> runningAppsChangedCallback,
@@ -168,7 +169,7 @@
deviceId,
/* inputController= */ null,
/* sensorController= */ null,
- listener,
+ onDeviceCloseListener,
pendingTrampolineCallback,
activityListener,
runningAppsChangedCallback,
@@ -184,7 +185,7 @@
int deviceId,
InputController inputController,
SensorController sensorController,
- OnDeviceCloseListener listener,
+ OnDeviceCloseListener onDeviceCloseListener,
PendingTrampolineCallback pendingTrampolineCallback,
IVirtualDeviceActivityListener activityListener,
Consumer<ArraySet<Integer>> runningAppsChangedCallback,
@@ -212,7 +213,7 @@
} else {
mSensorController = sensorController;
}
- mListener = listener;
+ mOnDeviceCloseListener = onDeviceCloseListener;
try {
token.linkToDeath(this, 0);
} catch (RemoteException e) {
@@ -330,7 +331,7 @@
mVirtualAudioController = null;
}
}
- mListener.onClose(mAssociationInfo.getId());
+ mOnDeviceCloseListener.onClose(mDeviceId);
mAppToken.unlinkToDeath(this, 0);
final long ident = Binder.clearCallingIdentity();
@@ -491,6 +492,38 @@
}
@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 @@
@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 @@
}
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 e092f49..da2c516 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
@@ -41,6 +41,7 @@
import android.os.IBinder;
import android.os.Looper;
import android.os.Parcel;
+import android.os.Process;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.ArraySet;
@@ -62,6 +63,7 @@
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
+import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
@@ -88,14 +90,20 @@
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<>();
@@ -158,7 +166,7 @@
@GuardedBy("mVirtualDeviceManagerLock")
private boolean isValidVirtualDeviceLocked(IVirtualDevice virtualDevice) {
try {
- return mVirtualDevices.contains(virtualDevice.getAssociationId());
+ return mVirtualDevices.contains(virtualDevice.getDeviceId());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -228,9 +236,9 @@
}
@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();
}
@@ -238,7 +246,7 @@
@VisibleForTesting
void addVirtualDevice(VirtualDeviceImpl virtualDevice) {
synchronized (mVirtualDeviceManagerLock) {
- mVirtualDevices.put(virtualDevice.getAssociationId(), virtualDevice);
+ mVirtualDevices.put(virtualDevice.getDeviceId(), virtualDevice);
}
}
@@ -266,60 +274,26 @@
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;
}
}
@@ -336,7 +310,7 @@
}
VirtualDeviceImpl virtualDeviceImpl;
synchronized (mVirtualDeviceManagerLock) {
- virtualDeviceImpl = mVirtualDevices.get(virtualDevice.getAssociationId());
+ virtualDeviceImpl = mVirtualDevices.get(virtualDevice.getDeviceId());
if (virtualDeviceImpl == null) {
throw new SecurityException("Invalid VirtualDevice");
}
@@ -417,8 +391,7 @@
@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++) {
@@ -434,6 +407,29 @@
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 {
@@ -495,6 +491,35 @@
}
@Override
+ public int getDeviceOwnerUid(int deviceId) {
+ synchronized (mVirtualDeviceManagerLock) {
+ int size = mVirtualDevices.size();
+ for (int i = 0; i < size; i++) {
+ VirtualDeviceImpl device = mVirtualDevices.valueAt(i);
+ if (device.getDeviceId() == deviceId) {
+ return device.getOwnerUid();
+ }
+ }
+ }
+ return Process.INVALID_UID;
+ }
+
+ @Override
+ public @NonNull Set<Integer> getDeviceIdsForUid(int uid) {
+ ArraySet<Integer> result = new ArraySet<>();
+ synchronized (mVirtualDeviceManagerLock) {
+ int size = mVirtualDevices.size();
+ for (int i = 0; i < size; i++) {
+ VirtualDeviceImpl device = mVirtualDevices.valueAt(i);
+ if (device.isAppRunningOnVirtualDevice(uid)) {
+ result.add(device.getDeviceId());
+ }
+ }
+ }
+ return result;
+ }
+
+ @Override
public void onVirtualDisplayCreated(int displayId) {
final VirtualDisplayListener[] listeners;
synchronized (mVirtualDeviceManagerLock) {
diff --git a/services/core/java/com/android/server/BinaryTransparencyService.java b/services/core/java/com/android/server/BinaryTransparencyService.java
index a79aa0b..17002d5 100644
--- a/services/core/java/com/android/server/BinaryTransparencyService.java
+++ b/services/core/java/com/android/server/BinaryTransparencyService.java
@@ -119,7 +119,7 @@
// used for indicating newly installed MBAs that are updated (but unused currently)
static final int MBA_STATUS_UPDATED_NEW_INSTALL = 4;
- private static final boolean DEBUG = true; // set this to false upon submission
+ private static final boolean DEBUG = false; // toggle this for local debug
private final Context mContext;
private String mVbmetaDigest;
diff --git a/services/core/java/com/android/server/PersistentDataBlockService.java b/services/core/java/com/android/server/PersistentDataBlockService.java
index b2fc574..5eb0db1 100644
--- a/services/core/java/com/android/server/PersistentDataBlockService.java
+++ b/services/core/java/com/android/server/PersistentDataBlockService.java
@@ -36,6 +36,7 @@
import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.DumpUtils;
+import com.android.server.pm.UserManagerInternal;
import libcore.io.IoUtils;
@@ -156,14 +157,19 @@
mBlockDeviceSize = -1; // Load lazily
}
- private int getAllowedUid(int userHandle) {
+ private int getAllowedUid() {
+ final UserManagerInternal umInternal = LocalServices.getService(UserManagerInternal.class);
+ final int mainUserId = umInternal.getMainUserId();
+ if (mainUserId < 0) {
+ return -1;
+ }
String allowedPackage = mContext.getResources()
.getString(R.string.config_persistentDataPackageName);
int allowedUid = -1;
if (!TextUtils.isEmpty(allowedPackage)) {
try {
allowedUid = mContext.getPackageManager().getPackageUidAsUser(
- allowedPackage, PackageManager.MATCH_SYSTEM_ONLY, userHandle);
+ allowedPackage, PackageManager.MATCH_SYSTEM_ONLY, mainUserId);
} catch (PackageManager.NameNotFoundException e) {
// not expected
Slog.e(TAG, "not able to find package " + allowedPackage, e);
@@ -176,7 +182,7 @@
public void onStart() {
// Do init on a separate thread, will join in PHASE_ACTIVITY_MANAGER_READY
SystemServerInitThreadPool.submit(() -> {
- mAllowedUid = getAllowedUid(UserHandle.USER_SYSTEM);
+ mAllowedUid = getAllowedUid();
enforceChecksumValidity();
formatIfOemUnlockEnabled();
publishBinderService(Context.PERSISTENT_DATA_BLOCK_SERVICE, mService);
diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java
index 32afcca..ee922f9 100644
--- a/services/core/java/com/android/server/TelephonyRegistry.java
+++ b/services/core/java/com/android/server/TelephonyRegistry.java
@@ -92,6 +92,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.IBatteryStats;
+import com.android.internal.telephony.ICarrierConfigChangeListener;
import com.android.internal.telephony.ICarrierPrivilegesCallback;
import com.android.internal.telephony.IOnSubscriptionsChangedListener;
import com.android.internal.telephony.IPhoneStateListener;
@@ -155,6 +156,7 @@
IOnSubscriptionsChangedListener onSubscriptionsChangedListenerCallback;
IOnSubscriptionsChangedListener onOpportunisticSubscriptionsChangedListenerCallback;
ICarrierPrivilegesCallback carrierPrivilegesCallback;
+ ICarrierConfigChangeListener carrierConfigChangeListener;
int callerUid;
int callerPid;
@@ -183,6 +185,10 @@
return carrierPrivilegesCallback != null;
}
+ boolean matchCarrierConfigChangeListener() {
+ return carrierConfigChangeListener != null;
+ }
+
boolean canReadCallLog() {
try {
return TelephonyPermissions.checkReadCallLog(
@@ -201,6 +207,7 @@
+ " onOpportunisticSubscriptionsChangedListenererCallback="
+ onOpportunisticSubscriptionsChangedListenerCallback
+ " carrierPrivilegesCallback=" + carrierPrivilegesCallback
+ + " carrierConfigChangeListener=" + carrierConfigChangeListener
+ " subId=" + subId + " phoneId=" + phoneId + " events=" + eventList + "}";
}
}
@@ -3044,6 +3051,82 @@
}
}
+ @Override
+ public void addCarrierConfigChangeListener(ICarrierConfigChangeListener listener,
+ String pkg, String featureId) {
+ final int callerUserId = UserHandle.getCallingUserId();
+ mAppOps.checkPackage(Binder.getCallingUid(), pkg);
+ if (VDBG) {
+ log("addCarrierConfigChangeListener pkg=" + pii(pkg) + " uid=" + Binder.getCallingUid()
+ + " myUserId=" + UserHandle.myUserId() + " callerUerId" + callerUserId
+ + " listener=" + listener + " listener.asBinder=" + listener.asBinder());
+ }
+
+ synchronized (mRecords) {
+ IBinder b = listener.asBinder();
+ boolean doesLimitApply = doesLimitApplyForListeners(Binder.getCallingUid(),
+ Process.myUid());
+ Record r = add(b, Binder.getCallingUid(), Binder.getCallingPid(), doesLimitApply);
+
+ if (r == null) {
+ loge("Can not create Record instance!");
+ return;
+ }
+
+ r.context = mContext;
+ r.carrierConfigChangeListener = listener;
+ r.callingPackage = pkg;
+ r.callingFeatureId = featureId;
+ r.callerUid = Binder.getCallingUid();
+ r.callerPid = Binder.getCallingPid();
+ r.eventList = new ArraySet<>();
+ if (DBG) {
+ log("addCarrierConfigChangeListener: Register r=" + r);
+ }
+ }
+ }
+
+ @Override
+ public void removeCarrierConfigChangeListener(ICarrierConfigChangeListener listener,
+ String pkg) {
+ if (DBG) log("removeCarrierConfigChangeListener listener=" + listener + ", pkg=" + pkg);
+ mAppOps.checkPackage(Binder.getCallingUid(), pkg);
+ remove(listener.asBinder());
+ }
+
+ @Override
+ public void notifyCarrierConfigChanged(int phoneId, int subId, int carrierId,
+ int specificCarrierId) {
+ if (!validatePhoneId(phoneId)) {
+ throw new IllegalArgumentException("Invalid phoneId: " + phoneId);
+ }
+ if (!checkNotifyPermission("notifyCarrierConfigChanged")) {
+ loge("Caller has no notify permission!");
+ return;
+ }
+ if (VDBG) {
+ log("notifyCarrierConfigChanged: phoneId=" + phoneId + ", subId=" + subId
+ + ", carrierId=" + carrierId + ", specificCarrierId=" + specificCarrierId);
+ }
+
+ synchronized (mRecords) {
+ mRemoveList.clear();
+ for (Record r : mRecords) {
+ // Listeners are "global", neither per-slot nor per-sub, so no idMatch check here
+ if (!r.matchCarrierConfigChangeListener()) {
+ continue;
+ }
+ try {
+ r.carrierConfigChangeListener.onCarrierConfigChanged(phoneId, subId, carrierId,
+ specificCarrierId);
+ } catch (RemoteException re) {
+ mRemoveList.add(r.binder);
+ }
+ }
+ handleRemoveListLocked();
+ }
+ }
+
@NeverCompile // Avoid size overhead of debugging code.
@Override
public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
diff --git a/services/core/java/com/android/server/VcnManagementService.java b/services/core/java/com/android/server/VcnManagementService.java
index 61f7f30..f652cb0 100644
--- a/services/core/java/com/android/server/VcnManagementService.java
+++ b/services/core/java/com/android/server/VcnManagementService.java
@@ -371,8 +371,9 @@
return new LocationPermissionChecker(context);
}
- /** Gets the transports that need to be marked as restricted by the VCN */
- public Set<Integer> getRestrictedTransports(
+ /** Gets transports that need to be marked as restricted by the VCN from CarrierConfig */
+ @VisibleForTesting(visibility = Visibility.PRIVATE)
+ public Set<Integer> getRestrictedTransportsFromCarrierConfig(
ParcelUuid subGrp, TelephonySubscriptionSnapshot lastSnapshot) {
if (!Build.IS_ENG && !Build.IS_USERDEBUG) {
return RESTRICTED_TRANSPORTS_DEFAULT;
@@ -398,6 +399,22 @@
}
return restrictedTransports;
}
+
+ /** Gets the transports that need to be marked as restricted by the VCN */
+ public Set<Integer> getRestrictedTransports(
+ ParcelUuid subGrp,
+ TelephonySubscriptionSnapshot lastSnapshot,
+ VcnConfig vcnConfig) {
+ final Set<Integer> restrictedTransports = new ArraySet<>();
+ restrictedTransports.addAll(vcnConfig.getRestrictedUnderlyingNetworkTransports());
+
+ // TODO: b/262269892 Remove the ability to configure restricted transports
+ // via CarrierConfig
+ restrictedTransports.addAll(
+ getRestrictedTransportsFromCarrierConfig(subGrp, lastSnapshot));
+
+ return restrictedTransports;
+ }
}
/** Notifies the VcnManagementService that external dependencies can be set up. */
@@ -719,6 +736,7 @@
if (mVcns.containsKey(subscriptionGroup)) {
final Vcn vcn = mVcns.get(subscriptionGroup);
vcn.updateConfig(config);
+ notifyAllPolicyListenersLocked();
} else {
// TODO(b/193687515): Support multiple VCNs active at the same time
if (isActiveSubGroup(subscriptionGroup, mLastSnapshot)) {
@@ -936,7 +954,6 @@
}
/** Adds the provided listener for receiving VcnUnderlyingNetworkPolicy updates. */
- @GuardedBy("mLock")
@Override
public void addVcnUnderlyingNetworkPolicyListener(
@NonNull IVcnUnderlyingNetworkPolicyListener listener) {
@@ -963,16 +980,7 @@
});
}
- @VisibleForTesting(visibility = Visibility.PRIVATE)
- void addVcnUnderlyingNetworkPolicyListenerForTest(
- @NonNull IVcnUnderlyingNetworkPolicyListener listener) {
- synchronized (mLock) {
- addVcnUnderlyingNetworkPolicyListener(listener);
- }
- }
-
/** Removes the provided listener from receiving VcnUnderlyingNetworkPolicy updates. */
- @GuardedBy("mLock")
@Override
public void removeVcnUnderlyingNetworkPolicyListener(
@NonNull IVcnUnderlyingNetworkPolicyListener listener) {
@@ -1062,8 +1070,8 @@
isVcnManagedNetwork = true;
}
- final Set<Integer> restrictedTransports =
- mDeps.getRestrictedTransports(subGrp, mLastSnapshot);
+ final Set<Integer> restrictedTransports = mDeps.getRestrictedTransports(
+ subGrp, mLastSnapshot, mConfigs.get(subGrp));
for (int restrictedTransport : restrictedTransports) {
if (ncCopy.hasTransport(restrictedTransport)) {
if (restrictedTransport == TRANSPORT_CELLULAR) {
diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java
index a2755be..88492ed 100644
--- a/services/core/java/com/android/server/accounts/AccountManagerService.java
+++ b/services/core/java/com/android/server/accounts/AccountManagerService.java
@@ -3558,8 +3558,7 @@
Bundle.setDefusable(result, true);
mNumResults++;
Intent intent = null;
- if (result != null
- && (intent = result.getParcelable(AccountManager.KEY_INTENT, android.content.Intent.class)) != null) {
+ if (result != null) {
if (!checkKeyIntent(
Binder.getCallingUid(),
result)) {
@@ -4928,8 +4927,10 @@
EventLog.writeEvent(0x534e4554, "250588548", authUid, "");
return false;
}
-
Intent intent = bundle.getParcelable(AccountManager.KEY_INTENT, Intent.class);
+ if (intent == null) {
+ return true;
+ }
// Explicitly set an empty ClipData to ensure that we don't offer to
// promote any Uris contained inside for granting purposes
if (intent.getClipData() == null) {
@@ -4979,8 +4980,12 @@
Bundle simulateBundle = p.readBundle();
p.recycle();
Intent intent = bundle.getParcelable(AccountManager.KEY_INTENT, Intent.class);
- return (intent.filterEquals(simulateBundle.getParcelable(AccountManager.KEY_INTENT,
- Intent.class)));
+ Intent simulateIntent = simulateBundle.getParcelable(AccountManager.KEY_INTENT,
+ Intent.class);
+ if (intent == null) {
+ return (simulateIntent == null);
+ }
+ return intent.filterEquals(simulateIntent);
}
private boolean isExportedSystemActivity(ActivityInfo activityInfo) {
@@ -5129,8 +5134,7 @@
}
}
}
- if (result != null
- && (intent = result.getParcelable(AccountManager.KEY_INTENT, android.content.Intent.class)) != null) {
+ if (result != null) {
if (!checkKeyIntent(
Binder.getCallingUid(),
result)) {
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 5a27af0..045c757 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -7438,11 +7438,10 @@
if (shareDescription != null) {
triggerShellBugreport.putExtra(EXTRA_DESCRIPTION, shareDescription);
}
- UserHandle callingUser = Binder.getCallingUserHandle();
final long identity = Binder.clearCallingIdentity();
try {
// Send broadcast to shell to trigger bugreport using Bugreport API
- mContext.sendBroadcastAsUser(triggerShellBugreport, callingUser);
+ mContext.sendBroadcastAsUser(triggerShellBugreport, UserHandle.SYSTEM);
} finally {
Binder.restoreCallingIdentity(identity);
}
@@ -13578,9 +13577,10 @@
// 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/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
index 94c15ba..4a6e5a3 100644
--- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
+++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
@@ -1413,6 +1413,7 @@
mPw.println("shortMsg: " + shortMsg);
mPw.println("longMsg: " + longMsg);
mPw.println("timeMillis: " + timeMillis);
+ mPw.println("uptime: " + SystemClock.uptimeMillis());
mPw.println("stack:");
mPw.print(stackTrace);
mPw.println("#");
@@ -1429,6 +1430,7 @@
mPw.println("processName: " + processName);
mPw.println("processPid: " + pid);
mPw.println("annotation: " + annotation);
+ mPw.println("uptime: " + SystemClock.uptimeMillis());
mPw.flush();
int result = waitControllerLocked(pid, STATE_EARLY_ANR);
if (result == RESULT_EARLY_ANR_KILL) return -1;
@@ -1442,6 +1444,7 @@
mPw.println("** ERROR: PROCESS NOT RESPONDING");
mPw.println("processName: " + processName);
mPw.println("processPid: " + pid);
+ mPw.println("uptime: " + SystemClock.uptimeMillis());
mPw.println("processStats:");
mPw.print(processStats);
mPw.println("#");
diff --git a/services/core/java/com/android/server/am/BroadcastProcessQueue.java b/services/core/java/com/android/server/am/BroadcastProcessQueue.java
index 7013df1..fceefad 100644
--- a/services/core/java/com/android/server/am/BroadcastProcessQueue.java
+++ b/services/core/java/com/android/server/am/BroadcastProcessQueue.java
@@ -532,15 +532,13 @@
}
public void traceActiveBegin() {
- final int cookie = mActive.receivers.get(mActiveIndex).hashCode();
Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
- runningTraceTrackName, mActive.toShortString() + " scheduled", cookie);
+ runningTraceTrackName, mActive.toShortString() + " scheduled", hashCode());
}
public void traceActiveEnd() {
- final int cookie = mActive.receivers.get(mActiveIndex).hashCode();
Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER,
- runningTraceTrackName, cookie);
+ runningTraceTrackName, hashCode());
}
/**
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index 280256f..e5123ef 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 @@
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 587fb04..ac25f4e 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.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 @@
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 @@
}
@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 @@
}
@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 @@
}
@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 ef3e368..d8d0d48 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 @@
* @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 @@
* @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 @@
* @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 @@
* @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 4436002..0000000
--- 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 af5b07e..f51200f2 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 @@
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 @@
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 @@
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 39338c6..9345422 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 @@
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.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.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.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.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 @@
*/
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 @@
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 @@
/** 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 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 @@
} 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 @@
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 @@
}
}
});
+
+ mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
}
/**
@@ -322,18 +1210,132 @@
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 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 @@
*/
@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 @@
@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 @@
}
}
- /**
- * Sets the appops {@link CheckOpsDelegate}
- */
public void setAppOpsServiceDelegate(CheckOpsDelegate delegate) {
synchronized (AppOpsService.this) {
final CheckOpsDelegateDispatcher oldDispatcher = mCheckOpsDelegateDispatcher;
@@ -467,7 +2294,58 @@
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 @@
@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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
int uid = Binder.getCallingUid();
- mAppOpsService.verifyPackage(uid, packageName);
+ verifyAndGetBypass(uid, packageName, null);
synchronized (this) {
return mUnforwardedAsyncNotedOps.remove(getAsyncNotedOpsKey(packageName, uid));
@@ -844,49 +2914,54 @@
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,68 +3009,147 @@
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);
-
- boolean isProxyAttributionTagValid = mAppOpsService.isAttributionTagValid(proxyUid,
- resolvedProxyPackageName, proxyAttributionTag, null);
-
- if (!shouldStartForMode(proxyAppOp, startIfModeDefault)) {
- return new SyncNotedAppOp(proxyAppOp, code,
- isProxyAttributionTagValid ? proxyAttributionTag : null,
- resolvedProxyPackageName);
- }
-
- if (shouldCollectAsyncNotedOp) {
- collectAsyncNotedOp(proxyUid, resolvedProxyPackageName, code,
- isProxyAttributionTagValid ? proxyAttributionTag : null, proxyFlags,
- message, shouldCollectMessage);
+ if (!shouldStartForMode(proxyAppOp.getOpMode(), startIfModeDefault)) {
+ return proxyAppOp;
}
}
- 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);
-
- if (shouldCollectAsyncNotedOp && proxiedAppOp == MODE_ALLOWED) {
- collectAsyncNotedOp(proxyUid, resolvedProxiedPackageName, code,
- isProxiedAttributionTagValid ? proxiedAttributionTag : null,
- proxiedAttributionFlags, message, shouldCollectMessage);
- }
-
- return new SyncNotedAppOp(proxiedAppOp, code,
- isProxiedAttributionTagValid ? proxiedAttributionTag : null,
- resolvedProxiedPackageName);
+ return startOperationUnchecked(clientId, code, proxiedUid, resolvedProxiedPackageName,
+ proxiedAttributionTag, proxyUid, resolvedProxyPackageName, proxyAttributionTag,
+ proxiedFlags, startIfModeDefault, shouldCollectAsyncNotedOp, message,
+ shouldCollectMessage, proxiedAttributionFlags, attributionChainId,
+ /*dryRun*/ false);
}
private boolean shouldStartForMode(int mode, boolean startIfModeDefault) {
return (mode == MODE_ALLOWED || (mode == MODE_DEFAULT && startIfModeDefault));
}
+ 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);
+ }
+
+ 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 && !dryRun && !isRestricted) {
+ collectAsyncNotedOp(uid, packageName, code, attributionTag, AppOpsManager.OP_FLAG_SELF,
+ message, shouldCollectMessage);
+ }
+
+ return new SyncNotedAppOp(isRestricted ? MODE_IGNORED : MODE_ALLOWED, code, attributionTag,
+ packageName);
+ }
+
@Override
public void finishOperation(IBinder clientId, int code, int uid, String packageName,
String attributionTag) {
@@ -1005,11 +3159,22 @@
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 @@
}
if (!skipProxyOperation) {
- mAppOpsService.finishOperationUnchecked(clientId, code, proxyUid,
- resolvedProxyPackageName, proxyAttributionTag);
+ finishOperationUnchecked(clientId, code, proxyUid, resolvedProxyPackageName,
+ proxyAttributionTag);
}
String resolvedProxiedPackageName = AppOpsManager.resolvePackageName(proxiedUid,
@@ -1051,12 +3216,209 @@
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 @@
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 @@
|| 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 @@
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 @@
int mode;
int packageUid;
int nonpackageUid;
+ final static Binder sBinder = new Binder();
IBinder mToken;
boolean targetsUid;
@@ -1198,7 +4392,7 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
}
}
- 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 c3d2717..0000000
--- 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 8420fcb..0000000
--- 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 c1434e4..5114bd5 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 @@
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 @@
AppOpsUidStateTrackerImpl(ActivityManagerInternal activityManagerInternal,
Handler handler, Executor lockingExecutor, Clock clock,
- AppOpsServiceImpl.Constants constants) {
+ AppOpsService.Constants constants) {
this(activityManagerInternal, new DelayableExecutor() {
@Override
@@ -102,7 +102,7 @@
@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 7970269..dcc36bc 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.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 @@
// @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 @@
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 @@
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 @@
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 @@
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 @@
@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 @@
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 @@
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 @@
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 @@
/**
* 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 @@
/**
* 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 fb6511c..9b433cf 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 @@
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 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 b920517..d30bec7 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 @@
}
}
}
+
+ 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/AudioSystemAdapter.java b/services/core/java/com/android/server/audio/AudioSystemAdapter.java
index 1d25e31..c176f29 100644
--- a/services/core/java/com/android/server/audio/AudioSystemAdapter.java
+++ b/services/core/java/com/android/server/audio/AudioSystemAdapter.java
@@ -192,15 +192,7 @@
synchronized (mDevicesForAttrCache) {
res = mDevicesForAttrCache.get(key);
if (res == null) {
- // result from AudioSystem guaranteed non-null, but could be invalid
- // if there is a failure to talk to APM
res = AudioSystem.getDevicesForAttributes(attributes, forVolume);
- if (res.size() > 1 && res.get(0) != null
- && res.get(0).getInternalType() == AudioSystem.DEVICE_NONE) {
- Log.e(TAG, "unable to get devices for " + attributes);
- // return now, do not put invalid value in cache
- return res;
- }
mDevicesForAttrCache.put(key, res);
if (DEBUG_CACHE) {
Log.d(TAG, mMethodNames[METHOD_GETDEVICESFORATTRIBUTES]
diff --git a/services/core/java/com/android/server/audio/SoundDoseHelper.java b/services/core/java/com/android/server/audio/SoundDoseHelper.java
index 0d0de8a..5fe9ada 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 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 @@
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 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 @@
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/broadcastradio/IRadioServiceAidlImpl.java b/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java
index 6a01042..b66120d 100644
--- a/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java
+++ b/services/core/java/com/android/server/broadcastradio/IRadioServiceAidlImpl.java
@@ -86,7 +86,8 @@
@Override
public ITuner openTuner(int moduleId, RadioManager.BandConfig bandConfig,
- boolean withAudio, ITunerCallback callback) throws RemoteException {
+ boolean withAudio, ITunerCallback callback, int targetSdkVersion)
+ throws RemoteException {
if (isDebugEnabled()) {
Slogf.d(TAG, "Opening module %d", moduleId);
}
@@ -94,7 +95,7 @@
if (callback == null) {
throw new IllegalArgumentException("Callback must not be null");
}
- return mHalAidl.openSession(moduleId, bandConfig, withAudio, callback);
+ return mHalAidl.openSession(moduleId, bandConfig, withAudio, callback, targetSdkVersion);
}
@Override
diff --git a/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java b/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java
index 408fba1..8a1ba19 100644
--- a/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java
+++ b/services/core/java/com/android/server/broadcastradio/IRadioServiceHidlImpl.java
@@ -92,7 +92,8 @@
@Override
public ITuner openTuner(int moduleId, RadioManager.BandConfig bandConfig,
- boolean withAudio, ITunerCallback callback) throws RemoteException {
+ boolean withAudio, ITunerCallback callback, int targetSdkVersion)
+ throws RemoteException {
if (isDebugEnabled()) {
Slog.d(TAG, "Opening module " + moduleId);
}
diff --git a/services/core/java/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImpl.java b/services/core/java/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImpl.java
index 03acf72..772cd41 100644
--- a/services/core/java/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImpl.java
+++ b/services/core/java/com/android/server/broadcastradio/aidl/BroadcastRadioServiceImpl.java
@@ -199,7 +199,8 @@
*/
@Nullable
public ITuner openSession(int moduleId, @Nullable RadioManager.BandConfig legacyConfig,
- boolean withAudio, ITunerCallback callback) throws RemoteException {
+ boolean withAudio, ITunerCallback callback, int targetSdkVersion)
+ throws RemoteException {
if (DEBUG) {
Slogf.d(TAG, "Open AIDL radio session");
}
@@ -222,7 +223,7 @@
}
}
- TunerSession tunerSession = radioModule.openSession(callback);
+ TunerSession tunerSession = radioModule.openSession(callback, targetSdkVersion);
if (legacyConfig != null) {
tunerSession.setConfiguration(legacyConfig);
}
diff --git a/services/core/java/com/android/server/broadcastradio/aidl/ConversionUtils.java b/services/core/java/com/android/server/broadcastradio/aidl/ConversionUtils.java
index d90f9c4..3c0fda8 100644
--- a/services/core/java/com/android/server/broadcastradio/aidl/ConversionUtils.java
+++ b/services/core/java/com/android/server/broadcastradio/aidl/ConversionUtils.java
@@ -33,6 +33,8 @@
import android.hardware.radio.ProgramSelector;
import android.hardware.radio.RadioManager;
import android.hardware.radio.RadioMetadata;
+import android.hardware.radio.RadioTuner;
+import android.os.Build;
import android.os.ParcelableException;
import android.os.ServiceSpecificException;
import android.util.ArrayMap;
@@ -62,6 +64,11 @@
throw new UnsupportedOperationException("ConversionUtils class is noninstantiable");
}
+ static boolean isAtLeastU(int targetSdkVersion) {
+ // TODO(b/261770108): Use version code for U.
+ return targetSdkVersion >= Build.VERSION_CODES.CUR_DEVELOPMENT;
+ }
+
static RuntimeException throwOnError(RuntimeException halException, String action) {
if (!(halException instanceof ServiceSpecificException)) {
return new ParcelableException(new RuntimeException(
@@ -89,6 +96,27 @@
}
}
+ @RadioTuner.TunerResultType
+ static int halResultToTunerResult(int result) {
+ switch (result) {
+ case Result.OK:
+ return RadioTuner.TUNER_RESULT_OK;
+ case Result.INTERNAL_ERROR:
+ return RadioTuner.TUNER_RESULT_INTERNAL_ERROR;
+ case Result.INVALID_ARGUMENTS:
+ return RadioTuner.TUNER_RESULT_INVALID_ARGUMENTS;
+ case Result.INVALID_STATE:
+ return RadioTuner.TUNER_RESULT_INVALID_STATE;
+ case Result.NOT_SUPPORTED:
+ return RadioTuner.TUNER_RESULT_NOT_SUPPORTED;
+ case Result.TIMEOUT:
+ return RadioTuner.TUNER_RESULT_TIMEOUT;
+ case Result.UNKNOWN_ERROR:
+ default:
+ return RadioTuner.TUNER_RESULT_UNKNOWN_ERROR;
+ }
+ }
+
static VendorKeyValue[] vendorInfoToHalVendorKeyValues(@Nullable Map<String, String> info) {
if (info == null) {
return new VendorKeyValue[]{};
@@ -143,6 +171,7 @@
case ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE:
case ProgramSelector.IDENTIFIER_TYPE_DAB_SCID:
case ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY:
+ case ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT:
return ProgramSelector.PROGRAM_TYPE_DAB;
case ProgramSelector.IDENTIFIER_TYPE_DRMO_SERVICE_ID:
case ProgramSelector.IDENTIFIER_TYPE_DRMO_FREQUENCY:
@@ -471,6 +500,60 @@
return new ProgramList.Chunk(chunk.purge, chunk.complete, modified, removed);
}
+ private static boolean isNewIdentifierInU(ProgramSelector.Identifier id) {
+ return id.getType() == ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT;
+ }
+
+ static boolean programSelectorMeetsSdkVersionRequirement(ProgramSelector sel,
+ int targetSdkVersion) {
+ if (isAtLeastU(targetSdkVersion)) {
+ return true;
+ }
+ if (sel.getPrimaryId().getType() == ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT) {
+ return false;
+ }
+ ProgramSelector.Identifier[] secondaryIds = sel.getSecondaryIds();
+ for (int i = 0; i < secondaryIds.length; i++) {
+ if (isNewIdentifierInU(secondaryIds[i])) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ static boolean programInfoMeetsSdkVersionRequirement(RadioManager.ProgramInfo info,
+ int targetSdkVersion) {
+ if (isAtLeastU(targetSdkVersion)) {
+ return true;
+ }
+ if (!programSelectorMeetsSdkVersionRequirement(info.getSelector(), targetSdkVersion)) {
+ return false;
+ }
+ if (isNewIdentifierInU(info.getLogicallyTunedTo())
+ || isNewIdentifierInU(info.getPhysicallyTunedTo())) {
+ return false;
+ }
+ Iterator<ProgramSelector.Identifier> relatedContentIt = info.getRelatedContent().iterator();
+ while (relatedContentIt.hasNext()) {
+ if (isNewIdentifierInU(relatedContentIt.next())) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ static ProgramList.Chunk convertChunkToTargetSdkVersion(ProgramList.Chunk chunk,
+ int targetSdkVersion) {
+ if (isAtLeastU(targetSdkVersion)) {
+ return chunk;
+ }
+ Set<RadioManager.ProgramInfo> modified = chunk.getModified();
+ modified.removeIf(info -> !programInfoMeetsSdkVersionRequirement(info, targetSdkVersion));
+ Set<ProgramSelector.Identifier> removed = chunk.getRemoved();
+ removed.removeIf(id -> isNewIdentifierInU(id));
+ return new ProgramList.Chunk(chunk.isPurge(), chunk.isComplete(), modified, removed);
+ }
+
public static android.hardware.radio.Announcement announcementFromHalAnnouncement(
Announcement hwAnnouncement) {
return new android.hardware.radio.Announcement(
diff --git a/services/core/java/com/android/server/broadcastradio/aidl/RadioModule.java b/services/core/java/com/android/server/broadcastradio/aidl/RadioModule.java
index e956a9c..6193f23 100644
--- a/services/core/java/com/android/server/broadcastradio/aidl/RadioModule.java
+++ b/services/core/java/com/android/server/broadcastradio/aidl/RadioModule.java
@@ -100,7 +100,16 @@
synchronized (mLock) {
android.hardware.radio.ProgramSelector csel =
ConversionUtils.programSelectorFromHalProgramSelector(programSelector);
- fanoutAidlCallbackLocked(cb -> cb.onTuneFailed(result, csel));
+ int tunerResult = ConversionUtils.halResultToTunerResult(result);
+ fanoutAidlCallbackLocked((cb, sdkVersion) -> {
+ if (csel != null && !ConversionUtils
+ .programSelectorMeetsSdkVersionRequirement(csel, sdkVersion)) {
+ Slogf.e(TAG, "onTuneFailed: cannot send program selector "
+ + "requiring higher target SDK version");
+ return;
+ }
+ cb.onTuneFailed(tunerResult, csel);
+ });
}
});
}
@@ -112,7 +121,13 @@
mCurrentProgramInfo =
ConversionUtils.programInfoFromHalProgramInfo(halProgramInfo);
RadioManager.ProgramInfo currentProgramInfo = mCurrentProgramInfo;
- fanoutAidlCallbackLocked(cb -> {
+ fanoutAidlCallbackLocked((cb, sdkVersion) -> {
+ if (!ConversionUtils.programInfoMeetsSdkVersionRequirement(
+ currentProgramInfo, sdkVersion)) {
+ Slogf.e(TAG, "onCurrentProgramInfoChanged: cannot send "
+ + "program info requiring higher target SDK version");
+ return;
+ }
cb.onCurrentProgramInfoChanged(currentProgramInfo);
});
}
@@ -139,7 +154,7 @@
fireLater(() -> {
synchronized (mLock) {
mAntennaConnected = connected;
- fanoutAidlCallbackLocked(cb -> cb.onAntennaState(connected));
+ fanoutAidlCallbackLocked((cb, sdkVersion) -> cb.onAntennaState(connected));
}
});
}
@@ -147,8 +162,11 @@
@Override
public void onConfigFlagUpdated(int flag, boolean value) {
fireLater(() -> {
- // TODO(b/243853343): implement config flag update method in
- // android.hardware.radio.ITunerCallback
+ synchronized (mLock) {
+ fanoutAidlCallbackLocked((cb, sdkVersion) -> {
+ cb.onConfigFlagUpdated(flag, value);
+ });
+ }
});
}
@@ -158,7 +176,9 @@
synchronized (mLock) {
Map<String, String> cparam =
ConversionUtils.vendorInfoFromHalVendorKeyValues(parameters);
- fanoutAidlCallbackLocked(cb -> cb.onParametersUpdated(cparam));
+ fanoutAidlCallbackLocked((cb, sdkVersion) -> {
+ cb.onParametersUpdated(cparam);
+ });
}
});
}
@@ -222,14 +242,14 @@
mService.setTunerCallback(mHalTunerCallback);
}
- TunerSession openSession(android.hardware.radio.ITunerCallback userCb)
+ TunerSession openSession(android.hardware.radio.ITunerCallback userCb, int targetSdkVersion)
throws RemoteException {
mLogger.logRadioEvent("Open TunerSession");
TunerSession tunerSession;
Boolean antennaConnected;
RadioManager.ProgramInfo currentProgramInfo;
synchronized (mLock) {
- tunerSession = new TunerSession(this, mService, userCb);
+ tunerSession = new TunerSession(this, mService, userCb, targetSdkVersion);
mAidlTunerSessions.add(tunerSession);
antennaConnected = mAntennaConnected;
currentProgramInfo = mCurrentProgramInfo;
@@ -382,7 +402,8 @@
}
interface AidlCallbackRunnable {
- void run(android.hardware.radio.ITunerCallback callback) throws RemoteException;
+ void run(android.hardware.radio.ITunerCallback callback, int targetSdkVersion)
+ throws RemoteException;
}
// Invokes runnable with each TunerSession currently open.
@@ -399,7 +420,8 @@
List<TunerSession> deadSessions = null;
for (int i = 0; i < mAidlTunerSessions.size(); i++) {
try {
- runnable.run(mAidlTunerSessions.valueAt(i).mCallback);
+ runnable.run(mAidlTunerSessions.valueAt(i).mCallback,
+ mAidlTunerSessions.valueAt(i).getTargetSdkVersion());
} catch (DeadObjectException ex) {
// The other side died without calling close(), so just purge it from our records.
Slogf.e(TAG, "Removing dead TunerSession");
diff --git a/services/core/java/com/android/server/broadcastradio/aidl/TunerSession.java b/services/core/java/com/android/server/broadcastradio/aidl/TunerSession.java
index 1ce4044..d700ed0 100644
--- a/services/core/java/com/android/server/broadcastradio/aidl/TunerSession.java
+++ b/services/core/java/com/android/server/broadcastradio/aidl/TunerSession.java
@@ -21,7 +21,6 @@
import android.hardware.broadcastradio.ConfigFlag;
import android.hardware.broadcastradio.IBroadcastRadio;
import android.hardware.radio.ITuner;
-import android.hardware.radio.ITunerCallback;
import android.hardware.radio.ProgramList;
import android.hardware.radio.ProgramSelector;
import android.hardware.radio.RadioManager;
@@ -47,6 +46,7 @@
private final RadioLogger mLogger;
private final RadioModule mModule;
final android.hardware.radio.ITunerCallback mCallback;
+ private final int mTargetSdkVersion;
private final IBroadcastRadio mService;
@GuardedBy("mLock")
@@ -61,10 +61,11 @@
private RadioManager.BandConfig mPlaceHolderConfig;
TunerSession(RadioModule radioModule, IBroadcastRadio service,
- android.hardware.radio.ITunerCallback callback) {
+ android.hardware.radio.ITunerCallback callback, int targetSdkVersion) {
mModule = Objects.requireNonNull(radioModule, "radioModule cannot be null");
mService = Objects.requireNonNull(service, "service cannot be null");
mCallback = Objects.requireNonNull(callback, "callback cannot be null");
+ mTargetSdkVersion = targetSdkVersion;
mLogger = new RadioLogger(TAG, TUNER_EVENT_LOGGER_QUEUE_SIZE);
}
@@ -129,7 +130,7 @@
mPlaceHolderConfig = Objects.requireNonNull(config, "config cannot be null");
}
Slogf.i(TAG, "Ignoring setConfiguration - not applicable for broadcastradio HAL AIDL");
- mModule.fanoutAidlCallback(cb -> cb.onConfigurationChanged(config));
+ mModule.fanoutAidlCallback((cb, sdkVersion) -> cb.onConfigurationChanged(config));
}
@Override
@@ -178,8 +179,8 @@
}
@Override
- public void scan(boolean directionDown, boolean skipSubChannel) throws RemoteException {
- mLogger.logRadioEvent("Scan with direction %s, skipSubChannel? %s",
+ public void seek(boolean directionDown, boolean skipSubChannel) throws RemoteException {
+ mLogger.logRadioEvent("Seek with direction %s, skipSubChannel? %s",
directionDown ? "down" : "up", skipSubChannel ? "yes" : "no");
if (!RadioServiceUserController.isCurrentOrSystemUser()) {
Slogf.w(TAG, "Cannot scan on AIDL HAL client from non-current user");
@@ -232,8 +233,7 @@
@Override
public void cancelAnnouncement() {
- // TODO(b/244485175): deperacte cancelAnnouncement
- Slogf.i(TAG, "Announcements control doesn't involve cancelling at the HAL level in AIDL");
+ Slogf.w(TAG, "Announcements control doesn't involve cancelling at the HAL level in AIDL");
}
@Override
@@ -244,12 +244,14 @@
@Override
public boolean startBackgroundScan() {
- Slogf.i(TAG, "Explicit background scan trigger is not supported with HAL AIDL");
+ Slogf.w(TAG, "Explicit background scan trigger is not supported with HAL AIDL");
if (!RadioServiceUserController.isCurrentOrSystemUser()) {
Slogf.w(TAG, "Cannot start background scan on AIDL HAL client from non-current user");
return false;
}
- mModule.fanoutAidlCallback(ITunerCallback::onBackgroundScanComplete);
+ mModule.fanoutAidlCallback((cb, sdkVersion) -> {
+ cb.onBackgroundScanComplete();
+ });
return true;
}
@@ -277,6 +279,10 @@
mModule.onTunerSessionProgramListFilterChanged(this);
}
+ int getTargetSdkVersion() {
+ return mTargetSdkVersion;
+ }
+
ProgramList.Filter getProgramListFilter() {
synchronized (mLock) {
return mProgramInfoCache == null ? null : mProgramInfoCache.getFilter();
@@ -312,7 +318,14 @@
}
for (int i = 0; i < chunks.size(); i++) {
try {
- mCallback.onProgramListUpdated(chunks.get(i));
+ if (!ConversionUtils.isAtLeastU(getTargetSdkVersion())) {
+ ProgramList.Chunk downgradedChunk =
+ ConversionUtils.convertChunkToTargetSdkVersion(chunks.get(i),
+ getTargetSdkVersion());
+ mCallback.onProgramListUpdated(downgradedChunk);
+ } else {
+ mCallback.onProgramListUpdated(chunks.get(i));
+ }
} catch (RemoteException ex) {
Slogf.w(TAG, ex, "mCallback.onProgramListUpdated() failed");
}
diff --git a/services/core/java/com/android/server/broadcastradio/hal1/Tuner.java b/services/core/java/com/android/server/broadcastradio/hal1/Tuner.java
index ed8a37a..8e5f6b5 100644
--- a/services/core/java/com/android/server/broadcastradio/hal1/Tuner.java
+++ b/services/core/java/com/android/server/broadcastradio/hal1/Tuner.java
@@ -189,9 +189,9 @@
}
@Override
- public void scan(boolean directionDown, boolean skipSubChannel) {
+ public void seek(boolean directionDown, boolean skipSubChannel) {
if (!RadioServiceUserController.isCurrentOrSystemUser()) {
- Slogf.w(TAG, "Cannot scan on HAL 1.x client from non-current user");
+ Slogf.w(TAG, "Cannot seek on HAL 1.x client from non-current user");
return;
}
synchronized (mLock) {
diff --git a/services/core/java/com/android/server/broadcastradio/hal1/TunerCallback.java b/services/core/java/com/android/server/broadcastradio/hal1/TunerCallback.java
index 0cc3833..aa43b75 100644
--- a/services/core/java/com/android/server/broadcastradio/hal1/TunerCallback.java
+++ b/services/core/java/com/android/server/broadcastradio/hal1/TunerCallback.java
@@ -174,8 +174,13 @@
}
@Override
+ public void onConfigFlagUpdated(int flag, boolean value) {
+ Slog.w(TAG, "Not applicable for HAL 1.x");
+ }
+
+ @Override
public void onParametersUpdated(Map<String, String> parameters) {
- Slog.e(TAG, "Not applicable for HAL 1.x");
+ Slog.w(TAG, "Not applicable for HAL 1.x");
}
@Override
diff --git a/services/core/java/com/android/server/broadcastradio/hal2/BroadcastRadioService.java b/services/core/java/com/android/server/broadcastradio/hal2/BroadcastRadioService.java
index 984bf51..1e31f20 100644
--- a/services/core/java/com/android/server/broadcastradio/hal2/BroadcastRadioService.java
+++ b/services/core/java/com/android/server/broadcastradio/hal2/BroadcastRadioService.java
@@ -169,7 +169,7 @@
}
public ITuner openSession(int moduleId, @Nullable RadioManager.BandConfig legacyConfig,
- boolean withAudio, @NonNull ITunerCallback callback) throws RemoteException {
+ boolean withAudio, @NonNull ITunerCallback callback) throws RemoteException {
Slog.v(TAG, "Open HIDL 2.0 session with module id " + moduleId);
if (!RadioServiceUserController.isCurrentOrSystemUser()) {
Slogf.e(TAG, "Cannot open tuner on HAL 2.0 client for non-current user");
diff --git a/services/core/java/com/android/server/broadcastradio/hal2/Convert.java b/services/core/java/com/android/server/broadcastradio/hal2/Convert.java
index 3daf1db..98a450f 100644
--- a/services/core/java/com/android/server/broadcastradio/hal2/Convert.java
+++ b/services/core/java/com/android/server/broadcastradio/hal2/Convert.java
@@ -36,6 +36,7 @@
import android.hardware.radio.ProgramSelector;
import android.hardware.radio.RadioManager;
import android.hardware.radio.RadioMetadata;
+import android.hardware.radio.RadioTuner;
import android.os.ParcelableException;
import android.util.Slog;
@@ -81,6 +82,27 @@
}
}
+ @RadioTuner.TunerResultType
+ static int halResultToTunerResult(int result) {
+ switch (result) {
+ case Result.OK:
+ return RadioTuner.TUNER_RESULT_OK;
+ case Result.INTERNAL_ERROR:
+ return RadioTuner.TUNER_RESULT_INTERNAL_ERROR;
+ case Result.INVALID_ARGUMENTS:
+ return RadioTuner.TUNER_RESULT_INVALID_ARGUMENTS;
+ case Result.INVALID_STATE:
+ return RadioTuner.TUNER_RESULT_INVALID_STATE;
+ case Result.NOT_SUPPORTED:
+ return RadioTuner.TUNER_RESULT_NOT_SUPPORTED;
+ case Result.TIMEOUT:
+ return RadioTuner.TUNER_RESULT_TIMEOUT;
+ case Result.UNKNOWN_ERROR:
+ default:
+ return RadioTuner.TUNER_RESULT_UNKNOWN_ERROR;
+ }
+ }
+
static @NonNull ArrayList<VendorKeyValue>
vendorInfoToHal(@Nullable Map<String, String> info) {
if (info == null) return new ArrayList<>();
@@ -130,6 +152,7 @@
case ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE:
case ProgramSelector.IDENTIFIER_TYPE_DAB_SCID:
case ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY:
+ case ProgramSelector.IDENTIFIER_TYPE_DAB_DMB_SID_EXT:
return ProgramSelector.PROGRAM_TYPE_DAB;
case ProgramSelector.IDENTIFIER_TYPE_DRMO_SERVICE_ID:
case ProgramSelector.IDENTIFIER_TYPE_DRMO_FREQUENCY:
diff --git a/services/core/java/com/android/server/broadcastradio/hal2/RadioModule.java b/services/core/java/com/android/server/broadcastradio/hal2/RadioModule.java
index 0ea5f0f..59a8154 100644
--- a/services/core/java/com/android/server/broadcastradio/hal2/RadioModule.java
+++ b/services/core/java/com/android/server/broadcastradio/hal2/RadioModule.java
@@ -87,8 +87,9 @@
fireLater(() -> {
android.hardware.radio.ProgramSelector csel =
Convert.programSelectorFromHal(programSelector);
+ int tunerResult = Convert.halResultToTunerResult(result);
synchronized (mLock) {
- fanoutAidlCallbackLocked(cb -> cb.onTuneFailed(result, csel));
+ fanoutAidlCallbackLocked(cb -> cb.onTuneFailed(tunerResult, csel));
}
});
}
diff --git a/services/core/java/com/android/server/broadcastradio/hal2/TunerSession.java b/services/core/java/com/android/server/broadcastradio/hal2/TunerSession.java
index 7afee27..204b964 100644
--- a/services/core/java/com/android/server/broadcastradio/hal2/TunerSession.java
+++ b/services/core/java/com/android/server/broadcastradio/hal2/TunerSession.java
@@ -171,8 +171,8 @@
}
@Override
- public void scan(boolean directionDown, boolean skipSubChannel) throws RemoteException {
- mEventLogger.logRadioEvent("Scan with direction %s, skipSubChannel? %s",
+ public void seek(boolean directionDown, boolean skipSubChannel) throws RemoteException {
+ mEventLogger.logRadioEvent("Seek with direction %s, skipSubChannel? %s",
directionDown ? "down" : "up", skipSubChannel ? "yes" : "no");
if (!RadioServiceUserController.isCurrentOrSystemUser()) {
Slogf.w(TAG, "Cannot scan on HAL 2.0 client from non-current user");
@@ -214,7 +214,7 @@
@Override
public void cancelAnnouncement() {
- Slog.i(TAG, "Announcements control doesn't involve cancelling at the HAL level in HAL 2.0");
+ Slog.w(TAG, "Announcements control doesn't involve cancelling at the HAL level in HAL 2.0");
}
@Override
@@ -225,7 +225,7 @@
@Override
public boolean startBackgroundScan() {
- Slog.i(TAG, "Explicit background scan trigger is not supported with HAL 2.0");
+ Slog.w(TAG, "Explicit background scan trigger is not supported with HAL 2.0");
if (!RadioServiceUserController.isCurrentOrSystemUser()) {
Slogf.w(TAG,
"Cannot start background scan on HAL 2.0 client from non-current user");
diff --git a/services/core/java/com/android/server/clipboard/EmulatorClipboardMonitor.java b/services/core/java/com/android/server/clipboard/EmulatorClipboardMonitor.java
index ab3b250..dce1c96 100644
--- a/services/core/java/com/android/server/clipboard/EmulatorClipboardMonitor.java
+++ b/services/core/java/com/android/server/clipboard/EmulatorClipboardMonitor.java
@@ -29,6 +29,7 @@
import java.io.EOFException;
import java.io.FileDescriptor;
import java.io.InterruptedIOException;
+import java.net.ProtocolException;
import java.net.SocketException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
@@ -39,12 +40,16 @@
// write contents of the host system's clipboard.
class EmulatorClipboardMonitor implements Consumer<ClipData> {
private static final String TAG = "EmulatorClipboardMonitor";
+
private static final String PIPE_NAME = "pipe:clipboard";
private static final int HOST_PORT = 5000;
- private final Thread mHostMonitorThread;
+
private static final boolean LOG_CLIBOARD_ACCESS =
SystemProperties.getBoolean("ro.boot.qemu.log_clipboard_access", false);
+ private static final int MAX_CLIPBOARD_BYTES = 128 << 20;
+
private FileDescriptor mPipe = null;
+ private final Thread mHostMonitorThread;
private static byte[] createOpenHandshake() {
// String.getBytes doesn't include the null terminator,
@@ -97,8 +102,8 @@
return fd;
}
- private static byte[] receiveMessage(final FileDescriptor fd) throws ErrnoException,
- InterruptedIOException, EOFException {
+ private byte[] receiveMessage(final FileDescriptor fd) throws ErrnoException,
+ InterruptedIOException, EOFException, ProtocolException {
final byte[] lengthBits = new byte[4];
readFully(fd, lengthBits, 0, lengthBits.length);
@@ -106,6 +111,10 @@
bb.order(ByteOrder.LITTLE_ENDIAN);
final int msgLen = bb.getInt();
+ if (msgLen <= 0 || msgLen > MAX_CLIPBOARD_BYTES) {
+ throw new ProtocolException("Clipboard message length: " + msgLen + " out of bounds.");
+ }
+
final byte[] msg = new byte[msgLen];
readFully(fd, msg, 0, msg.length);
@@ -150,7 +159,8 @@
}
setAndroidClipboard.accept(clip);
} catch (ErrnoException | EOFException | InterruptedIOException
- | InterruptedException e) {
+ | InterruptedException | ProtocolException | OutOfMemoryError e) {
+ Slog.w(TAG, "Failure to read from host clipboard", e);
setPipeFD(null);
try {
diff --git a/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java b/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java
index 218be9d..09bec5e 100644
--- a/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java
+++ b/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java
@@ -36,7 +36,6 @@
void onVirtualDisplayRemoved(int displayId);
}
-
/** Interface to listen to the changes on the list of app UIDs running on any virtual device. */
public interface AppsOnVirtualDeviceListener {
/** Notifies that running apps on any virtual device has changed */
@@ -72,6 +71,25 @@
public abstract boolean isValidVirtualDevice(IVirtualDevice virtualDevice);
/**
+ * Gets the owner uid for a deviceId.
+ *
+ * @param deviceId which device we're asking about
+ * @return the uid of the app which created and owns the VirtualDevice with the given deviceId,
+ * or {@link android.os.Process#INVALID_UID} if no such device exists.
+ */
+ public abstract int getDeviceOwnerUid(int deviceId);
+
+ /**
+ * Finds VirtualDevices where an app is running.
+ *
+ * @param uid - the app's uid
+ * @return a set of id's of VirtualDevices where the app with the given uid is running.
+ * *Note* this only checks VirtualDevices, and does not include information about whether
+ * the app is running on the default device or not.
+ */
+ public abstract @NonNull Set<Integer> getDeviceIdsForUid(int uid);
+
+ /**
* Notifies that a virtual display is created.
*
* @param displayId The display id of the created virtual display.
diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java
index 4fcde97..19dbee7 100644
--- a/services/core/java/com/android/server/connectivity/Vpn.java
+++ b/services/core/java/com/android/server/connectivity/Vpn.java
@@ -646,7 +646,10 @@
.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED)
.setTransportInfo(new VpnTransportInfo(
- VpnManager.TYPE_VPN_NONE, null /* sessionId */, false /* bypassable */))
+ VpnManager.TYPE_VPN_NONE,
+ null /* sessionId */,
+ false /* bypassable */,
+ false /* longLivedTcpConnectionsExpensive */))
.build();
loadAlwaysOnPackage();
@@ -711,7 +714,10 @@
mNetworkCapabilities = new NetworkCapabilities.Builder(mNetworkCapabilities)
.setUids(null)
.setTransportInfo(new VpnTransportInfo(
- VpnManager.TYPE_VPN_NONE, null /* sessionId */, false /* bypassable */))
+ VpnManager.TYPE_VPN_NONE,
+ null /* sessionId */,
+ false /* bypassable */,
+ false /* longLivedTcpConnectionsExpensive */))
.build();
}
@@ -1570,7 +1576,8 @@
mConfig.allowedApplications, mConfig.disallowedApplications));
capsBuilder.setTransportInfo(
- new VpnTransportInfo(getActiveVpnType(), mConfig.session, mConfig.allowBypass));
+ new VpnTransportInfo(getActiveVpnType(), mConfig.session, mConfig.allowBypass,
+ false /* longLivedTcpConnectionsExpensive */));
// Only apps targeting Q and above can explicitly declare themselves as metered.
// These VPNs are assumed metered unless they state otherwise.
diff --git a/services/core/java/com/android/server/display/ColorFade.java b/services/core/java/com/android/server/display/ColorFade.java
index 372bc8a..a4bd6a6 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 c700ccb..329e3ca 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 @@
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 @@
@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 9d47892..75415cd 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 @@
// 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 @@
@Override
public void onSwitchUser(@UserIdInt int newUserId) {
handleSettingsChange(true /* userSwitch */);
+ handleBrightnessModeChange();
if (mBrightnessTracker != null) {
mBrightnessTracker.onSwitchUser(newUserId);
}
@@ -930,6 +933,10 @@
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 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 @@
|| 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 @@
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 @@
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 @@
@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 346b340..111caefa 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.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 @@
// 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 @@
// 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 @@
mDisplayPowerProximityStateController = mInjector.getDisplayPowerProximityStateController(
mWakelockController, mDisplayDeviceConfig, mHandler.getLooper(),
() -> updatePowerState(), mDisplayId, mSensorManager);
+ mDisplayStateController = new DisplayStateController(mDisplayPowerProximityStateController);
mTag = "DisplayPowerController2[" + mDisplayId + "]";
mDisplayDevice = mLogicalDisplay.getPrimaryDisplayDeviceLocked();
@@ -600,6 +607,7 @@
@Override
public void onSwitchUser(@UserIdInt int newUserId) {
handleSettingsChange(true /* userSwitch */);
+ handleBrightnessModeChange();
if (mBrightnessTracker != null) {
mBrightnessTracker.onSwitchUser(newUserId);
}
@@ -842,6 +850,10 @@
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 @@
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 @@
// 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 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 @@
|| 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 @@
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 @@
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 @@
if (mDisplayBrightnessController != null) {
mDisplayBrightnessController.dump(pw);
}
-
- if (mDisplayPowerProximityStateController != null) {
- mDisplayPowerProximityStateController.dumpLocal(pw);
- }
}
@@ -2499,7 +2488,11 @@
@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 c7b27de..ad426b5 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 @@
*/
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 80f47a1..d7983ae 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 @@
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.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 java.io.PrintWriter;
import java.util.Arrays;
-import java.util.Set;
import java.util.function.Consumer;
/**
@@ -324,58 +323,44 @@
}
/**
- * 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;
}
- return displayInfos;
+ // 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;
+ }
+ 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 a118b2f..7c647cf 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 @@
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 0000000..546478e
--- /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/graphics/fonts/FontManagerService.java b/services/core/java/com/android/server/graphics/fonts/FontManagerService.java
index 3c5b067..01cae42 100644
--- a/services/core/java/com/android/server/graphics/fonts/FontManagerService.java
+++ b/services/core/java/com/android/server/graphics/fonts/FontManagerService.java
@@ -188,7 +188,7 @@
@Override
public void setUpFsverity(String filePath) throws IOException {
- VerityUtils.setUpFsverity(filePath, /* signature */ (byte[]) null);
+ VerityUtils.setUpFsverity(filePath);
}
@Override
diff --git a/services/core/java/com/android/server/incident/IncidentCompanionService.java b/services/core/java/com/android/server/incident/IncidentCompanionService.java
index b8e7d49..87fe785 100644
--- a/services/core/java/com/android/server/incident/IncidentCompanionService.java
+++ b/services/core/java/com/android/server/incident/IncidentCompanionService.java
@@ -34,6 +34,7 @@
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.UserHandle;
+import android.os.UserManager;
import android.util.Log;
import com.android.internal.util.DumpUtils;
@@ -127,21 +128,21 @@
try {
final Context context = getContext();
- // Get the current admin user. Only they can do incident reports.
- final int currentAdminUser = getCurrentUserIfAdmin();
- if (currentAdminUser == UserHandle.USER_NULL) {
+ final int primaryUser = getAndValidateUser(context);
+ if (primaryUser == UserHandle.USER_NULL) {
return;
}
final Intent intent = new Intent(Intent.ACTION_INCIDENT_REPORT_READY);
intent.setComponent(new ComponentName(pkg, cls));
- Log.d(TAG, "sendReportReadyBroadcast sending currentUser=" + currentAdminUser
- + " userHandle=" + UserHandle.of(currentAdminUser)
+ Log.d(TAG, "sendReportReadyBroadcast sending primaryUser=" + primaryUser
+ + " userHandle=" + UserHandle.getUserHandleForUid(primaryUser)
+ " intent=" + intent);
+ // Send it to the primary user. Only they can do incident reports.
context.sendBroadcastAsUserMultiplePermissions(intent,
- UserHandle.of(currentAdminUser),
+ UserHandle.getUserHandleForUid(primaryUser),
DUMP_AND_USAGE_STATS_PERMISSIONS);
} finally {
Binder.restoreCallingIdentity(ident);
@@ -413,10 +414,10 @@
}
/**
- * Check whether the current user is an admin user, and return the user id if they are.
+ * Check whether the current user is the primary user, and return the user id if they are.
* Returns UserHandle.USER_NULL if not valid.
*/
- public static int getCurrentUserIfAdmin() {
+ public static int getAndValidateUser(Context context) {
// Current user
UserInfo currentUser;
try {
@@ -426,21 +427,28 @@
throw new RuntimeException(ex);
}
+ // Primary user
+ final UserManager um = UserManager.get(context);
+ final UserInfo primaryUser = um.getPrimaryUser();
+
// Check that we're using the right user.
if (currentUser == null) {
Log.w(TAG, "No current user. Nobody to approve the report."
+ " The report will be denied.");
return UserHandle.USER_NULL;
}
-
- if (!currentUser.isAdmin()) {
- Log.w(TAG, "Only an admin user running in foreground can approve "
- + "bugreports, but the current foreground user is not an admin user. "
- + "The report will be denied.");
+ if (primaryUser == null) {
+ Log.w(TAG, "No primary user. Nobody to approve the report."
+ + " The report will be denied.");
+ return UserHandle.USER_NULL;
+ }
+ if (primaryUser.id != currentUser.id) {
+ Log.w(TAG, "Only the primary user can approve bugreports, but they are not"
+ + " the current user. The report will be denied.");
return UserHandle.USER_NULL;
}
- return currentUser.id;
+ return primaryUser.id;
}
}
diff --git a/services/core/java/com/android/server/incident/PendingReports.java b/services/core/java/com/android/server/incident/PendingReports.java
index 6285bc3..f39bebf 100644
--- a/services/core/java/com/android/server/incident/PendingReports.java
+++ b/services/core/java/com/android/server/incident/PendingReports.java
@@ -16,7 +16,6 @@
package com.android.server.incident;
-import android.annotation.UserIdInt;
import android.app.AppOpsManager;
import android.app.BroadcastOptions;
import android.content.ComponentName;
@@ -273,19 +272,15 @@
return;
}
- // Find the current user of the device and check if they are an admin.
- final int currentAdminUser = getCurrentUserIfAdmin();
-
- // Deny the report if the current admin user is null
- // or not the user who requested the report.
- if (currentAdminUser == UserHandle.USER_NULL
- || currentAdminUser != UserHandle.getUserId(callingUid)) {
+ // Find the primary user of this device.
+ final int primaryUser = getAndValidateUser();
+ if (primaryUser == UserHandle.USER_NULL) {
denyReportBeforeAddingRec(listener, callingPackage);
return;
}
// Find the approver app (hint: it's PermissionController).
- final ComponentName receiver = getApproverComponent(currentAdminUser);
+ final ComponentName receiver = getApproverComponent(primaryUser);
if (receiver == null) {
// We couldn't find an approver... so deny the request here and now, before we
// do anything else.
@@ -303,26 +298,26 @@
try {
listener.asBinder().linkToDeath(() -> {
Log.i(TAG, "Got death notification listener=" + listener);
- cancelReportImpl(listener, receiver, currentAdminUser);
+ cancelReportImpl(listener, receiver, primaryUser);
}, 0);
} catch (RemoteException ex) {
Log.e(TAG, "Remote died while trying to register death listener: " + rec.getUri());
// First, remove from our list.
- cancelReportImpl(listener, receiver, currentAdminUser);
+ cancelReportImpl(listener, receiver, primaryUser);
}
// Go tell Permission controller to start asking the user.
- sendBroadcast(receiver, currentAdminUser);
+ sendBroadcast(receiver, primaryUser);
}
/**
* Cancel a pending report request (because of an explicit call to cancel)
*/
private void cancelReportImpl(IIncidentAuthListener listener) {
- final int currentAdminUser = getCurrentUserIfAdmin();
- final ComponentName receiver = getApproverComponent(currentAdminUser);
- if (currentAdminUser != UserHandle.USER_NULL && receiver != null) {
- cancelReportImpl(listener, receiver, currentAdminUser);
+ final int primaryUser = getAndValidateUser();
+ final ComponentName receiver = getApproverComponent(primaryUser);
+ if (primaryUser != UserHandle.USER_NULL && receiver != null) {
+ cancelReportImpl(listener, receiver, primaryUser);
}
}
@@ -331,13 +326,13 @@
* by the calling app, or because of a binder death).
*/
private void cancelReportImpl(IIncidentAuthListener listener, ComponentName receiver,
- @UserIdInt int user) {
+ int primaryUser) {
// First, remove from our list.
synchronized (mLock) {
removePendingReportRecLocked(listener);
}
// Second, call back to PermissionController to say it's canceled.
- sendBroadcast(receiver, user);
+ sendBroadcast(receiver, primaryUser);
}
/**
@@ -347,21 +342,21 @@
* cleanup cases to keep the apps' list in sync with ours.
*/
private void sendBroadcast() {
- final int currentAdminUser = getCurrentUserIfAdmin();
- if (currentAdminUser == UserHandle.USER_NULL) {
+ final int primaryUser = getAndValidateUser();
+ if (primaryUser == UserHandle.USER_NULL) {
return;
}
- final ComponentName receiver = getApproverComponent(currentAdminUser);
+ final ComponentName receiver = getApproverComponent(primaryUser);
if (receiver == null) {
return;
}
- sendBroadcast(receiver, currentAdminUser);
+ sendBroadcast(receiver, primaryUser);
}
/**
* Send the confirmation broadcast.
*/
- private void sendBroadcast(ComponentName receiver, int currentUser) {
+ private void sendBroadcast(ComponentName receiver, int primaryUser) {
final Intent intent = new Intent(Intent.ACTION_PENDING_INCIDENT_REPORTS_CHANGED);
intent.setComponent(receiver);
intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
@@ -369,8 +364,8 @@
final BroadcastOptions options = BroadcastOptions.makeBasic();
options.setBackgroundActivityStartsAllowed(true);
- // Send it to the current user.
- mContext.sendBroadcastAsUser(intent, UserHandle.of(currentUser),
+ // Send it to the primary user.
+ mContext.sendBroadcastAsUser(intent, UserHandle.getUserHandleForUid(primaryUser),
android.Manifest.permission.APPROVE_INCIDENT_REPORTS, options.toBundle());
}
@@ -425,11 +420,11 @@
}
/**
- * Check whether the current user is an admin user, and return the user id if they are.
+ * Check whether the current user is the primary user, and return the user id if they are.
* Returns UserHandle.USER_NULL if not valid.
*/
- private int getCurrentUserIfAdmin() {
- return IncidentCompanionService.getCurrentUserIfAdmin();
+ private int getAndValidateUser() {
+ return IncidentCompanionService.getAndValidateUser(mContext);
}
/**
diff --git a/services/core/java/com/android/server/input/InputManagerInternal.java b/services/core/java/com/android/server/input/InputManagerInternal.java
index 298098a..01a564d 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 @@
* {@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 c62abf0..1809b18 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 @@
@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 @@
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 @@
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 @@
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 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 8781c6e..184bc0e 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 @@
void changeUniqueIdAssociation();
+ void changeTypeAssociation();
+
void notifyPointerDisplayIdChanged();
void setDisplayEligibilityForPointerCapture(int displayId, boolean enabled);
@@ -400,6 +402,9 @@
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 2dc1e1c..5f39a52 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 @@
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 @@
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 @@
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);
@@ -1027,11 +1026,18 @@
}
private void enforceFrpResolved() {
+ final int mainUserId = mInjector.getUserManagerInternal().getMainUserId();
+ if (mainUserId < 0) {
+ Slog.i(TAG, "No Main user on device; skip enforceFrpResolved");
+ return;
+ }
final ContentResolver cr = mContext.getContentResolver();
+
final boolean inSetupWizard = Settings.Secure.getIntForUser(cr,
- Settings.Secure.USER_SETUP_COMPLETE, 0, UserHandle.USER_SYSTEM) == 0;
- final boolean secureFrp = Settings.Secure.getIntForUser(cr,
- Settings.Secure.SECURE_FRP_MODE, 0, UserHandle.USER_SYSTEM) == 1;
+ Settings.Secure.USER_SETUP_COMPLETE, 0, mainUserId) == 0;
+ final boolean secureFrp = Settings.Global.getInt(cr,
+ Settings.Global.SECURE_FRP_MODE, 0) == 1;
+
if (inSetupWizard && secureFrp) {
throw new SecurityException("Cannot change credential in SUW while factory reset"
+ " protection is not resolved yet");
@@ -1645,13 +1651,7 @@
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
@@ -2315,9 +2315,7 @@
return;
}
removeStateForReusedUserIdIfNecessary(userId, userSerialNumber);
- synchronized (mSpManager) {
- initializeSyntheticPasswordLocked(userId);
- }
+ initializeSyntheticPassword(userId);
}
}
@@ -2643,21 +2641,22 @@
* 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");
+ 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;
+ 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
@@ -2673,13 +2672,6 @@
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;
@@ -2918,19 +2910,14 @@
@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 ed8d852..50e1fca 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 @@
@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 @@
} 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 @@
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 @@
}
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 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/os/BugreportManagerServiceImpl.java b/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java
index 2fdc4cd..58428ca 100644
--- a/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java
+++ b/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java
@@ -33,8 +33,8 @@
import android.os.SystemClock;
import android.os.SystemProperties;
import android.os.UserHandle;
+import android.os.UserManager;
import android.telephony.TelephonyManager;
-import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Slog;
@@ -189,10 +189,10 @@
}
/**
- * Validates that the current user is an admin user or, when bugreport is requested remotely
- * that the current user is an affiliated user.
+ * Validates that the current user is the primary user or when bugreport is requested remotely
+ * and current user is affiliated user.
*
- * @throws IllegalArgumentException if the current user is not an admin user
+ * @throws IllegalArgumentException if the current user is not the primary user
*/
private void ensureUserCanTakeBugReport(int bugreportMode) {
UserInfo currentUser = null;
@@ -202,17 +202,20 @@
// Impossible to get RemoteException for an in-process call.
}
+ UserInfo primaryUser = UserManager.get(mContext).getPrimaryUser();
if (currentUser == null) {
- logAndThrow("There is no current user, so no bugreport can be requested.");
+ logAndThrow("No current user. Only primary user is allowed to take bugreports.");
}
-
- if (!currentUser.isAdmin()) {
+ if (primaryUser == null) {
+ logAndThrow("No primary user. Only primary user is allowed to take bugreports.");
+ }
+ if (primaryUser.id != currentUser.id) {
if (bugreportMode == BugreportParams.BUGREPORT_MODE_REMOTE
&& isCurrentUserAffiliated(currentUser.id)) {
return;
}
- logAndThrow(TextUtils.formatSimple("Current user %s is not an admin user."
- + " Only admin users are allowed to take bugreport.", currentUser.id));
+ logAndThrow("Current user not primary user. Only primary user"
+ + " is allowed to take bugreports.");
}
}
diff --git a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java
index df95f86..d4c4c69 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 @@
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 @@
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);
- }
-
- // 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);
- }
- return packages;
+ return mService.getBackgroundInstalledPackages(flags, userId);
}
}
- /**
- * Called when the system service should publish a binder service using
- * {@link #publishBinderService(String, IBinder).}
- */
+ @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;
+
+ 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);
+ }
+ }
+ }
+
+ 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 @@
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 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 60621a0..ea84283 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 @@
@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 b8fba51..06aadd92 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.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 @@
}
@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/CrossProfileIntentFilterHelper.java b/services/core/java/com/android/server/pm/CrossProfileIntentFilterHelper.java
new file mode 100644
index 0000000..e682586
--- /dev/null
+++ b/services/core/java/com/android/server/pm/CrossProfileIntentFilterHelper.java
@@ -0,0 +1,100 @@
+/*
+ * 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 android.content.Context;
+import android.content.pm.UserInfo;
+import android.content.pm.UserProperties;
+import android.util.ArraySet;
+
+/**
+ * Helper class to manage {@link com.android.server.pm.CrossProfileIntentFilter}s.
+ */
+public class CrossProfileIntentFilterHelper {
+ private Context mContext;
+ private UserManagerInternal mUserManagerInternal;
+ private Settings mSettings;
+ private UserManagerService mUserManagerService;
+ private PackageManagerTracedLock mLock;
+
+ public CrossProfileIntentFilterHelper(Settings settings, UserManagerService userManagerService,
+ PackageManagerTracedLock lock, UserManagerInternal userManagerInternal,
+ Context context) {
+ mSettings = settings;
+ mUserManagerService = userManagerService;
+ mLock = lock;
+ mContext = context;
+ mUserManagerInternal = userManagerInternal;
+ }
+
+ /**
+ * For users that have
+ * {@link android.content.pm.UserProperties#getUpdateCrossProfileIntentFiltersOnOTA} set, this
+ * task will update default {@link com.android.server.pm.CrossProfileIntentFilter} between that
+ * user and its parent. This will only update CrossProfileIntentFilters set by system package.
+ * The new default are configured in {@link UserTypeDetails}.
+ */
+ public void updateDefaultCrossProfileIntentFilter() {
+ for (UserInfo userInfo : mUserManagerInternal.getUsers(false)) {
+
+ UserProperties currentUserProperties = mUserManagerInternal
+ .getUserProperties(userInfo.id);
+
+ if (currentUserProperties.getUpdateCrossProfileIntentFiltersOnOTA()) {
+ int parentUserId = mUserManagerInternal.getProfileParentId(userInfo.id);
+ if (parentUserId != userInfo.id) {
+ clearCrossProfileIntentFilters(userInfo.id,
+ mContext.getOpPackageName(), parentUserId);
+ clearCrossProfileIntentFilters(parentUserId,
+ mContext.getOpPackageName(), userInfo.id);
+
+ mUserManagerInternal.setDefaultCrossProfileIntentFilters(parentUserId,
+ userInfo.id);
+ }
+ }
+ }
+ }
+
+ /**
+ * Clear {@link CrossProfileIntentFilter}s configured on source user by ownerPackage
+ * targeting the targetUserId. If targetUserId is null then it will clear
+ * {@link CrossProfileIntentFilter} for any target user.
+ * @param sourceUserId source user for whom CrossProfileIntentFilter would be configured
+ * @param ownerPackage package who would have configured CrossProfileIntentFilter
+ * @param targetUserId user id for which CrossProfileIntentFilter will be removed.
+ * This can be null in which case it will clear for any target user.
+ */
+ public void clearCrossProfileIntentFilters(int sourceUserId, String ownerPackage,
+ Integer targetUserId) {
+ synchronized (mLock) {
+ CrossProfileIntentResolver resolver = mSettings
+ .editCrossProfileIntentResolverLPw(sourceUserId);
+ ArraySet<CrossProfileIntentFilter> set =
+ new ArraySet<>(resolver.filterSet());
+ for (CrossProfileIntentFilter filter : set) {
+ //Only remove if calling user is allowed based on access control of
+ // {@link CrossProfileIntentFilter}
+ if (filter.getOwnerPackage().equals(ownerPackage)
+ && (targetUserId == null || filter.mTargetUserId == targetUserId)
+ && mUserManagerService.isCrossProfileIntentFilterAccessible(sourceUserId,
+ filter.mTargetUserId, /* addCrossProfileIntentFilter */ false)) {
+ resolver.removeFilter(filter);
+ }
+ }
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/pm/DexOptHelper.java b/services/core/java/com/android/server/pm/DexOptHelper.java
index 0066592..5661399 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.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 @@
}
/**
- * 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/GentleUpdateHelper.java b/services/core/java/com/android/server/pm/GentleUpdateHelper.java
index 247ac90..7cb096d 100644
--- a/services/core/java/com/android/server/pm/GentleUpdateHelper.java
+++ b/services/core/java/com/android/server/pm/GentleUpdateHelper.java
@@ -16,7 +16,12 @@
package com.android.server.pm;
+import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
+import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE;
+
import android.annotation.WorkerThread;
+import android.app.ActivityManager;
+import android.app.ActivityManager.RunningAppProcessInfo;
import android.app.ActivityThread;
import android.app.job.JobInfo;
import android.app.job.JobParameters;
@@ -28,6 +33,9 @@
import android.content.pm.PackageInstaller.InstallConstraintsResult;
import android.os.Handler;
import android.os.Looper;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.text.format.DateUtils;
import android.util.Slog;
import java.util.ArrayDeque;
@@ -73,12 +81,24 @@
public final List<String> packageNames;
public final InstallConstraints constraints;
public final CompletableFuture<InstallConstraintsResult> future;
+ private final long mFinishTime;
+
+ /**
+ * Note {@code timeoutMillis} will be clamped to 0 ~ one week to avoid overflow.
+ */
PendingInstallConstraintsCheck(List<String> packageNames,
InstallConstraints constraints,
- CompletableFuture<InstallConstraintsResult> future) {
+ CompletableFuture<InstallConstraintsResult> future,
+ long timeoutMillis) {
this.packageNames = packageNames;
this.constraints = constraints;
this.future = future;
+
+ timeoutMillis = Math.max(0, Math.min(DateUtils.WEEK_IN_MILLIS, timeoutMillis));
+ mFinishTime = SystemClock.elapsedRealtime() + timeoutMillis;
+ }
+ public boolean isTimedOut() {
+ return SystemClock.elapsedRealtime() >= mFinishTime;
}
}
@@ -95,15 +115,31 @@
mAppStateHelper = appStateHelper;
}
+ void systemReady() {
+ var am = mContext.getSystemService(ActivityManager.class);
+ // Monitor top-visible apps
+ am.addOnUidImportanceListener(this::onUidImportance, IMPORTANCE_FOREGROUND);
+ // Monitor foreground apps
+ am.addOnUidImportanceListener(this::onUidImportance, IMPORTANCE_FOREGROUND_SERVICE);
+ }
+
/**
* Checks if install constraints are satisfied for the given packages.
*/
CompletableFuture<InstallConstraintsResult> checkInstallConstraints(
- List<String> packageNames, InstallConstraints constraints) {
+ List<String> packageNames, InstallConstraints constraints,
+ long timeoutMillis) {
var future = new CompletableFuture<InstallConstraintsResult>();
mHandler.post(() -> {
+ long clampedTimeoutMillis = timeoutMillis;
+ if (constraints.isRequireDeviceIdle()) {
+ // Device-idle-constraint is required. Clamp the timeout to ensure
+ // timeout-check happens after device-idle-check.
+ clampedTimeoutMillis = Math.max(timeoutMillis, PENDING_CHECK_MILLIS);
+ }
+
var pendingCheck = new PendingInstallConstraintsCheck(
- packageNames, constraints, future);
+ packageNames, constraints, future, clampedTimeoutMillis);
if (constraints.isRequireDeviceIdle()) {
mPendingChecks.add(pendingCheck);
// JobScheduler doesn't provide queries about whether the device is idle.
@@ -113,8 +149,17 @@
scheduleIdleJob();
mHandler.postDelayed(() -> processPendingCheck(pendingCheck, false),
PENDING_CHECK_MILLIS);
- } else {
- processPendingCheck(pendingCheck, false);
+ } else if (!processPendingCheck(pendingCheck, false)) {
+ // Not resolved. Schedule a job for re-check
+ mPendingChecks.add(pendingCheck);
+ scheduleIdleJob();
+ }
+
+ if (!future.isDone()) {
+ // Ensure the pending check is resolved after timeout, no matter constraints
+ // satisfied or not.
+ mHandler.postDelayed(() -> processPendingCheck(pendingCheck, false),
+ clampedTimeoutMillis);
}
});
return future;
@@ -143,29 +188,77 @@
}
@WorkerThread
- private void processPendingCheck(PendingInstallConstraintsCheck pendingCheck, boolean isIdle) {
+ private boolean areConstraintsSatisfied(List<String> packageNames,
+ InstallConstraints constraints, boolean isIdle) {
+ return (!constraints.isRequireDeviceIdle() || isIdle)
+ && (!constraints.isRequireAppNotForeground()
+ || !mAppStateHelper.hasForegroundApp(packageNames))
+ && (!constraints.isRequireAppNotInteracting()
+ || !mAppStateHelper.hasInteractingApp(packageNames))
+ && (!constraints.isRequireAppNotTopVisible()
+ || !mAppStateHelper.hasTopVisibleApp(packageNames))
+ && (!constraints.isRequireNotInCall()
+ || !mAppStateHelper.isInCall());
+ }
+
+ @WorkerThread
+ private boolean processPendingCheck(
+ PendingInstallConstraintsCheck pendingCheck, boolean isIdle) {
var future = pendingCheck.future;
if (future.isDone()) {
- return;
+ return true;
}
var constraints = pendingCheck.constraints;
var packageNames = mAppStateHelper.getDependencyPackages(pendingCheck.packageNames);
- var constraintsSatisfied = (!constraints.isRequireDeviceIdle() || isIdle)
- && (!constraints.isRequireAppNotForeground()
- || !mAppStateHelper.hasForegroundApp(packageNames))
- && (!constraints.isRequireAppNotInteracting()
- || !mAppStateHelper.hasInteractingApp(packageNames))
- && (!constraints.isRequireAppNotTopVisible()
- || !mAppStateHelper.hasTopVisibleApp(packageNames))
- && (!constraints.isRequireNotInCall()
- || !mAppStateHelper.isInCall());
- future.complete(new InstallConstraintsResult((constraintsSatisfied)));
+ var satisfied = areConstraintsSatisfied(packageNames, constraints, isIdle);
+ if (satisfied || pendingCheck.isTimedOut()) {
+ future.complete(new InstallConstraintsResult((satisfied)));
+ return true;
+ }
+ return false;
}
@WorkerThread
private void processPendingChecksInIdle() {
- while (!mPendingChecks.isEmpty()) {
- processPendingCheck(mPendingChecks.remove(), true);
+ int size = mPendingChecks.size();
+ for (int i = 0; i < size; ++i) {
+ var pendingCheck = mPendingChecks.remove();
+ if (!processPendingCheck(pendingCheck, true)) {
+ // Not resolved. Put it back in the queue.
+ mPendingChecks.add(pendingCheck);
+ }
+ }
+ if (!mPendingChecks.isEmpty()) {
+ // Schedule a job for remaining pending checks
+ scheduleIdleJob();
+ }
+ }
+
+ @WorkerThread
+ private void onUidImportance(String packageName,
+ @RunningAppProcessInfo.Importance int importance) {
+ int size = mPendingChecks.size();
+ for (int i = 0; i < size; ++i) {
+ var pendingCheck = mPendingChecks.remove();
+ var dependencyPackages =
+ mAppStateHelper.getDependencyPackages(pendingCheck.packageNames);
+ if (!dependencyPackages.contains(packageName)
+ || !processPendingCheck(pendingCheck, false)) {
+ mPendingChecks.add(pendingCheck);
+ }
+ }
+ if (!mPendingChecks.isEmpty()) {
+ // Schedule a job for remaining pending checks
+ scheduleIdleJob();
+ }
+ }
+
+ private void onUidImportance(int uid, @RunningAppProcessInfo.Importance int importance) {
+ var pm = ActivityThread.getPackageManager();
+ try {
+ var packageName = pm.getNameForUid(uid);
+ mHandler.post(() -> onUidImportance(packageName, importance));
+ } catch (RemoteException ignore) {
}
}
}
diff --git a/services/core/java/com/android/server/pm/IPackageManagerBase.java b/services/core/java/com/android/server/pm/IPackageManagerBase.java
index 5c3890c..38efc10 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 @@
@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/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index 911cfbd..34bb6ec 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -1872,7 +1872,7 @@
if (new File(signaturePath).exists()) {
// If signature is provided, enable fs-verity first so that the file can be
// measured for signature check below.
- VerityUtils.setUpFsverity(filePath, (byte[]) null);
+ VerityUtils.setUpFsverity(filePath);
if (!fis.verifyPkcs7DetachedSignature(signaturePath, filePath)) {
throw new PrepareFailure(PackageManager.INSTALL_FAILED_BAD_SIGNATURE,
@@ -2385,7 +2385,7 @@
for (String path : apkPaths) {
if (!VerityUtils.hasFsverity(path)) {
try {
- VerityUtils.setUpFsverity(path, (byte[]) null);
+ VerityUtils.setUpFsverity(path);
} catch (IOException e) {
// There's nothing we can do if the setup failed. Since fs-verity is
// optional, just ignore the error for now.
diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java
index 4803c5e..9c60795 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerService.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerService.java
@@ -45,6 +45,7 @@
import android.content.pm.PackageInfo;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageInstaller.InstallConstraints;
+import android.content.pm.PackageInstaller.InstallConstraintsResult;
import android.content.pm.PackageInstaller.SessionInfo;
import android.content.pm.PackageInstaller.SessionParams;
import android.content.pm.PackageItemInfo;
@@ -91,7 +92,6 @@
import com.android.internal.notification.SystemNotificationChannels;
import com.android.internal.util.ImageUtils;
import com.android.internal.util.IndentingPrintWriter;
-import com.android.internal.util.Preconditions;
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;
import com.android.server.IoThread;
@@ -123,6 +123,7 @@
import java.util.Random;
import java.util.TreeMap;
import java.util.TreeSet;
+import java.util.concurrent.CompletableFuture;
import java.util.function.IntPredicate;
import java.util.function.Supplier;
@@ -153,6 +154,8 @@
private static final long MAX_HISTORICAL_SESSIONS = 1048576;
/** Destroy sessions older than this on storage free request */
private static final long MAX_SESSION_AGE_ON_LOW_STORAGE_MILLIS = 8 * DateUtils.HOUR_IN_MILLIS;
+ /** Maximum time to wait for install constraints to be satisfied */
+ private static final long MAX_INSTALL_CONSTRAINTS_TIMEOUT_MILLIS = DateUtils.WEEK_IN_MILLIS;
/** Threshold of historical sessions size */
private static final int HISTORICAL_SESSIONS_THRESHOLD = 500;
@@ -300,6 +303,7 @@
public void systemReady() {
mAppOps = mContext.getSystemService(AppOpsManager.class);
mStagingManager.systemReady();
+ mGentleUpdateHelper.systemReady();
synchronized (mSessions) {
readSessionsLocked();
@@ -1248,12 +1252,11 @@
}
}
- @Override
- public void checkInstallConstraints(String installerPackageName, List<String> packageNames,
- InstallConstraints constraints, RemoteCallback callback) {
- Preconditions.checkArgument(packageNames != null);
- Preconditions.checkArgument(constraints != null);
- Preconditions.checkArgument(callback != null);
+ private CompletableFuture<InstallConstraintsResult> checkInstallConstraintsInternal(
+ String installerPackageName, List<String> packageNames,
+ InstallConstraints constraints, long timeoutMillis) {
+ Objects.requireNonNull(packageNames);
+ Objects.requireNonNull(constraints);
final var snapshot = mPm.snapshotComputer();
final int callingUid = Binder.getCallingUid();
@@ -1267,7 +1270,16 @@
}
}
- var future = mGentleUpdateHelper.checkInstallConstraints(packageNames, constraints);
+ return mGentleUpdateHelper.checkInstallConstraints(
+ packageNames, constraints, timeoutMillis);
+ }
+
+ @Override
+ public void checkInstallConstraints(String installerPackageName, List<String> packageNames,
+ InstallConstraints constraints, RemoteCallback callback) {
+ Objects.requireNonNull(callback);
+ var future = checkInstallConstraintsInternal(
+ installerPackageName, packageNames, constraints, /*timeoutMillis=*/0);
future.thenAccept(result -> {
var b = new Bundle();
b.putParcelable("result", result);
@@ -1276,6 +1288,27 @@
}
@Override
+ public void waitForInstallConstraints(String installerPackageName, List<String> packageNames,
+ InstallConstraints constraints, IntentSender callback, long timeoutMillis) {
+ Objects.requireNonNull(callback);
+ if (timeoutMillis < 0 || timeoutMillis > MAX_INSTALL_CONSTRAINTS_TIMEOUT_MILLIS) {
+ throw new IllegalArgumentException("Invalid timeoutMillis=" + timeoutMillis);
+ }
+ var future = checkInstallConstraintsInternal(
+ installerPackageName, packageNames, constraints, timeoutMillis);
+ future.thenAccept(result -> {
+ final var intent = new Intent();
+ intent.putExtra(Intent.EXTRA_PACKAGES, packageNames.toArray(new String[0]));
+ intent.putExtra(PackageInstaller.EXTRA_INSTALL_CONSTRAINTS, constraints);
+ intent.putExtra(PackageInstaller.EXTRA_INSTALL_CONSTRAINTS_RESULT, result);
+ try {
+ callback.sendIntent(mContext, 0, intent, null, null);
+ } catch (SendIntentException ignore) {
+ }
+ });
+ }
+
+ @Override
public void registerCallback(IPackageInstallerCallback callback, int userId) {
final Computer snapshot = mPm.snapshotComputer();
snapshot.enforceCrossUserPermission(Binder.getCallingUid(), userId, true, false,
diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java
index 3983acf..9aaf685 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerSession.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java
@@ -125,7 +125,7 @@
import android.os.incremental.PerUidReadTimeouts;
import android.os.incremental.StorageHealthCheckParams;
import android.os.storage.StorageManager;
-import android.provider.Settings.Secure;
+import android.provider.Settings.Global;
import android.stats.devicepolicy.DevicePolicyEnums;
import android.system.ErrnoException;
import android.system.Int64Ref;
@@ -1842,7 +1842,8 @@
assertNoWriteFileTransfersOpenLocked();
final boolean isSecureFrpEnabled =
- (Secure.getInt(mContext.getContentResolver(), Secure.SECURE_FRP_MODE, 0) == 1);
+ Global.getInt(mContext.getContentResolver(), Global.SECURE_FRP_MODE, 0) == 1;
+
if (isSecureFrpEnabled
&& !isSecureFrpInstallAllowed(mContext, Binder.getCallingUid())) {
throw new SecurityException("Can't install packages while in secure FRP");
@@ -2785,6 +2786,7 @@
// Default to require only if existing base apk has fs-verity signature.
mVerityFoundForApks = PackageManagerServiceUtils.isApkVerityEnabled()
&& params.mode == SessionParams.MODE_INHERIT_EXISTING
+ && VerityUtils.hasFsverity(pkgInfo.applicationInfo.getBaseCodePath())
&& (new File(VerityUtils.getFsveritySignatureFilePath(
pkgInfo.applicationInfo.getBaseCodePath()))).exists();
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 91f7011..759ec67 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.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.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;
@@ -1537,7 +1539,10 @@
(i, pm) -> new BackgroundDexOptService(i.getContext(), i.getDexManager(), pm),
(i, pm) -> IBackupManager.Stub.asInterface(ServiceManager.getService(
Context.BACKUP_SERVICE)),
- (i, pm) -> new SharedLibrariesImpl(pm, i));
+ (i, pm) -> new SharedLibrariesImpl(pm, i),
+ (i, pm) -> new CrossProfileIntentFilterHelper(i.getSettings(),
+ i.getUserManagerService(), i.getLock(), i.getUserManagerInternal(),
+ context));
if (Build.VERSION.SDK_INT <= 0) {
Slog.w(TAG, "**** ro.build.version.sdk not set!");
@@ -2029,13 +2034,21 @@
final WatchedArrayMap<String, PackageSetting> packageSettings =
mSettings.getPackagesLocked();
- // Save the names of pre-existing packages prior to scanning, so we can determine
- // which system packages are completely new due to an upgrade.
if (isDeviceUpgrading()) {
+ // Save the names of pre-existing packages prior to scanning, so we can determine
+ // which system packages are completely new due to an upgrade.
mExistingPackages = new ArraySet<>(packageSettings.size());
for (PackageSetting ps : packageSettings.values()) {
mExistingPackages.add(ps.getPackageName());
}
+
+ // Triggering {@link com.android.server.pm.crossprofile.
+ // CrossProfileIntentFilterHelper.updateDefaultCrossProfileIntentFilter} to update
+ // {@link CrossProfileIntentFilter}s between eligible users and their parent
+ t.traceBegin("cross profile intent filter update");
+ mInjector.getCrossProfileIntentFilterHelper()
+ .updateDefaultCrossProfileIntentFilter();
+ t.traceEnd();
}
mCacheDir = PackageManagerServiceUtils.preparePackageParserCache(
@@ -3255,6 +3268,7 @@
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 {
@@ -3281,6 +3295,9 @@
if (dpm.packageHasActiveAdmins(packageName, users[i])) {
return true;
}
+ if (isDeviceManagementRoleHolder(packageName, users[i])) {
+ return true;
+ }
}
}
} catch (RemoteException e) {
@@ -3288,6 +3305,24 @@
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) {
@@ -3448,6 +3483,7 @@
scheduleWritePackageRestrictions(sourceUserId);
}
+
// Enforcing that callingUid is owning pkg on userId
private void enforceOwnerRights(@NonNull Computer snapshot, String pkg, int callingUid) {
// The system owns everything.
@@ -4638,21 +4674,9 @@
enforceOwnerRights(snapshot, ownerPackage, callingUid);
PackageManagerServiceUtils.enforceShellRestriction(mInjector.getUserManagerInternal(),
UserManager.DISALLOW_DEBUGGING_FEATURES, callingUid, sourceUserId);
- synchronized (mLock) {
- CrossProfileIntentResolver resolver =
- mSettings.editCrossProfileIntentResolverLPw(sourceUserId);
- ArraySet<CrossProfileIntentFilter> set =
- new ArraySet<>(resolver.filterSet());
- for (CrossProfileIntentFilter filter : set) {
- //Only remove if calling user is allowed based on access control of
- // {@link CrossProfileIntentFilter}
- if (filter.getOwnerPackage().equals(ownerPackage)
- && mUserManager.isCrossProfileIntentFilterAccessible(sourceUserId,
- filter.mTargetUserId, /* addCrossProfileIntentFilter */ false)) {
- resolver.removeFilter(filter);
- }
- }
- }
+ PackageManagerService.this.mInjector.getCrossProfileIntentFilterHelper()
+ .clearCrossProfileIntentFilters(sourceUserId, ownerPackage,
+ null);
scheduleWritePackageRestrictions(sourceUserId);
}
@@ -5308,18 +5332,70 @@
return;
}
- // TODO(b/254043366): Call `ArtManagerLocal.notifyDexLoad`.
+ UserHandle user = Binder.getCallingUserHandle();
+ int userId = user.getIdentifier();
- 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;
+ // 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;
+ }
+ }
+ }
+
+ // 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/PackageManagerServiceInjector.java b/services/core/java/com/android/server/pm/PackageManagerServiceInjector.java
index 396994b..76e6e45f 100644
--- a/services/core/java/com/android/server/pm/PackageManagerServiceInjector.java
+++ b/services/core/java/com/android/server/pm/PackageManagerServiceInjector.java
@@ -139,6 +139,7 @@
private final Singleton<BackgroundDexOptService> mBackgroundDexOptService;
private final Singleton<IBackupManager> mIBackupManager;
private final Singleton<SharedLibrariesImpl> mSharedLibrariesProducer;
+ private final Singleton<CrossProfileIntentFilterHelper> mCrossProfileIntentFilterHelperProducer;
PackageManagerServiceInjector(Context context, PackageManagerTracedLock lock,
Installer installer, Object installLock, PackageAbiHelper abiHelper,
@@ -176,7 +177,8 @@
ServiceProducer getSystemServiceProducer,
Producer<BackgroundDexOptService> backgroundDexOptService,
Producer<IBackupManager> iBackupManager,
- Producer<SharedLibrariesImpl> sharedLibrariesProducer) {
+ Producer<SharedLibrariesImpl> sharedLibrariesProducer,
+ Producer<CrossProfileIntentFilterHelper> crossProfileIntentFilterHelperProducer) {
mContext = context;
mLock = lock;
mInstaller = installer;
@@ -228,6 +230,8 @@
mBackgroundDexOptService = new Singleton<>(backgroundDexOptService);
mIBackupManager = new Singleton<>(iBackupManager);
mSharedLibrariesProducer = new Singleton<>(sharedLibrariesProducer);
+ mCrossProfileIntentFilterHelperProducer = new Singleton<>(
+ crossProfileIntentFilterHelperProducer);
}
/**
@@ -262,6 +266,14 @@
return mLock;
}
+ /**
+ * {@link CrossProfileIntentFilterHelper} which manages {@link CrossProfileIntentFilter}
+ * @return CrossProfileIntentFilterHelper
+ */
+ public CrossProfileIntentFilterHelper getCrossProfileIntentFilterHelper() {
+ return mCrossProfileIntentFilterHelperProducer.get(this, mPackageManager);
+ }
+
public Installer getInstaller() {
return mInstaller;
}
diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java
index 01dee13..7fec0eb00f 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 @@
// 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 @@
mRuntimePermissionsPersistence = null;
mPermissionDataProvider = null;
mSettingsFilename = null;
- mCompressedSettingsFilename = null;
mPreviousSettingsFilename = null;
mPackageListFilename = null;
mStoppedPackagesFilename = null;
@@ -713,7 +710,6 @@
|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 @@
mLock = null;
mRuntimePermissionsPersistence = r.mRuntimePermissionsPersistence;
mSettingsFilename = null;
- mCompressedSettingsFilename = null;
mPreviousSettingsFilename = null;
mPackageListFilename = null;
mStoppedPackagesFilename = null;
@@ -2597,8 +2592,6 @@
Slog.w(PackageManagerService.TAG, "Preserving older settings backup");
}
}
- // Compressed settings are not valid anymore.
- mCompressedSettingsFilename.delete();
mPastSignatures.clear();
@@ -2688,30 +2681,10 @@
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 @@
//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 0a650c9..251da84 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 @@
@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/UserTypeFactory.java b/services/core/java/com/android/server/pm/UserTypeFactory.java
index 8fb5773..2bb72b8 100644
--- a/services/core/java/com/android/server/pm/UserTypeFactory.java
+++ b/services/core/java/com/android/server/pm/UserTypeFactory.java
@@ -132,7 +132,8 @@
.setShowInLauncher(UserProperties.SHOW_IN_LAUNCHER_WITH_PARENT)
.setShowInSettings(UserProperties.SHOW_IN_SETTINGS_WITH_PARENT)
.setInheritDevicePolicy(UserProperties.INHERIT_DEVICE_POLICY_FROM_PARENT)
- .setUseParentsContacts(true));
+ .setUseParentsContacts(true)
+ .setUpdateCrossProfileIntentFiltersOnOTA(true));
}
/**
diff --git a/services/core/java/com/android/server/pm/UserVisibilityMediator.java b/services/core/java/com/android/server/pm/UserVisibilityMediator.java
index 92e8f55..40d87bc 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 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.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 @@
* 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 @@
? 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 @@
visibleUsersBefore = getVisibleUsers();
// Set current user / profiles state
- if (foreground) {
+ if (userStartMode == USER_START_MODE_FOREGROUND) {
mCurrentUserId = userId;
}
if (DBG) {
@@ -228,8 +236,23 @@
@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 7919e19..025348b 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -181,6 +181,7 @@
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.view.autofill.AutofillManagerInternal;
+import android.widget.Toast;
import com.android.internal.R;
import com.android.internal.accessibility.AccessibilityShortcutController;
@@ -204,6 +205,7 @@
import com.android.server.GestureLauncherService;
import com.android.server.LocalServices;
import com.android.server.SystemServiceManager;
+import com.android.server.UiThread;
import com.android.server.input.InputManagerInternal;
import com.android.server.inputmethod.InputMethodManagerInternal;
import com.android.server.policy.KeyCombinationManager.TwoKeysCombinationRule;
@@ -2900,6 +2902,11 @@
}
}
break;
+ case KeyEvent.KEYCODE_RECENT_APPS:
+ if (down && repeatCount == 0) {
+ showRecentApps(false /* triggeredFromAltTab */);
+ }
+ return key_consumed;
case KeyEvent.KEYCODE_APP_SWITCH:
if (!keyguardOn) {
if (down && repeatCount == 0) {
@@ -3217,8 +3224,23 @@
boolean isEnabled = mSensorPrivacyManager.isSensorPrivacyEnabled(
SensorPrivacyManager.TOGGLE_TYPE_SOFTWARE,
SensorPrivacyManager.Sensors.MICROPHONE);
+
mSensorPrivacyManager.setSensorPrivacy(SensorPrivacyManager.Sensors.MICROPHONE,
!isEnabled);
+
+ int toastTextResId;
+ if (isEnabled) {
+ toastTextResId = R.string.mic_access_on_toast;
+ } else {
+ toastTextResId = R.string.mic_access_off_toast;
+ }
+
+ Toast.makeText(
+ mContext,
+ UiThread.get().getLooper(),
+ mContext.getString(toastTextResId),
+ Toast.LENGTH_SHORT)
+ .show();
}
}
diff --git a/services/core/java/com/android/server/power/Notifier.java b/services/core/java/com/android/server/power/Notifier.java
index 1fe82f4..09a7b29 100644
--- a/services/core/java/com/android/server/power/Notifier.java
+++ b/services/core/java/com/android/server/power/Notifier.java
@@ -25,7 +25,6 @@
import android.content.Context;
import android.content.IIntentReceiver;
import android.content.Intent;
-import android.content.IntentFilter;
import android.hardware.display.DisplayManagerInternal;
import android.media.AudioManager;
import android.media.Ringtone;
@@ -67,6 +66,7 @@
import com.android.server.statusbar.StatusBarManagerInternal;
import java.io.PrintWriter;
+import java.util.UUID;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -140,9 +140,8 @@
private final NotifierHandler mHandler;
private final Executor mBackgroundExecutor;
private final Intent mScreenOnIntent;
- private final Bundle mScreenOnOptions;
private final Intent mScreenOffIntent;
- private final Bundle mScreenOffOptions;
+ private final Bundle mScreenOnOffOptions;
// True if the device should suspend when the screen is off due to proximity.
private final boolean mSuspendWhenScreenOffDueToProximityConfig;
@@ -204,14 +203,11 @@
mScreenOnIntent.addFlags(
Intent.FLAG_RECEIVER_REGISTERED_ONLY | Intent.FLAG_RECEIVER_FOREGROUND
| Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS);
- mScreenOnOptions = BroadcastOptions.makeRemovingMatchingFilter(
- new IntentFilter(Intent.ACTION_SCREEN_OFF)).toBundle();
mScreenOffIntent = new Intent(Intent.ACTION_SCREEN_OFF);
mScreenOffIntent.addFlags(
Intent.FLAG_RECEIVER_REGISTERED_ONLY | Intent.FLAG_RECEIVER_FOREGROUND
| Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS);
- mScreenOffOptions = BroadcastOptions.makeRemovingMatchingFilter(
- new IntentFilter(Intent.ACTION_SCREEN_ON)).toBundle();
+ mScreenOnOffOptions = createScreenOnOffBroadcastOptions();
mSuspendWhenScreenOffDueToProximityConfig = context.getResources().getBoolean(
com.android.internal.R.bool.config_suspendWhenScreenOffDueToProximity);
@@ -229,6 +225,25 @@
}
/**
+ * Create the {@link BroadcastOptions} bundle that will be used with sending the
+ * {@link Intent#ACTION_SCREEN_ON} and {@link Intent#ACTION_SCREEN_OFF} broadcasts.
+ */
+ private Bundle createScreenOnOffBroadcastOptions() {
+ final BroadcastOptions options = BroadcastOptions.makeBasic();
+ // This allows the broadcasting system to discard any older broadcasts
+ // waiting to be delivered to a process.
+ options.setDeliveryGroupPolicy(BroadcastOptions.DELIVERY_GROUP_POLICY_MOST_RECENT);
+ // Set namespace and key to identify which older broadcasts can be discarded.
+ // We could use any strings here but namespace needs to be unlikely to be reused with in
+ // the system_server process, as that could result in potentially discarding some
+ // non-screen on/off related broadcast.
+ options.setDeliveryGroupMatchingKey(
+ UUID.randomUUID().toString(),
+ Intent.ACTION_SCREEN_ON);
+ return options.toBundle();
+ }
+
+ /**
* Called when a wake lock is acquired.
*/
public void onWakeLockAcquired(int flags, String tag, String packageName,
@@ -798,7 +813,7 @@
if (mActivityManagerInternal.isSystemReady()) {
final boolean ordered = !mActivityManagerInternal.isModernQueueEnabled();
mActivityManagerInternal.broadcastIntent(mScreenOnIntent, mWakeUpBroadcastDone,
- null, ordered, UserHandle.USER_ALL, null, null, mScreenOnOptions);
+ null, ordered, UserHandle.USER_ALL, null, null, mScreenOnOffOptions);
} else {
EventLog.writeEvent(EventLogTags.POWER_SCREEN_BROADCAST_STOP, 2, 1);
sendNextBroadcast();
@@ -823,7 +838,7 @@
if (mActivityManagerInternal.isSystemReady()) {
final boolean ordered = !mActivityManagerInternal.isModernQueueEnabled();
mActivityManagerInternal.broadcastIntent(mScreenOffIntent, mGoToSleepBroadcastDone,
- null, ordered, UserHandle.USER_ALL, null, null, mScreenOffOptions);
+ null, ordered, UserHandle.USER_ALL, null, null, mScreenOnOffOptions);
} else {
EventLog.writeEvent(EventLogTags.POWER_SCREEN_BROADCAST_STOP, 3, 1);
sendNextBroadcast();
diff --git a/services/core/java/com/android/server/power/PowerGroup.java b/services/core/java/com/android/server/power/PowerGroup.java
index 1c4e143..522c6c8 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 @@
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 6e3c827..b5ddc06 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 @@
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 @@
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 @@
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 @@
powerGroup.getUserActivitySummaryLocked())
+ ", mBootCompleted=" + mBootCompleted
+ ", screenBrightnessOverride=" + screenBrightnessOverride
- + ", useAutoBrightness=" + autoBrightness
+ ", mScreenBrightnessBoostInProgress="
+ mScreenBrightnessBoostInProgress
+ ", sQuiescent=" + sQuiescent);
@@ -4488,7 +4474,6 @@
+ mMaximumScreenOffTimeoutFromDeviceAdmin + " (enforced="
+ isMaximumScreenOffTimeoutFromDeviceAdminEnforcedLocked() + ")");
pw.println(" mStayOnWhilePluggedInSetting=" + mStayOnWhilePluggedInSetting);
- pw.println(" mScreenBrightnessModeSetting=" + mScreenBrightnessModeSetting);
pw.println(" mScreenBrightnessOverrideFromWindowManager="
+ mScreenBrightnessOverrideFromWindowManager);
pw.println(" mUserActivityTimeoutOverrideFromWindowManager="
@@ -4866,9 +4851,6 @@
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/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
index 2a88473..855fcaf 100644
--- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
+++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
@@ -763,8 +763,6 @@
@NonNull
private final BatteryStatsHistory mHistory;
- private BatteryStatsHistoryIterator mBatteryStatsHistoryIterator;
-
int mStartCount;
/**
@@ -11242,17 +11240,11 @@
return mHistory.getHistoryUsedSize();
}
- @Override
- public boolean startIteratingHistoryLocked() {
- mBatteryStatsHistoryIterator = createBatteryStatsHistoryIterator();
- return true;
- }
-
/**
* Creates an iterator for battery stats history.
*/
- @VisibleForTesting
- public BatteryStatsHistoryIterator createBatteryStatsHistoryIterator() {
+ @Override
+ public BatteryStatsHistoryIterator iterateBatteryStatsHistory() {
return mHistory.iterate();
}
@@ -11277,17 +11269,6 @@
}
@Override
- public boolean getNextHistoryLocked(HistoryItem out) {
- return mBatteryStatsHistoryIterator.next(out);
- }
-
- @Override
- public void finishIteratingHistoryLocked() {
- mBatteryStatsHistoryIterator.close();
- mBatteryStatsHistoryIterator = null;
- }
-
- @Override
public int getStartCount() {
return mStartCount;
}
diff --git a/services/core/java/com/android/server/trust/TrustManagerService.java b/services/core/java/com/android/server/trust/TrustManagerService.java
index c192057..721872b 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 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 @@
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 05df22f..3be16a1 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.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 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.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 @@
}
}
+ /**
+ * 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 @@
@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 @@
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 @@
mUnderlyingNetworkController.teardown();
mGatewayStatusCallback.onQuit();
+
+ mConnectivityDiagnosticsManager.unregisterConnectivityDiagnosticsCallback(
+ mConnectivityDiagnosticsCallback);
}
/**
@@ -828,6 +885,20 @@
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 @@
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 @@
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 @@
}
}
+ 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 @@
}
@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 4480d52..37450ac 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 @@
* 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 af430f9..2b49a81 100644
--- a/services/core/java/com/android/server/wm/ActivityClientController.java
+++ b/services/core/java/com/android/server/wm/ActivityClientController.java
@@ -17,7 +17,15 @@
package com.android.server.wm;
import static android.Manifest.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS;
+import static android.app.Activity.FULLSCREEN_MODE_REQUEST_ENTER;
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+import static android.app.ActivityTaskManager.INVALID_WINDOWING_MODE;
+import static android.app.FullscreenRequestHandler.REMOTE_CALLBACK_RESULT_KEY;
+import static android.app.FullscreenRequestHandler.RESULT_APPROVED;
+import static android.app.FullscreenRequestHandler.RESULT_FAILED_NOT_DEFAULT_FREEFORM;
+import static android.app.FullscreenRequestHandler.RESULT_FAILED_NOT_IN_FREEFORM;
+import static android.app.FullscreenRequestHandler.RESULT_FAILED_NOT_IN_FULLSCREEN_WITH_HISTORY;
+import static android.app.FullscreenRequestHandler.RESULT_FAILED_NOT_TOP_FOCUSED;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
@@ -27,6 +35,7 @@
import static android.service.voice.VoiceInteractionSession.SHOW_SOURCE_APPLICATION;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.Display.INVALID_DISPLAY;
+import static android.view.WindowManager.TRANSIT_CHANGE;
import static android.view.WindowManager.TRANSIT_TO_BACK;
import static android.view.WindowManager.TRANSIT_TO_FRONT;
@@ -52,6 +61,7 @@
import android.app.Activity;
import android.app.ActivityManager;
import android.app.ActivityTaskManager;
+import android.app.FullscreenRequestHandler;
import android.app.IActivityClientController;
import android.app.ICompatCameraControlCallback;
import android.app.IRequestFinishCallback;
@@ -71,6 +81,7 @@
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
+import android.os.IRemoteCallback;
import android.os.Parcel;
import android.os.PersistableBundle;
import android.os.RemoteException;
@@ -85,6 +96,7 @@
import com.android.internal.app.AssistUtils;
import com.android.internal.policy.IKeyguardDismissCallback;
+import com.android.internal.protolog.ProtoLogGroup;
import com.android.internal.protolog.common.ProtoLog;
import com.android.internal.util.FrameworkStatsLog;
import com.android.server.LocalServices;
@@ -675,29 +687,32 @@
@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;
}
@@ -1056,6 +1071,133 @@
}
}
+ private @FullscreenRequestHandler.RequestResult int validateMultiwindowFullscreenRequestLocked(
+ Task topFocusedRootTask, int fullscreenRequest, ActivityRecord requesterActivity) {
+ // If the mode is not by default freeform, the freeform will be a user-driven event.
+ if (topFocusedRootTask.getParent().getWindowingMode() != WINDOWING_MODE_FREEFORM) {
+ return RESULT_FAILED_NOT_DEFAULT_FREEFORM;
+ }
+ // If this is not coming from the currently top-most activity, reject the request.
+ if (requesterActivity != topFocusedRootTask.getTopMostActivity()) {
+ return RESULT_FAILED_NOT_TOP_FOCUSED;
+ }
+ if (fullscreenRequest == FULLSCREEN_MODE_REQUEST_ENTER) {
+ if (topFocusedRootTask.getWindowingMode() != WINDOWING_MODE_FREEFORM) {
+ return RESULT_FAILED_NOT_IN_FREEFORM;
+ }
+ } else {
+ if (topFocusedRootTask.getWindowingMode() != WINDOWING_MODE_FULLSCREEN) {
+ return RESULT_FAILED_NOT_IN_FULLSCREEN_WITH_HISTORY;
+ }
+ if (topFocusedRootTask.mMultiWindowRestoreWindowingMode == INVALID_WINDOWING_MODE) {
+ return RESULT_FAILED_NOT_IN_FULLSCREEN_WITH_HISTORY;
+ }
+ }
+ return RESULT_APPROVED;
+ }
+
+ @Override
+ public void requestMultiwindowFullscreen(IBinder callingActivity, int fullscreenRequest,
+ IRemoteCallback callback) {
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ synchronized (mGlobalLock) {
+ requestMultiwindowFullscreenLocked(callingActivity, fullscreenRequest, callback);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ private void requestMultiwindowFullscreenLocked(IBinder callingActivity, int fullscreenRequest,
+ IRemoteCallback callback) {
+ final ActivityRecord r = ActivityRecord.forTokenLocked(callingActivity);
+ if (r == null) {
+ return;
+ }
+
+ // If the shell transition is not enabled, just execute and done.
+ final TransitionController controller = r.mTransitionController;
+ if (!controller.isShellTransitionsEnabled()) {
+ final @FullscreenRequestHandler.RequestResult int validateResult;
+ final Task topFocusedRootTask;
+ topFocusedRootTask = mService.getTopDisplayFocusedRootTask();
+ validateResult = validateMultiwindowFullscreenRequestLocked(topFocusedRootTask,
+ fullscreenRequest, r);
+ reportMultiwindowFullscreenRequestValidatingResult(callback, validateResult);
+ if (validateResult == RESULT_APPROVED) {
+ executeMultiWindowFullscreenRequest(fullscreenRequest, topFocusedRootTask);
+ }
+ return;
+ }
+ // Initiate the transition.
+ final Transition transition = new Transition(TRANSIT_CHANGE, 0 /* flags */, controller,
+ mService.mWindowManager.mSyncEngine);
+ if (mService.mWindowManager.mSyncEngine.hasActiveSync()) {
+ ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
+ "Creating Pending Multiwindow Fullscreen Request: %s", transition);
+ mService.mWindowManager.mSyncEngine.queueSyncSet(
+ () -> r.mTransitionController.moveToCollecting(transition),
+ () -> {
+ executeFullscreenRequestTransition(fullscreenRequest, callback, r,
+ transition, true /* queued */);
+ });
+ } else {
+ executeFullscreenRequestTransition(fullscreenRequest, callback, r, transition,
+ false /* queued */);
+ }
+ }
+
+ private void executeFullscreenRequestTransition(int fullscreenRequest, IRemoteCallback callback,
+ ActivityRecord r, Transition transition, boolean queued) {
+ final @FullscreenRequestHandler.RequestResult int validateResult;
+ final Task topFocusedRootTask;
+ topFocusedRootTask = mService.getTopDisplayFocusedRootTask();
+ validateResult = validateMultiwindowFullscreenRequestLocked(topFocusedRootTask,
+ fullscreenRequest, r);
+ reportMultiwindowFullscreenRequestValidatingResult(callback, validateResult);
+ if (validateResult != RESULT_APPROVED) {
+ if (queued) {
+ transition.abort();
+ }
+ return;
+ }
+ r.mTransitionController.requestStartTransition(transition, topFocusedRootTask,
+ null /* remoteTransition */, null /* displayChange */);
+ executeMultiWindowFullscreenRequest(fullscreenRequest, topFocusedRootTask);
+ transition.setReady(topFocusedRootTask, true);
+ }
+
+ private static void reportMultiwindowFullscreenRequestValidatingResult(IRemoteCallback callback,
+ @FullscreenRequestHandler.RequestResult int result) {
+ if (callback == null) {
+ return;
+ }
+ Bundle res = new Bundle();
+ res.putInt(REMOTE_CALLBACK_RESULT_KEY, result);
+ try {
+ callback.sendResult(res);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "client throws an exception back to the server, ignore it");
+ }
+ }
+
+ private static void executeMultiWindowFullscreenRequest(int fullscreenRequest, Task requester) {
+ final int targetWindowingMode;
+ if (fullscreenRequest == FULLSCREEN_MODE_REQUEST_ENTER) {
+ requester.mMultiWindowRestoreWindowingMode =
+ requester.getRequestedOverrideWindowingMode();
+ targetWindowingMode = WINDOWING_MODE_FULLSCREEN;
+ } else {
+ targetWindowingMode = requester.mMultiWindowRestoreWindowingMode;
+ requester.mMultiWindowRestoreWindowingMode = INVALID_WINDOWING_MODE;
+ }
+ requester.setWindowingMode(targetWindowingMode);
+ if (targetWindowingMode == WINDOWING_MODE_FULLSCREEN) {
+ requester.setBounds(null);
+ }
+ }
+
@Override
public void startLockTaskModeByToken(IBinder token) {
synchronized (mGlobalLock) {
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index f725d1a..efd0ffa 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 @@
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 @@
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 @@
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 @@
newParent.setResumedActivity(this, "onParentChanged");
mImeInsetsFrozenUntilStartInput = false;
}
+ mLetterboxUiController.onActivityParentChanged(newParent);
}
if (rootTask != null && rootTask.topRunningActivity() == this) {
@@ -1997,6 +1999,7 @@
mOverrideTaskTransition = options.getOverrideTaskTransition();
mDismissKeyguard = options.getDismissKeyguard();
+ mShareIdentity = options.getShareIdentity();
}
ColorDisplayService.ColorDisplayServiceInternal cds = LocalServices.getService(
@@ -5470,7 +5473,8 @@
// 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 @@
}
@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 @@
@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 @@
@Nullable
CompatDisplayInsets getCompatDisplayInsets() {
+ if (mLetterboxUiController.hasInheritedLetterboxBehavior()) {
+ return mLetterboxUiController.getInheritedCompatDisplayInsets();
+ }
return mCompatDisplayInsets;
}
@@ -7847,6 +7868,10 @@
// 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 @@
@Override
float getCompatScale() {
+ if (mLetterboxUiController.hasInheritedLetterboxBehavior()) {
+ return mLetterboxUiController.getInheritedSizeCompatScale();
+ }
return hasSizeCompatBounds() ? mSizeCompatScale : super.getCompatScale();
}
@@ -8017,6 +8045,16 @@
}
/**
+ * @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 @@
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 @@
}
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 @@
// 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 @@
// 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 @@
* 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 @@
&& 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/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index 2f4d154..2762c45 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -1823,7 +1823,7 @@
if (mPreferredTaskDisplayArea != null) {
final DisplayContent displayContent = mRootWindowContainer.getDisplayContentOrCreate(
mPreferredTaskDisplayArea.getDisplayId());
- if (displayContent != null && displayContent.mDwpcHelper.hasController()) {
+ if (displayContent != null) {
final int targetWindowingMode = (targetTask != null)
? targetTask.getWindowingMode() : displayContent.getWindowingMode();
final int launchingFromDisplayId =
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index 8a247cf..33c90a0 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -395,7 +395,7 @@
final DisplayContent displayContent =
mRootWindowContainer.getDisplayContentOrCreate(displayId);
- if (displayContent != null && displayContent.mDwpcHelper.hasController()) {
+ if (displayContent != null) {
final ArrayList<ActivityInfo> activities = new ArrayList<>();
if (activityInfo != null) {
activities.add(activityInfo);
@@ -405,10 +405,8 @@
activities.add(r.info);
});
}
- if (!displayContent.mDwpcHelper.canContainActivities(activities,
- displayContent.getWindowingMode())) {
- return false;
- }
+ return displayContent.mDwpcHelper.canContainActivities(activities,
+ displayContent.getWindowingMode());
}
return true;
diff --git a/services/core/java/com/android/server/wm/ContentRecorder.java b/services/core/java/com/android/server/wm/ContentRecorder.java
index 6e23ed9..8d5d0d5 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.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 @@
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 @@
.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 @@
@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/DisplayWindowPolicyControllerHelper.java b/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java
index 69fd00c..72ed108 100644
--- a/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java
+++ b/services/core/java/com/android/server/wm/DisplayWindowPolicyControllerHelper.java
@@ -22,12 +22,14 @@
import android.content.pm.ActivityInfo;
import android.os.UserHandle;
import android.util.ArraySet;
+import android.util.Slog;
import android.window.DisplayWindowPolicyController;
import java.io.PrintWriter;
import java.util.List;
class DisplayWindowPolicyControllerHelper {
+ private static final String TAG = "DisplayWindowPolicyControllerHelper";
private final DisplayContent mDisplayContent;
@@ -69,6 +71,17 @@
public boolean canContainActivities(@NonNull List<ActivityInfo> activities,
@WindowConfiguration.WindowingMode int windowingMode) {
if (mDisplayWindowPolicyController == null) {
+ for (int i = activities.size() - 1; i >= 0; i--) {
+ final ActivityInfo aInfo = activities.get(i);
+ if (aInfo.requiredDisplayCategory != null) {
+ Slog.e(TAG,
+ String.format("Activity with requiredDisplayCategory='%s' cannot be"
+ + " displayed on display %d because that display does"
+ + " not have a matching category",
+ aInfo.requiredDisplayCategory, mDisplayContent.mDisplayId));
+ return false;
+ }
+ }
return true;
}
return mDisplayWindowPolicyController.canContainActivities(activities, windowingMode);
@@ -81,6 +94,14 @@
@WindowConfiguration.WindowingMode int windowingMode, int launchingFromDisplayId,
boolean isNewTask) {
if (mDisplayWindowPolicyController == null) {
+ if (activityInfo.requiredDisplayCategory != null) {
+ Slog.e(TAG,
+ String.format("Activity with requiredDisplayCategory='%s' cannot be"
+ + " launched on display %d because that display does"
+ + " not have a matching category",
+ activityInfo.requiredDisplayCategory, mDisplayContent.mDisplayId));
+ return false;
+ }
return true;
}
return mDisplayWindowPolicyController.canActivityBeLaunched(activityInfo, windowingMode,
diff --git a/services/core/java/com/android/server/wm/LetterboxConfiguration.java b/services/core/java/com/android/server/wm/LetterboxConfiguration.java
index c19353c..127a7bf 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.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 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 @@
// 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 @@
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 @@
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 @@
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 bcea6f4..a53a5fc 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_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 @@
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 @@
: 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 @@
}
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 @@
}
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 @@
&& 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 @@
&& 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 @@
// 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 @@
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 449e77f..d395f12 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.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.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 @@
// 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 03e3e93..07e3b83 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -18,6 +18,7 @@
import static android.app.ActivityManager.isStartResultSuccessful;
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
+import static android.app.ActivityTaskManager.INVALID_WINDOWING_MODE;
import static android.app.ActivityTaskManager.RESIZE_MODE_FORCED;
import static android.app.ActivityTaskManager.RESIZE_MODE_SYSTEM_SCREEN_ROTATION;
import static android.app.ITaskStackListener.FORCED_RESIZEABLE_REASON_SPLIT_SCREEN;
@@ -443,6 +444,8 @@
@Surface.Rotation
private int mRotation;
+ int mMultiWindowRestoreWindowingMode = INVALID_WINDOWING_MODE;
+
/**
* Last requested orientation reported to DisplayContent. This is different from {@link
* #mOrientation} in the sense that this takes activities' requested orientation into
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 9126586..d1cc4b3 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -2540,7 +2540,7 @@
mRemoteToken.toWindowContainerToken(),
getConfiguration(),
getNonFinishingActivityCount(),
- isVisibleRequested(),
+ shouldBeVisible(null /* starting */),
childActivities,
positionInParent,
mClearedTaskForReuse,
@@ -2830,6 +2830,14 @@
return getWindowingMode() == WINDOWING_MODE_FULLSCREEN || matchParentBounds();
}
+ @Override
+ protected boolean onChildVisibleRequestedChanged(@Nullable WindowContainer child) {
+ if (!super.onChildVisibleRequestedChanged(child)) return false;
+ // Send the info changed to update the TaskFragment visibility.
+ sendTaskFragmentInfoChanged();
+ return true;
+ }
+
@Nullable
@Override
TaskFragment getTaskFragment(Predicate<TaskFragment> callback) {
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index 1d17cd4..64574a7 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 @@
if (showSurfaceOnCreation()) {
getSyncTransaction().show(mSurfaceControl);
}
- onSurfaceShown(getSyncTransaction());
updateSurfacePositionNonOrganized();
if (mLastMagnificationSpec != null) {
applyMagnificationSpec(getSyncTransaction(), mLastMagnificationSpec);
@@ -697,13 +696,6 @@
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 @@
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/WindowManagerShellCommand.java b/services/core/java/com/android/server/wm/WindowManagerShellCommand.java
index 46a30fb..060784d 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 @@
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 @@
case "--isSplitScreenAspectRatioForUnresizableAppsEnabled":
runSetLetterboxIsSplitScreenAspectRatioForUnresizableAppsEnabled(pw);
break;
+ case "--isTranslucentLetterboxingEnabled":
+ runSetTranslucentLetterboxingEnabled(pw);
+ break;
default:
getErrPrintWriter().println(
"Error: Unrecognized letterbox style option: " + arg);
@@ -1096,6 +1122,9 @@
mLetterboxConfiguration
.getIsSplitScreenAspectRatioForUnresizableAppsEnabled();
break;
+ case "isTranslucentLetterboxingEnabled":
+ mLetterboxConfiguration.resetTranslucentLetterboxingEnabled();
+ break;
default:
getErrPrintWriter().println(
"Error: Unrecognized letterbox style option: " + arg);
@@ -1196,6 +1225,7 @@
mLetterboxConfiguration.resetDefaultPositionForVerticalReachability();
mLetterboxConfiguration.resetIsEducationEnabled();
mLetterboxConfiguration.resetIsSplitScreenAspectRatioForUnresizableAppsEnabled();
+ mLetterboxConfiguration.resetTranslucentLetterboxingEnabled();
}
}
@@ -1232,7 +1262,6 @@
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 @@
+ 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 @@
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/utils/CoordinateTransforms.java b/services/core/java/com/android/server/wm/utils/CoordinateTransforms.java
index a2f37a5..7dc8a31 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_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 @@
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 c11a6d0..0000000
--- 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 07819b9..e661688 100644
--- a/services/core/jni/Android.bp
+++ b/services/core/jni/Android.bp
@@ -71,7 +71,6 @@
"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 @@
"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 @@
"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 145e088..c36c571 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 @@
jmethodID getExcludedDeviceNames;
jmethodID getInputPortAssociations;
jmethodID getInputUniqueIdAssociations;
+ jmethodID getDeviceTypeAssociations;
jmethodID getKeyRepeatTimeout;
jmethodID getKeyRepeatDelay;
jmethodID getHoverTapTimeout;
@@ -411,6 +412,8 @@
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 @@
}
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 @@
} // 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 @@
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 @@
{"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 @@
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 9633a11..0000000
--- 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 1845057..00f851f 100644
--- a/services/core/jni/onload.cpp
+++ b/services/core/jni/onload.cpp
@@ -56,7 +56,6 @@
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 @@
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/core/jni/onload_settings.cpp b/services/core/jni/onload_settings.cpp
deleted file mode 100644
index b21c34a..0000000
--- a/services/core/jni/onload_settings.cpp
+++ /dev/null
@@ -1,39 +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.
- */
-
-#include "jni.h"
-#include "utils/Log.h"
-
-namespace android {
-int register_android_server_com_android_server_pm_Settings(JNIEnv* env);
-};
-
-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);
-
- return JNI_VERSION_1_4;
-}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java b/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java
index 9af30ba..4634ff5 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/ActiveAdmin.java
@@ -163,6 +163,7 @@
"preferential_network_service_config";
private static final String TAG_PROTECTED_PACKAGES = "protected_packages";
private static final String TAG_SUSPENDED_PACKAGES = "suspended-packages";
+ private static final String TAG_MTE_POLICY = "mte-policy";
private static final String ATTR_VALUE = "value";
private static final String ATTR_LAST_NETWORK_LOGGING_NOTIFICATION = "last-notification";
private static final String ATTR_NUM_NETWORK_LOGGING_NOTIFICATIONS = "num-notifications";
@@ -222,6 +223,8 @@
int numNetworkLoggingNotifications = 0;
long lastNetworkLoggingNotificationTimeMs = 0; // Time in milliseconds since epoch
+ @DevicePolicyManager.MtePolicy int mtePolicy = DevicePolicyManager.MTE_NOT_CONTROLLED_BY_POLICY;
+
ActiveAdmin parentAdmin;
final boolean isParent;
@@ -620,6 +623,9 @@
}
out.endTag(null, TAG_PREFERENTIAL_NETWORK_SERVICE_CONFIGS);
}
+ if (mtePolicy != DevicePolicyManager.MTE_NOT_CONTROLLED_BY_POLICY) {
+ writeAttributeValueToXml(out, TAG_MTE_POLICY, mtePolicy);
+ }
}
private List<String> ssidsToStrings(Set<WifiSsid> ssids) {
@@ -906,6 +912,8 @@
if (!configs.isEmpty()) {
mPreferentialNetworkServiceConfigs = configs;
}
+ } else if (TAG_MTE_POLICY.equals(tag)) {
+ mtePolicy = parser.getAttributeInt(null, ATTR_VALUE);
} else {
Slogf.w(LOG_TAG, "Unknown admin tag: %s", tag);
XmlUtils.skipCurrentTag(parser);
@@ -1338,5 +1346,8 @@
}
pw.decreaseIndent();
}
+
+ pw.print("mtePolicy=");
+ pw.println(mtePolicy);
}
}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index e93809d..c42ddf8 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -5387,7 +5387,8 @@
}
if (!mInjector.storageManagerIsFileBasedEncryptionEnabled()) {
throw new UnsupportedOperationException(
- "FLAG_EVICT_CREDENTIAL_ENCRYPTION_KEY only applies to FBE devices");
+ "FLAG_EVICT_CREDENTIAL_ENCRYPTION_KEY only applies to FBE"
+ + " devices");
}
mUserManager.evictCredentialEncryptionKey(callingUserId);
}
@@ -19240,4 +19241,55 @@
KEEP_PROFILES_RUNNING_FLAG,
DEFAULT_KEEP_PROFILES_RUNNING_FLAG);
}
+
+ @Override
+ public void setMtePolicy(int flags) {
+ final Set<Integer> allowedModes =
+ Set.of(
+ DevicePolicyManager.MTE_NOT_CONTROLLED_BY_POLICY,
+ DevicePolicyManager.MTE_DISABLED,
+ DevicePolicyManager.MTE_ENABLED);
+ Preconditions.checkArgument(
+ allowedModes.contains(flags), "Provided mode is not one of the allowed values.");
+ final CallerIdentity caller = getCallerIdentity();
+ if (flags == DevicePolicyManager.MTE_DISABLED) {
+ Preconditions.checkCallAuthorization(isDefaultDeviceOwner(caller));
+ } else {
+ Preconditions.checkCallAuthorization(
+ isDefaultDeviceOwner(caller)
+ || isProfileOwnerOfOrganizationOwnedDevice(caller));
+ }
+ synchronized (getLockObject()) {
+ ActiveAdmin admin =
+ getDeviceOwnerOrProfileOwnerOfOrganizationOwnedDeviceLocked(
+ UserHandle.USER_SYSTEM);
+ if (admin != null) {
+ final String memtagProperty = "arm64.memtag.bootctl";
+ if (flags == DevicePolicyManager.MTE_ENABLED) {
+ mInjector.systemPropertiesSet(memtagProperty, "memtag");
+ } else if (flags == DevicePolicyManager.MTE_DISABLED) {
+ mInjector.systemPropertiesSet(memtagProperty, "memtag-off");
+ }
+ admin.mtePolicy = flags;
+ saveSettingsLocked(caller.getUserId());
+ }
+ }
+ }
+
+ @Override
+ public int getMtePolicy() {
+ final CallerIdentity caller = getCallerIdentity();
+ Preconditions.checkCallAuthorization(
+ isDefaultDeviceOwner(caller)
+ || isProfileOwnerOfOrganizationOwnedDevice(caller)
+ || isSystemUid(caller));
+ synchronized (getLockObject()) {
+ ActiveAdmin admin =
+ getDeviceOwnerOrProfileOwnerOfOrganizationOwnedDeviceLocked(
+ UserHandle.USER_SYSTEM);
+ return admin != null
+ ? admin.mtePolicy
+ : DevicePolicyManager.MTE_NOT_CONTROLLED_BY_POLICY;
+ }
+ }
}
diff --git a/services/java/com/android/server/BootUserInitializer.java b/services/java/com/android/server/BootUserInitializer.java
index c3329795..deebfc7 100644
--- a/services/java/com/android/server/BootUserInitializer.java
+++ b/services/java/com/android/server/BootUserInitializer.java
@@ -102,6 +102,7 @@
switchToInitialUser(initialUserId);
}
+ /* TODO(b/261791491): STOPSHIP - SUW should be responsible for this. */
private void provisionHeadlessSystemUser() {
if (isDeviceProvisioned()) {
Slogf.d(TAG, "provisionHeadlessSystemUser(): already provisioned");
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 509d75b..c68eea8 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.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 @@
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 @@
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 73c2ccc..f493b89 100644
--- a/services/permission/java/com/android/server/permission/access/AccessCheckingService.kt
+++ b/services/permission/java/com/android/server/permission/access/AccessCheckingService.kt
@@ -20,15 +20,15 @@
import com.android.internal.annotations.Keep
import com.android.server.LocalManagerRegistry
import com.android.server.LocalServices
+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
import com.android.server.pm.permission.PermissionManagerServiceInterface
-import com.android.server.pm.permission.PermissionManagerServiceInternal
import com.android.server.pm.pkg.PackageState
@Keep
@@ -46,6 +46,7 @@
private lateinit var packageManagerLocal: PackageManagerLocal
private lateinit var userManagerService: UserManagerService
+ private lateinit var systemConfig: SystemConfig
override fun onStart() {
appOpService = AppOpService(this)
@@ -59,12 +60,16 @@
packageManagerLocal =
LocalManagerRegistry.getManagerOrThrow(PackageManagerLocal::class.java)
userManagerService = UserManagerService.getInstance()
+ systemConfig = SystemConfig.getInstance()
val userIds = IntSet(userManagerService.userIdsIncludingPreCreated)
- val packageStates = packageManagerLocal.packageStates
+ val (packageStates, disabledSystemPackageStates) = packageManagerLocal.allPackageStates
+ val permissionAllowlist = systemConfig.permissionAllowlist
val state = AccessState()
- policy.initialize(state, userIds, packageStates)
+ policy.initialize(
+ state, userIds, packageStates, disabledSystemPackageStates, permissionAllowlist
+ )
persistence.read(state)
this.state = state
@@ -96,51 +101,60 @@
}
internal fun onStorageVolumeMounted(volumeUuid: String?, isSystemUpdated: Boolean) {
- val packageStates = packageManagerLocal.packageStates
+ val (packageStates, disabledSystemPackageStates) = packageManagerLocal.allPackageStates
mutateState {
- with(policy) { onStorageVolumeMounted(packageStates, volumeUuid, isSystemUpdated) }
+ with(policy) {
+ onStorageVolumeMounted(
+ packageStates, disabledSystemPackageStates, volumeUuid, isSystemUpdated
+ )
+ }
}
}
internal fun onPackageAdded(packageName: String) {
- val packageStates = packageManagerLocal.packageStates
+ val (packageStates, disabledSystemPackageStates) = packageManagerLocal.allPackageStates
mutateState {
- with(policy) { onPackageAdded(packageStates, packageName) }
+ with(policy) { onPackageAdded(packageStates, disabledSystemPackageStates, packageName) }
}
}
internal fun onPackageRemoved(packageName: String, appId: Int) {
- val packageStates = packageManagerLocal.packageStates
+ val (packageStates, disabledSystemPackageStates) = packageManagerLocal.allPackageStates
mutateState {
- with(policy) { onPackageRemoved(packageStates, packageName, appId) }
+ with(policy) {
+ onPackageRemoved(packageStates, disabledSystemPackageStates, packageName, appId)
+ }
}
}
- internal fun onPackageInstalled(
- packageName: String,
- params: PermissionManagerServiceInternal.PackageInstalledParams,
- userId: Int
- ) {
- val packageStates = packageManagerLocal.packageStates
+ internal fun onPackageInstalled(packageName: String, userId: Int) {
+ val (packageStates, disabledSystemPackageStates) = packageManagerLocal.allPackageStates
mutateState {
- with(policy) { onPackageInstalled(packageStates, packageName, params, userId) }
+ with(policy) {
+ onPackageInstalled(packageStates, disabledSystemPackageStates, packageName, userId)
+ }
}
}
internal fun onPackageUninstalled(packageName: String, appId: Int, userId: Int) {
- val packageStates = packageManagerLocal.packageStates
+ val (packageStates, disabledSystemPackageStates) = packageManagerLocal.allPackageStates
mutateState {
- with(policy) { onPackageUninstalled(packageStates, packageName, appId, userId) }
+ with(policy) {
+ onPackageUninstalled(
+ packageStates, disabledSystemPackageStates, packageName, appId, userId
+ )
+ }
}
}
- private val PackageManagerLocal.packageStates: Map<String, PackageState>
- get() = withUnfilteredSnapshot().use { it.packageStates }
+ private val PackageManagerLocal.allPackageStates:
+ Pair<Map<String, PackageState>, Map<String, PackageState>>
+ get() = withUnfilteredSnapshot().use { it.packageStates to it.disabledSystemPackageStates }
internal inline fun <T> getState(action: GetStateScope.() -> T): T =
GetStateScope(state).action()
- internal inline fun mutateState(action: MutateStateScope.() -> Unit) {
+ internal inline fun mutateState(crossinline action: MutateStateScope.() -> Unit) {
synchronized(stateLock) {
val oldState = state
val newState = oldState.copy()
diff --git a/services/permission/java/com/android/server/permission/access/AccessPolicy.kt b/services/permission/java/com/android/server/permission/access/AccessPolicy.kt
index 0201dd0..89316c2 100644
--- a/services/permission/java/com/android/server/permission/access/AccessPolicy.kt
+++ b/services/permission/java/com/android/server/permission/access/AccessPolicy.kt
@@ -26,7 +26,7 @@
import com.android.server.permission.access.util.forEachTag
import com.android.server.permission.access.util.tag
import com.android.server.permission.access.util.tagName
-import com.android.server.pm.permission.PermissionManagerServiceInternal
+import com.android.server.pm.permission.PermissionAllowlist
import com.android.server.pm.pkg.PackageState
class AccessPolicy private constructor(
@@ -54,14 +54,22 @@
with(getSchemePolicy(subject, `object`)) { setDecision(subject, `object`, decision) }
}
- fun initialize(state: AccessState, userIds: IntSet, packageStates: Map<String, PackageState>) {
+ fun initialize(
+ state: AccessState,
+ userIds: IntSet,
+ packageStates: Map<String, PackageState>,
+ disabledSystemPackageStates: Map<String, PackageState>,
+ permissionAllowlist: PermissionAllowlist
+ ) {
state.systemState.apply {
this.userIds += userIds
this.packageStates = packageStates
+ this.disabledSystemPackageStates = disabledSystemPackageStates
packageStates.forEach { (_, packageState) ->
appIds.getOrPut(packageState.appId) { IndexedListSet() }
.add(packageState.packageName)
}
+ this.permissionAllowlist = permissionAllowlist
}
}
@@ -83,10 +91,14 @@
fun MutateStateScope.onStorageVolumeMounted(
packageStates: Map<String, PackageState>,
+ disabledSystemPackageStates: Map<String, PackageState>,
volumeUuid: String?,
isSystemUpdated: Boolean
) {
- newState.systemState.packageStates = packageStates
+ newState.systemState.apply {
+ this.packageStates = packageStates
+ this.disabledSystemPackageStates = disabledSystemPackageStates
+ }
forEachSchemePolicy {
with(it) { onStorageVolumeMounted(volumeUuid, isSystemUpdated) }
}
@@ -94,18 +106,24 @@
fun MutateStateScope.onPackageAdded(
packageStates: Map<String, PackageState>,
+ disabledSystemPackageStates: Map<String, PackageState>,
packageName: String
) {
- newState.systemState.packageStates = packageStates
- var isAppIdAdded = false
val packageState = packageStates[packageName]
- // TODO(zhanghai): Remove check before submission.
- checkNotNull(packageState)
+ // TODO(zhanghai): STOPSHIP: Remove check before feature enable.
+ checkNotNull(packageState) {
+ "Added package $packageName isn't found in packageStates in onPackageAdded()"
+ }
val appId = packageState.appId
- newState.systemState.appIds.getOrPut(appId) {
- isAppIdAdded = true
- IndexedListSet()
- }.add(packageName)
+ var isAppIdAdded = false
+ newState.systemState.apply {
+ this.packageStates = packageStates
+ this.disabledSystemPackageStates = disabledSystemPackageStates
+ appIds.getOrPut(appId) {
+ isAppIdAdded = true
+ IndexedListSet()
+ }.add(packageName)
+ }
if (isAppIdAdded) {
forEachSchemePolicy {
with(it) { onAppIdAdded(appId) }
@@ -118,18 +136,22 @@
fun MutateStateScope.onPackageRemoved(
packageStates: Map<String, PackageState>,
+ disabledSystemPackageStates: Map<String, PackageState>,
packageName: String,
appId: Int
) {
- newState.systemState.packageStates = packageStates
+ // TODO(zhanghai): STOPSHIP: Remove check before feature enable.
+ check(packageName !in packageStates) {
+ "Removed package $packageName is still in packageStates in onPackageRemoved()"
+ }
var isAppIdRemoved = false
- // TODO(zhanghai): Remove check before submission.
- check(packageName !in packageStates)
- newState.systemState.appIds.apply appIds@{
- this[appId]?.apply {
+ newState.systemState.apply {
+ this.packageStates = packageStates
+ this.disabledSystemPackageStates = disabledSystemPackageStates
+ appIds[appId]?.apply {
this -= packageName
if (isEmpty()) {
- this@appIds -= appId
+ appIds -= appId
isAppIdRemoved = true
}
}
@@ -146,26 +168,35 @@
fun MutateStateScope.onPackageInstalled(
packageStates: Map<String, PackageState>,
+ disabledSystemPackageStates: Map<String, PackageState>,
packageName: String,
- params: PermissionManagerServiceInternal.PackageInstalledParams,
userId: Int
) {
- newState.systemState.packageStates = packageStates
+ newState.systemState.apply {
+ this.packageStates = packageStates
+ this.disabledSystemPackageStates = disabledSystemPackageStates
+ }
val packageState = packageStates[packageName]
- // TODO(zhanghai): Remove check before submission.
- checkNotNull(packageState)
+ // TODO(zhanghai): STOPSHIP: Remove check before feature enable.
+ checkNotNull(packageState) {
+ "Installed package $packageName isn't found in packageStates in onPackageInstalled()"
+ }
forEachSchemePolicy {
- with(it) { onPackageInstalled(packageState, params, userId) }
+ with(it) { onPackageInstalled(packageState, userId) }
}
}
fun MutateStateScope.onPackageUninstalled(
packageStates: Map<String, PackageState>,
+ disabledSystemPackageStates: Map<String, PackageState>,
packageName: String,
appId: Int,
userId: Int
) {
- newState.systemState.packageStates = packageStates
+ newState.systemState.apply {
+ this.packageStates = packageStates
+ this.disabledSystemPackageStates = disabledSystemPackageStates
+ }
forEachSchemePolicy {
with(it) { onPackageUninstalled(packageName, appId, userId) }
}
@@ -241,10 +272,6 @@
}
abstract class SchemePolicy {
- @Volatile
- private var onDecisionChangedListeners = IndexedListSet<OnDecisionChangedListener>()
- private val onDecisionChangedListenersLock = Any()
-
abstract val subjectScheme: String
abstract val objectScheme: String
@@ -257,30 +284,6 @@
decision: Int
)
- fun addOnDecisionChangedListener(listener: OnDecisionChangedListener) {
- synchronized(onDecisionChangedListenersLock) {
- onDecisionChangedListeners = onDecisionChangedListeners + listener
- }
- }
-
- fun removeOnDecisionChangedListener(listener: OnDecisionChangedListener) {
- synchronized(onDecisionChangedListenersLock) {
- onDecisionChangedListeners = onDecisionChangedListeners - listener
- }
- }
-
- protected fun notifyOnDecisionChangedListeners(
- subject: AccessUri,
- `object`: AccessUri,
- oldDecision: Int,
- newDecision: Int
- ) {
- val listeners = onDecisionChangedListeners
- listeners.forEachIndexed { _, it ->
- it.onDecisionChanged(subject, `object`, oldDecision, newDecision)
- }
- }
-
open fun MutateStateScope.onUserAdded(userId: Int) {}
open fun MutateStateScope.onUserRemoved(userId: Int) {}
@@ -298,11 +301,7 @@
open fun MutateStateScope.onPackageRemoved(packageName: String, appId: Int) {}
- open fun MutateStateScope.onPackageInstalled(
- packageState: PackageState,
- params: PermissionManagerServiceInternal.PackageInstalledParams,
- userId: Int
- ) {}
+ open fun MutateStateScope.onPackageInstalled(packageState: PackageState, userId: Int) {}
open fun MutateStateScope.onPackageUninstalled(packageName: String, appId: Int, userId: Int) {}
@@ -313,13 +312,4 @@
open fun BinaryXmlPullParser.parseUserState(userId: Int, userState: UserState) {}
open fun BinaryXmlSerializer.serializeUserState(userId: Int, userState: UserState) {}
-
- fun interface OnDecisionChangedListener {
- fun onDecisionChanged(
- subject: AccessUri,
- `object`: AccessUri,
- oldDecision: Int,
- newDecision: Int
- )
- }
}
diff --git a/services/permission/java/com/android/server/permission/access/AccessState.kt b/services/permission/java/com/android/server/permission/access/AccessState.kt
index 4a2c78a..fae1ec3 100644
--- a/services/permission/java/com/android/server/permission/access/AccessState.kt
+++ b/services/permission/java/com/android/server/permission/access/AccessState.kt
@@ -19,15 +19,22 @@
import android.content.pm.PermissionGroupInfo
import com.android.server.permission.access.collection.* // ktlint-disable no-wildcard-imports
import com.android.server.permission.access.permission.Permission
+import com.android.server.pm.permission.PermissionAllowlist
import com.android.server.pm.pkg.PackageState
class AccessState private constructor(
val systemState: SystemState,
val userStates: IntMap<UserState>
) {
- constructor() : this(SystemState(), IntMap())
+ constructor() : this(
+ SystemState(),
+ IntMap()
+ )
- fun copy(): AccessState = AccessState(systemState.copy(), userStates.copy { it.copy() })
+ fun copy(): AccessState = AccessState(
+ systemState.copy(),
+ userStates.copy { it.copy() }
+ )
}
class SystemState private constructor(
@@ -39,30 +46,26 @@
val knownPackages: IntMap<IndexedListSet<String>>,
// A map of userId to packageName
val deviceAndProfileOwners: IntMap<String>,
- // A map of packageName to (A map of oem permission name to whether it's granted)
- val oemPermissions: IndexedMap<String, IndexedMap<String, Boolean>>,
val privilegedPermissionAllowlistSourcePackageNames: IndexedListSet<String>,
- // A map of packageName to a set of vendor priv app permission names
- val vendorPrivAppPermissions: Map<String, Set<String>>,
- val productPrivAppPermissions: Map<String, Set<String>>,
- val systemExtPrivAppPermissions: Map<String, Set<String>>,
- val privAppPermissions: Map<String, Set<String>>,
- val apexPrivAppPermissions: Map<String, Map<String, Set<String>>>,
- val vendorPrivAppDenyPermissions: Map<String, Set<String>>,
- val productPrivAppDenyPermissions: Map<String, Set<String>>,
- val systemExtPrivAppDenyPermissions: Map<String, Set<String>>,
- val apexPrivAppDenyPermissions: Map<String, Map<String, Set<String>>>,
- val privAppDenyPermissions: Map<String, Set<String>>,
+ var permissionAllowlist: PermissionAllowlist,
val implicitToSourcePermissions: Map<String, Set<String>>,
val permissionGroups: IndexedMap<String, PermissionGroupInfo>,
val permissionTrees: IndexedMap<String, Permission>,
val permissions: IndexedMap<String, Permission>
) : WritableState() {
constructor() : this(
- IntSet(), emptyMap(), emptyMap(), IntMap(), IntMap(), IntMap(), IndexedMap(),
- IndexedListSet(), IndexedMap(), IndexedMap(), IndexedMap(), IndexedMap(), IndexedMap(),
- IndexedMap(), IndexedMap(), IndexedMap(), IndexedMap(), IndexedMap(), IndexedMap(),
- IndexedMap(), IndexedMap(), IndexedMap()
+ IntSet(),
+ emptyMap(),
+ emptyMap(),
+ IntMap(),
+ IntMap(),
+ IntMap(),
+ IndexedListSet(),
+ PermissionAllowlist(),
+ IndexedMap(),
+ IndexedMap(),
+ IndexedMap(),
+ IndexedMap()
)
fun copy(): SystemState =
@@ -73,18 +76,8 @@
appIds.copy { it.copy() },
knownPackages.copy { it.copy() },
deviceAndProfileOwners.copy { it },
- oemPermissions.copy { it.copy { it } },
privilegedPermissionAllowlistSourcePackageNames.copy(),
- vendorPrivAppPermissions,
- productPrivAppPermissions,
- systemExtPrivAppPermissions,
- privAppPermissions,
- apexPrivAppPermissions,
- vendorPrivAppDenyPermissions,
- productPrivAppDenyPermissions,
- systemExtPrivAppDenyPermissions,
- apexPrivAppDenyPermissions,
- privAppDenyPermissions,
+ permissionAllowlist,
implicitToSourcePermissions,
permissionGroups.copy { it },
permissionTrees.copy { it },
@@ -98,7 +91,11 @@
val uidAppOpModes: IntMap<IndexedMap<String, Int>>,
val packageAppOpModes: IndexedMap<String, IndexedMap<String, Int>>
) : WritableState() {
- constructor() : this(IntMap(), IntMap(), IndexedMap())
+ constructor() : this(
+ IntMap(),
+ IntMap(),
+ IndexedMap()
+ )
fun copy(): UserState = UserState(
uidPermissionFlags.copy { it.copy { it } },
diff --git a/services/permission/java/com/android/server/permission/access/appop/BaseAppOpPolicy.kt b/services/permission/java/com/android/server/permission/access/appop/BaseAppOpPolicy.kt
index a1a5e2d..7f4e0f7 100644
--- a/services/permission/java/com/android/server/permission/access/appop/BaseAppOpPolicy.kt
+++ b/services/permission/java/com/android/server/permission/access/appop/BaseAppOpPolicy.kt
@@ -16,50 +16,17 @@
package com.android.server.permission.access.appop
-import android.app.AppOpsManager
import com.android.modules.utils.BinaryXmlPullParser
import com.android.modules.utils.BinaryXmlSerializer
-import com.android.server.permission.access.AccessUri
import com.android.server.permission.access.AppOpUri
-import com.android.server.permission.access.GetStateScope
-import com.android.server.permission.access.MutateStateScope
import com.android.server.permission.access.SchemePolicy
import com.android.server.permission.access.UserState
-import com.android.server.permission.access.collection.* // ktlint-disable no-wildcard-imports
-abstract class BaseAppOpPolicy(private val persistence: BaseAppOpPersistence) : SchemePolicy() {
- override fun GetStateScope.getDecision(subject: AccessUri, `object`: AccessUri): Int {
- `object` as AppOpUri
- return getModes(subject)
- .getWithDefault(`object`.appOpName, opToDefaultMode(`object`.appOpName))
- }
-
- override fun MutateStateScope.setDecision(
- subject: AccessUri,
- `object`: AccessUri,
- decision: Int
- ) {
- `object` as AppOpUri
- val modes = getOrCreateModes(subject)
- val oldMode = modes.putWithDefault(`object`.appOpName, decision,
- opToDefaultMode(`object`.appOpName))
- if (modes.isEmpty()) {
- removeModes(subject)
- }
- if (oldMode != decision) {
- notifyOnDecisionChangedListeners(subject, `object`, oldMode, decision)
- }
- }
-
- abstract fun GetStateScope.getModes(subject: AccessUri): IndexedMap<String, Int>?
-
- abstract fun MutateStateScope.getOrCreateModes(subject: AccessUri): IndexedMap<String, Int>
-
- abstract fun MutateStateScope.removeModes(subject: AccessUri)
-
- // TODO need to check that [AppOpsManager.getSystemAlertWindowDefault] works; likely no issue
- // since running in system process.
- private fun opToDefaultMode(appOpName: String) = AppOpsManager.opToDefaultMode(appOpName)
+abstract class BaseAppOpPolicy(
+ private val persistence: BaseAppOpPersistence
+) : SchemePolicy() {
+ override val objectScheme: String
+ get() = AppOpUri.SCHEME
override fun BinaryXmlPullParser.parseUserState(userId: Int, userState: UserState) {
with(persistence) { this@parseUserState.parseUserState(userId, userState) }
diff --git a/services/permission/java/com/android/server/permission/access/appop/PackageAppOpPolicy.kt b/services/permission/java/com/android/server/permission/access/appop/PackageAppOpPolicy.kt
index f4d6bfd..607e512 100644
--- a/services/permission/java/com/android/server/permission/access/appop/PackageAppOpPolicy.kt
+++ b/services/permission/java/com/android/server/permission/access/appop/PackageAppOpPolicy.kt
@@ -16,40 +16,101 @@
package com.android.server.permission.access.appop
+import android.app.AppOpsManager
import com.android.server.permission.access.AccessUri
import com.android.server.permission.access.AppOpUri
import com.android.server.permission.access.GetStateScope
import com.android.server.permission.access.MutateStateScope
import com.android.server.permission.access.PackageUri
-import com.android.server.permission.access.UserState
import com.android.server.permission.access.collection.* // ktlint-disable no-wildcard-imports
class PackageAppOpPolicy : BaseAppOpPolicy(PackageAppOpPersistence()) {
+ @Volatile
+ private var onAppOpModeChangedListeners = IndexedListSet<OnAppOpModeChangedListener>()
+ private val onAppOpModeChangedListenersLock = Any()
+
override val subjectScheme: String
get() = PackageUri.SCHEME
- override val objectScheme: String
- get() = AppOpUri.SCHEME
-
- override fun GetStateScope.getModes(subject: AccessUri): IndexedMap<String, Int>? {
+ override fun GetStateScope.getDecision(subject: AccessUri, `object`: AccessUri): Int {
subject as PackageUri
- return state.userStates[subject.userId]?.packageAppOpModes?.get(subject.packageName)
+ `object` as AppOpUri
+ return getAppOpMode(subject.packageName, subject.userId, `object`.appOpName)
}
- override fun MutateStateScope.getOrCreateModes(subject: AccessUri): IndexedMap<String, Int> {
+ override fun MutateStateScope.setDecision(
+ subject: AccessUri,
+ `object`: AccessUri,
+ decision: Int
+ ) {
subject as PackageUri
- return newState.userStates.getOrPut(subject.userId) { UserState() }
- .packageAppOpModes.getOrPut(subject.packageName) { IndexedMap() }
- }
-
- override fun MutateStateScope.removeModes(subject: AccessUri) {
- subject as PackageUri
- newState.userStates[subject.userId]?.packageAppOpModes?.remove(subject.packageName)
+ `object` as AppOpUri
+ setAppOpMode(subject.packageName, subject.userId, `object`.appOpName, decision)
}
override fun MutateStateScope.onPackageRemoved(packageName: String, appId: Int) {
newState.userStates.forEachIndexed { _, _, userState ->
userState.packageAppOpModes -= packageName
+ userState.requestWrite()
+ // Skip notifying the change listeners since the package no longer exists.
}
}
+
+ fun MutateStateScope.removeAppOpModes(packageName: String, userId: Int): Boolean =
+ newState.userStates[userId].packageAppOpModes.remove(packageName) != null
+
+ fun GetStateScope.getAppOpMode(packageName: String, userId: Int, appOpName: String): Int =
+ state.userStates[userId].packageAppOpModes[packageName]
+ .getWithDefault(appOpName, AppOpsManager.opToDefaultMode(appOpName))
+
+ fun MutateStateScope.setAppOpMode(
+ packageName: String,
+ userId: Int,
+ appOpName: String,
+ mode: Int
+ ): Boolean {
+ val userState = newState.userStates[userId]
+ val packageAppOpModes = userState.packageAppOpModes
+ var appOpModes = packageAppOpModes[packageName]
+ val defaultMode = AppOpsManager.opToDefaultMode(appOpName)
+ val oldMode = appOpModes.getWithDefault(appOpName, defaultMode)
+ if (oldMode == mode) {
+ return false
+ }
+ if (appOpModes == null) {
+ appOpModes = IndexedMap()
+ packageAppOpModes[packageName] = appOpModes
+ }
+ appOpModes.putWithDefault(appOpName, mode, defaultMode)
+ if (appOpModes.isEmpty()) {
+ packageAppOpModes -= packageName
+ }
+ userState.requestWrite()
+ onAppOpModeChangedListeners.forEachIndexed { _, it ->
+ it.onAppOpModeChanged(packageName, userId, appOpName, oldMode, mode)
+ }
+ return true
+ }
+
+ fun addOnAppOpModeChangedListener(listener: OnAppOpModeChangedListener) {
+ synchronized(onAppOpModeChangedListenersLock) {
+ onAppOpModeChangedListeners = onAppOpModeChangedListeners + listener
+ }
+ }
+
+ fun removeOnAppOpModeChangedListener(listener: OnAppOpModeChangedListener) {
+ synchronized(onAppOpModeChangedListenersLock) {
+ onAppOpModeChangedListeners = onAppOpModeChangedListeners - listener
+ }
+ }
+
+ fun interface OnAppOpModeChangedListener {
+ fun onAppOpModeChanged(
+ packageName: String,
+ userId: Int,
+ appOpName: String,
+ oldMode: Int,
+ newMode: Int
+ )
+ }
}
diff --git a/services/permission/java/com/android/server/permission/access/appop/UidAppOpPolicy.kt b/services/permission/java/com/android/server/permission/access/appop/UidAppOpPolicy.kt
index 862db8f..0b01038 100644
--- a/services/permission/java/com/android/server/permission/access/appop/UidAppOpPolicy.kt
+++ b/services/permission/java/com/android/server/permission/access/appop/UidAppOpPolicy.kt
@@ -16,40 +16,104 @@
package com.android.server.permission.access.appop
+import android.app.AppOpsManager
import com.android.server.permission.access.AccessUri
import com.android.server.permission.access.AppOpUri
import com.android.server.permission.access.GetStateScope
import com.android.server.permission.access.MutateStateScope
import com.android.server.permission.access.UidUri
-import com.android.server.permission.access.UserState
import com.android.server.permission.access.collection.* // ktlint-disable no-wildcard-imports
class UidAppOpPolicy : BaseAppOpPolicy(UidAppOpPersistence()) {
+ @Volatile
+ private var onAppOpModeChangedListeners = IndexedListSet<OnAppOpModeChangedListener>()
+ private val onAppOpModeChangedListenersLock = Any()
+
override val subjectScheme: String
get() = UidUri.SCHEME
- override val objectScheme: String
- get() = AppOpUri.SCHEME
-
- override fun GetStateScope.getModes(subject: AccessUri): IndexedMap<String, Int>? {
+ override fun GetStateScope.getDecision(subject: AccessUri, `object`: AccessUri): Int {
subject as UidUri
- return state.userStates[subject.userId]?.uidAppOpModes?.get(subject.appId)
+ `object` as AppOpUri
+ return getAppOpMode(subject.appId, subject.userId, `object`.appOpName)
}
- override fun MutateStateScope.getOrCreateModes(subject: AccessUri): IndexedMap<String, Int> {
+ override fun MutateStateScope.setDecision(
+ subject: AccessUri,
+ `object`: AccessUri,
+ decision: Int
+ ) {
subject as UidUri
- return newState.userStates.getOrPut(subject.userId) { UserState() }
- .uidAppOpModes.getOrPut(subject.appId) { IndexedMap() }
- }
-
- override fun MutateStateScope.removeModes(subject: AccessUri) {
- subject as UidUri
- newState.userStates[subject.userId]?.uidAppOpModes?.remove(subject.appId)
+ `object` as AppOpUri
+ setAppOpMode(subject.appId, subject.userId, `object`.appOpName, decision)
}
override fun MutateStateScope.onAppIdRemoved(appId: Int) {
newState.userStates.forEachIndexed { _, _, userState ->
userState.uidAppOpModes -= appId
+ userState.requestWrite()
+ // Skip notifying the change listeners since the app ID no longer exists.
}
}
+
+ fun GetStateScope.getAppOpModes(appId: Int, userId: Int): IndexedMap<String, Int>? =
+ state.userStates[userId].uidAppOpModes[appId]
+
+ fun MutateStateScope.removeAppOpModes(appId: Int, userId: Int): Boolean =
+ newState.userStates[userId].uidAppOpModes.removeReturnOld(appId) != null
+
+ fun GetStateScope.getAppOpMode(appId: Int, userId: Int, appOpName: String): Int =
+ state.userStates[userId].uidAppOpModes[appId]
+ .getWithDefault(appOpName, AppOpsManager.opToDefaultMode(appOpName))
+
+ fun MutateStateScope.setAppOpMode(
+ appId: Int,
+ userId: Int,
+ appOpName: String,
+ mode: Int
+ ): Boolean {
+ val userState = newState.userStates[userId]
+ val uidAppOpModes = userState.uidAppOpModes
+ var appOpModes = uidAppOpModes[appId]
+ val defaultMode = AppOpsManager.opToDefaultMode(appOpName)
+ val oldMode = appOpModes.getWithDefault(appOpName, defaultMode)
+ if (oldMode == mode) {
+ return false
+ }
+ if (appOpModes == null) {
+ appOpModes = IndexedMap()
+ uidAppOpModes[appId] = appOpModes
+ }
+ appOpModes.putWithDefault(appOpName, mode, defaultMode)
+ if (appOpModes.isEmpty()) {
+ uidAppOpModes -= appId
+ }
+ userState.requestWrite()
+ onAppOpModeChangedListeners.forEachIndexed { _, it ->
+ it.onAppOpModeChanged(appId, userId, appOpName, oldMode, mode)
+ }
+ return true
+ }
+
+ fun addOnAppOpModeChangedListener(listener: OnAppOpModeChangedListener) {
+ synchronized(onAppOpModeChangedListenersLock) {
+ onAppOpModeChangedListeners = onAppOpModeChangedListeners + listener
+ }
+ }
+
+ fun removeOnAppOpModeChangedListener(listener: OnAppOpModeChangedListener) {
+ synchronized(onAppOpModeChangedListenersLock) {
+ onAppOpModeChangedListeners = onAppOpModeChangedListeners - listener
+ }
+ }
+
+ fun interface OnAppOpModeChangedListener {
+ fun onAppOpModeChanged(
+ appId: Int,
+ userId: Int,
+ appOpName: String,
+ oldMode: Int,
+ newMode: Int
+ )
+ }
}
diff --git a/services/permission/java/com/android/server/permission/access/collection/IndexedList.kt b/services/permission/java/com/android/server/permission/access/collection/IndexedList.kt
index 9cb2e86..c4d07fe 100644
--- a/services/permission/java/com/android/server/permission/access/collection/IndexedList.kt
+++ b/services/permission/java/com/android/server/permission/access/collection/IndexedList.kt
@@ -19,8 +19,8 @@
typealias IndexedList<T> = ArrayList<T>
inline fun <T> IndexedList<T>.allIndexed(predicate: (Int, T) -> Boolean): Boolean {
- for (index in 0 until size) {
- if (!predicate(index, this[index])) {
+ forEachIndexed { index, element ->
+ if (!predicate(index, element)) {
return false
}
}
@@ -28,8 +28,8 @@
}
inline fun <T> IndexedList<T>.anyIndexed(predicate: (Int, T) -> Boolean): Boolean {
- for (index in 0 until size) {
- if (predicate(index, this[index])) {
+ forEachIndexed { index, element ->
+ if (predicate(index, element)) {
return true
}
}
@@ -45,6 +45,12 @@
}
}
+inline fun <T> IndexedList<T>.forEachReversedIndexed(action: (Int, T) -> Unit) {
+ for (index in lastIndex downTo 0) {
+ action(index, this[index])
+ }
+}
+
@Suppress("NOTHING_TO_INLINE")
inline operator fun <T> IndexedList<T>.minus(element: T): IndexedList<T> =
copy().apply { this -= element }
@@ -55,8 +61,8 @@
}
inline fun <T> IndexedList<T>.noneIndexed(predicate: (Int, T) -> Boolean): Boolean {
- for (index in 0 until size) {
- if (predicate(index, this[index])) {
+ forEachIndexed { index, element ->
+ if (predicate(index, element)) {
return false
}
}
@@ -74,8 +80,8 @@
inline fun <T> IndexedList<T>.removeAllIndexed(predicate: (Int, T) -> Boolean): Boolean {
var isChanged = false
- for (index in lastIndex downTo 0) {
- if (predicate(index, this[index])) {
+ forEachReversedIndexed { index, element ->
+ if (predicate(index, element)) {
removeAt(index)
isChanged = true
}
@@ -85,8 +91,8 @@
inline fun <T> IndexedList<T>.retainAllIndexed(predicate: (Int, T) -> Boolean): Boolean {
var isChanged = false
- for (index in lastIndex downTo 0) {
- if (!predicate(index, this[index])) {
+ forEachReversedIndexed { index, element ->
+ if (!predicate(index, element)) {
removeAt(index)
isChanged = true
}
diff --git a/services/permission/java/com/android/server/permission/access/collection/IndexedListSet.kt b/services/permission/java/com/android/server/permission/access/collection/IndexedListSet.kt
index 1c42c50..c40f7ee 100644
--- a/services/permission/java/com/android/server/permission/access/collection/IndexedListSet.kt
+++ b/services/permission/java/com/android/server/permission/access/collection/IndexedListSet.kt
@@ -70,8 +70,8 @@
}
inline fun <T> IndexedListSet<T>.allIndexed(predicate: (Int, T) -> Boolean): Boolean {
- for (index in 0 until size) {
- if (!predicate(index, elementAt(index))) {
+ forEachIndexed { index, element ->
+ if (!predicate(index, element)) {
return false
}
}
@@ -79,8 +79,8 @@
}
inline fun <T> IndexedListSet<T>.anyIndexed(predicate: (Int, T) -> Boolean): Boolean {
- for (index in 0 until size) {
- if (predicate(index, elementAt(index))) {
+ forEachIndexed { index, element ->
+ if (predicate(index, element)) {
return true
}
}
@@ -93,6 +93,12 @@
}
}
+inline fun <T> IndexedListSet<T>.forEachReversedIndexed(action: (Int, T) -> Unit) {
+ for (index in lastIndex downTo 0) {
+ action(index, elementAt(index))
+ }
+}
+
inline val <T> IndexedListSet<T>.lastIndex: Int
get() = size - 1
@@ -106,8 +112,8 @@
}
inline fun <T> IndexedListSet<T>.noneIndexed(predicate: (Int, T) -> Boolean): Boolean {
- for (index in 0 until size) {
- if (predicate(index, elementAt(index))) {
+ forEachIndexed { index, element ->
+ if (predicate(index, element)) {
return false
}
}
@@ -125,8 +131,8 @@
inline fun <T> IndexedListSet<T>.removeAllIndexed(predicate: (Int, T) -> Boolean): Boolean {
var isChanged = false
- for (index in lastIndex downTo 0) {
- if (predicate(index, elementAt(index))) {
+ forEachReversedIndexed { index, element ->
+ if (predicate(index, element)) {
removeAt(index)
isChanged = true
}
@@ -136,8 +142,8 @@
inline fun <T> IndexedListSet<T>.retainAllIndexed(predicate: (Int, T) -> Boolean): Boolean {
var isChanged = false
- for (index in lastIndex downTo 0) {
- if (!predicate(index, elementAt(index))) {
+ forEachReversedIndexed { index, element ->
+ if (!predicate(index, element)) {
removeAt(index)
isChanged = true
}
diff --git a/services/permission/java/com/android/server/permission/access/collection/IndexedMap.kt b/services/permission/java/com/android/server/permission/access/collection/IndexedMap.kt
index 2448ff0..43f18e2 100644
--- a/services/permission/java/com/android/server/permission/access/collection/IndexedMap.kt
+++ b/services/permission/java/com/android/server/permission/access/collection/IndexedMap.kt
@@ -21,8 +21,8 @@
typealias IndexedMap<K, V> = ArrayMap<K, V>
inline fun <K, V> IndexedMap<K, V>.allIndexed(predicate: (Int, K, V) -> Boolean): Boolean {
- for (index in 0 until size) {
- if (!predicate(index, keyAt(index), valueAt(index))) {
+ forEachIndexed { index, key, value ->
+ if (!predicate(index, key, value)) {
return false
}
}
@@ -30,8 +30,8 @@
}
inline fun <K, V> IndexedMap<K, V>.anyIndexed(predicate: (Int, K, V) -> Boolean): Boolean {
- for (index in 0 until size) {
- if (predicate(index, keyAt(index), valueAt(index))) {
+ forEachIndexed { index, key, value ->
+ if (predicate(index, key, value)) {
return true
}
}
@@ -46,8 +46,8 @@
}
inline fun <K, V, R> IndexedMap<K, V>.firstNotNullOfOrNullIndexed(transform: (Int, K, V) -> R): R? {
- for (index in 0 until size) {
- transform(index, keyAt(index), valueAt(index))?.let { return it }
+ forEachIndexed { index, key, value ->
+ transform(index, key, value)?.let { return it }
}
return null
}
@@ -64,6 +64,12 @@
}
}
+inline fun <K, V> IndexedMap<K, V>.forEachReversedIndexed(action: (Int, K, V) -> Unit) {
+ for (index in lastIndex downTo 0) {
+ action(index, keyAt(index), valueAt(index))
+ }
+}
+
inline fun <K, V> IndexedMap<K, V>.forEachValueIndexed(action: (Int, V) -> Unit) {
for (index in 0 until size) {
action(index, valueAt(index))
@@ -90,6 +96,15 @@
remove(key)
}
+inline fun <K, V> IndexedMap<K, V>.noneIndexed(predicate: (Int, K, V) -> Boolean): Boolean {
+ forEachIndexed { index, key, value ->
+ if (predicate(index, key, value)) {
+ return false
+ }
+ }
+ return true
+}
+
@Suppress("NOTHING_TO_INLINE")
inline fun <K, V> IndexedMap<K, V>.putWithDefault(key: K, value: V, defaultValue: V): V {
val index = indexOfKey(key)
@@ -113,8 +128,8 @@
inline fun <K, V> IndexedMap<K, V>.removeAllIndexed(predicate: (Int, K, V) -> Boolean): Boolean {
var isChanged = false
- for (index in lastIndex downTo 0) {
- if (predicate(index, keyAt(index), valueAt(index))) {
+ forEachReversedIndexed { index, key, value ->
+ if (predicate(index, key, value)) {
removeAt(index)
isChanged = true
}
@@ -124,8 +139,8 @@
inline fun <K, V> IndexedMap<K, V>.retainAllIndexed(predicate: (Int, K, V) -> Boolean): Boolean {
var isChanged = false
- for (index in lastIndex downTo 0) {
- if (!predicate(index, keyAt(index), valueAt(index))) {
+ forEachReversedIndexed { index, key, value ->
+ if (!predicate(index, key, value)) {
removeAt(index)
isChanged = true
}
diff --git a/services/permission/java/com/android/server/permission/access/collection/IndexedSet.kt b/services/permission/java/com/android/server/permission/access/collection/IndexedSet.kt
index faaa6d3..13fa31f 100644
--- a/services/permission/java/com/android/server/permission/access/collection/IndexedSet.kt
+++ b/services/permission/java/com/android/server/permission/access/collection/IndexedSet.kt
@@ -21,8 +21,8 @@
typealias IndexedSet<T> = ArraySet<T>
inline fun <T> IndexedSet<T>.allIndexed(predicate: (Int, T) -> Boolean): Boolean {
- for (index in 0 until size) {
- if (!predicate(index, elementAt(index))) {
+ forEachIndexed { index, element ->
+ if (!predicate(index, element)) {
return false
}
}
@@ -30,8 +30,8 @@
}
inline fun <T> IndexedSet<T>.anyIndexed(predicate: (Int, T) -> Boolean): Boolean {
- for (index in 0 until size) {
- if (predicate(index, elementAt(index))) {
+ forEachIndexed { index, element ->
+ if (predicate(index, element)) {
return true
}
}
@@ -50,6 +50,12 @@
}
}
+inline fun <T> IndexedSet<T>.forEachReversedIndexed(action: (Int, T) -> Unit) {
+ for (index in lastIndex downTo 0) {
+ action(index, elementAt(index))
+ }
+}
+
inline val <T> IndexedSet<T>.lastIndex: Int
get() = size - 1
@@ -63,8 +69,8 @@
}
inline fun <T> IndexedSet<T>.noneIndexed(predicate: (Int, T) -> Boolean): Boolean {
- for (index in 0 until size) {
- if (predicate(index, elementAt(index))) {
+ forEachIndexed { index, element ->
+ if (predicate(index, element)) {
return false
}
}
@@ -82,8 +88,8 @@
inline fun <T> IndexedSet<T>.removeAllIndexed(predicate: (Int, T) -> Boolean): Boolean {
var isChanged = false
- for (index in lastIndex downTo 0) {
- if (predicate(index, elementAt(index))) {
+ forEachReversedIndexed { index, element ->
+ if (predicate(index, element)) {
removeAt(index)
isChanged = true
}
@@ -93,8 +99,8 @@
inline fun <T> IndexedSet<T>.retainAllIndexed(predicate: (Int, T) -> Boolean): Boolean {
var isChanged = false
- for (index in lastIndex downTo 0) {
- if (!predicate(index, elementAt(index))) {
+ forEachReversedIndexed { index, element ->
+ if (!predicate(index, element)) {
removeAt(index)
isChanged = true
}
diff --git a/services/permission/java/com/android/server/permission/access/collection/IntMap.kt b/services/permission/java/com/android/server/permission/access/collection/IntMap.kt
index 0044b73..e905567 100644
--- a/services/permission/java/com/android/server/permission/access/collection/IntMap.kt
+++ b/services/permission/java/com/android/server/permission/access/collection/IntMap.kt
@@ -21,8 +21,8 @@
typealias IntMap<T> = SparseArray<T>
inline fun <T> IntMap<T>.allIndexed(predicate: (Int, Int, T) -> Boolean): Boolean {
- for (index in 0 until size) {
- if (!predicate(index, keyAt(index), valueAt(index))) {
+ forEachIndexed { index, key, value ->
+ if (!predicate(index, key, value)) {
return false
}
}
@@ -30,8 +30,8 @@
}
inline fun <T> IntMap<T>.anyIndexed(predicate: (Int, Int, T) -> Boolean): Boolean {
- for (index in 0 until size) {
- if (predicate(index, keyAt(index), valueAt(index))) {
+ forEachIndexed { index, key, value ->
+ if (predicate(index, key, value)) {
return true
}
}
@@ -46,8 +46,8 @@
}
inline fun <T, R> IntMap<T>.firstNotNullOfOrNullIndexed(transform: (Int, Int, T) -> R): R? {
- for (index in 0 until size) {
- transform(index, keyAt(index), valueAt(index))?.let { return it }
+ forEachIndexed { index, key, value ->
+ transform(index, key, value)?.let { return it }
}
return null
}
@@ -64,6 +64,12 @@
}
}
+inline fun <T> IntMap<T>.forEachReversedIndexed(action: (Int, Int, T) -> Unit) {
+ for (index in lastIndex downTo 0) {
+ action(index, keyAt(index), valueAt(index))
+ }
+}
+
inline fun <T> IntMap<T>.forEachValueIndexed(action: (Int, T) -> Unit) {
for (index in 0 until size) {
action(index, valueAt(index))
@@ -91,8 +97,8 @@
}
inline fun <T> IntMap<T>.noneIndexed(predicate: (Int, Int, T) -> Boolean): Boolean {
- for (index in 0 until size) {
- if (predicate(index, keyAt(index), valueAt(index))) {
+ forEachIndexed { index, key, value ->
+ if (predicate(index, key, value)) {
return false
}
}
@@ -120,10 +126,22 @@
}
}
+// SparseArray.removeReturnOld() is @hide, so a backup once we move to APIs.
+fun <T> IntMap<T>.removeReturnOld(key: Int): T? {
+ val index = indexOfKey(key)
+ return if (index >= 0) {
+ val oldValue = valueAt(index)
+ removeAt(index)
+ oldValue
+ } else {
+ null
+ }
+}
+
inline fun <T> IntMap<T>.removeAllIndexed(predicate: (Int, Int, T) -> Boolean): Boolean {
var isChanged = false
- for (index in lastIndex downTo 0) {
- if (predicate(index, keyAt(index), valueAt(index))) {
+ forEachReversedIndexed { index, key, value ->
+ if (predicate(index, key, value)) {
removeAt(index)
isChanged = true
}
@@ -133,8 +151,8 @@
inline fun <T> IntMap<T>.retainAllIndexed(predicate: (Int, Int, T) -> Boolean): Boolean {
var isChanged = false
- for (index in lastIndex downTo 0) {
- if (!predicate(index, keyAt(index), valueAt(index))) {
+ forEachReversedIndexed { index, key, value ->
+ if (!predicate(index, key, value)) {
removeAt(index)
isChanged = true
}
diff --git a/services/permission/java/com/android/server/permission/access/collection/IntSet.kt b/services/permission/java/com/android/server/permission/access/collection/IntSet.kt
index 0d75a4c..4717251 100644
--- a/services/permission/java/com/android/server/permission/access/collection/IntSet.kt
+++ b/services/permission/java/com/android/server/permission/access/collection/IntSet.kt
@@ -54,8 +54,8 @@
fun IntSet(values: IntArray): IntSet = IntSet().apply{ this += values }
inline fun IntSet.allIndexed(predicate: (Int, Int) -> Boolean): Boolean {
- for (index in 0 until size) {
- if (!predicate(index, elementAt(index))) {
+ forEachIndexed { index, element ->
+ if (!predicate(index, element)) {
return false
}
}
@@ -63,8 +63,8 @@
}
inline fun IntSet.anyIndexed(predicate: (Int, Int) -> Boolean): Boolean {
- for (index in 0 until size) {
- if (predicate(index, elementAt(index))) {
+ forEachIndexed { index, element ->
+ if (predicate(index, element)) {
return true
}
}
@@ -77,6 +77,12 @@
}
}
+inline fun IntSet.forEachReversedIndexed(action: (Int, Int) -> Unit) {
+ for (index in lastIndex downTo 0) {
+ action(index, elementAt(index))
+ }
+}
+
inline val IntSet.lastIndex: Int
get() = size - 1
@@ -89,8 +95,8 @@
}
inline fun IntSet.noneIndexed(predicate: (Int, Int) -> Boolean): Boolean {
- for (index in 0 until size) {
- if (predicate(index, elementAt(index))) {
+ forEachIndexed { index, element ->
+ if (predicate(index, element)) {
return false
}
}
@@ -115,8 +121,8 @@
inline fun IntSet.removeAllIndexed(predicate: (Int, Int) -> Boolean): Boolean {
var isChanged = false
- for (index in lastIndex downTo 0) {
- if (predicate(index, elementAt(index))) {
+ forEachReversedIndexed { index, element ->
+ if (predicate(index, element)) {
removeAt(index)
isChanged = true
}
@@ -126,8 +132,8 @@
inline fun IntSet.retainAllIndexed(predicate: (Int, Int) -> Boolean): Boolean {
var isChanged = false
- for (index in lastIndex downTo 0) {
- if (!predicate(index, elementAt(index))) {
+ forEachReversedIndexed { index, element ->
+ if (!predicate(index, element)) {
removeAt(index)
isChanged = true
}
diff --git a/services/permission/java/com/android/server/permission/access/collection/List.kt b/services/permission/java/com/android/server/permission/access/collection/List.kt
index d35e69e..91f15bc 100644
--- a/services/permission/java/com/android/server/permission/access/collection/List.kt
+++ b/services/permission/java/com/android/server/permission/access/collection/List.kt
@@ -17,8 +17,8 @@
package com.android.server.permission.access.collection
inline fun <T> List<T>.allIndexed(predicate: (Int, T) -> Boolean): Boolean {
- for (index in 0 until size) {
- if (!predicate(index, this[index])) {
+ forEachIndexed { index, element ->
+ if (!predicate(index, element)) {
return false
}
}
@@ -26,8 +26,8 @@
}
inline fun <T> List<T>.anyIndexed(predicate: (Int, T) -> Boolean): Boolean {
- for (index in 0 until size) {
- if (predicate(index, this[index])) {
+ forEachIndexed { index, element ->
+ if (predicate(index, element)) {
return true
}
}
@@ -40,9 +40,15 @@
}
}
+inline fun <T> List<T>.forEachReversedIndexed(action: (Int, T) -> Unit) {
+ for (index in lastIndex downTo 0) {
+ action(index, this[index])
+ }
+}
+
inline fun <T> List<T>.noneIndexed(predicate: (Int, T) -> Boolean): Boolean {
- for (index in 0 until size) {
- if (predicate(index, this[index])) {
+ forEachIndexed { index, element ->
+ if (predicate(index, element)) {
return false
}
}
@@ -51,8 +57,8 @@
inline fun <T> MutableList<T>.removeAllIndexed(predicate: (Int, T) -> Boolean): Boolean {
var isChanged = false
- for (index in lastIndex downTo 0) {
- if (predicate(index, this[index])) {
+ forEachReversedIndexed { index, element ->
+ if (predicate(index, element)) {
removeAt(index)
isChanged = true
}
@@ -62,8 +68,8 @@
inline fun <T> MutableList<T>.retainAllIndexed(predicate: (Int, T) -> Boolean): Boolean {
var isChanged = false
- for (index in lastIndex downTo 0) {
- if (!predicate(index, this[index])) {
+ forEachReversedIndexed { index, element ->
+ if (!predicate(index, element)) {
removeAt(index)
isChanged = true
}
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 1b05520..6b2b1856 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
-object PermissionFlags {
- const val INSTALL_GRANTED = 1 shl 0
- const val INSTALL_REVOKED = 1 shl 1
- 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
+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
+
+ /**
+ * 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 1e6996b..8201736 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 @@
}
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 @@
}
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(
@@ -435,7 +445,8 @@
} else {
intArrayOf(userId)
}
- userIds.forEach { service.onPackageInstalled(androidPackage.packageName, params, it) }
+ userIds.forEach { service.onPackageInstalled(androidPackage.packageName, it) }
+ // TODO: Handle params.
}
override fun onPackageUninstalled(
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 3d6d2ce..b2f52cc 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.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 @@
// 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 @@
"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 @@
}
newFlags = newFlags or (sourceFlags and PermissionFlags.MASK_RUNTIME)
if (!sourcePermission.isRuntime && isSourceGranted) {
- newFlags = newFlags or PermissionFlags.OTHER_GRANTED
+ newFlags = newFlags or PermissionFlags.IMPLICIT_GRANTED
}
}
}
@@ -597,11 +596,9 @@
newState.systemState.privilegedPermissionAllowlistSourcePackageNames) {
return true
}
- if (isInSystemConfigPrivAppPermissions(androidPackage, permission.name)) {
- return true
- }
- if (isInSystemConfigPrivAppDenyPermissions(androidPackage, permission.name)) {
- return false
+ val allowlistState = getPrivilegedPermissionAllowlistState(androidPackage, permission.name)
+ if (allowlistState != null) {
+ return allowlistState
}
// Updated system apps do not need to be allowlisted
if (packageState.isUpdatedSystemApp) {
@@ -611,66 +608,51 @@
return !RoSystemProperties.CONTROL_PRIVAPP_PERMISSIONS_ENFORCE
}
- private fun MutateStateScope.isInSystemConfigPrivAppPermissions(
+ /**
+ * Get the whether a privileged permission is explicitly allowed or denied for a package in the
+ * allowlist, or `null` if it's not in the allowlist.
+ */
+ private fun MutateStateScope.getPrivilegedPermissionAllowlistState(
androidPackage: AndroidPackage,
permissionName: String
- ): Boolean {
+ ): Boolean? {
+ val permissionAllowlist = newState.systemState.permissionAllowlist
// TODO(b/261913353): STOPSHIP: Add AndroidPackage.apexModuleName. The below is only for
// passing compilation but won't actually work.
//val apexModuleName = androidPackage.apexModuleName
val apexModuleName = androidPackage.packageName
- val systemState = newState.systemState
val packageName = androidPackage.packageName
- val permissionNames = when {
- androidPackage.isVendor -> systemState.vendorPrivAppPermissions[packageName]
- androidPackage.isProduct -> systemState.productPrivAppPermissions[packageName]
- androidPackage.isSystemExt -> systemState.systemExtPrivAppPermissions[packageName]
+ return when {
+ androidPackage.isVendor -> permissionAllowlist.getVendorPrivilegedAppAllowlistState(
+ packageName, permissionName
+ )
+ androidPackage.isProduct -> permissionAllowlist.getProductPrivilegedAppAllowlistState(
+ packageName, permissionName
+ )
+ androidPackage.isSystemExt ->
+ permissionAllowlist.getSystemExtPrivilegedAppAllowlistState(
+ packageName, permissionName
+ )
apexModuleName != null -> {
- val apexPrivAppPermissions = systemState.apexPrivAppPermissions[apexModuleName]
- ?.get(packageName)
- val privAppPermissions = systemState.privAppPermissions[packageName]
- when {
- apexPrivAppPermissions == null -> privAppPermissions
- privAppPermissions == null -> apexPrivAppPermissions
- else -> apexPrivAppPermissions + privAppPermissions
+ val nonApexAllowlistState = permissionAllowlist.getPrivilegedAppAllowlistState(
+ packageName, permissionName
+ )
+ if (nonApexAllowlistState != null) {
+ // TODO(andreionea): Remove check as soon as all apk-in-apex
+ // permission allowlists are migrated.
+ Log.w(
+ LOG_TAG, "Package $packageName is an APK in APEX but has permission" +
+ " allowlist on the system image, please bundle the allowlist in the" +
+ " $apexModuleName APEX instead"
+ )
}
+ val apexAllowlistState = permissionAllowlist.getApexPrivilegedAppAllowlistState(
+ apexModuleName, packageName, permissionName
+ )
+ apexAllowlistState ?: nonApexAllowlistState
}
- else -> systemState.privAppPermissions[packageName]
+ else -> permissionAllowlist.getPrivilegedAppAllowlistState(packageName, permissionName)
}
- return permissionNames?.contains(permissionName) == true
- }
-
- private fun MutateStateScope.isInSystemConfigPrivAppDenyPermissions(
- androidPackage: AndroidPackage,
- permissionName: String
- ): Boolean {
- // Different from the previous implementation, which may incorrectly use the APEX package
- // name, we now use the APEX module name to be consistent with the allowlist.
- // TODO(b/261913353): STOPSHIP: Add AndroidPackage.apexModuleName. The below is only for
- // passing compilation but won't actually work.
- //val apexModuleName = androidPackage.apexModuleName
- val apexModuleName = androidPackage.packageName
- val systemState = newState.systemState
- val packageName = androidPackage.packageName
- val permissionNames = when {
- androidPackage.isVendor -> systemState.vendorPrivAppDenyPermissions[packageName]
- androidPackage.isProduct -> systemState.productPrivAppDenyPermissions[packageName]
- androidPackage.isSystemExt -> systemState.systemExtPrivAppDenyPermissions[packageName]
- // Different from the previous implementation, which ignores the regular priv app
- // denylist in this case, we now respect it as well to be consistent with the allowlist.
- apexModuleName != null -> {
- val apexPrivAppDenyPermissions = systemState
- .apexPrivAppDenyPermissions[apexModuleName]?.get(packageName)
- val privAppDenyPermissions = systemState.privAppDenyPermissions[packageName]
- when {
- apexPrivAppDenyPermissions == null -> privAppDenyPermissions
- privAppDenyPermissions == null -> apexPrivAppDenyPermissions
- else -> apexPrivAppDenyPermissions + privAppDenyPermissions
- }
- }
- else -> systemState.privAppDenyPermissions[packageName]
- }
- return permissionNames?.contains(permissionName) == true
}
private fun MutateStateScope.anyPackageInAppId(
@@ -811,13 +793,13 @@
}
permission.isOem -> {
if (androidPackage.isOem) {
- val isOemAllowlisted = newState.systemState
- .oemPermissions[packageName]?.get(permissionName)
- checkNotNull(isOemAllowlisted) {
+ val allowlistState = newState.systemState.permissionAllowlist
+ .getOemAppAllowlistState(packageName, permissionName)
+ checkNotNull(allowlistState) {
"OEM permission $permissionName requested by package" +
" $packageName must be explicitly declared granted or not"
}
- return isOemAllowlisted
+ return allowlistState
}
}
}
@@ -853,6 +835,9 @@
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,
@@ -871,7 +856,8 @@
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,
@@ -892,7 +878,7 @@
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 6cdf11c..ba4560f 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 cc26593..ebd6b64 100644
--- a/services/tests/PackageManagerServiceTests/server/Android.bp
+++ b/services/tests/PackageManagerServiceTests/server/Android.bp
@@ -94,7 +94,6 @@
"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/AppsFilterImplTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/AppsFilterImplTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/AppsFilterImplTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/AppsFilterImplTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/BundleUtilsTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/BundleUtilsTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/BundleUtilsTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/BundleUtilsTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/CompatibilityModeTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/CompatibilityModeTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/CompatibilityModeTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/CompatibilityModeTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/CrossProfileAppsServiceImplTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/CrossProfileAppsServiceImplTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/CrossProfileAppsServiceImplTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/CrossProfileAppsServiceImplTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/InstallerTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/InstallerTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/InstallerTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/InstallerTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/KeySetManagerServiceTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/KeySetManagerServiceTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/KeySetManagerServiceTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/KeySetManagerServiceTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/KeySetStrings.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/KeySetStrings.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/KeySetStrings.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/KeySetStrings.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/KeySetUtils.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/KeySetUtils.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/KeySetUtils.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/KeySetUtils.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/ModuleInfoProviderTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/ModuleInfoProviderTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/ModuleInfoProviderTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/ModuleInfoProviderTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/PackageInstallerSessionTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageInstallerSessionTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/PackageInstallerSessionTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageInstallerSessionTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/PackageManagerServiceTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerServiceTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/PackageManagerServiceTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerServiceTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/PackageManagerSettingsTests.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java
similarity index 99%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/PackageManagerSettingsTests.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java
index b3f64b6..3727d66 100644
--- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/PackageManagerSettingsTests.java
+++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java
@@ -111,10 +111,6 @@
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/PackageManagerServiceTests/server/src/com/android/server/PackageManagerTests.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerTests.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/PackageManagerTests.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerTests.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/PackageParserTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageParserTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/PackageParserTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageParserTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/PackageSignaturesTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageSignaturesTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/PackageSignaturesTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageSignaturesTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/PackageUserStateTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageUserStateTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/PackageUserStateTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageUserStateTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/PackageVerificationStateTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageVerificationStateTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/PackageVerificationStateTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageVerificationStateTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/ParallelPackageParserTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/ParallelPackageParserTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/ParallelPackageParserTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/ParallelPackageParserTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/PreferredComponentTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PreferredComponentTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/PreferredComponentTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PreferredComponentTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/RestrictionsSetTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/RestrictionsSetTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/RestrictionsSetTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/RestrictionsSetTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/ScanRequestBuilder.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/ScanRequestBuilder.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/ScanRequestBuilder.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/ScanRequestBuilder.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/ScanTests.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/ScanTests.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/ScanTests.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/ScanTests.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/UserDataPreparerTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/UserDataPreparerTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/UserDataPreparerTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/UserDataPreparerTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/WatchedIntentHandlingTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/WatchedIntentHandlingTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/WatchedIntentHandlingTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/WatchedIntentHandlingTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/PackageParserLegacyCoreTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/PackageParserLegacyCoreTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/PackageParserLegacyCoreTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/PackageParserLegacyCoreTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/PackageParsingDeferErrorTest.kt b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/PackageParsingDeferErrorTest.kt
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/PackageParsingDeferErrorTest.kt
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/PackageParsingDeferErrorTest.kt
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/SystemPartitionParseTest.kt b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/SystemPartitionParseTest.kt
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/SystemPartitionParseTest.kt
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/SystemPartitionParseTest.kt
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/library/AndroidHidlUpdaterTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/library/AndroidHidlUpdaterTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/library/AndroidHidlUpdaterTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/library/AndroidHidlUpdaterTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/library/AndroidNetIpSecIkeUpdaterTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/library/AndroidNetIpSecIkeUpdaterTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/library/AndroidNetIpSecIkeUpdaterTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/library/AndroidNetIpSecIkeUpdaterTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/library/AndroidTestBaseUpdaterTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/library/AndroidTestBaseUpdaterTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/library/AndroidTestBaseUpdaterTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/library/AndroidTestBaseUpdaterTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/library/AndroidTestRunnerSplitUpdaterTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/library/AndroidTestRunnerSplitUpdaterTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/library/AndroidTestRunnerSplitUpdaterTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/library/AndroidTestRunnerSplitUpdaterTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/library/ApexSharedLibraryUpdaterTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/library/ApexSharedLibraryUpdaterTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/library/ApexSharedLibraryUpdaterTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/library/ApexSharedLibraryUpdaterTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/library/ComGoogleAndroidMapsUpdaterTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/library/ComGoogleAndroidMapsUpdaterTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/library/ComGoogleAndroidMapsUpdaterTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/library/ComGoogleAndroidMapsUpdaterTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/library/OptionalClassRunner.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/library/OptionalClassRunner.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/library/OptionalClassRunner.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/library/OptionalClassRunner.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/library/OrgApacheHttpLegacyUpdaterTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/library/OrgApacheHttpLegacyUpdaterTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/library/OrgApacheHttpLegacyUpdaterTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/library/OrgApacheHttpLegacyUpdaterTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/library/PackageBackwardCompatibilityTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/library/PackageBackwardCompatibilityTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/library/PackageBackwardCompatibilityTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/library/PackageBackwardCompatibilityTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/library/PackageSharedLibraryUpdaterTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/library/PackageSharedLibraryUpdaterTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/library/PackageSharedLibraryUpdaterTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/library/PackageSharedLibraryUpdaterTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/library/RemoveUnnecessaryAndroidTestBaseLibraryTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/library/RemoveUnnecessaryAndroidTestBaseLibraryTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/library/RemoveUnnecessaryAndroidTestBaseLibraryTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/library/RemoveUnnecessaryAndroidTestBaseLibraryTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/library/RemoveUnnecessaryOrgApacheHttpLegacyLibraryTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/library/RemoveUnnecessaryOrgApacheHttpLegacyLibraryTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/parsing/library/RemoveUnnecessaryOrgApacheHttpLegacyLibraryTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/parsing/library/RemoveUnnecessaryOrgApacheHttpLegacyLibraryTest.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/utils/OWNERS b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/utils/OWNERS
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/utils/OWNERS
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/utils/OWNERS
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/utils/WatchableTester.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/utils/WatchableTester.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/utils/WatchableTester.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/utils/WatchableTester.java
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/utils/WatcherTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/utils/WatcherTest.java
similarity index 100%
rename from services/tests/PackageManagerServiceTests/server/src/com/android/server/utils/WatcherTest.java
rename to services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/utils/WatcherTest.java
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 be13bad..021d01c 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 @@
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 7d4bc6f..c0688d1 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_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.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 @@
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 @@
@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 @@
@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 @@
// 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 @@
// 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 @@
@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 @@
@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 @@
@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 3efd5e7..98e895a 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 @@
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 302fa0f..9eed6ad 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 @@
}
}
- 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 @@
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 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 @@
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 @@
} 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 @@
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 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 @@
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 @@
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 @@
testService.writeState();
assertTrue(parser.parse());
- assertEquals(AppOpsServiceImpl.CURRENT_VERSION, parser.mVersion);
+ assertEquals(AppOpsService.CURRENT_VERSION, parser.mVersion);
}
@Test
@@ -330,8 +330,8 @@
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 @@
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 0000000..880501f
--- /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 a9dc4af..480a4f3 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 @@
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 @@
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 @@
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 @@
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 @@
}
@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/MockSystem.kt b/services/tests/mockingservicestests/src/com/android/server/pm/MockSystem.kt
index 27d0662..4f56271 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/MockSystem.kt
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/MockSystem.kt
@@ -193,6 +193,7 @@
val incrementalManager: IncrementalManager = mock()
val platformCompat: PlatformCompat = mock()
val settings: Settings = mock()
+ val crossProfileIntentFilterHelper: CrossProfileIntentFilterHelper = mock()
val resources: Resources = mock()
val systemConfig: SystemConfig = mock()
val apexManager: ApexManager = mock()
@@ -279,6 +280,8 @@
whenever(mocks.injector.incrementalManager).thenReturn(mocks.incrementalManager)
whenever(mocks.injector.compatibility).thenReturn(mocks.platformCompat)
whenever(mocks.injector.settings).thenReturn(mocks.settings)
+ whenever(mocks.injector.crossProfileIntentFilterHelper)
+ .thenReturn(mocks.crossProfileIntentFilterHelper)
whenever(mocks.injector.dexManager).thenReturn(mocks.dexManager)
whenever(mocks.injector.systemConfig).thenReturn(mocks.systemConfig)
whenever(mocks.injector.apexManager).thenReturn(mocks.apexManager)
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 6d8910e..58cff94 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 @@
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 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 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 @@
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 @@
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 @@
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 @@
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 1065392..3d64c29 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 @@
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 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 4487d13..74fd9ff 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 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 @@
*/
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 @@
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 @@
}
@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 @@
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 @@
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 @@
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 @@
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 @@
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 98037d7..0dfe664 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 @@
}
@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 d2f2af1..9c7c574 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 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 @@
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 62ef523..31e53d5 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.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;
@@ -97,6 +98,8 @@
import com.android.server.input.InputManagerInternal;
import com.android.server.sensors.SensorManagerInternal;
+import com.google.android.collect.Sets;
+
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -108,6 +111,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
+import java.util.Set;
import java.util.function.Consumer;
@Presubmit
@@ -124,12 +128,13 @@
private static final String GOOGLE_MAPS_PACKAGE_NAME = "com.google.android.apps.maps";
private static final String DEVICE_NAME = "device name";
private static final int DISPLAY_ID = 2;
+ private static final int DISPLAY_ID_2 = 3;
+ private static final int DEVICE_OWNER_UID_1 = 50;
+ private static final int DEVICE_OWNER_UID_2 = 51;
private static final int UID_1 = 0;
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";
@@ -171,6 +176,14 @@
.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;
@@ -301,22 +314,13 @@
mContext.getSystemService(WindowManager.class), threadVerifier);
mSensorController = new SensorController(new Object(), VIRTUAL_DEVICE_ID);
- mAssociationInfo = new AssociationInfo(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();
-
- VirtualDeviceParams params = new VirtualDeviceParams
- .Builder()
- .setBlockedActivities(getBlockedActivities())
- .build();
- mDeviceImpl = new VirtualDeviceImpl(mContext,
- mAssociationInfo, new Binder(), /* ownerUid= */ 0, VIRTUAL_DEVICE_ID,
- mInputController, mSensorController, (int associationId) -> {},
- mPendingTrampolineCallback, mActivityListener, mRunningAppsChangedCallback, params);
- mVdms.addVirtualDevice(mDeviceImpl);
+ mDeviceImpl = createVirtualDevice(VIRTUAL_DEVICE_ID, DEVICE_OWNER_UID_1);
}
@Test
@@ -380,7 +384,8 @@
.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);
@@ -389,6 +394,106 @@
}
@Test
+ public void getDeviceOwnerUid_oneDevice_returnsCorrectId() {
+ int ownerUid = mLocalService.getDeviceOwnerUid(mDeviceImpl.getDeviceId());
+ assertThat(ownerUid).isEqualTo(mDeviceImpl.getOwnerUid());
+ }
+
+ @Test
+ public void getDeviceOwnerUid_twoDevices_returnsCorrectId() {
+ int firstDeviceId = mDeviceImpl.getDeviceId();
+ int secondDeviceId = VIRTUAL_DEVICE_ID + 1;
+
+ createVirtualDevice(secondDeviceId, DEVICE_OWNER_UID_2);
+
+ int secondDeviceOwner = mLocalService.getDeviceOwnerUid(secondDeviceId);
+ assertThat(secondDeviceOwner).isEqualTo(DEVICE_OWNER_UID_2);
+
+ int firstDeviceOwner = mLocalService.getDeviceOwnerUid(firstDeviceId);
+ assertThat(firstDeviceOwner).isEqualTo(DEVICE_OWNER_UID_1);
+ }
+
+ @Test
+ public void getDeviceOwnerUid_nonExistentDevice_returnsInvalidUid() {
+ int nonExistentDeviceId = DEVICE_ID_DEFAULT;
+ int ownerUid = mLocalService.getDeviceOwnerUid(nonExistentDeviceId);
+ assertThat(ownerUid).isEqualTo(Process.INVALID_UID);
+ }
+
+ @Test
+ public void getDeviceIdsForUid_noRunningApps_returnsNull() {
+ Set<Integer> deviceIds = mLocalService.getDeviceIdsForUid(UID_1);
+ assertThat(deviceIds).isEmpty();
+ }
+
+ @Test
+ public void getDeviceIdsForUid_differentUidOnDevice_returnsNull() {
+ GenericWindowPolicyController gwpc =
+ mDeviceImpl.createWindowPolicyController(new ArrayList<>());
+ mDeviceImpl.onVirtualDisplayCreatedLocked(gwpc, DISPLAY_ID);
+ gwpc.onRunningAppsChanged(Sets.newArraySet(UID_2));
+
+ Set<Integer> deviceIds = mLocalService.getDeviceIdsForUid(UID_1);
+ assertThat(deviceIds).isEmpty();
+ }
+
+ @Test
+ public void getDeviceIdsForUid_oneUidOnDevice_returnsCorrectId() {
+ GenericWindowPolicyController gwpc =
+ mDeviceImpl.createWindowPolicyController(new ArrayList<>());
+ mDeviceImpl.onVirtualDisplayCreatedLocked(gwpc, DISPLAY_ID);
+ gwpc.onRunningAppsChanged(Sets.newArraySet(UID_1));
+
+ Set<Integer> deviceIds = mLocalService.getDeviceIdsForUid(UID_1);
+ assertThat(deviceIds).containsExactly(mDeviceImpl.getDeviceId());
+ }
+
+ @Test
+ public void getDeviceIdsForUid_twoUidsOnDevice_returnsCorrectId() {
+ GenericWindowPolicyController gwpc =
+ mDeviceImpl.createWindowPolicyController(new ArrayList<>());
+ mDeviceImpl.onVirtualDisplayCreatedLocked(gwpc, DISPLAY_ID);
+ gwpc.onRunningAppsChanged(Sets.newArraySet(UID_1, UID_2));
+
+ Set<Integer> deviceIds = mLocalService.getDeviceIdsForUid(UID_1);
+ assertThat(deviceIds).containsExactly(mDeviceImpl.getDeviceId());
+ }
+
+ @Test
+ public void getDeviceIdsForUid_twoDevicesUidOnOne_returnsCorrectId() {
+ int secondDeviceId = VIRTUAL_DEVICE_ID + 1;
+
+ VirtualDeviceImpl secondDevice = createVirtualDevice(secondDeviceId, DEVICE_OWNER_UID_2);
+
+ GenericWindowPolicyController gwpc =
+ secondDevice.createWindowPolicyController(new ArrayList<>());
+ secondDevice.onVirtualDisplayCreatedLocked(gwpc, DISPLAY_ID_2);
+ gwpc.onRunningAppsChanged(Sets.newArraySet(UID_1));
+
+ Set<Integer> deviceIds = mLocalService.getDeviceIdsForUid(UID_1);
+ assertThat(deviceIds).containsExactly(secondDevice.getDeviceId());
+ }
+
+ @Test
+ public void getDeviceIdsForUid_twoDevicesUidOnBoth_returnsCorrectId() {
+ int secondDeviceId = VIRTUAL_DEVICE_ID + 1;
+
+ VirtualDeviceImpl secondDevice = createVirtualDevice(secondDeviceId, DEVICE_OWNER_UID_2);
+ GenericWindowPolicyController gwpc1 =
+ mDeviceImpl.createWindowPolicyController(new ArrayList<>());
+ GenericWindowPolicyController gwpc2 =
+ mDeviceImpl.createWindowPolicyController(new ArrayList<>());
+ mDeviceImpl.onVirtualDisplayCreatedLocked(gwpc1, DISPLAY_ID);
+ secondDevice.onVirtualDisplayCreatedLocked(gwpc2, DISPLAY_ID_2);
+ gwpc1.onRunningAppsChanged(Sets.newArraySet(UID_1));
+ gwpc2.onRunningAppsChanged(Sets.newArraySet(UID_1, UID_2));
+
+ Set<Integer> deviceIds = mLocalService.getDeviceIdsForUid(UID_1);
+ assertThat(deviceIds).containsExactly(
+ mDeviceImpl.getDeviceId(), secondDevice.getDeviceId());
+ }
+
+ @Test
public void onVirtualDisplayRemovedLocked_doesNotThrowException() {
mDeviceImpl.onVirtualDisplayCreatedLocked(
mDeviceImpl.createWindowPolicyController(new ArrayList<>()), DISPLAY_ID);
@@ -423,7 +528,7 @@
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);
@@ -436,13 +541,13 @@
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(
@@ -450,7 +555,7 @@
// 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.
@@ -459,7 +564,7 @@
// 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.
@@ -604,8 +709,67 @@
.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
@@ -652,6 +816,16 @@
}
@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());
@@ -715,7 +889,18 @@
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));
}
@@ -1160,7 +1345,6 @@
/* targetDisplayCategory= */ null);
verify(mContext).startActivityAsUser(argThat(intent ->
intent.filterEquals(blockedAppIntent)), any(), any());
-
}
@Test
@@ -1185,4 +1369,18 @@
verify(mContext).startActivityAsUser(argThat(intent ->
intent.filterEquals(blockedAppIntent)), any(), any());
}
+
+ private VirtualDeviceImpl createVirtualDevice(int virtualDeviceId, int ownerUid) {
+ VirtualDeviceParams params = new VirtualDeviceParams
+ .Builder()
+ .setBlockedActivities(getBlockedActivities())
+ .build();
+ VirtualDeviceImpl virtualDeviceImpl = new VirtualDeviceImpl(mContext,
+ 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 c7caa43..c6a0b0f 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 @@
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 java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
-import java.util.Set;
@SmallTest
@RunWith(AndroidJUnit4.class)
@@ -155,8 +158,8 @@
@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 @@
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 @@
@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 @@
@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 @@
@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 @@
@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 @@
@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 @@
}
@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));
+ 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);
- 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);
+
+ 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));
+ 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);
- 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);
- @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));
+ Layout layout1 = new Layout();
+ layout1.createDisplayLocked(info(device1).address,
+ /* isDefault= */ true, /* isEnabled= */ true, mIdProducer);
+ when(mDeviceStateToLayoutMapSpy.get(STATE_DEFAULT)).thenReturn(layout1);
- 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);
- }
+ 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 @@
@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 @@
@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 @@
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 @@
/* isEnabled= */ true,
mIdProducer);
- when(mDeviceStateToLayoutMapSpy.get(DeviceStateToLayoutMap.STATE_DEFAULT))
+ when(mDeviceStateToLayoutMapSpy.get(STATE_DEFAULT))
.thenReturn(threeDevicesEnabledLayout);
LogicalDisplay display1 = add(device1);
@@ -735,6 +762,14 @@
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 e390bcc..3326f80f 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.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 @@
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/BaseLockSettingsServiceTests.java b/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java
index 4668e5d..5ca695b 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java
@@ -85,7 +85,8 @@
protected static final int SECONDARY_USER_ID = 20;
private static final UserInfo PRIMARY_USER_INFO = new UserInfo(PRIMARY_USER_ID, null, null,
- UserInfo.FLAG_INITIALIZED | UserInfo.FLAG_ADMIN | UserInfo.FLAG_PRIMARY);
+ UserInfo.FLAG_INITIALIZED | UserInfo.FLAG_ADMIN | UserInfo.FLAG_PRIMARY
+ | UserInfo.FLAG_MAIN);
private static final UserInfo SECONDARY_USER_INFO = new UserInfo(SECONDARY_USER_ID, null, null,
UserInfo.FLAG_INITIALIZED);
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 196226a..41e3a08 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 @@
@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 @@
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 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 @@
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 @@
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 @@
@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 @@
@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 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 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 @@
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 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 @@
}
@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 @@
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 @@
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 @@
@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 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 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 @@
}
@Test
- public void testCredentialChangeNotPossibleInSecureFrpModeDuringSuw() {
+ public void testSetCredentialNotPossibleInSecureFrpModeDuringSuw() {
setUserSetupComplete(false);
setSecureFrpMode(true);
try {
@@ -433,21 +389,17 @@
}
@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 @@
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 @@
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 @@
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 @@
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 fc0ca7e..10869da 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 @@
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 b00467c..57593cf 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 @@
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.mockito.ArgumentCaptor;
import java.io.File;
-import java.util.ArrayList;
-import java.util.Arrays;
-
/**
* atest FrameworksServicesTests:SyntheticPasswordTests
@@ -127,21 +123,11 @@
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 @@
@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 @@
@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 @@
@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 @@
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 @@
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 @@
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 @@
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 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 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 @@
@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 @@
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 @@
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 @@
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 @@
}
@Test
- public void testEscrowTokenActivatedImmediatelyIfNoUserPasswordNeedsMigration()
- throws RemoteException {
+ public void testEscrowTokenActivatedImmediatelyIfNoUserPassword() throws RemoteException {
final byte[] token = "some-high-entropy-secure-token".getBytes();
+ 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 testEscrowTokenActivatedImmediatelyIfNoUserPasswordNoMigration()
- 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));
-
- 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 @@
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 @@
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 @@
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 @@
@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 @@
@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 @@
@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 @@
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 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 @@
@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 @@
@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 51ddcef..2c9ba34 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.LockscreenCredential;
import com.android.internal.widget.VerifyCredentialResponse;
+import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -49,6 +50,11 @@
@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 6c13a6f..966c047 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 @@
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 @@
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 0000000..54fa272
--- /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 b034b0d..fe31b9c 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 @@
.build();
mPowerGroup.updateLocked(/* screenBrightnessOverride= */ BRIGHTNESS,
- /* autoBrightness = */ false,
/* useProximitySensor= */ false,
/* boostScreenBrightness= */ false,
/* dozeScreenStateOverride= */ Display.STATE_ON,
@@ -273,7 +272,6 @@
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 @@
mPowerGroup.setWakeLockSummaryLocked(WAKE_LOCK_DOZE);
mPowerGroup.updateLocked(/* screenBrightnessOverride= */ BRIGHTNESS,
- /* autoBrightness = */ true,
/* useProximitySensor= */ true,
/* boostScreenBrightness= */ true,
/* dozeScreenStateOverride= */ Display.STATE_ON,
@@ -313,7 +310,6 @@
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 @@
assertThat(mPowerGroup.getWakefulnessLocked()).isEqualTo(WAKEFULNESS_DOZING);
mPowerGroup.updateLocked(/* screenBrightnessOverride= */ BRIGHTNESS,
- /* autoBrightness = */ true,
/* useProximitySensor= */ true,
/* boostScreenBrightness= */ true,
/* dozeScreenStateOverride= */ Display.STATE_ON,
@@ -352,7 +347,6 @@
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 @@
assertThat(mPowerGroup.getWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE);
mPowerGroup.updateLocked(/* screenBrightnessOverride= */ BRIGHTNESS,
- /* autoBrightness = */ true,
/* useProximitySensor= */ true,
/* boostScreenBrightness= */ true,
/* dozeScreenStateOverride= */ Display.STATE_ON,
@@ -390,7 +383,6 @@
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 @@
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 @@
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 @@
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 @@
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 @@
.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 @@
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 @@
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 @@
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 @@
.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 @@
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/servicestests/src/com/android/server/power/stats/BatteryStatsHistoryIteratorTest.java b/services/tests/servicestests/src/com/android/server/power/stats/BatteryStatsHistoryIteratorTest.java
index 22e44f8..71c8c1d 100644
--- a/services/tests/servicestests/src/com/android/server/power/stats/BatteryStatsHistoryIteratorTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/stats/BatteryStatsHistoryIteratorTest.java
@@ -93,43 +93,44 @@
}
final BatteryStatsHistoryIterator iterator =
- mBatteryStats.createBatteryStatsHistoryIterator();
+ mBatteryStats.iterateBatteryStatsHistory();
- BatteryStats.HistoryItem item = new BatteryStats.HistoryItem();
+ BatteryStats.HistoryItem item;
- assertThat(iterator.next(item)).isTrue();
+ assertThat(item = iterator.next()).isNotNull();
assertHistoryItem(item,
BatteryStats.HistoryItem.CMD_RESET, BatteryStats.HistoryItem.EVENT_NONE,
null, 0, 3_600_000, 90, 1_000_000);
- assertThat(iterator.next(item)).isTrue();
+ assertThat(item = iterator.next()).isNotNull();
assertHistoryItem(item,
BatteryStats.HistoryItem.CMD_UPDATE, BatteryStats.HistoryItem.EVENT_NONE,
null, 0, 3_600_000, 90, 1_000_000);
- assertThat(iterator.next(item)).isTrue();
+ assertThat(item = iterator.next()).isNotNull();
assertHistoryItem(item,
BatteryStats.HistoryItem.CMD_UPDATE, BatteryStats.HistoryItem.EVENT_NONE,
null, 0, 2_400_000, 80, 2_000_000);
- assertThat(iterator.next(item)).isTrue();
+ assertThat(item = iterator.next()).isNotNull();
assertHistoryItem(item,
BatteryStats.HistoryItem.CMD_UPDATE, BatteryStats.HistoryItem.EVENT_NONE,
null, 0, 2_400_000, 80, 2_000_000);
- assertThat(iterator.next(item)).isTrue();
+ assertThat(item = iterator.next()).isNotNull();
assertHistoryItem(item,
BatteryStats.HistoryItem.CMD_UPDATE,
BatteryStats.HistoryItem.EVENT_ALARM | BatteryStats.HistoryItem.EVENT_FLAG_START,
"foo", APP_UID, 2_400_000, 80, 3_000_000);
- assertThat(iterator.next(item)).isTrue();
+ assertThat(item = iterator.next()).isNotNull();
assertHistoryItem(item,
BatteryStats.HistoryItem.CMD_UPDATE,
BatteryStats.HistoryItem.EVENT_ALARM | BatteryStats.HistoryItem.EVENT_FLAG_FINISH,
"foo", APP_UID, 2_400_000, 80, 3_001_000);
- assertThat(iterator.next(item)).isFalse();
+ assertThat(iterator.hasNext()).isFalse();
+ assertThat(iterator.next()).isNull();
}
// Test history that spans multiple buffers and uses more than 32k different strings.
@@ -163,21 +164,21 @@
}
final BatteryStatsHistoryIterator iterator =
- mBatteryStats.createBatteryStatsHistoryIterator();
+ mBatteryStats.iterateBatteryStatsHistory();
- BatteryStats.HistoryItem item = new BatteryStats.HistoryItem();
- assertThat(iterator.next(item)).isTrue();
+ BatteryStats.HistoryItem item;
+ assertThat(item = iterator.next()).isNotNull();
assertThat(item.cmd).isEqualTo((int) BatteryStats.HistoryItem.CMD_RESET);
assertThat(item.eventCode).isEqualTo(BatteryStats.HistoryItem.EVENT_NONE);
assertThat(item.eventTag).isNull();
- assertThat(iterator.next(item)).isTrue();
+ assertThat(item = iterator.next()).isNotNull();
assertThat(item.cmd).isEqualTo((int) BatteryStats.HistoryItem.CMD_UPDATE);
assertThat(item.eventCode).isEqualTo(BatteryStats.HistoryItem.EVENT_NONE);
assertThat(item.eventTag).isNull();
assertThat(item.time).isEqualTo(1_000_000);
- assertThat(iterator.next(item)).isTrue();
+ assertThat(item = iterator.next()).isNotNull();
assertThat(item.cmd).isEqualTo((int) BatteryStats.HistoryItem.CMD_UPDATE);
assertThat(item.eventCode).isEqualTo(BatteryStats.HistoryItem.EVENT_NONE);
assertThat(item.eventTag).isNull();
@@ -186,7 +187,7 @@
for (int i = 0; i < eventCount; i++) {
String name = "a" + (i % 10);
do {
- assertThat(iterator.next(item)).isTrue();
+ assertThat(item = iterator.next()).isNotNull();
// Skip a blank event inserted at the start of every buffer
} while (item.cmd != BatteryStats.HistoryItem.CMD_UPDATE
|| item.eventCode == BatteryStats.HistoryItem.EVENT_NONE);
@@ -196,7 +197,7 @@
assertThat(item.eventTag.string).isEqualTo(name);
do {
- assertThat(iterator.next(item)).isTrue();
+ assertThat(item = iterator.next()).isNotNull();
} while (item.cmd != BatteryStats.HistoryItem.CMD_UPDATE
|| item.eventCode == BatteryStats.HistoryItem.EVENT_NONE);
@@ -205,7 +206,8 @@
assertThat(item.eventTag.string).isEqualTo(name);
}
- assertThat(iterator.next(item)).isFalse();
+ assertThat(iterator.hasNext()).isFalse();
+ assertThat(iterator.next()).isNull();
}
private void assertHistoryItem(BatteryStats.HistoryItem item, int command, int eventCode,
diff --git a/services/tests/servicestests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java b/services/tests/servicestests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java
index 3f5d331..22a7e8d 100644
--- a/services/tests/servicestests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java
@@ -309,10 +309,10 @@
mHistory.recordMeasuredEnergyDetails(200, 200, details);
BatteryStatsHistoryIterator iterator = mHistory.iterate();
- BatteryStats.HistoryItem item = new BatteryStats.HistoryItem();
- assertThat(iterator.next(item)).isTrue(); // First item contains current time only
+ BatteryStats.HistoryItem item;
+ assertThat(item = iterator.next()).isNotNull(); // First item contains current time only
- assertThat(iterator.next(item)).isTrue();
+ assertThat(item = iterator.next()).isNotNull();
String dump = toString(item, /* checkin */ false);
assertThat(dump).contains("+200ms");
@@ -344,9 +344,9 @@
BatteryStatsHistoryIterator iterator = mHistory.iterate();
BatteryStats.HistoryItem item = new BatteryStats.HistoryItem();
- assertThat(iterator.next(item)).isTrue(); // First item contains current time only
+ assertThat(item = iterator.next()).isNotNull(); // First item contains current time only
- assertThat(iterator.next(item)).isTrue();
+ assertThat(item = iterator.next()).isNotNull();
String dump = toString(item, /* checkin */ false);
assertThat(dump).contains("+200ms");
@@ -361,7 +361,7 @@
assertThat(checkin).contains("XB,3,2,HIGH");
assertThat(checkin).contains("XC,10123,100,200,300");
- assertThat(iterator.next(item)).isTrue();
+ assertThat(item = iterator.next()).isNotNull();
dump = toString(item, /* checkin */ false);
assertThat(dump).contains("+300ms");
diff --git a/services/tests/servicestests/src/com/android/server/power/stats/BatteryStatsNoteTest.java b/services/tests/servicestests/src/com/android/server/power/stats/BatteryStatsNoteTest.java
index 5b51868..773a2dc 100644
--- a/services/tests/servicestests/src/com/android/server/power/stats/BatteryStatsNoteTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/stats/BatteryStatsNoteTest.java
@@ -83,6 +83,7 @@
* Run: adb shell am instrument -e class BatteryStatsNoteTest -w \
* com.android.frameworks.coretests/androidx.test.runner.AndroidJUnitRunner
*/
+@SuppressWarnings("GuardedBy")
public class BatteryStatsNoteTest extends TestCase {
private static final String TAG = BatteryStatsNoteTest.class.getSimpleName();
@@ -271,11 +272,11 @@
clocks.realtime = clocks.uptime = 220;
bi.noteLongPartialWakelockFinish(name, historyName, ISOLATED_UID);
- final BatteryStatsHistoryIterator iterator = bi.createBatteryStatsHistoryIterator();
+ final BatteryStatsHistoryIterator iterator = bi.iterateBatteryStatsHistory();
- BatteryStats.HistoryItem item = new BatteryStats.HistoryItem();
+ BatteryStats.HistoryItem item;
- while (iterator.next(item)) {
+ while ((item = iterator.next()) != null) {
if (item.eventCode == HistoryItem.EVENT_LONG_WAKE_LOCK_START) break;
}
assertThat(item.eventCode).isEqualTo(HistoryItem.EVENT_LONG_WAKE_LOCK_START);
@@ -283,7 +284,7 @@
assertThat(item.eventTag.string).isEqualTo(historyName);
assertThat(item.eventTag.uid).isEqualTo(UID);
- while (iterator.next(item)) {
+ while ((item = iterator.next()) != null) {
if (item.eventCode == HistoryItem.EVENT_LONG_WAKE_LOCK_FINISH) break;
}
assertThat(item.eventCode).isEqualTo(HistoryItem.EVENT_LONG_WAKE_LOCK_FINISH);
@@ -327,11 +328,11 @@
clocks.realtime = clocks.uptime = 220;
bi.noteLongPartialWakelockFinish(name, historyName, ISOLATED_UID);
- final BatteryStatsHistoryIterator iterator = bi.createBatteryStatsHistoryIterator();
+ final BatteryStatsHistoryIterator iterator = bi.iterateBatteryStatsHistory();
- BatteryStats.HistoryItem item = new BatteryStats.HistoryItem();
+ BatteryStats.HistoryItem item;
- while (iterator.next(item)) {
+ while ((item = iterator.next()) != null) {
if (item.eventCode == HistoryItem.EVENT_LONG_WAKE_LOCK_START) break;
}
assertThat(item.eventCode).isEqualTo(HistoryItem.EVENT_LONG_WAKE_LOCK_START);
@@ -339,7 +340,7 @@
assertThat(item.eventTag.string).isEqualTo(historyName);
assertThat(item.eventTag.uid).isEqualTo(UID);
- while (iterator.next(item)) {
+ while ((item = iterator.next()) != null) {
if (item.eventCode == HistoryItem.EVENT_LONG_WAKE_LOCK_FINISH) break;
}
assertThat(item.eventCode).isEqualTo(HistoryItem.EVENT_LONG_WAKE_LOCK_FINISH);
@@ -941,26 +942,27 @@
clocks.realtime = clocks.uptime = 5000;
bi.noteAlarmFinishLocked("foo", null, UID);
- HistoryItem item = new HistoryItem();
- assertTrue(bi.startIteratingHistoryLocked());
+ BatteryStatsHistoryIterator iterator = bi.iterateBatteryStatsHistory();
+ HistoryItem item;
- assertTrue(bi.getNextHistoryLocked(item));
+ assertThat(item = iterator.next()).isNotNull();
assertEquals(HistoryItem.EVENT_ALARM_START, item.eventCode);
assertEquals("foo", item.eventTag.string);
assertEquals(UID, item.eventTag.uid);
// TODO(narayan): Figure out why this event is written to the history buffer. See
// test below where it is being interspersed between multiple START events too.
- assertTrue(bi.getNextHistoryLocked(item));
+ assertThat(item = iterator.next()).isNotNull();
assertEquals(HistoryItem.EVENT_NONE, item.eventCode);
- assertTrue(bi.getNextHistoryLocked(item));
+ assertThat(item = iterator.next()).isNotNull();
assertEquals(HistoryItem.EVENT_ALARM_FINISH, item.eventCode);
assertTrue(item.isDeltaData());
assertEquals("foo", item.eventTag.string);
assertEquals(UID, item.eventTag.uid);
- assertFalse(bi.getNextHistoryLocked(item));
+ assertThat(iterator.hasNext()).isFalse();
+ assertThat(iterator.next()).isNull();
}
@SmallTest
@@ -980,28 +982,28 @@
clocks.realtime = clocks.uptime = 5000;
bi.noteAlarmFinishLocked("foo", ws, UID);
- HistoryItem item = new HistoryItem();
- assertTrue(bi.startIteratingHistoryLocked());
+ BatteryStatsHistoryIterator iterator = bi.iterateBatteryStatsHistory();
+ HistoryItem item;
- assertTrue(bi.getNextHistoryLocked(item));
+ assertThat(item = iterator.next()).isNotNull();
assertEquals(HistoryItem.EVENT_ALARM_START, item.eventCode);
assertEquals("foo", item.eventTag.string);
assertEquals(100, item.eventTag.uid);
- assertTrue(bi.getNextHistoryLocked(item));
+ assertThat(item = iterator.next()).isNotNull();
assertEquals(HistoryItem.EVENT_NONE, item.eventCode);
- assertTrue(bi.getNextHistoryLocked(item));
+ assertThat(item = iterator.next()).isNotNull();
assertEquals(HistoryItem.EVENT_ALARM_START, item.eventCode);
assertEquals("foo", item.eventTag.string);
assertEquals(500, item.eventTag.uid);
- assertTrue(bi.getNextHistoryLocked(item));
+ assertThat(item = iterator.next()).isNotNull();
assertEquals(HistoryItem.EVENT_ALARM_FINISH, item.eventCode);
assertEquals("foo", item.eventTag.string);
assertEquals(100, item.eventTag.uid);
- assertTrue(bi.getNextHistoryLocked(item));
+ assertThat(item = iterator.next()).isNotNull();
assertEquals(HistoryItem.EVENT_ALARM_FINISH, item.eventCode);
assertEquals("foo", item.eventTag.string);
assertEquals(500, item.eventTag.uid);
diff --git a/services/tests/servicestests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java b/services/tests/servicestests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java
index 9d46e11..968609b 100644
--- a/services/tests/servicestests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java
+++ b/services/tests/servicestests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java
@@ -219,36 +219,37 @@
final BatteryStatsHistoryIterator iterator =
unparceled.iterateBatteryStatsHistory();
- BatteryStats.HistoryItem item = new BatteryStats.HistoryItem();
+ BatteryStats.HistoryItem item;
- assertThat(iterator.next(item)).isTrue();
+ assertThat(item = iterator.next()).isNotNull();
assertHistoryItem(item,
BatteryStats.HistoryItem.CMD_RESET, BatteryStats.HistoryItem.EVENT_NONE,
null, 0, 3_600_000, 90, 1_000_000);
- assertThat(iterator.next(item)).isTrue();
+ assertThat(item = iterator.next()).isNotNull();
assertHistoryItem(item,
BatteryStats.HistoryItem.CMD_UPDATE, BatteryStats.HistoryItem.EVENT_NONE,
null, 0, 3_600_000, 90, 1_000_000);
- assertThat(iterator.next(item)).isTrue();
+ assertThat(item = iterator.next()).isNotNull();
assertHistoryItem(item,
BatteryStats.HistoryItem.CMD_UPDATE, BatteryStats.HistoryItem.EVENT_NONE,
null, 0, 3_600_000, 90, 2_000_000);
- assertThat(iterator.next(item)).isTrue();
+ assertThat(item = iterator.next()).isNotNull();
assertHistoryItem(item,
BatteryStats.HistoryItem.CMD_UPDATE,
BatteryStats.HistoryItem.EVENT_ALARM | BatteryStats.HistoryItem.EVENT_FLAG_START,
"foo", APP_UID, 3_600_000, 90, 3_000_000);
- assertThat(iterator.next(item)).isTrue();
+ assertThat(item = iterator.next()).isNotNull();
assertHistoryItem(item,
BatteryStats.HistoryItem.CMD_UPDATE,
BatteryStats.HistoryItem.EVENT_ALARM | BatteryStats.HistoryItem.EVENT_FLAG_FINISH,
"foo", APP_UID, 3_600_000, 90, 3_001_000);
- assertThat(iterator.next(item)).isFalse();
+ assertThat(iterator.hasNext()).isFalse();
+ assertThat(iterator.next()).isNull();
}
@Test
@@ -304,16 +305,16 @@
BatteryUsageStats.class);
BatteryStatsHistoryIterator iterator = unparceled.iterateBatteryStatsHistory();
- BatteryStats.HistoryItem item = new BatteryStats.HistoryItem();
+ BatteryStats.HistoryItem item;
- assertThat(iterator.next(item)).isTrue();
+ assertThat(item = iterator.next()).isNotNull();
assertThat(item.cmd).isEqualTo((int) BatteryStats.HistoryItem.CMD_RESET);
int expectedUid = 1;
- while (iterator.next(item)) {
+ while ((item = iterator.next()) != null) {
while (item.cmd != BatteryStats.HistoryItem.CMD_UPDATE
|| item.eventCode == BatteryStats.HistoryItem.EVENT_NONE) {
- assertThat(iterator.next(item)).isTrue();
+ assertThat(item = iterator.next()).isNotNull();
}
int uid = item.eventTag.uid;
assertThat(uid).isEqualTo(expectedUid++);
diff --git a/services/tests/wmtests/AndroidManifest.xml b/services/tests/wmtests/AndroidManifest.xml
index 107bbe1..593ee4a 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/ActivityStarterTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
index d16e11b..43627f4 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
@@ -1637,6 +1637,8 @@
final ActivityRecord target = new ActivityBuilder(mAtm).setAffinity(info.taskAffinity)
.build();
final ActivityStarter starter = prepareStarter(FLAG_ACTIVITY_NEW_TASK, false);
+ spyOn(starter);
+ doReturn(START_SUCCESS).when(starter).isAllowedToStart(any(), anyBoolean(), any());
startActivityInner(starter, target, null /* source */, null /* options */,
null /* inTask */, null /* inTaskFragment */);
@@ -1661,6 +1663,8 @@
final ActivityRecord target = new ActivityBuilder(mAtm).setRequiredDisplayCategory("auto")
.setAffinity(info.taskAffinity).build();
final ActivityStarter starter = prepareStarter(0, false);
+ spyOn(starter);
+ doReturn(START_SUCCESS).when(starter).isAllowedToStart(any(), anyBoolean(), any());
startActivityInner(starter, target, task.getBottomMostActivity(), null /* options */,
null /* inTask */, null /* inTaskFragment */);
@@ -1685,7 +1689,10 @@
final ActivityRecord target = new ActivityBuilder(mAtm)
.setRequiredDisplayCategory(info.requiredDisplayCategory)
.setAffinity(info.taskAffinity).build();
+
final ActivityStarter starter = prepareStarter(0, false);
+ spyOn(starter);
+ doReturn(START_SUCCESS).when(starter).isAllowedToStart(any(), anyBoolean(), any());
startActivityInner(starter, target, task.getBottomMostActivity(), null /* options */,
null /* inTask */, null /* inTaskFragment */);
@@ -1706,6 +1713,8 @@
inTask.inRecents = true;
final ActivityStarter starter = prepareStarter(0, false);
+ spyOn(starter);
+ doReturn(START_SUCCESS).when(starter).isAllowedToStart(any(), anyBoolean(), any());
final ActivityRecord target = new ActivityBuilder(mAtm).build();
startActivityInner(starter, target, null /* source */, null /* options */, inTask,
null /* inTaskFragment */);
@@ -1724,6 +1733,8 @@
inTask.inRecents = true;
final ActivityStarter starter = prepareStarter(0, false);
+ spyOn(starter);
+ doReturn(START_SUCCESS).when(starter).isAllowedToStart(any(), anyBoolean(), any());
final ActivityRecord target = new ActivityBuilder(mAtm).setRequiredDisplayCategory("auto")
.build();
startActivityInner(starter, target, null /* source */, null /* options */, inTask,
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 49fd1ab..92dd047 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 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 @@
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 @@
}
@Test
- public void testOnTaskConfigurationChanged_resizesSurface() {
+ public void testOnTaskOrientationConfigurationChanged_resizesSurface() {
mContentRecorder.setContentRecordingSession(mTaskSession);
mContentRecorder.updateRecording();
@@ -256,6 +258,29 @@
}
@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 @@
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/DisplayWindowPolicyControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowPolicyControllerTests.java
index ba68a25..2ce1d60 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowPolicyControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowPolicyControllerTests.java
@@ -193,6 +193,30 @@
assertEquals(result, START_ABORTED);
}
+ @Test
+ public void testCanActivityBeLaunched_requiredDisplayCategory() {
+ ActivityStarter starter = new ActivityStarter(mock(ActivityStartController.class), mAtm,
+ mSupervisor, mock(ActivityStartInterceptor.class));
+ final Task task = new TaskBuilder(mSupervisor).setDisplay(mSecondaryDisplay).build();
+ final ActivityRecord sourceRecord = new ActivityBuilder(mAtm).setTask(task).build();
+ final ActivityRecord disallowedRecord =
+ new ActivityBuilder(mAtm).setRequiredDisplayCategory("auto").build();
+
+ int result = starter.startActivityInner(
+ disallowedRecord,
+ sourceRecord,
+ /* voiceSession= */null,
+ /* voiceInteractor= */ null,
+ /* startFlags= */ 0,
+ /* options= */null,
+ /* inTask= */null,
+ /* inTaskFragment= */ null,
+ /* restrictedBgActivity= */false,
+ /* intentGrants= */null);
+
+ assertEquals(result, START_ABORTED);
+ }
+
private class TestDisplayWindowPolicyController extends DisplayWindowPolicyController {
public ComponentName DISALLOWED_ACTIVITY =
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 13ea99a..6877e4f 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 @@
}
@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 5e1fae0..7d9f29c 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.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.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 @@
@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 @@
@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 @@
@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 @@
}
@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 @@
}
@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 @@
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/utils/CoordinateTransformsTest.java b/services/tests/wmtests/src/com/android/server/wm/utils/CoordinateTransformsTest.java
index 99ceb20..3e74626 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_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 @@
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 cd4d65d..ff43ff7 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 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 @@
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 @@
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 2812264..84bd7160 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.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 @@
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")
@@ -106,12 +108,14 @@
}
HotwordMetricsLogger.writeKeyphraseTriggerEvent(
HotwordDetector.DETECTOR_TYPE_TRUSTED_HOTWORD_DSP,
- HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__DETECTED);
+ HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__DETECTED,
+ mVoiceInteractionServiceUid);
if (!mValidatingDspTrigger) {
Slog.i(TAG, "Ignoring #onDetected due to a process restart");
HotwordMetricsLogger.writeKeyphraseTriggerEvent(
HotwordDetector.DETECTOR_TYPE_TRUSTED_HOTWORD_DSP,
- METRICS_KEYPHRASE_TRIGGERED_DETECT_UNEXPECTED_CALLBACK);
+ METRICS_KEYPHRASE_TRIGGERED_DETECT_UNEXPECTED_CALLBACK,
+ mVoiceInteractionServiceUid);
return;
}
mValidatingDspTrigger = false;
@@ -122,7 +126,8 @@
Slog.i(TAG, "Ignoring #onDetected due to a SecurityException", e);
HotwordMetricsLogger.writeKeyphraseTriggerEvent(
HotwordDetector.DETECTOR_TYPE_TRUSTED_HOTWORD_DSP,
- METRICS_KEYPHRASE_TRIGGERED_DETECT_SECURITY_EXCEPTION);
+ METRICS_KEYPHRASE_TRIGGERED_DETECT_SECURITY_EXCEPTION,
+ mVoiceInteractionServiceUid);
externalCallback.onError(CALLBACK_ONDETECTED_GOT_SECURITY_EXCEPTION);
return;
}
@@ -157,12 +162,14 @@
}
HotwordMetricsLogger.writeKeyphraseTriggerEvent(
HotwordDetector.DETECTOR_TYPE_TRUSTED_HOTWORD_DSP,
- HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__REJECTED);
+ HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__REJECTED,
+ mVoiceInteractionServiceUid);
if (!mValidatingDspTrigger) {
Slog.i(TAG, "Ignoring #onRejected due to a process restart");
HotwordMetricsLogger.writeKeyphraseTriggerEvent(
HotwordDetector.DETECTOR_TYPE_TRUSTED_HOTWORD_DSP,
- METRICS_KEYPHRASE_TRIGGERED_REJECT_UNEXPECTED_CALLBACK);
+ METRICS_KEYPHRASE_TRIGGERED_REJECT_UNEXPECTED_CALLBACK,
+ mVoiceInteractionServiceUid);
return;
}
mValidatingDspTrigger = false;
@@ -187,7 +194,8 @@
Slog.w(TAG, "Timed out on #detectFromDspSource");
HotwordMetricsLogger.writeKeyphraseTriggerEvent(
HotwordDetector.DETECTOR_TYPE_TRUSTED_HOTWORD_DSP,
- HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__DETECT_TIMEOUT);
+ HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__DETECT_TIMEOUT,
+ mVoiceInteractionServiceUid);
try {
externalCallback.onError(CALLBACK_DETECT_TIMEOUT);
} catch (RemoteException e) {
@@ -220,7 +228,8 @@
mCallback.onRejected(new HotwordRejectedResult.Builder().build());
HotwordMetricsLogger.writeKeyphraseTriggerEvent(
HotwordDetector.DETECTOR_TYPE_TRUSTED_HOTWORD_DSP,
- HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__REJECTED_FROM_RESTART);
+ HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__REJECTED_FROM_RESTART,
+ mVoiceInteractionServiceUid);
} catch (RemoteException e) {
Slog.w(TAG, "Failed to call #rejected");
HotwordMetricsLogger.writeDetectorEvent(
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
index 2a4d4a1..6a7a2f9 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.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.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 @@
@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 @@
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 {
@@ -165,7 +154,8 @@
synchronized (mLock) {
restartProcessLocked();
HotwordMetricsLogger.writeServiceRestartEvent(mDetectorType,
- HOTWORD_DETECTION_SERVICE_RESTARTED__REASON__SCHEDULE);
+ HOTWORD_DETECTION_SERVICE_RESTARTED__REASON__SCHEDULE,
+ mVoiceInteractionServiceUid);
}
}, mReStartPeriodSeconds, mReStartPeriodSeconds, TimeUnit.SECONDS);
}
@@ -200,7 +190,8 @@
// conditions with audio reading in the service.
restartProcessLocked();
HotwordMetricsLogger.writeServiceRestartEvent(mDetectorType,
- HOTWORD_DETECTION_SERVICE_RESTARTED__REASON__AUDIO_SERVICE_DIED);
+ HOTWORD_DETECTION_SERVICE_RESTARTED__REASON__AUDIO_SERVICE_DIED,
+ mVoiceInteractionServiceUid);
}
}
@@ -208,7 +199,10 @@
void cancelLocked() {
Slog.v(TAG, "cancelLocked");
clearDebugHotwordLoggingTimeoutLocked();
- mHotwordDetectorSession.destroyLocked();
+ runForEachHotwordDetectorSessionLocked((session) -> {
+ session.destroyLocked();
+ });
+ mHotwordDetectorSessions.clear();
mDebugHotwordLogging = false;
mRemoteHotwordDetectionService.unbind();
LocalServices.getService(PermissionManagerServiceInternal.class)
@@ -218,7 +212,7 @@
}
mIdentity = null;
if (mCancellationTaskFuture != null) {
- mCancellationTaskFuture.cancel(/* may interrupt */ true);
+ mCancellationTaskFuture.cancel(/* mayInterruptIfRunning= */ true);
}
if (mAudioFlinger != null) {
mAudioFlinger.unlinkToDeath(mAudioServerDeathRecipient, /* flags= */ 0);
@@ -226,57 +220,65 @@
}
@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(
@@ -293,13 +295,16 @@
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);
}
}
@@ -315,7 +320,9 @@
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
@@ -323,7 +330,9 @@
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);
}
@@ -331,7 +340,7 @@
private void clearDebugHotwordLoggingTimeoutLocked() {
if (mDebugHotwordLoggingTimeoutFuture != null) {
- mDebugHotwordLoggingTimeoutFuture.cancel(/* mayInterruptIfRunning= */true);
+ mDebugHotwordLoggingTimeoutFuture.cancel(/* mayInterruptIfRunning= */ true);
mDebugHotwordLoggingTimeoutFuture = null;
}
}
@@ -348,12 +357,14 @@
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) {
@@ -364,11 +375,13 @@
static final class SoundTriggerCallback extends IRecognitionStatusCallback.Stub {
private final HotwordDetectionConnection mHotwordDetectionConnection;
private final IHotwordRecognitionStatusCallback mExternalCallback;
+ private final int mVoiceInteractionServiceUid;
SoundTriggerCallback(IHotwordRecognitionStatusCallback callback,
- HotwordDetectionConnection connection) {
+ HotwordDetectionConnection connection, int uid) {
mHotwordDetectionConnection = connection;
mExternalCallback = callback;
+ mVoiceInteractionServiceUid = uid;
}
@Override
@@ -381,13 +394,15 @@
if (useHotwordDetectionService) {
HotwordMetricsLogger.writeKeyphraseTriggerEvent(
HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__DETECTOR_TYPE__TRUSTED_DETECTOR_DSP,
- HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__KEYPHRASE_TRIGGER);
+ HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__KEYPHRASE_TRIGGER,
+ mVoiceInteractionServiceUid);
mHotwordDetectionConnection.detectFromDspSource(
recognitionEvent, mExternalCallback);
} else {
HotwordMetricsLogger.writeKeyphraseTriggerEvent(
HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__DETECTOR_TYPE__NORMAL_DETECTOR,
- HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__KEYPHRASE_TRIGGER);
+ HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__KEYPHRASE_TRIGGER,
+ mVoiceInteractionServiceUid);
mExternalCallback.onKeyphraseDetected(recognitionEvent, null);
}
}
@@ -425,8 +440,10 @@
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);
+ });
}
}
@@ -444,7 +461,7 @@
ServiceConnection createLocked() {
ServiceConnection connection =
new ServiceConnection(mContext, mIntent, mBindingFlags, mUser,
- IHotwordDetectionService.Stub::asInterface,
+ ISandboxedDetectionService.Stub::asInterface,
mRestartCount++ % MAX_ISOLATED_PROCESS_NUMBER);
connection.connect();
@@ -456,7 +473,7 @@
}
}
- class ServiceConnection extends ServiceConnector.Impl<IHotwordDetectionService> {
+ class ServiceConnection extends ServiceConnector.Impl<ISandboxedDetectionService> {
private final Object mLock = new Object();
private final Intent mIntent;
@@ -469,7 +486,7 @@
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;
@@ -478,7 +495,7 @@
}
@Override // from ServiceConnector.Impl
- protected void onServiceConnectionStatusChanged(IHotwordDetectionService service,
+ protected void onServiceConnectionStatusChanged(ISandboxedDetectionService service,
boolean connected) {
if (DEBUG) {
Slog.d(TAG, "onServiceConnectionStatusChanged connected = " + connected);
@@ -519,8 +536,10 @@
}
}
synchronized (HotwordDetectionConnection.this.mLock) {
- mHotwordDetectorSession.reportErrorLocked(
- HotwordDetectorSession.HOTWORD_DETECTION_SERVICE_DIED);
+ runForEachHotwordDetectorSessionLocked((session) -> {
+ session.reportErrorLocked(
+ HotwordDetectorSession.HOTWORD_DETECTION_SERVICE_DIED);
+ });
}
}
@@ -565,6 +584,93 @@
}
}
+ @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 f9f43c9..689423a 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.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 @@
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;
@@ -238,7 +241,7 @@
try {
mCallback.onStatusReported(status);
HotwordMetricsLogger.writeServiceInitResultEvent(getDetectorType(),
- initResultMetricsResult);
+ initResultMetricsResult, mVoiceInteractionServiceUid);
} catch (RemoteException e) {
Slog.w(TAG, "Failed to report initialization status: " + e);
HotwordMetricsLogger.writeDetectorEvent(getDetectorType(),
@@ -269,7 +272,7 @@
try {
mCallback.onStatusReported(INITIALIZATION_STATUS_UNKNOWN);
HotwordMetricsLogger.writeServiceInitResultEvent(getDetectorType(),
- METRICS_INIT_UNKNOWN_TIMEOUT);
+ METRICS_INIT_UNKNOWN_TIMEOUT, mVoiceInteractionServiceUid);
} catch (RemoteException e) {
Slog.w(TAG, "Failed to report initialization status UNKNOWN", e);
HotwordMetricsLogger.writeDetectorEvent(getDetectorType(),
@@ -474,8 +477,8 @@
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 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/HotwordMetricsLogger.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordMetricsLogger.java
index 940aed3..61c18be 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordMetricsLogger.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordMetricsLogger.java
@@ -64,28 +64,28 @@
/**
* Logs information related to hotword detection service init result.
*/
- public static void writeServiceInitResultEvent(int detectorType, int result) {
+ public static void writeServiceInitResultEvent(int detectorType, int result, int uid) {
int metricsDetectorType = getInitMetricsDetectorType(detectorType);
FrameworkStatsLog.write(FrameworkStatsLog.HOTWORD_DETECTION_SERVICE_INIT_RESULT_REPORTED,
- metricsDetectorType, result);
+ metricsDetectorType, result, uid);
}
/**
* Logs information related to hotword detection service restarting.
*/
- public static void writeServiceRestartEvent(int detectorType, int reason) {
+ public static void writeServiceRestartEvent(int detectorType, int reason, int uid) {
int metricsDetectorType = getRestartMetricsDetectorType(detectorType);
FrameworkStatsLog.write(FrameworkStatsLog.HOTWORD_DETECTION_SERVICE_RESTARTED,
- metricsDetectorType, reason);
+ metricsDetectorType, reason, uid);
}
/**
* Logs information related to keyphrase trigger.
*/
- public static void writeKeyphraseTriggerEvent(int detectorType, int result) {
+ public static void writeKeyphraseTriggerEvent(int detectorType, int result, int uid) {
int metricsDetectorType = getKeyphraseMetricsDetectorType(detectorType);
FrameworkStatsLog.write(FrameworkStatsLog.HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED,
- metricsDetectorType, result);
+ metricsDetectorType, result, uid);
}
/**
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/SoftwareTrustedHotwordDetectorSession.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/SoftwareTrustedHotwordDetectorSession.java
index 6930689..4eb997a 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.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.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 @@
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")
@@ -101,12 +103,14 @@
synchronized (mLock) {
HotwordMetricsLogger.writeKeyphraseTriggerEvent(
HotwordDetector.DETECTOR_TYPE_TRUSTED_HOTWORD_SOFTWARE,
- HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__DETECTED);
+ HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__DETECTED,
+ mVoiceInteractionServiceUid);
if (!mPerformingSoftwareHotwordDetection) {
Slog.i(TAG, "Hotword detection has already completed");
HotwordMetricsLogger.writeKeyphraseTriggerEvent(
HotwordDetector.DETECTOR_TYPE_TRUSTED_HOTWORD_SOFTWARE,
- METRICS_KEYPHRASE_TRIGGERED_DETECT_UNEXPECTED_CALLBACK);
+ METRICS_KEYPHRASE_TRIGGERED_DETECT_UNEXPECTED_CALLBACK,
+ mVoiceInteractionServiceUid);
return;
}
mPerformingSoftwareHotwordDetection = false;
@@ -115,7 +119,8 @@
} catch (SecurityException e) {
HotwordMetricsLogger.writeKeyphraseTriggerEvent(
HotwordDetector.DETECTOR_TYPE_TRUSTED_HOTWORD_SOFTWARE,
- METRICS_KEYPHRASE_TRIGGERED_DETECT_SECURITY_EXCEPTION);
+ METRICS_KEYPHRASE_TRIGGERED_DETECT_SECURITY_EXCEPTION,
+ mVoiceInteractionServiceUid);
mSoftwareCallback.onError();
return;
}
@@ -144,7 +149,8 @@
}
HotwordMetricsLogger.writeKeyphraseTriggerEvent(
HotwordDetector.DETECTOR_TYPE_TRUSTED_HOTWORD_SOFTWARE,
- HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__REJECTED);
+ HOTWORD_DETECTOR_KEYPHRASE_TRIGGERED__RESULT__REJECTED,
+ mVoiceInteractionServiceUid);
// onRejected isn't allowed here, and we are not expecting it.
}
};
@@ -163,9 +169,9 @@
}
@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");
@@ -173,7 +179,7 @@
}
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 7207e373..9a02188 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 @@
@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 @@
@NonNull Identity voiceInteractorIdentity,
@Nullable PersistableBundle options,
@Nullable SharedMemory sharedMemory,
+ @NonNull IBinder token,
IHotwordRecognitionStatusCallback callback,
int detectorType) {
super.initAndVerifyDetector_enforcePermission();
@@ -1274,7 +1276,20 @@
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 @@
ParcelFileDescriptor audioStream,
AudioFormat audioFormat,
PersistableBundle options,
+ @NonNull IBinder token,
IMicrophoneHotwordDetectionVoiceInteractionCallback callback)
throws RemoteException {
synchronized (this) {
@@ -1338,8 +1354,8 @@
}
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 0a660b0..f041adc 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.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 @@
VoiceInteractionSessionConnection mActiveSession;
int mDisabledShowContext;
- int mDetectorType;
final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
@@ -552,7 +550,8 @@
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 @@
throw new IllegalStateException("Hotword detection connection not found");
}
synchronized (mHotwordDetectionConnection.mLock) {
- mHotwordDetectionConnection.updateStateLocked(options, sharedMemory);
+ mHotwordDetectionConnection.updateStateLocked(options, sharedMemory, token);
}
}
@@ -573,6 +572,7 @@
@NonNull Identity voiceInteractorIdentity,
@Nullable PersistableBundle options,
@Nullable SharedMemory sharedMemory,
+ @NonNull IBinder token,
IHotwordRecognitionStatusCallback callback,
int detectorType) {
Slog.v(TAG, "initAndVerifyDetectorLocked");
@@ -624,16 +624,26 @@
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 @@
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 @@
AudioFormat audioFormat,
IMicrophoneHotwordDetectionVoiceInteractionCallback callback) {
if (DEBUG) {
- Slog.d(TAG, "startListeningFromMic");
+ Slog.d(TAG, "startListeningFromMicLocked");
}
if (mHotwordDetectionConnection == null) {
@@ -671,16 +678,17 @@
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 @@
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(
@@ -730,7 +738,7 @@
Slog.d(TAG, "createSoundTriggerCallbackLocked");
}
return new HotwordDetectionConnection.SoundTriggerCallback(callback,
- mHotwordDetectionConnection);
+ mHotwordDetectionConnection, mInfo.getServiceInfo().applicationInfo.uid);
}
private static ServiceInfo getServiceInfoLocked(@NonNull ComponentName componentName,
@@ -809,8 +817,6 @@
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 @@
@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 22cd31a..c4744ef 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -17,6 +17,7 @@
package android.telephony;
import android.Manifest;
+import android.annotation.CallbackExecutor;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -52,6 +53,7 @@
import com.android.telephony.Rlog;
import java.util.List;
+import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
@@ -9957,10 +9959,13 @@
* @param subId the subscription ID, normally obtained from {@link SubscriptionManager}.
* @return A {@link PersistableBundle} containing the config for the given subId, or default
* values for an invalid subId.
+ *
+ * @deprecated Use {@link #getConfigForSubId(int, String...)} instead.
*/
@SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges
@RequiresPermission(Manifest.permission.READ_PHONE_STATE)
@Nullable
+ @Deprecated
public PersistableBundle getConfigForSubId(int subId) {
try {
ICarrierConfigLoader loader = getICarrierConfigLoader();
@@ -9979,6 +9984,59 @@
}
/**
+ * 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. 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
+ * any carrier specific configuration has been applied.
+ *
+ * <p>Note that on success, the key/value for {@link #KEY_CARRIER_CONFIG_VERSION_STRING} and
+ * {@link #KEY_CARRIER_CONFIG_APPLIED_BOOL} are always in the returned bundle, no matter if they
+ * were explicitly requested.
+ *
+ * <p>Requires Permission:
+ * {@link android.Manifest.permission#READ_PHONE_STATE READ_PHONE_STATE}, or the calling app
+ * has carrier privileges on the specified subscription (see
+ * {@link TelephonyManager#hasCarrierPrivileges()}).
+ *
+ * @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 the calling app
+ * has no permission).
+ */
+ @RequiresPermission(anyOf = {
+ Manifest.permission.READ_PHONE_STATE,
+ "carrier privileges",
+ })
+ @NonNull
+ public PersistableBundle getConfigForSubId(int subId, @NonNull String... keys) {
+ Objects.requireNonNull(keys, "Config keys should be non-null");
+ for (String key : keys) {
+ Objects.requireNonNull(key, "Config key should be non-null");
+ }
+
+ try {
+ ICarrierConfigLoader loader = getICarrierConfigLoader();
+ if (loader == null) {
+ Rlog.w(TAG, "Error getting config for subId " + subId
+ + " ICarrierConfigLoader is null");
+ throw new IllegalStateException("Carrier config loader is not available.");
+ }
+ return loader.getConfigSubsetForSubIdWithFeature(subId, mContext.getOpPackageName(),
+ mContext.getAttributionTag(), keys);
+ } catch (RemoteException ex) {
+ Rlog.e(TAG, "Error getting config for subId " + subId + ": " + ex);
+ ex.rethrowAsRuntimeException();
+ }
+ return new PersistableBundle();
+ }
+
+ /**
* Overrides the carrier config of the provided subscription ID with the provided values.
*
* Any further queries to carrier config from any process will return the overridden values
@@ -10052,15 +10110,52 @@
* has carrier privileges (see {@link TelephonyManager#hasCarrierPrivileges()}).
*
* @see #getConfigForSubId
+ * @see #getConfig(String...)
+ * @deprecated use {@link #getConfig(String...)} instead.
*/
@SuppressAutoDoc // Blocked by b/72967236 - no support for carrier privileges
@RequiresPermission(Manifest.permission.READ_PHONE_STATE)
@Nullable
+ @Deprecated
public PersistableBundle getConfig() {
return getConfigForSubId(SubscriptionManager.getDefaultSubscriptionId());
}
/**
+ * 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.
+ *
+ * <p>Note that on success, the key/value for {@link #KEY_CARRIER_CONFIG_VERSION_STRING} and
+ * {@link #KEY_CARRIER_CONFIG_APPLIED_BOOL} are always in the returned bundle, no matter if
+ * they were explicitly requested.
+ *
+ * <p>Requires Permission:
+ * {@link android.Manifest.permission#READ_PHONE_STATE READ_PHONE_STATE}, or the calling app
+ * has carrier privileges for the default subscription (see
+ * {@link TelephonyManager#hasCarrierPrivileges()}).
+ *
+ * @param keys The config keys to retrieve values
+ * @return A {@link PersistableBundle} with key/value mapping for the specified carrier
+ * configs on success, or an empty (but never null) bundle on failure.
+ * @see #getConfigForSubId(int, String...)
+ * @see SubscriptionManager#getDefaultSubscriptionId()
+ */
+ @RequiresPermission(anyOf = {
+ Manifest.permission.READ_PHONE_STATE,
+ "carrier privileges",
+ })
+ @NonNull
+ public PersistableBundle getConfig(@NonNull String... keys) {
+ return getConfigForSubId(SubscriptionManager.getDefaultSubscriptionId(), keys);
+ }
+
+ /**
* Determines whether a configuration {@link PersistableBundle} obtained from
* {@link #getConfig()} or {@link #getConfigForSubId(int)} corresponds to an identified carrier.
*
@@ -10246,4 +10341,85 @@
configs.putPersistableBundle(key, (PersistableBundle) value);
}
}
+
+ /**
+ * Listener interface to get a notification when the carrier configurations have changed.
+ *
+ * Use this listener to receive timely updates when the carrier configuration changes. System
+ * components should prefer this listener over {@link #ACTION_CARRIER_CONFIG_CHANGED}
+ * whenever possible.
+ *
+ * To register the listener, call
+ * {@link #registerCarrierConfigChangeListener(Executor, CarrierConfigChangeListener)}.
+ * To unregister, call
+ * {@link #unregisterCarrierConfigChangeListener(CarrierConfigChangeListener)}.
+ *
+ * Note that on registration, registrants will NOT receive a notification on last carrier config
+ * change. Only carrier configs change AFTER the registration will be sent to registrants. And
+ * unlike {@link #ACTION_CARRIER_CONFIG_CHANGED}, notification wouldn't send when the device is
+ * unlocked. Registrants only receive the notification when there has been real carrier config
+ * changes.
+ *
+ * @see #registerCarrierConfigChangeListener(Executor, CarrierConfigChangeListener)
+ * @see #unregisterCarrierConfigChangeListener(CarrierConfigChangeListener)
+ * @see #ACTION_CARRIER_CONFIG_CHANGED
+ * @see #getConfig(String...)
+ * @see #getConfigForSubId(int, String...)
+ */
+ public interface CarrierConfigChangeListener {
+ /**
+ * Called when carrier configurations have changed.
+ *
+ * @param logicalSlotIndex The logical SIM slot index on which to monitor and get
+ * notification. It is guaranteed to be valid.
+ * @param subscriptionId The subscription on the SIM slot. May be
+ * {@link SubscriptionManager#INVALID_SUBSCRIPTION_ID}.
+ * @param carrierId The optional carrier Id, may be
+ * {@link TelephonyManager#UNKNOWN_CARRIER_ID}.
+ * See {@link TelephonyManager#getSimCarrierId()}.
+ * @param specificCarrierId The optional fine-grained carrierId, may be {@link
+ * TelephonyManager#UNKNOWN_CARRIER_ID}. A specific carrierId may
+ * be different from the carrierId above in a MVNO scenario. See
+ * detail in {@link TelephonyManager#getSimSpecificCarrierId()}.
+ */
+ void onCarrierConfigChanged(int logicalSlotIndex, int subscriptionId, int carrierId,
+ int specificCarrierId);
+ }
+
+ /**
+ * Register a {@link CarrierConfigChangeListener} to get a notification when carrier
+ * configurations have changed.
+ *
+ * @param executor The executor on which the listener will be called.
+ * @param listener The CarrierConfigChangeListener called when carrier configs has changed.
+ */
+ public void registerCarrierConfigChangeListener(@NonNull @CallbackExecutor Executor executor,
+ @NonNull CarrierConfigChangeListener listener) {
+ Objects.requireNonNull(executor, "Executor should be non-null.");
+ Objects.requireNonNull(listener, "Listener should be non-null.");
+
+ TelephonyRegistryManager trm = mContext.getSystemService(TelephonyRegistryManager.class);
+ if (trm == null) {
+ throw new IllegalStateException("Telephony registry service is null");
+ }
+ trm.addCarrierConfigChangedListener(executor, listener);
+ }
+
+ /**
+ * Unregister the {@link CarrierConfigChangeListener} to stop notification on carrier
+ * configurations change.
+ *
+ * @param listener The CarrierConfigChangeListener which was registered with method
+ * {@link #registerCarrierConfigChangeListener(Executor, CarrierConfigChangeListener)}.
+ */
+ public void unregisterCarrierConfigChangeListener(
+ @NonNull CarrierConfigChangeListener listener) {
+ Objects.requireNonNull(listener, "Listener should be non-null.");
+
+ TelephonyRegistryManager trm = mContext.getSystemService(TelephonyRegistryManager.class);
+ if (trm == null) {
+ throw new IllegalStateException("Telephony registry service is null");
+ }
+ trm.removeCarrierConfigChangedListener(listener);
+ }
}
diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java
index 9b566fb..8e8755d 100644
--- a/telephony/java/android/telephony/SubscriptionManager.java
+++ b/telephony/java/android/telephony/SubscriptionManager.java
@@ -58,6 +58,7 @@
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 @@
/**
* 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 @@
* 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 @@
*
* 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 @@
*
* <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 @@
*
* @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 @@
}
/**
- * 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 @@
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 @@
}
/**
- * 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 @@
}
/**
- * 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
+ * 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 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
- * @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 @@
* 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 @@
* </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 @@
*
* 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 @@
*
* 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 @@
/**
* 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 @@
@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 @@
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 @@
* @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 @@
* 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 @@
* <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 @@
* 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 @@
* 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 @@
}
/**
- * Check if a subscription is active.
+ * Check if the subscription is currently active in any slot.
*
- * @param subscriptionId The subscription id to check.
- *
- * @return {@code true} if the subscription is active.
- *
- * @throws IllegalArgumentException if the provided slot index is invalid.
+ * @param subscriptionId The subscription id.
*
* @hide
*/
@@ -3730,15 +3756,14 @@
}
/**
- * 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 @@
* 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 @@
}
/**
- * 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 @@
}
/**
- * 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 @@
/**
* 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 @@
* 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 @@
* @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 09daa2d..5d49413 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -2935,7 +2935,7 @@
public static final int NETWORK_TYPE_HSPA = TelephonyProtoEnums.NETWORK_TYPE_HSPA; // = 10.
/**
* Current network is iDen
- * @deprecated Legacy network type no longer being used.
+ * @deprecated Legacy network type no longer being used starting in Android U.
*/
@Deprecated
public static final int NETWORK_TYPE_IDEN = TelephonyProtoEnums.NETWORK_TYPE_IDEN; // = 11.
@@ -13960,7 +13960,7 @@
* If used, will be converted to {@link #NETWORK_TYPE_BITMASK_LTE}.
* network type bitmask indicating the support of radio tech LTE CA (carrier aggregation).
*
- * @deprecated Please use {@link #NETWORK_TYPE_BITMASK_LTE} instead.
+ * @deprecated Please use {@link #NETWORK_TYPE_BITMASK_LTE} instead. Deprecated in Android U.
*/
@Deprecated
public static final long NETWORK_TYPE_BITMASK_LTE_CA = (1 << (NETWORK_TYPE_LTE_CA -1));
@@ -17808,11 +17808,11 @@
* @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 @@
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 5246470..c8c8724 100644
--- a/telephony/java/android/telephony/ims/aidl/IImsMmTelListener.aidl
+++ b/telephony/java/android/telephony/ims/aidl/IImsMmTelListener.aidl
@@ -34,4 +34,6 @@
void onIncomingCall(IImsCallSession c, in Bundle extras);
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 a380241..1c7e9b9 100644
--- a/telephony/java/android/telephony/ims/feature/MmTelFeature.java
+++ b/telephony/java/android/telephony/ims/feature/MmTelFeature.java
@@ -579,6 +579,28 @@
public void onVoiceMessageCountUpdate(int count) {
}
+
+ /**
+ * Called to set the audio handler for this connection.
+ * @param imsAudioHandler an {@link ImsAudioHandler} used to handle the audio
+ * for this IMS call.
+ * @hide
+ */
+ @Override
+ 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) {
+
+ }
}
/**
@@ -628,6 +650,71 @@
public static final String EXTRA_IS_UNKNOWN_CALL =
"android.telephony.ims.feature.extra.IS_UNKNOWN_CALL";
+ /** @hide */
+ @IntDef(flag = true,
+ value = {
+ AUDIO_HANDLER_ANDROID,
+ AUDIO_HANDLER_BASEBAND
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ImsAudioHandler {}
+
+ /**
+ * Audio Handler - Android
+ * @hide
+ */
+ @SystemApi
+ public static final int AUDIO_HANDLER_ANDROID = 0;
+
+ /**
+ * Audio Handler - Baseband
+ * @hide
+ */
+ @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;
/**
@@ -774,6 +861,46 @@
}
/**
+ * Sets the audio handler for this connection. The vendor IMS stack will invoke this API
+ * to inform Telephony/Telecom layers about which audio handlers i.e. either Android or Modem
+ * shall be used for handling the IMS call audio.
+ *
+ * @param imsAudioHandler {@link MmTelFeature#ImsAudioHandler} used to handle the audio
+ * for this IMS call.
+ * @hide
+ */
+ @SystemApi
+ public final void setCallAudioHandler(@ImsAudioHandler int imsAudioHandler) {
+ IImsMmTelListener listener = getListener();
+ if (listener == null) {
+ throw new IllegalStateException("Session is not available.");
+ }
+ try {
+ listener.onAudioModeIsVoipChanged(imsAudioHandler);
+ } catch (RemoteException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * 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 897b57f..ad8a936 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.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 @@
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 @@
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/ICarrierConfigLoader.aidl b/telephony/java/com/android/internal/telephony/ICarrierConfigLoader.aidl
index 89620ea..1788bda 100644
--- a/telephony/java/com/android/internal/telephony/ICarrierConfigLoader.aidl
+++ b/telephony/java/com/android/internal/telephony/ICarrierConfigLoader.aidl
@@ -38,4 +38,6 @@
String getDefaultCarrierServicePackageName();
+ PersistableBundle getConfigSubsetForSubIdWithFeature(int subId, String callingPackage,
+ String callingFeatureId, in String[] carrierConfigs);
}
diff --git a/telephony/java/com/android/internal/telephony/ISub.aidl b/telephony/java/com/android/internal/telephony/ISub.aidl
index 280d259..c5f6902 100644
--- a/telephony/java/com/android/internal/telephony/ISub.aidl
+++ b/telephony/java/com/android/internal/telephony/ISub.aidl
@@ -242,8 +242,6 @@
int getDefaultSubId();
- int clearSubInfo();
-
int getPhoneId(int subId);
/**
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt
index a4609f7..948288a 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt
@@ -18,12 +18,14 @@
import android.app.Instrumentation
import android.platform.test.annotations.Presubmit
+import android.util.Log
import androidx.test.platform.app.InstrumentationRegistry
import com.android.launcher3.tapl.LauncherInstrumentation
import com.android.server.wm.flicker.junit.FlickerBuilderProvider
import com.android.server.wm.traces.common.ComponentNameMatcher
import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
import org.junit.Assume
+import org.junit.AssumptionViolatedException
import org.junit.Test
/**
@@ -48,6 +50,8 @@
tapl.setExpectedRotationCheckEnabled(true)
}
+ private val logTag = this::class.java.simpleName
+
/** Specification of the test transition to execute */
abstract val transition: FlickerBuilder.() -> Unit
@@ -100,10 +104,24 @@
@Test
open fun navBarWindowIsAlwaysVisible() {
Assume.assumeFalse(flicker.scenario.isTablet)
+ Assume.assumeFalse(flicker.scenario.isLandscapeOrSeascapeAtStart)
flicker.navBarWindowIsAlwaysVisible()
}
/**
+ * Checks that the [ComponentNameMatcher.NAV_BAR] window is visible at the start and end of
+ * the transition
+ *
+ * Note: Phones only
+ */
+ @Presubmit
+ @Test
+ open fun navBarWindowIsVisibleAtStartAndEnd() {
+ Assume.assumeFalse(flicker.scenario.isTablet)
+ flicker.navBarWindowIsVisibleAtStartAndEnd()
+ }
+
+ /**
* Checks that the [ComponentNameMatcher.TASK_BAR] window is visible at the start and end of the
* transition
*
@@ -179,13 +197,18 @@
statusBarWindowIsAlwaysVisible()
visibleLayersShownMoreThanOneConsecutiveEntry()
visibleWindowsShownMoreThanOneConsecutiveEntry()
+ runAndIgnoreAssumptionViolation { taskBarLayerIsVisibleAtStartAndEnd() }
+ runAndIgnoreAssumptionViolation { taskBarWindowIsAlwaysVisible() }
+ runAndIgnoreAssumptionViolation { navBarLayerIsVisibleAtStartAndEnd() }
+ runAndIgnoreAssumptionViolation { navBarWindowIsAlwaysVisible() }
+ runAndIgnoreAssumptionViolation { navBarWindowIsVisibleAtStartAndEnd() }
+ }
- if (flicker.scenario.isTablet) {
- taskBarLayerIsVisibleAtStartAndEnd()
- taskBarWindowIsAlwaysVisible()
- } else {
- navBarLayerIsVisibleAtStartAndEnd()
- navBarWindowIsAlwaysVisible()
+ protected fun runAndIgnoreAssumptionViolation(predicate: () -> Unit) {
+ try {
+ predicate()
+ } catch (e: AssumptionViolatedException) {
+ Log.e(logTag, "Assumption violation on CUJ complete", e)
}
}
}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToAppTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToAppTest.kt
index afc5f65..098c082 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToAppTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToAppTest.kt
@@ -106,10 +106,10 @@
@IwTest(focusArea = "ime")
override fun cujCompleted() {
super.cujCompleted()
- navBarLayerPositionAtStartAndEnd()
imeLayerBecomesInvisible()
imeAppLayerIsAlwaysVisible()
imeAppWindowIsAlwaysVisible()
+ runAndIgnoreAssumptionViolation { navBarLayerPositionAtStartAndEnd() }
}
companion object {
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToHomeTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToHomeTest.kt
index aedf965..f110e54 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToHomeTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/CloseImeWindowToHomeTest.kt
@@ -104,11 +104,11 @@
@IwTest(focusArea = "ime")
override fun cujCompleted() {
super.cujCompleted()
- navBarLayerPositionAtStartAndEnd()
imeLayerBecomesInvisible()
imeAppWindowBecomesInvisible()
imeWindowBecomesInvisible()
imeLayerBecomesInvisible()
+ runAndIgnoreAssumptionViolation { navBarLayerPositionAtStartAndEnd() }
}
companion object {
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest_ShellTransit.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest_ShellTransit.kt
index c599b10..e297e89 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest_ShellTransit.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/ime/SwitchImeWindowsFromGestureNavTest_ShellTransit.kt
@@ -21,8 +21,6 @@
import com.android.server.wm.flicker.FlickerTest
import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.navBarWindowIsVisibleAtStartAndEnd
-import com.android.server.wm.traces.common.ComponentNameMatcher
import org.junit.Assume
import org.junit.Before
import org.junit.FixMethodOrder
@@ -68,15 +66,4 @@
@Ignore("Nav bar window becomes invisible during quick switch")
@Test
override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
-
- /**
- * Checks that [ComponentNameMatcher.NAV_BAR] window is visible and above the app windows at the
- * start and end of the WM trace
- */
- @Presubmit
- @Test
- fun navBarWindowIsVisibleAtStartAndEnd() {
- Assume.assumeFalse(flicker.scenario.isTablet)
- flicker.navBarWindowIsVisibleAtStartAndEnd()
- }
}
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationCold.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationCold.kt
index f5f7190..26898f8 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationCold.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationCold.kt
@@ -89,6 +89,11 @@
override fun statusBarLayerIsVisibleAtStartAndEnd() =
super.statusBarLayerIsVisibleAtStartAndEnd()
+ /** {@inheritDoc} */
+ @Test
+ @Ignore("Not applicable to this CUJ. Display starts locked and app is full screen at the end")
+ override fun navBarWindowIsVisibleAtStartAndEnd() = super.navBarWindowIsVisibleAtStartAndEnd()
+
/**
* Checks the position of the [ComponentNameMatcher.STATUS_BAR] at the start and end of the
* transition
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWarm.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWarm.kt
index fe49c61..c44ad83 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWarm.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWarm.kt
@@ -112,6 +112,11 @@
/** {@inheritDoc} */
@Test
@Ignore("Not applicable to this CUJ. Display starts locked and app is full screen at the end")
+ override fun navBarWindowIsVisibleAtStartAndEnd() = super.navBarWindowIsVisibleAtStartAndEnd()
+
+ /** {@inheritDoc} */
+ @Test
+ @Ignore("Not applicable to this CUJ. Display starts locked and app is full screen at the end")
override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
/** {@inheritDoc} */
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWithLockOverlayApp.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWithLockOverlayApp.kt
index d9a3ad2..e3ffb45 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWithLockOverlayApp.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockNotificationWithLockOverlayApp.kt
@@ -109,6 +109,11 @@
@Test
override fun navBarLayerIsVisibleAtStartAndEnd() = super.navBarLayerIsVisibleAtStartAndEnd()
+ /** {@inheritDoc} */
+ @FlakyTest(bugId = 209599395)
+ @Test
+ override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
+
companion object {
/**
* Creates the test configurations.
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppNonResizeableTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppNonResizeableTest.kt
index f295ce3..142c688 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppNonResizeableTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppNonResizeableTest.kt
@@ -146,6 +146,11 @@
/** {@inheritDoc} */
@Test
+ @Ignore("Not applicable to this CUJ. Display starts locked and app is full screen at the end")
+ override fun navBarWindowIsVisibleAtStartAndEnd() = super.navBarWindowIsVisibleAtStartAndEnd()
+
+ /** {@inheritDoc} */
+ @Test
@Ignore("Not applicable to this CUJ. Display starts off and app is full screen at the end")
override fun statusBarWindowIsAlwaysVisible() {}
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 4e7ab7a..b064695 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.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.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 @@
/**
* 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 @@
@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 @@
/** {@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 @@
* 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 @@
* 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 coversExactly displayBounds") {
- it.visibleRegion(testApp.componentMatcher).coversExactly(displayBounds)
+ .invoke(
+ "LAUNCH_NEW_TASK_ACTIVITY's splashscreen coversExactly displayBounds",
+ isOptional = true
+ ) {
+ it.visibleRegion(
+ ComponentSplashScreenMatcher(launchNewTaskApp.componentMatcher))
+ .coversExactly(displayBounds)
}
- .isInvisible(bgColorLayer)
+ .invoke("LAUNCH_NEW_TASK_ACTIVITY coversExactly displayBounds") {
+ it.visibleRegion(launchNewTaskApp.componentMatcher).coversExactly(displayBounds)
+ }
+ .isInvisible(backgroundColorLayer)
+ .hasNoColor(backgroundColorLayer)
}
}
@@ -158,7 +197,7 @@
@Test
fun newTaskOpensOnTopAndThenCloses() {
flicker.assertWm {
- this.isAppWindowOnTop(testApp.componentMatcher)
+ this.isAppWindowOnTop(launchNewTaskApp.componentMatcher)
.then()
.isAppWindowOnTop(SPLASH_SCREEN, isOptional = true)
.then()
@@ -166,66 +205,15 @@
.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/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest_ShellTransit.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest_ShellTransit.kt
index ec4e35c..6dc11b5 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest_ShellTransit.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsBackTest_ShellTransit.kt
@@ -17,13 +17,10 @@
package com.android.server.wm.flicker.quickswitch
import android.platform.test.annotations.FlakyTest
-import android.platform.test.annotations.Presubmit
import android.platform.test.annotations.RequiresDevice
import com.android.server.wm.flicker.FlickerTest
import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.navBarWindowIsVisibleAtStartAndEnd
-import com.android.server.wm.traces.common.ComponentNameMatcher
import org.junit.Assume
import org.junit.Before
import org.junit.FixMethodOrder
@@ -61,17 +58,6 @@
@Test
override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
- /**
- * Checks that [ComponentNameMatcher.NAV_BAR] window is visible and above the app windows at the
- * start and end of the WM trace
- */
- @Presubmit
- @Test
- fun navBarWindowIsVisibleAtStartAndEnd() {
- Assume.assumeFalse(flicker.scenario.isTablet)
- flicker.navBarWindowIsVisibleAtStartAndEnd()
- }
-
/** {@inheritDoc} */
@FlakyTest(bugId = 250520840)
@Test
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest_ShellTransit.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest_ShellTransit.kt
index 477b419..5a78868 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest_ShellTransit.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchBetweenTwoAppsForwardTest_ShellTransit.kt
@@ -17,13 +17,10 @@
package com.android.server.wm.flicker.quickswitch
import android.platform.test.annotations.FlakyTest
-import android.platform.test.annotations.Presubmit
import android.platform.test.annotations.RequiresDevice
import com.android.server.wm.flicker.FlickerTest
import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.navBarWindowIsVisibleAtStartAndEnd
-import com.android.server.wm.traces.common.ComponentNameMatcher
import org.junit.Assume
import org.junit.Before
import org.junit.FixMethodOrder
@@ -62,17 +59,6 @@
@Test
override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
- /**
- * Checks that [ComponentNameMatcher.NAV_BAR] window is visible and above the app windows at the
- * start and end of the WM trace
- */
- @Presubmit
- @Test
- fun navBarWindowIsVisibleAtStartAndEnd() {
- Assume.assumeFalse(flicker.scenario.isTablet)
- flicker.navBarWindowIsVisibleAtStartAndEnd()
- }
-
@FlakyTest(bugId = 246284708)
@Test
override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTest.kt
index 8c8220f..456eab1 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/quickswitch/QuickSwitchFromLauncherTest.kt
@@ -26,7 +26,6 @@
import com.android.server.wm.flicker.helpers.SimpleAppHelper
import com.android.server.wm.flicker.helpers.isShellTransitionsEnabled
import com.android.server.wm.flicker.junit.FlickerParametersRunnerFactory
-import com.android.server.wm.flicker.navBarWindowIsVisibleAtStartAndEnd
import com.android.server.wm.traces.common.ComponentNameMatcher
import com.android.server.wm.traces.common.Rect
import com.android.server.wm.traces.common.service.PlatformConsts
@@ -262,17 +261,6 @@
@Test
override fun navBarWindowIsAlwaysVisible() = super.navBarWindowIsAlwaysVisible()
- /**
- * Checks that [ComponentNameMatcher.NAV_BAR] window is visible and above the app windows at the
- * start and end of the WM trace
- */
- @Presubmit
- @Test
- fun navBarWindowIsVisibleAtStartAndEnd() {
- Assume.assumeFalse(flicker.scenario.isTablet)
- flicker.navBarWindowIsVisibleAtStartAndEnd()
- }
-
@Presubmit
@Test
override fun visibleLayersShownMoreThanOneConsecutiveEntry() {
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/ChangeAppRotationTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/ChangeAppRotationTest.kt
index 5b52c75..e3296a5 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/ChangeAppRotationTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/ChangeAppRotationTest.kt
@@ -127,7 +127,7 @@
override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
@Test
- @IwTest(focusArea = "ime")
+ @IwTest(focusArea = "framework")
override fun cujCompleted() {
super.cujCompleted()
focusChanges()
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTest.kt
index 54f38c3..741ae51 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/rotation/SeamlessAppRotationTest.kt
@@ -207,14 +207,8 @@
override fun navBarLayerPositionAtStartAndEnd() = super.navBarLayerPositionAtStartAndEnd()
@Test
- @IwTest(focusArea = "ime")
+ @IwTest(focusArea = "framework")
override fun cujCompleted() {
- if (!flicker.scenario.isTablet) {
- // not yet tablet compatible
- appLayerRotates()
- appLayerAlwaysVisible()
- }
-
appWindowFullScreen()
appWindowSeamlessRotation()
focusDoesNotChange()
@@ -223,12 +217,15 @@
appLayerRotates_StartingPos()
appLayerRotates_EndingPos()
entireScreenCovered()
- navBarLayerIsVisibleAtStartAndEnd()
- navBarWindowIsAlwaysVisible()
- taskBarLayerIsVisibleAtStartAndEnd()
- taskBarWindowIsAlwaysVisible()
visibleLayersShownMoreThanOneConsecutiveEntry()
visibleWindowsShownMoreThanOneConsecutiveEntry()
+
+ runAndIgnoreAssumptionViolation { appLayerRotates() }
+ runAndIgnoreAssumptionViolation { appLayerAlwaysVisible() }
+ runAndIgnoreAssumptionViolation { navBarLayerIsVisibleAtStartAndEnd() }
+ runAndIgnoreAssumptionViolation { navBarWindowIsAlwaysVisible() }
+ runAndIgnoreAssumptionViolation { taskBarLayerIsVisibleAtStartAndEnd() }
+ runAndIgnoreAssumptionViolation { taskBarWindowIsAlwaysVisible() }
}
companion object {
diff --git a/tests/vcn/java/android/net/vcn/VcnConfigTest.java b/tests/vcn/java/android/net/vcn/VcnConfigTest.java
index 7ac51b7..b313c9f 100644
--- a/tests/vcn/java/android/net/vcn/VcnConfigTest.java
+++ b/tests/vcn/java/android/net/vcn/VcnConfigTest.java
@@ -16,7 +16,12 @@
package android.net.vcn;
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
@@ -24,6 +29,7 @@
import android.annotation.NonNull;
import android.content.Context;
import android.os.Parcel;
+import android.util.ArraySet;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
@@ -42,19 +48,36 @@
private static final Set<VcnGatewayConnectionConfig> GATEWAY_CONNECTION_CONFIGS =
Collections.singleton(VcnGatewayConnectionConfigTest.buildTestConfig());
+ private static final Set<Integer> RESTRICTED_TRANSPORTS = new ArraySet<>();
+
+ static {
+ RESTRICTED_TRANSPORTS.add(TRANSPORT_WIFI);
+ RESTRICTED_TRANSPORTS.add(TRANSPORT_CELLULAR);
+ }
+
private final Context mContext = mock(Context.class);
// Public visibility for VcnManagementServiceTest
- public static VcnConfig buildTestConfig(@NonNull Context context) {
+ public static VcnConfig buildTestConfig(
+ @NonNull Context context, Set<Integer> restrictedTransports) {
VcnConfig.Builder builder = new VcnConfig.Builder(context);
for (VcnGatewayConnectionConfig gatewayConnectionConfig : GATEWAY_CONNECTION_CONFIGS) {
builder.addGatewayConnectionConfig(gatewayConnectionConfig);
}
+ if (restrictedTransports != null) {
+ builder.setRestrictedUnderlyingNetworkTransports(restrictedTransports);
+ }
+
return builder.build();
}
+ // Public visibility for VcnManagementServiceTest
+ public static VcnConfig buildTestConfig(@NonNull Context context) {
+ return buildTestConfig(context, null);
+ }
+
@Before
public void setUp() throws Exception {
doReturn(TEST_PACKAGE_NAME).when(mContext).getOpPackageName();
@@ -91,11 +114,25 @@
}
@Test
- public void testBuilderAndGetters() {
+ public void testBuilderAndGettersDefaultValues() {
final VcnConfig config = buildTestConfig(mContext);
assertEquals(TEST_PACKAGE_NAME, config.getProvisioningPackageName());
assertEquals(GATEWAY_CONNECTION_CONFIGS, config.getGatewayConnectionConfigs());
+ assertFalse(config.isTestModeProfile());
+ assertEquals(
+ Collections.singleton(TRANSPORT_WIFI),
+ config.getRestrictedUnderlyingNetworkTransports());
+ }
+
+ @Test
+ public void testBuilderAndGettersConfigRestrictedTransports() {
+ final VcnConfig config = buildTestConfig(mContext, RESTRICTED_TRANSPORTS);
+
+ assertEquals(TEST_PACKAGE_NAME, config.getProvisioningPackageName());
+ assertEquals(GATEWAY_CONNECTION_CONFIGS, config.getGatewayConnectionConfigs());
+ assertFalse(config.isTestModeProfile());
+ assertEquals(RESTRICTED_TRANSPORTS, config.getRestrictedUnderlyingNetworkTransports());
}
@Test
@@ -106,6 +143,24 @@
}
@Test
+ public void testPersistableBundleWithRestrictedTransports() {
+ final VcnConfig config = buildTestConfig(mContext, RESTRICTED_TRANSPORTS);
+
+ assertEquals(config, new VcnConfig(config.toPersistableBundle()));
+ }
+
+ @Test
+ public void testEqualityWithRestrictedTransports() {
+ final VcnConfig config = buildTestConfig(mContext, RESTRICTED_TRANSPORTS);
+ final VcnConfig configEqual = buildTestConfig(mContext, RESTRICTED_TRANSPORTS);
+ final VcnConfig configNotEqual =
+ buildTestConfig(mContext, Collections.singleton(TRANSPORT_WIFI));
+
+ assertEquals(config, configEqual);
+ assertNotEquals(config, configNotEqual);
+ }
+
+ @Test
public void testParceling() {
final VcnConfig config = buildTestConfig(mContext);
diff --git a/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java b/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java
index 2aef9ae..4040888 100644
--- a/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java
+++ b/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java
@@ -19,9 +19,11 @@
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 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 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 @@
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 @@
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 @@
}
@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 @@
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 @@
}
@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 @@
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/VcnManagementServiceTest.java b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java
index 258642ac..075bc5e 100644
--- a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java
+++ b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java
@@ -258,7 +258,7 @@
doReturn(Collections.singleton(TRANSPORT_WIFI))
.when(mMockDeps)
- .getRestrictedTransports(any(), any());
+ .getRestrictedTransports(any(), any(), any());
}
@@ -1038,18 +1038,18 @@
new LinkProperties());
}
- private void checkGetRestrictedTransports(
+ private void checkGetRestrictedTransportsFromCarrierConfig(
ParcelUuid subGrp,
TelephonySubscriptionSnapshot lastSnapshot,
Set<Integer> expectedTransports) {
Set<Integer> result =
new VcnManagementService.Dependencies()
- .getRestrictedTransports(subGrp, lastSnapshot);
+ .getRestrictedTransportsFromCarrierConfig(subGrp, lastSnapshot);
assertEquals(expectedTransports, result);
}
@Test
- public void testGetRestrictedTransports() {
+ public void testGetRestrictedTransportsFromCarrierConfig() {
final Set<Integer> restrictedTransports = new ArraySet<>();
restrictedTransports.add(TRANSPORT_CELLULAR);
restrictedTransports.add(TRANSPORT_WIFI);
@@ -1065,11 +1065,12 @@
mock(TelephonySubscriptionSnapshot.class);
doReturn(carrierConfig).when(lastSnapshot).getCarrierConfigForSubGrp(eq(TEST_UUID_2));
- checkGetRestrictedTransports(TEST_UUID_2, lastSnapshot, restrictedTransports);
+ checkGetRestrictedTransportsFromCarrierConfig(
+ TEST_UUID_2, lastSnapshot, restrictedTransports);
}
@Test
- public void testGetRestrictedTransports_noRestrictPolicyConfigured() {
+ public void testGetRestrictedTransportsFromCarrierConfig_noRestrictPolicyConfigured() {
final Set<Integer> restrictedTransports = Collections.singleton(TRANSPORT_WIFI);
final PersistableBundleWrapper carrierConfig =
@@ -1078,17 +1079,54 @@
mock(TelephonySubscriptionSnapshot.class);
doReturn(carrierConfig).when(lastSnapshot).getCarrierConfigForSubGrp(eq(TEST_UUID_2));
- checkGetRestrictedTransports(TEST_UUID_2, lastSnapshot, restrictedTransports);
+ checkGetRestrictedTransportsFromCarrierConfig(
+ TEST_UUID_2, lastSnapshot, restrictedTransports);
}
@Test
- public void testGetRestrictedTransports_noCarrierConfig() {
+ public void testGetRestrictedTransportsFromCarrierConfig_noCarrierConfig() {
final Set<Integer> restrictedTransports = Collections.singleton(TRANSPORT_WIFI);
final TelephonySubscriptionSnapshot lastSnapshot =
mock(TelephonySubscriptionSnapshot.class);
- checkGetRestrictedTransports(TEST_UUID_2, lastSnapshot, restrictedTransports);
+ checkGetRestrictedTransportsFromCarrierConfig(
+ TEST_UUID_2, lastSnapshot, restrictedTransports);
+ }
+
+ @Test
+ public void testGetRestrictedTransportsFromCarrierConfigAndVcnConfig() {
+ // Configure restricted transport in CarrierConfig
+ final Set<Integer> restrictedTransportInCarrierConfig =
+ Collections.singleton(TRANSPORT_WIFI);
+
+ PersistableBundle carrierConfigBundle = new PersistableBundle();
+ carrierConfigBundle.putIntArray(
+ VCN_RESTRICTED_TRANSPORTS_INT_ARRAY_KEY,
+ restrictedTransportInCarrierConfig.stream().mapToInt(i -> i).toArray());
+ final PersistableBundleWrapper carrierConfig =
+ new PersistableBundleWrapper(carrierConfigBundle);
+
+ final TelephonySubscriptionSnapshot lastSnapshot =
+ mock(TelephonySubscriptionSnapshot.class);
+ doReturn(carrierConfig).when(lastSnapshot).getCarrierConfigForSubGrp(eq(TEST_UUID_2));
+
+ // Configure restricted transport in VcnConfig
+ final Context mockContext = mock(Context.class);
+ doReturn(TEST_PACKAGE_NAME).when(mockContext).getOpPackageName();
+ final VcnConfig vcnConfig =
+ VcnConfigTest.buildTestConfig(
+ mockContext, Collections.singleton(TRANSPORT_CELLULAR));
+
+ // Verifications
+ final Set<Integer> expectedTransports = new ArraySet<>();
+ expectedTransports.add(TRANSPORT_CELLULAR);
+ expectedTransports.add(TRANSPORT_WIFI);
+
+ Set<Integer> result =
+ new VcnManagementService.Dependencies()
+ .getRestrictedTransports(TEST_UUID_2, lastSnapshot, vcnConfig);
+ assertEquals(expectedTransports, result);
}
private void checkGetUnderlyingNetworkPolicy(
@@ -1103,7 +1141,7 @@
if (isTransportRestricted) {
restrictedTransports.add(transportType);
}
- doReturn(restrictedTransports).when(mMockDeps).getRestrictedTransports(any(), any());
+ doReturn(restrictedTransports).when(mMockDeps).getRestrictedTransports(any(), any(), any());
final VcnUnderlyingNetworkPolicy policy =
startVcnAndGetPolicyForTransport(
@@ -1201,7 +1239,7 @@
public void testGetUnderlyingNetworkPolicyCell_restrictWifi() throws Exception {
doReturn(Collections.singleton(TRANSPORT_WIFI))
.when(mMockDeps)
- .getRestrictedTransports(any(), any());
+ .getRestrictedTransports(any(), any(), any());
setupSubscriptionAndStartVcn(TEST_SUBSCRIPTION_ID, TEST_UUID_2, true /* isVcnActive */);
@@ -1344,6 +1382,23 @@
}
@Test
+ public void testVcnConfigChangeUpdatesPolicyListener() throws Exception {
+ setupActiveSubscription(TEST_UUID_2);
+
+ mVcnMgmtSvc.setVcnConfig(TEST_UUID_2, TEST_VCN_CONFIG, TEST_PACKAGE_NAME);
+ mVcnMgmtSvc.addVcnUnderlyingNetworkPolicyListener(mMockPolicyListener);
+
+ final Context mockContext = mock(Context.class);
+ doReturn(TEST_PACKAGE_NAME).when(mockContext).getOpPackageName();
+ final VcnConfig vcnConfig =
+ VcnConfigTest.buildTestConfig(
+ mockContext, Collections.singleton(TRANSPORT_CELLULAR));
+ mVcnMgmtSvc.setVcnConfig(TEST_UUID_2, vcnConfig, TEST_PACKAGE_NAME);
+
+ verify(mMockPolicyListener).onPolicyChanged();
+ }
+
+ @Test
public void testRemoveVcnUpdatesPolicyListener() throws Exception {
setupActiveSubscription(TEST_UUID_2);
@@ -1375,7 +1430,7 @@
setupActiveSubscription(TEST_UUID_2);
mVcnMgmtSvc.setVcnConfig(TEST_UUID_2, TEST_VCN_CONFIG, TEST_PACKAGE_NAME);
- mVcnMgmtSvc.addVcnUnderlyingNetworkPolicyListenerForTest(mMockPolicyListener);
+ mVcnMgmtSvc.addVcnUnderlyingNetworkPolicyListener(mMockPolicyListener);
final TelephonySubscriptionSnapshot snapshot =
buildSubscriptionSnapshot(
diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java
index 15d4f10..1c21a06 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 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.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 @@
public class VcnGatewayConnectionConnectedStateTest extends VcnGatewayConnectionTestBase {
private VcnIkeSession mIkeSession;
private VcnNetworkAgent mNetworkAgent;
+ private Network mVcnNetwork;
@Before
public void setUp() throws Exception {
@@ -98,6 +103,9 @@
.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 @@
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 6a9a1e2..a4ee2de 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_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.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 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.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 @@
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 785bff1..7bafd24 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 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 @@
@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 @@
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();
diff --git a/tools/aapt2/cmd/Dump_test.cpp b/tools/aapt2/cmd/Dump_test.cpp
index b1c69cd..df35ebb 100644
--- a/tools/aapt2/cmd/Dump_test.cpp
+++ b/tools/aapt2/cmd/Dump_test.cpp
@@ -108,4 +108,21 @@
ASSERT_EQ(output, expected);
}
+TEST_F(DumpTest, DumpBadgingApkBuiltWithAaptAndTagsInWrongPlace) {
+ auto apk_path = file::BuildPath({android::base::GetExecutableDirectory(), "integration-tests",
+ "DumpTest", "built_with_aapt.apk"});
+ auto loaded_apk = LoadedApk::LoadApkFromPath(apk_path, &noop_diag);
+
+ std::string output;
+ DumpBadgingToString(loaded_apk.get(), &output, /* include_meta_data= */ false,
+ /* only_permissions= */ false);
+
+ std::string expected;
+ auto expected_path =
+ file::BuildPath({android::base::GetExecutableDirectory(), "integration-tests", "DumpTest",
+ "built_with_aapt_expected.txt"});
+ ::android::base::ReadFileToString(expected_path, &expected);
+ ASSERT_EQ(output, expected);
+}
+
} // namespace aapt
diff --git a/tools/aapt2/dump/DumpManifest.cpp b/tools/aapt2/dump/DumpManifest.cpp
index 76a6db6..d1957fb 100644
--- a/tools/aapt2/dump/DumpManifest.cpp
+++ b/tools/aapt2/dump/DumpManifest.cpp
@@ -223,7 +223,8 @@
Element() = default;
virtual ~Element() = default;
- static std::unique_ptr<Element> Inflate(ManifestExtractor* extractor, xml::Element* el);
+ static std::unique_ptr<Element> Inflate(ManifestExtractor* extractor, xml::Element* el,
+ const std::string& parent_tag);
/** Writes out the extracted contents of the element. */
virtual void Print(text::Printer* printer) {
@@ -249,10 +250,15 @@
}
/** Retrieves the extracted xml element tag. */
- const std::string tag() const {
+ const std::string& tag() const {
return tag_;
}
+ /** Whether this element has special Extract/Print/ToProto logic. */
+ bool is_featured() const {
+ return featured_;
+ }
+
protected:
ManifestExtractor* extractor() const {
return extractor_;
@@ -394,6 +400,8 @@
return &(*intValue->value);
} else if (RawString* rawValue = ValueCast<RawString>(value)) {
return &(*rawValue->value);
+ } else if (StyledString* styledStrValue = ValueCast<StyledString>(value)) {
+ return &(styledStrValue->value->value);
} else if (FileReference* strValue = ValueCast<FileReference>(value)) {
return &(*strValue->path);
}
@@ -424,6 +432,7 @@
ManifestExtractor* extractor_;
std::vector<std::unique_ptr<Element>> children_;
std::string tag_;
+ bool featured_ = false;
};
friend Element;
@@ -446,7 +455,7 @@
bool DumpProto(pb::Badging* out_badging);
/** Recursively visit the xml element tree and return a processed badging element tree. */
- std::unique_ptr<Element> Visit(xml::Element* element);
+ std::unique_ptr<Element> Visit(xml::Element* element, const std::string& parent_tag);
/** Resets target SDK to 0. */
void ResetTargetSdk() {
@@ -485,7 +494,7 @@
}
/** Retrieves the current stack of parent during data extraction. */
- const std::vector<Element*> parent_stack() const {
+ const std::vector<Element*>& parent_stack() const {
return parent_stack_;
}
@@ -533,8 +542,9 @@
if (f(root)) {
return root;
}
- for (auto& child : root->children()) {
- if (auto b2 = FindElement(child.get(), f)) {
+ const auto& children = root->children();
+ for (auto it = children.rbegin(); it != children.rend(); ++it) {
+ if (auto b2 = FindElement(it->get(), f)) {
return b2;
}
}
@@ -1348,11 +1358,6 @@
std::string impliedReason;
void Extract(xml::Element* element) override {
- const auto parent_stack = extractor()->parent_stack();
- if (!extractor()->options_.only_permissions &&
- (parent_stack.size() != 1 || !ElementCast<Manifest>(parent_stack[0]))) {
- return;
- }
name = GetAttributeStringDefault(FindAttribute(element, NAME_ATTR), "");
std::string feature =
GetAttributeStringDefault(FindAttribute(element, REQUIRED_FEATURE_ATTR), "");
@@ -1477,11 +1482,6 @@
const int32_t* maxSdkVersion = nullptr;
void Extract(xml::Element* element) override {
- const auto parent_stack = extractor()->parent_stack();
- if (!extractor()->options_.only_permissions &&
- (parent_stack.size() != 1 || !ElementCast<Manifest>(parent_stack[0]))) {
- return;
- }
name = GetAttributeString(FindAttribute(element, NAME_ATTR));
maxSdkVersion = GetAttributeInteger(FindAttribute(element, MAX_SDK_VERSION_ATTR));
@@ -1717,11 +1717,8 @@
int required;
void Extract(xml::Element* element) override {
- auto parent_stack = extractor()->parent_stack();
- if (parent_stack.size() > 0 && ElementCast<Application>(parent_stack[0])) {
- name = GetAttributeStringDefault(FindAttribute(element, NAME_ATTR), "");
- required = GetAttributeIntegerDefault(FindAttribute(element, REQUIRED_ATTR), 1);
- }
+ name = GetAttributeStringDefault(FindAttribute(element, NAME_ATTR), "");
+ required = GetAttributeIntegerDefault(FindAttribute(element, REQUIRED_ATTR), 1);
}
void Print(text::Printer* printer) override {
@@ -1749,12 +1746,9 @@
int versionMajor;
void Extract(xml::Element* element) override {
- auto parent_stack = extractor()->parent_stack();
- if (parent_stack.size() > 0 && ElementCast<Application>(parent_stack[0])) {
- name = GetAttributeStringDefault(FindAttribute(element, NAME_ATTR), "");
- version = GetAttributeIntegerDefault(FindAttribute(element, VERSION_ATTR), 0);
- versionMajor = GetAttributeIntegerDefault(FindAttribute(element, VERSION_MAJOR_ATTR), 0);
- }
+ name = GetAttributeStringDefault(FindAttribute(element, NAME_ATTR), "");
+ version = GetAttributeIntegerDefault(FindAttribute(element, VERSION_ATTR), 0);
+ versionMajor = GetAttributeIntegerDefault(FindAttribute(element, VERSION_MAJOR_ATTR), 0);
}
void Print(text::Printer* printer) override {
@@ -1781,13 +1775,10 @@
std::vector<std::string> certDigests;
void Extract(xml::Element* element) override {
- auto parent_stack = extractor()->parent_stack();
- if (parent_stack.size() > 0 && ElementCast<Application>(parent_stack[0])) {
- name = GetAttributeStringDefault(FindAttribute(element, NAME_ATTR), "");
- version = GetAttributeIntegerDefault(FindAttribute(element, VERSION_ATTR), 0);
- versionMajor = GetAttributeIntegerDefault(FindAttribute(element, VERSION_MAJOR_ATTR), 0);
- AddCertDigest(element);
- }
+ name = GetAttributeStringDefault(FindAttribute(element, NAME_ATTR), "");
+ version = GetAttributeIntegerDefault(FindAttribute(element, VERSION_ATTR), 0);
+ versionMajor = GetAttributeIntegerDefault(FindAttribute(element, VERSION_MAJOR_ATTR), 0);
+ AddCertDigest(element);
}
void AddCertDigest(xml::Element* element) {
@@ -1829,11 +1820,8 @@
int versionMajor;
void Extract(xml::Element* element) override {
- auto parent_stack = extractor()->parent_stack();
- if (parent_stack.size() > 0 && ElementCast<Application>(parent_stack[0])) {
- name = GetAttributeStringDefault(FindAttribute(element, NAME_ATTR), "");
- versionMajor = GetAttributeIntegerDefault(FindAttribute(element, VERSION_MAJOR_ATTR), 0);
- }
+ name = GetAttributeStringDefault(FindAttribute(element, NAME_ATTR), "");
+ versionMajor = GetAttributeIntegerDefault(FindAttribute(element, VERSION_MAJOR_ATTR), 0);
}
void Print(text::Printer* printer) override {
@@ -1857,12 +1845,9 @@
std::vector<std::string> certDigests;
void Extract(xml::Element* element) override {
- auto parent_stack = extractor()->parent_stack();
- if (parent_stack.size() > 0 && ElementCast<Application>(parent_stack[0])) {
- name = GetAttributeStringDefault(FindAttribute(element, NAME_ATTR), "");
- versionMajor = GetAttributeIntegerDefault(FindAttribute(element, VERSION_MAJOR_ATTR), 0);
- AddCertDigest(element);
- }
+ name = GetAttributeStringDefault(FindAttribute(element, NAME_ATTR), "");
+ versionMajor = GetAttributeIntegerDefault(FindAttribute(element, VERSION_MAJOR_ATTR), 0);
+ AddCertDigest(element);
}
void AddCertDigest(xml::Element* element) {
@@ -1902,11 +1887,8 @@
int required;
void Extract(xml::Element* element) override {
- auto parent_stack = extractor()->parent_stack();
- if (parent_stack.size() > 0 && ElementCast<Application>(parent_stack[0])) {
- name = GetAttributeStringDefault(FindAttribute(element, NAME_ATTR), "");
- required = GetAttributeIntegerDefault(FindAttribute(element, REQUIRED_ATTR), 1);
- }
+ name = GetAttributeStringDefault(FindAttribute(element, NAME_ATTR), "");
+ required = GetAttributeIntegerDefault(FindAttribute(element, REQUIRED_ATTR), 1);
}
void Print(text::Printer* printer) override {
@@ -2251,14 +2233,11 @@
std::vector<std::string> certDigests;
void Extract(xml::Element* element) override {
- auto parent_stack = extractor()->parent_stack();
- if (parent_stack.size() > 0 && ElementCast<Application>(parent_stack[0])) {
- packageType = GetAttributeString(FindAttribute(element, PACKAGE_TYPE_ATTR));
- name = GetAttributeString(FindAttribute(element, NAME_ATTR));
- version = GetAttributeIntegerDefault(FindAttribute(element, VERSION_ATTR), 0);
- versionMajor = GetAttributeIntegerDefault(FindAttribute(element, VERSION_MAJOR_ATTR), 0);
- AddCertDigest(element);
- }
+ packageType = GetAttributeString(FindAttribute(element, PACKAGE_TYPE_ATTR));
+ name = GetAttributeString(FindAttribute(element, NAME_ATTR));
+ version = GetAttributeIntegerDefault(FindAttribute(element, VERSION_ATTR), 0);
+ versionMajor = GetAttributeIntegerDefault(FindAttribute(element, VERSION_MAJOR_ATTR), 0);
+ AddCertDigest(element);
}
void AddCertDigest(xml::Element* element) {
@@ -2480,7 +2459,7 @@
// Print only the <uses-permission>, <uses-permission-sdk23>, and <permission> elements if
// printing only permission elements is requested
if (options_.only_permissions) {
- root_element_ = ManifestExtractor::Element::Inflate(this, element);
+ root_element_ = ManifestExtractor::Element::Inflate(this, element, "");
if (auto manifest = ElementCast<Manifest>(root_element_.get())) {
manifest->only_package_name = true;
@@ -2489,7 +2468,7 @@
if (child->name == "uses-permission" || child->name == "uses-permission-sdk-23"
|| child->name == "permission") {
// Inflate the element and its descendants
- auto permission_element = Visit(child);
+ auto permission_element = Visit(child, "manifest");
manifest->AddChild(permission_element);
}
}
@@ -2528,7 +2507,7 @@
}
// Extract badging information
- root_element_ = Visit(element);
+ root_element_ = Visit(element, "");
// Filter out all "uses-sdk" tags besides the very last tag. The android runtime only uses the
// attribute values from the last defined tag.
@@ -2683,7 +2662,7 @@
(meta_data->name == "android.nfc.cardemulation.off_host_apdu_service" &&
offhost_apdu_action)) {
// Attempt to load the resource file
- if (!meta_data->resource.empty()) {
+ if (meta_data->resource.empty()) {
return;
}
auto resource = this->apk_->LoadXml(meta_data->resource, diag);
@@ -2878,58 +2857,66 @@
return true;
}
+template <typename T>
+constexpr const char* GetExpectedTagForType() {
+ // This array does not appear at runtime, as GetExpectedTagForType function is used by compiler
+ // to inject proper 'expected_tag' into ElementCast.
+ std::array<std::pair<const char*, bool>, 37> tags = {
+ std::make_pair("action", std::is_same<Action, T>::value),
+ std::make_pair("activity", std::is_same<Activity, T>::value),
+ std::make_pair("additional-certificate", std::is_same<AdditionalCertificate, T>::value),
+ std::make_pair("application", std::is_same<Application, T>::value),
+ std::make_pair("category", std::is_same<Category, T>::value),
+ std::make_pair("compatible-screens", std::is_same<CompatibleScreens, T>::value),
+ std::make_pair("feature-group", std::is_same<FeatureGroup, T>::value),
+ std::make_pair("input-type", std::is_same<InputType, T>::value),
+ std::make_pair("intent-filter", std::is_same<IntentFilter, T>::value),
+ std::make_pair("meta-data", std::is_same<MetaData, T>::value),
+ std::make_pair("manifest", std::is_same<Manifest, T>::value),
+ std::make_pair("original-package", std::is_same<OriginalPackage, T>::value),
+ std::make_pair("overlay", std::is_same<Overlay, T>::value),
+ std::make_pair("package-verifier", std::is_same<PackageVerifier, T>::value),
+ std::make_pair("permission", std::is_same<Permission, T>::value),
+ std::make_pair("property", std::is_same<Property, T>::value),
+ std::make_pair("provider", std::is_same<Provider, T>::value),
+ std::make_pair("receiver", std::is_same<Receiver, T>::value),
+ std::make_pair("required-feature", std::is_same<RequiredFeature, T>::value),
+ std::make_pair("required-not-feature", std::is_same<RequiredNotFeature, T>::value),
+ std::make_pair("screen", std::is_same<Screen, T>::value),
+ std::make_pair("service", std::is_same<Service, T>::value),
+ std::make_pair("sdk-library", std::is_same<SdkLibrary, T>::value),
+ std::make_pair("static-library", std::is_same<StaticLibrary, T>::value),
+ std::make_pair("supports-gl-texture", std::is_same<SupportsGlTexture, T>::value),
+ std::make_pair("supports-input", std::is_same<SupportsInput, T>::value),
+ std::make_pair("supports-screens", std::is_same<SupportsScreen, T>::value),
+ std::make_pair("uses-configuration", std::is_same<UsesConfiguarion, T>::value),
+ std::make_pair("uses-feature", std::is_same<UsesFeature, T>::value),
+ std::make_pair("uses-library", std::is_same<UsesLibrary, T>::value),
+ std::make_pair("uses-native-library", std::is_same<UsesNativeLibrary, T>::value),
+ std::make_pair("uses-package", std::is_same<UsesPackage, T>::value),
+ std::make_pair("uses-permission", std::is_same<UsesPermission, T>::value),
+ std::make_pair("uses-permission-sdk-23", std::is_same<UsesPermissionSdk23, T>::value),
+ std::make_pair("uses-sdk", std::is_same<UsesSdkBadging, T>::value),
+ std::make_pair("uses-sdk-library", std::is_same<UsesSdkLibrary, T>::value),
+ std::make_pair("uses-static-library", std::is_same<UsesStaticLibrary, T>::value),
+ };
+ for (const auto& pair : tags) {
+ if (pair.second) {
+ return pair.first;
+ }
+ }
+ return nullptr;
+}
+
/**
* Returns the element casted to the type if the element is of that type. Otherwise, returns a null
* pointer.
**/
template<typename T>
T* ElementCast(ManifestExtractor::Element* element) {
- if (element == nullptr) {
- return nullptr;
- }
-
- const std::unordered_map<std::string, bool> kTagCheck = {
- {"action", std::is_base_of<Action, T>::value},
- {"activity", std::is_base_of<Activity, T>::value},
- {"additional-certificate", std::is_base_of<AdditionalCertificate, T>::value},
- {"application", std::is_base_of<Application, T>::value},
- {"category", std::is_base_of<Category, T>::value},
- {"compatible-screens", std::is_base_of<CompatibleScreens, T>::value},
- {"feature-group", std::is_base_of<FeatureGroup, T>::value},
- {"input-type", std::is_base_of<InputType, T>::value},
- {"intent-filter", std::is_base_of<IntentFilter, T>::value},
- {"meta-data", std::is_base_of<MetaData, T>::value},
- {"manifest", std::is_base_of<Manifest, T>::value},
- {"original-package", std::is_base_of<OriginalPackage, T>::value},
- {"overlay", std::is_base_of<Overlay, T>::value},
- {"package-verifier", std::is_base_of<PackageVerifier, T>::value},
- {"permission", std::is_base_of<Permission, T>::value},
- {"property", std::is_base_of<Property, T>::value},
- {"provider", std::is_base_of<Provider, T>::value},
- {"receiver", std::is_base_of<Receiver, T>::value},
- {"required-feature", std::is_base_of<RequiredFeature, T>::value},
- {"required-not-feature", std::is_base_of<RequiredNotFeature, T>::value},
- {"screen", std::is_base_of<Screen, T>::value},
- {"service", std::is_base_of<Service, T>::value},
- {"sdk-library", std::is_base_of<SdkLibrary, T>::value},
- {"static-library", std::is_base_of<StaticLibrary, T>::value},
- {"supports-gl-texture", std::is_base_of<SupportsGlTexture, T>::value},
- {"supports-input", std::is_base_of<SupportsInput, T>::value},
- {"supports-screens", std::is_base_of<SupportsScreen, T>::value},
- {"uses-configuration", std::is_base_of<UsesConfiguarion, T>::value},
- {"uses-feature", std::is_base_of<UsesFeature, T>::value},
- {"uses-library", std::is_base_of<UsesLibrary, T>::value},
- {"uses-native-library", std::is_base_of<UsesNativeLibrary, T>::value},
- {"uses-package", std::is_base_of<UsesPackage, T>::value},
- {"uses-permission", std::is_base_of<UsesPermission, T>::value},
- {"uses-permission-sdk-23", std::is_base_of<UsesPermissionSdk23, T>::value},
- {"uses-sdk", std::is_base_of<UsesSdkBadging, T>::value},
- {"uses-sdk-library", std::is_base_of<UsesSdkLibrary, T>::value},
- {"uses-static-library", std::is_base_of<UsesStaticLibrary, T>::value},
- };
-
- auto check = kTagCheck.find(element->tag());
- if (check != kTagCheck.end() && check->second) {
+ constexpr const char* expected_tag = GetExpectedTagForType<T>();
+ if (element != nullptr && expected_tag != nullptr && element->is_featured() &&
+ element->tag() == expected_tag) {
return static_cast<T*>(element);
}
return nullptr;
@@ -2941,9 +2928,9 @@
}
std::unique_ptr<ManifestExtractor::Element> ManifestExtractor::Element::Inflate(
- ManifestExtractor* extractor, xml::Element* el) {
- const std::unordered_map<std::string,
- std::function<std::unique_ptr<ManifestExtractor::Element>()>>
+ ManifestExtractor* extractor, xml::Element* el, const std::string& parent_tag) {
+ static const std::unordered_map<std::string_view,
+ std::function<std::unique_ptr<ManifestExtractor::Element>()>>
kTagCheck = {
{"action", &CreateType<Action>},
{"activity", &CreateType<Activity>},
@@ -2983,12 +2970,71 @@
{"uses-sdk-library", &CreateType<UsesSdkLibrary>},
{"uses-static-library", &CreateType<UsesStaticLibrary>},
};
-
+ static constexpr std::array<std::pair<std::string_view, std::string_view>, 53>
+ kValidChildParentTags = {
+ std::make_pair("action", "intent-filter"),
+ std::make_pair("activity", "application"),
+ std::make_pair("additional-certificate", "uses-package"),
+ std::make_pair("additional-certificate", "uses-static-library"),
+ std::make_pair("application", "manifest"),
+ std::make_pair("category", "intent-filter"),
+ std::make_pair("compatible-screens", "manifest"),
+ std::make_pair("feature-group", "manifest"),
+ std::make_pair("input-type", "supports-input"),
+ std::make_pair("intent-filter", "activity"),
+ std::make_pair("intent-filter", "activity-alias"),
+ std::make_pair("intent-filter", "service"),
+ std::make_pair("intent-filter", "receiver"),
+ std::make_pair("intent-filter", "provider"),
+ std::make_pair("manifest", ""),
+ std::make_pair("meta-data", "activity"),
+ std::make_pair("meta-data", "activity-alias"),
+ std::make_pair("meta-data", "application"),
+ std::make_pair("meta-data", "service"),
+ std::make_pair("meta-data", "receiver"),
+ std::make_pair("meta-data", "provider"),
+ std::make_pair("original-package", "manifest"),
+ std::make_pair("overlay", "manifest"),
+ std::make_pair("package-verifier", "manifest"),
+ std::make_pair("permission", "manifest"),
+ std::make_pair("property", "activity"),
+ std::make_pair("property", "activity-alias"),
+ std::make_pair("property", "application"),
+ std::make_pair("property", "service"),
+ std::make_pair("property", "receiver"),
+ std::make_pair("property", "provider"),
+ std::make_pair("provider", "application"),
+ std::make_pair("receiver", "application"),
+ std::make_pair("required-feature", "uses-permission"),
+ std::make_pair("required-not-feature", "uses-permission"),
+ std::make_pair("screen", "compatible-screens"),
+ std::make_pair("service", "application"),
+ std::make_pair("sdk-library", "application"),
+ std::make_pair("static-library", "application"),
+ std::make_pair("supports-gl-texture", "manifest"),
+ std::make_pair("supports-input", "manifest"),
+ std::make_pair("supports-screens", "manifest"),
+ std::make_pair("uses-configuration", "manifest"),
+ std::make_pair("uses-feature", "feature-group"),
+ std::make_pair("uses-feature", "manifest"),
+ std::make_pair("uses-library", "application"),
+ std::make_pair("uses-native-library", "application"),
+ std::make_pair("uses-package", "application"),
+ std::make_pair("uses-permission", "manifest"),
+ std::make_pair("uses-permission-sdk-23", "manifest"),
+ std::make_pair("uses-sdk", "manifest"),
+ std::make_pair("uses-sdk-library", "application"),
+ std::make_pair("uses-static-library", "application"),
+ };
+ bool is_valid_tag = std::find(kValidChildParentTags.begin(), kValidChildParentTags.end(),
+ std::make_pair<std::string_view, std::string_view>(
+ el->name, parent_tag)) != kValidChildParentTags.end();
// Attempt to map the xml tag to a element inflater
std::unique_ptr<ManifestExtractor::Element> element;
auto check = kTagCheck.find(el->name);
- if (check != kTagCheck.end()) {
+ if (check != kTagCheck.end() && is_valid_tag) {
element = check->second();
+ element->featured_ = true;
} else {
element = util::make_unique<ManifestExtractor::Element>();
}
@@ -2999,13 +3045,14 @@
return element;
}
-std::unique_ptr<ManifestExtractor::Element> ManifestExtractor::Visit(xml::Element* el) {
- auto element = ManifestExtractor::Element::Inflate(this, el);
+std::unique_ptr<ManifestExtractor::Element> ManifestExtractor::Visit(
+ xml::Element* el, const std::string& parent_tag) {
+ auto element = ManifestExtractor::Element::Inflate(this, el, parent_tag);
parent_stack_.insert(parent_stack_.begin(), element.get());
// Process the element and recursively visit the children
for (xml::Element* child : el->GetChildElements()) {
- auto v = Visit(child);
+ auto v = Visit(child, el->name);
element->AddChild(v);
}
diff --git a/tools/aapt2/integration-tests/DumpTest/built_with_aapt.apk b/tools/aapt2/integration-tests/DumpTest/built_with_aapt.apk
new file mode 100644
index 0000000..090ebe56
--- /dev/null
+++ b/tools/aapt2/integration-tests/DumpTest/built_with_aapt.apk
Binary files differ
diff --git a/tools/aapt2/integration-tests/DumpTest/built_with_aapt_expected.txt b/tools/aapt2/integration-tests/DumpTest/built_with_aapt_expected.txt
new file mode 100644
index 0000000..cc0b3bf
--- /dev/null
+++ b/tools/aapt2/integration-tests/DumpTest/built_with_aapt_expected.txt
@@ -0,0 +1,11 @@
+package: name='com.aapt.app' versionCode='222' versionName='222' platformBuildVersionName='12' platformBuildVersionCode='32' compileSdkVersion='32' compileSdkVersionCodename='12'
+sdkVersion:'22'
+targetSdkVersion:'32'
+application: label='App' icon=''
+feature-group: label=''
+ uses-feature: name='android.hardware.faketouch'
+ uses-implied-feature: name='android.hardware.faketouch' reason='default feature for all apps'
+supports-screens: 'small' 'normal' 'large' 'xlarge'
+supports-any-density: 'true'
+locales:
+densities:
diff --git a/tools/aapt2/integration-tests/DumpTest/components_expected.txt b/tools/aapt2/integration-tests/DumpTest/components_expected.txt
index 79b6706..9c81fb8 100644
--- a/tools/aapt2/integration-tests/DumpTest/components_expected.txt
+++ b/tools/aapt2/integration-tests/DumpTest/components_expected.txt
@@ -36,6 +36,7 @@
provides-component:'wallpaper'
provides-component:'accessibility'
provides-component:'print-service'
+provides-component:'payment'
provides-component:'search'
provides-component:'document-provider'
provides-component:'notification-listener'
diff --git a/tools/aapt2/integration-tests/DumpTest/components_expected_proto.txt b/tools/aapt2/integration-tests/DumpTest/components_expected_proto.txt
index 456a5a7..d866479 100644
--- a/tools/aapt2/integration-tests/DumpTest/components_expected_proto.txt
+++ b/tools/aapt2/integration-tests/DumpTest/components_expected_proto.txt
@@ -80,6 +80,7 @@
provided_components: "wallpaper"
provided_components: "accessibility"
provided_components: "print-service"
+ provided_components: "payment"
provided_components: "search"
provided_components: "document-provider"
provided_components: "notification-listener"
diff --git a/tools/aapt2/integration-tests/DumpTest/components_full_proto.txt b/tools/aapt2/integration-tests/DumpTest/components_full_proto.txt
index a7e8353..6da6fc6 100644
--- a/tools/aapt2/integration-tests/DumpTest/components_full_proto.txt
+++ b/tools/aapt2/integration-tests/DumpTest/components_full_proto.txt
@@ -80,6 +80,7 @@
provided_components: "wallpaper"
provided_components: "accessibility"
provided_components: "print-service"
+ provided_components: "payment"
provided_components: "search"
provided_components: "document-provider"
provided_components: "notification-listener"
diff --git a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionDetector.kt b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionDetector.kt
index bcef917..2665b3c 100644
--- a/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionDetector.kt
+++ b/tools/lint/global/checks/src/main/java/com/google/android/lint/aidl/EnforcePermissionDetector.kt
@@ -31,10 +31,11 @@
import com.android.tools.lint.detector.api.Severity
import com.android.tools.lint.detector.api.SourceCodeScanner
import com.intellij.psi.PsiAnnotation
+import com.intellij.psi.PsiArrayInitializerMemberValue
import com.intellij.psi.PsiClass
+import com.intellij.psi.PsiElement
import com.intellij.psi.PsiMethod
import org.jetbrains.uast.UAnnotation
-import org.jetbrains.uast.UClass
import org.jetbrains.uast.UElement
import org.jetbrains.uast.UMethod
@@ -65,6 +66,12 @@
return listOf(UAnnotation::class.java)
}
+ private fun annotationValueGetChildren(elem: PsiElement): Array<PsiElement> {
+ if (elem is PsiArrayInitializerMemberValue)
+ return elem.getInitializers().map { it as PsiElement }.toTypedArray()
+ return elem.getChildren()
+ }
+
private fun areAnnotationsEquivalent(
context: JavaContext,
anno1: PsiAnnotation,
@@ -82,18 +89,28 @@
if (attr1[i].name != attr2[i].name) {
return false
}
- val value1 = attr1[i].value
- val value2 = attr2[i].value
- if (value1 == null && value2 == null) {
- continue
- }
- if (value1 == null || value2 == null) {
- return false
- }
+ val value1 = attr1[i].value ?: return false
+ val value2 = attr2[i].value ?: return false
+ // Try to compare values directly with each other.
val v1 = ConstantEvaluator.evaluate(context, value1)
val v2 = ConstantEvaluator.evaluate(context, value2)
- if (v1 != v2) {
- return false
+ if (v1 != null && v2 != null) {
+ if (v1 != v2) {
+ return false
+ }
+ } else {
+ val children1 = annotationValueGetChildren(value1)
+ val children2 = annotationValueGetChildren(value2)
+ if (children1.size != children2.size) {
+ return false
+ }
+ for (j in children1.indices) {
+ val c1 = ConstantEvaluator.evaluate(context, children1[j])
+ val c2 = ConstantEvaluator.evaluate(context, children2[j])
+ if (c1 != c2) {
+ return false
+ }
+ }
}
}
return true
@@ -140,50 +157,13 @@
}
}
- private fun compareClasses(
- context: JavaContext,
- element: UElement,
- newClass: PsiClass,
- extendedClass: PsiClass,
- checkEquivalence: Boolean = true
- ) {
- val newAnnotation = newClass.getAnnotation(ANNOTATION_ENFORCE_PERMISSION)
- val extendedAnnotation = extendedClass.getAnnotation(ANNOTATION_ENFORCE_PERMISSION)
-
- val location = context.getLocation(element)
- val newClassName = newClass.qualifiedName
- val extendedClassName = extendedClass.qualifiedName
- if (newAnnotation == null) {
- val msg = "The class $newClassName extends the class $extendedClassName which " +
- "is annotated with @EnforcePermission. The same annotation must be used " +
- "on $newClassName."
- context.report(ISSUE_MISSING_ENFORCE_PERMISSION, element, location, msg)
- } else if (extendedAnnotation == null) {
- val msg = "The class $newClassName extends the class $extendedClassName which " +
- "is not annotated with @EnforcePermission. The same annotation must be used " +
- "on $extendedClassName. Did you forget to annotate the AIDL definition?"
- context.report(ISSUE_MISSING_ENFORCE_PERMISSION, element, location, msg)
- } else if (checkEquivalence && !areAnnotationsEquivalent(
- context, newAnnotation, extendedAnnotation)) {
- val msg = "The class $newClassName is annotated with ${newAnnotation.text} " +
- "which differs from the parent class $extendedClassName: " +
- "${extendedAnnotation.text}. The same annotation must be used for " +
- "both classes."
- context.report(ISSUE_MISMATCHING_ENFORCE_PERMISSION, element, location, msg)
- }
- }
-
override fun visitAnnotationUsage(
context: JavaContext,
element: UElement,
annotationInfo: AnnotationInfo,
usageInfo: AnnotationUsageInfo
) {
- if (usageInfo.type == AnnotationUsageType.EXTENDS) {
- val newClass = element.sourcePsi?.parent?.parent as PsiClass
- val extendedClass: PsiClass = usageInfo.referenced as PsiClass
- compareClasses(context, element, newClass, extendedClass)
- } else if (usageInfo.type == AnnotationUsageType.METHOD_OVERRIDE &&
+ if (usageInfo.type == AnnotationUsageType.METHOD_OVERRIDE &&
annotationInfo.origin == AnnotationOrigin.METHOD) {
val overridingMethod = element.sourcePsi as PsiMethod
val overriddenMethod = usageInfo.referenced as PsiMethod
@@ -198,17 +178,7 @@
return
}
val method = node.uastParent as? UMethod
- val klass = node.uastParent as? UClass
- if (klass != null) {
- val newClass = klass as PsiClass
- val extendedClass = newClass.getSuperClass()
- if (extendedClass != null && extendedClass.qualifiedName != JAVA_OBJECT) {
- // The equivalence check can be skipped, if both classes are
- // annotated, it will be verified by visitAnnotationUsage.
- compareClasses(context, klass, newClass,
- extendedClass, checkEquivalence = false)
- }
- } else if (method != null) {
+ if (method != null) {
val overridingMethod = method as PsiMethod
val parents = overridingMethod.findSuperMethods()
for (overriddenMethod in parents) {
diff --git a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionDetectorTest.kt b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionDetectorTest.kt
index 618bbcc..4ed68a8 100644
--- a/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionDetectorTest.kt
+++ b/tools/lint/global/checks/src/test/java/com/google/android/lint/aidl/EnforcePermissionDetectorTest.kt
@@ -33,21 +33,6 @@
override fun lint(): TestLintTask = super.lint().allowMissingSdk(true)
- fun testDoesNotDetectIssuesCorrectAnnotationOnClass() {
- lint().files(java(
- """
- package test.pkg;
- @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE)
- public class TestClass1 extends IFoo.Stub {
- public void testMethod() {}
- }
- """).indented(),
- *stubs
- )
- .run()
- .expectClean()
- }
-
fun testDoesNotDetectIssuesCorrectAnnotationOnMethod() {
lint().files(java(
"""
@@ -65,26 +50,72 @@
.expectClean()
}
- fun testDetectIssuesMismatchingAnnotationOnClass() {
+ fun testDoesNotDetectIssuesCorrectAnnotationAllOnMethod() {
lint().files(java(
"""
package test.pkg;
- @android.annotation.EnforcePermission(android.Manifest.permission.INTERNET)
- public class TestClass3 extends IFoo.Stub {
- public void testMethod() {}
+ import android.annotation.EnforcePermission;
+ public class TestClass11 extends IFooMethod.Stub {
+ @Override
+ @EnforcePermission(allOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE})
+ public void testMethodAll() {}
}
""").indented(),
*stubs
)
.run()
- .expect("""src/test/pkg/TestClass3.java:3: Error: The class test.pkg.TestClass3 is \
-annotated with @android.annotation.EnforcePermission(android.Manifest.permission.INTERNET) \
-which differs from the parent class IFoo.Stub: \
-@android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE). The \
-same annotation must be used for both classes. [MismatchingEnforcePermissionAnnotation]
-public class TestClass3 extends IFoo.Stub {
- ~~~~~~~~~
-1 errors, 0 warnings""".addLineContinuation())
+ .expectClean()
+ }
+
+ fun testDoesNotDetectIssuesCorrectAnnotationAllLiteralOnMethod() {
+ lint().files(java(
+ """
+ package test.pkg;
+ import android.annotation.EnforcePermission;
+ public class TestClass111 extends IFooMethod.Stub {
+ @Override
+ @EnforcePermission(allOf={"android.permission.INTERNET", android.Manifest.permission.READ_PHONE_STATE})
+ public void testMethodAllLiteral() {}
+ }
+ """).indented(),
+ *stubs
+ )
+ .run()
+ .expectClean()
+ }
+
+ fun testDoesNotDetectIssuesCorrectAnnotationAnyOnMethod() {
+ lint().files(java(
+ """
+ package test.pkg;
+ import android.annotation.EnforcePermission;
+ public class TestClass12 extends IFooMethod.Stub {
+ @Override
+ @EnforcePermission(anyOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE})
+ public void testMethodAny() {}
+ }
+ """).indented(),
+ *stubs
+ )
+ .run()
+ .expectClean()
+ }
+
+ fun testDoesNotDetectIssuesCorrectAnnotationAnyLiteralOnMethod() {
+ lint().files(java(
+ """
+ package test.pkg;
+ import android.annotation.EnforcePermission;
+ public class TestClass121 extends IFooMethod.Stub {
+ @Override
+ @EnforcePermission(anyOf={"android.permission.INTERNET", android.Manifest.permission.READ_PHONE_STATE})
+ public void testMethodAnyLiteral() {}
+ }
+ """).indented(),
+ *stubs
+ )
+ .run()
+ .expectClean()
}
fun testDetectIssuesMismatchingAnnotationOnMethod() {
@@ -99,33 +130,132 @@
*stubs
)
.run()
- .expect("""src/test/pkg/TestClass4.java:4: Error: The method TestClass4.testMethod is \
-annotated with @android.annotation.EnforcePermission(android.Manifest.permission.INTERNET) \
-which differs from the overridden method Stub.testMethod: \
-@android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE). The same \
-annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation]
- public void testMethod() {}
- ~~~~~~~~~~
-1 errors, 0 warnings""".addLineContinuation())
+ .expect("""
+ src/test/pkg/TestClass4.java:4: Error: The method TestClass4.testMethod is annotated with @android.annotation.EnforcePermission(android.Manifest.permission.INTERNET) \
+ which differs from the overridden method Stub.testMethod: @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE). \
+ The same annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation]
+ public void testMethod() {}
+ ~~~~~~~~~~
+ 1 errors, 0 warnings
+ """.addLineContinuation())
}
- fun testDetectIssuesMissingAnnotationOnClass() {
+ fun testDetectIssuesEmptyAnnotationOnMethod() {
lint().files(java(
"""
package test.pkg;
- public class TestClass5 extends IFoo.Stub {
+ public class TestClass41 extends IFooMethod.Stub {
+ @android.annotation.EnforcePermission
public void testMethod() {}
}
""").indented(),
*stubs
)
.run()
- .expect("""src/test/pkg/TestClass5.java:2: Error: The class test.pkg.TestClass5 extends \
-the class IFoo.Stub which is annotated with @EnforcePermission. The same annotation must be \
-used on test.pkg.TestClass5. [MissingEnforcePermissionAnnotation]
-public class TestClass5 extends IFoo.Stub {
- ~~~~~~~~~
-1 errors, 0 warnings""".addLineContinuation())
+ .expect("""
+ src/test/pkg/TestClass41.java:4: Error: The method TestClass41.testMethod is annotated with @android.annotation.EnforcePermission \
+ which differs from the overridden method Stub.testMethod: @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE). \
+ The same annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation]
+ public void testMethod() {}
+ ~~~~~~~~~~
+ 1 errors, 0 warnings
+ """.addLineContinuation())
+ }
+
+ fun testDetectIssuesMismatchingAnyAnnotationOnMethod() {
+ lint().files(java(
+ """
+ package test.pkg;
+ public class TestClass9 extends IFooMethod.Stub {
+ @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, android.Manifest.permission.NFC})
+ public void testMethodAny() {}
+ }
+ """).indented(),
+ *stubs
+ )
+ .run()
+ .expect("""
+ src/test/pkg/TestClass9.java:4: Error: The method TestClass9.testMethodAny is annotated with \
+ @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, android.Manifest.permission.NFC}) \
+ which differs from the overridden method Stub.testMethodAny: \
+ @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE}). \
+ The same annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation]
+ public void testMethodAny() {}
+ ~~~~~~~~~~~~~
+ 1 errors, 0 warnings
+ """.addLineContinuation())
+ }
+
+ fun testDetectIssuesMismatchingAnyLiteralAnnotationOnMethod() {
+ lint().files(java(
+ """
+ package test.pkg;
+ public class TestClass91 extends IFooMethod.Stub {
+ @android.annotation.EnforcePermission(anyOf={"android.permission.INTERNET", "android.permissionoopsthisisatypo.READ_PHONE_STATE"})
+ public void testMethodAnyLiteral() {}
+ }
+ """).indented(),
+ *stubs
+ )
+ .run()
+ .expect("""
+ src/test/pkg/TestClass91.java:4: Error: The method TestClass91.testMethodAnyLiteral is annotated with \
+ @android.annotation.EnforcePermission(anyOf={"android.permission.INTERNET", "android.permissionoopsthisisatypo.READ_PHONE_STATE"}) \
+ which differs from the overridden method Stub.testMethodAnyLiteral: \
+ @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, "android.permission.READ_PHONE_STATE"}). \
+ The same annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation]
+ public void testMethodAnyLiteral() {}
+ ~~~~~~~~~~~~~~~~~~~~
+ 1 errors, 0 warnings
+ """.addLineContinuation())
+ }
+
+ fun testDetectIssuesMismatchingAllAnnotationOnMethod() {
+ lint().files(java(
+ """
+ package test.pkg;
+ public class TestClass10 extends IFooMethod.Stub {
+ @android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, android.Manifest.permission.NFC})
+ public void testMethodAll() {}
+ }
+ """).indented(),
+ *stubs
+ )
+ .run()
+ .expect("""
+ src/test/pkg/TestClass10.java:4: Error: The method TestClass10.testMethodAll is annotated with \
+ @android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, android.Manifest.permission.NFC}) \
+ which differs from the overridden method Stub.testMethodAll: \
+ @android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE}). \
+ The same annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation]
+ public void testMethodAll() {}
+ ~~~~~~~~~~~~~
+ 1 errors, 0 warnings
+ """.addLineContinuation())
+ }
+
+ fun testDetectIssuesMismatchingAllLiteralAnnotationOnMethod() {
+ lint().files(java(
+ """
+ package test.pkg;
+ public class TestClass101 extends IFooMethod.Stub {
+ @android.annotation.EnforcePermission(allOf={"android.permission.INTERNET", "android.permissionoopsthisisatypo.READ_PHONE_STATE"})
+ public void testMethodAllLiteral() {}
+ }
+ """).indented(),
+ *stubs
+ )
+ .run()
+ .expect("""
+ src/test/pkg/TestClass101.java:4: Error: The method TestClass101.testMethodAllLiteral is annotated with \
+ @android.annotation.EnforcePermission(allOf={"android.permission.INTERNET", "android.permissionoopsthisisatypo.READ_PHONE_STATE"}) \
+ which differs from the overridden method Stub.testMethodAllLiteral: \
+ @android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, "android.permission.READ_PHONE_STATE"}). \
+ The same annotation must be used for both methods. [MismatchingEnforcePermissionAnnotation]
+ public void testMethodAllLiteral() {}
+ ~~~~~~~~~~~~~~~~~~~~
+ 1 errors, 0 warnings
+ """.addLineContinuation())
}
fun testDetectIssuesMissingAnnotationOnMethod() {
@@ -139,12 +269,13 @@
*stubs
)
.run()
- .expect("""src/test/pkg/TestClass6.java:3: Error: The method TestClass6.testMethod \
-overrides the method Stub.testMethod which is annotated with @EnforcePermission. The same \
-annotation must be used on TestClass6.testMethod [MissingEnforcePermissionAnnotation]
- public void testMethod() {}
- ~~~~~~~~~~
-1 errors, 0 warnings""".addLineContinuation())
+ .expect("""
+ src/test/pkg/TestClass6.java:3: Error: The method TestClass6.testMethod overrides the method Stub.testMethod which is annotated with @EnforcePermission. \
+ The same annotation must be used on TestClass6.testMethod [MissingEnforcePermissionAnnotation]
+ public void testMethod() {}
+ ~~~~~~~~~~
+ 1 errors, 0 warnings
+ """.addLineContinuation())
}
fun testDetectIssuesExtraAnnotationMethod() {
@@ -159,34 +290,13 @@
*stubs
)
.run()
- .expect("""src/test/pkg/TestClass7.java:4: Error: The method TestClass7.testMethod \
-overrides the method Stub.testMethod which is not annotated with @EnforcePermission. The same \
-annotation must be used on Stub.testMethod. Did you forget to annotate the AIDL definition? \
-[MissingEnforcePermissionAnnotation]
- public void testMethod() {}
- ~~~~~~~~~~
-1 errors, 0 warnings""".addLineContinuation())
- }
-
- fun testDetectIssuesExtraAnnotationInterface() {
- lint().files(java(
- """
- package test.pkg;
- @android.annotation.EnforcePermission(android.Manifest.permission.INTERNET)
- public class TestClass8 extends IBar.Stub {
- public void testMethod() {}
- }
- """).indented(),
- *stubs
- )
- .run()
- .expect("""src/test/pkg/TestClass8.java:2: Error: The class test.pkg.TestClass8 \
-extends the class IBar.Stub which is not annotated with @EnforcePermission. The same annotation \
-must be used on IBar.Stub. Did you forget to annotate the AIDL definition? \
-[MissingEnforcePermissionAnnotation]
-@android.annotation.EnforcePermission(android.Manifest.permission.INTERNET)
-^
-1 errors, 0 warnings""".addLineContinuation())
+ .expect("""
+ src/test/pkg/TestClass7.java:4: Error: The method TestClass7.testMethod overrides the method Stub.testMethod which is not annotated with @EnforcePermission. \
+ The same annotation must be used on Stub.testMethod. Did you forget to annotate the AIDL definition? [MissingEnforcePermissionAnnotation]
+ public void testMethod() {}
+ ~~~~~~~~~~
+ 1 errors, 0 warnings
+ """.addLineContinuation())
}
fun testDetectIssuesMissingAnnotationOnMethodWhenClassIsCalledDefault() {
@@ -213,21 +323,6 @@
/* Stubs */
- // A service with permission annotation on the class.
- private val interfaceIFooStub: TestFile = java(
- """
- @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE)
- public interface IFoo {
- @android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE)
- public static abstract class Stub extends android.os.Binder implements IFoo {
- @Override
- public void testMethod() {}
- }
- public void testMethod();
- }
- """
- ).indented()
-
// A service with permission annotation on the method.
private val interfaceIFooMethodStub: TestFile = java(
"""
@@ -236,9 +331,29 @@
@Override
@android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE)
public void testMethod() {}
+ @Override
+ @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE})
+ public void testMethodAny() {}
+ @Override
+ @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, "android.permission.READ_PHONE_STATE"})
+ public void testMethodAnyLiteral() {}
+ @Override
+ @android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE})
+ public void testMethodAll() {}
+ @Override
+ @android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, "android.permission.READ_PHONE_STATE"})
+ public void testMethodAllLiteral() {}
}
@android.annotation.EnforcePermission(android.Manifest.permission.READ_PHONE_STATE)
public void testMethod();
+ @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE})
+ public void testMethodAny() {}
+ @android.annotation.EnforcePermission(anyOf={android.Manifest.permission.INTERNET, "android.permission.READ_PHONE_STATE"})
+ public void testMethodAnyLiteral() {}
+ @android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, android.Manifest.permission.READ_PHONE_STATE})
+ public void testMethodAll() {}
+ @android.annotation.EnforcePermission(allOf={android.Manifest.permission.INTERNET, "android.permission.READ_PHONE_STATE"})
+ public void testMethodAllLiteral() {}
}
"""
).indented()
@@ -261,6 +376,7 @@
package android.Manifest;
class permission {
public static final String READ_PHONE_STATE = "android.permission.READ_PHONE_STATE";
+ public static final String NFC = "android.permission.NFC";
public static final String INTERNET = "android.permission.INTERNET";
}
"""
@@ -273,7 +389,7 @@
"""
).indented()
- private val stubs = arrayOf(interfaceIFooStub, interfaceIFooMethodStub, interfaceIBarStub,
+ private val stubs = arrayOf(interfaceIFooMethodStub, interfaceIBarStub,
manifestPermissionStub, enforcePermissionAnnotationStub)
// Substitutes "backslash + new line" with an empty string to imitate line continuation